Python爬虫进阶之多线程爬取数据并保存到数据库

今天刚看完崔大佬的《python3网络爬虫开发实战》,顿时觉得自己有行了,准备用appium登录QQ爬取列表中好友信息,接踵而来的是一步一步的坑,前期配置无数出错,安装之后连接也是好多错误,把这些错误解决之后,找APPactivity的值又让我绕了一大圈,找到值后又在权限上无法授权。在这里插入图片描述
正当我手足无措的准备放弃的时候,我突然看到Charles,可以直接对APP抓包,在配置上有事一步一个坑后,当我准备抓取抖音视频评论的时候一堆乱码,我反手就是一巴掌
在这里插入图片描述
为了避免这种尴尬,以及我突然想写博客的心情,我决定还是为大家在进行一次简易爬虫展示,总体程序我会利用多线程的方式来充分利用CPU的空闲时间,其中我也会加入数据库,有机会的话还想用邮件模块来为大家展示定时给自己发送邮件。哈哈哈,本博主就是喜欢如此花里胡哨。

一、网页分析

首先我的想法是爬取一个网站来获取所有品牌的电脑,以及价格和主要的参数,并将这些数据写入MySQL中,然后根据自己理想的价格和配置,将适合的数据存入表格并用附件的方式发给自己的邮件(本来是想当某款喜欢的电脑降价之后邮件通知我的,想想太麻烦了,就没这么做)。
今天我们要爬取的是中关村在线网站,这是个常见并且还比较权威的评测网站,每天都会发布各类产品的促销信息及各种文章新闻。
在这里插入图片描述
首先我们打开这个网站,并在搜索框中输入电脑,点击产品,就出现了很多品牌的电脑列表。然后打开我们都自带基础抓包神器-开发模式(按F12或者其他游览器点击设置中的开发者工具 ),一般点击network下的第一条数据就是我们的源代码了
在这里插入图片描述
大家可以看到,响应信息中就包含了页面上出现的文字和数据,还有其他图片的链接等等,这是个比较简单的静态网页,那对于数据的抓取就简单多了。接下来我就可以利用开发工具自带页面元素检查按钮来查看数据位于节点的位置了
在这里插入图片描述
如果想要查看具体的节点位置,可在绿色方框中点击元素右键出现一个copy,根据自己利用的解析工具会有不同的复制格式,这里我统一用的是Xpath。后面的步骤就不详说了,直接来说思路:
首先利用请求主页的URL来获取主页源代码,在源代码中利用Xpath把不同品牌的URL提取出来,在根据这些URL分别请求不同品牌的列表页面,再利用Xpath将列表中所有款式的URL提取出来,然后请求出单个款式的页面,名字价格参数都在里面。由于数据过大,就利用了队列与多线程来控制程序,分别有三个队列:
1.用来存放URL的队列
2.用来存放单个款式页面的队列
3.用来存放最后提取的数据的队列
利用消息队列可以很好的保证在线程结束前数据不会丢失,保证了数据的传递,并且还是先进先出的数据格式。

二、代码解析

接下来我们来分析代码:

定义__init__方法创建URL队列,页面队列和数据队列

def __init__(self):
        super(comSpider, self).__init__()
        self.headers={'User-Agent':'Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36'}
        self.url_head='http://detail.zol.com.cn'
        #url队列
        self.url_queue=Queue()
        #单个页面队列
        self.page_queue=Queue()
        #数据队列
        self.data_queue=Queue()

定义addurl_queue方法获取不同品牌不同款式的所有URL,把URL都放进URL队列中,传递给下个方法。

    def addurl_queue(self):
        L1=[]
        home_url='http://detail.zol.com.cn/notebook_index/subcate16_list_1.html'
        home_data=requests.get(home_url,headers=self.headers).text
        home_html = etree.HTML(home_data)
        brand_url=home_html.xpath('//*[@id="J_ParamBrand"]/a/@href')
        for x in range(len(brand_url)):
            brand_data=requests.get('http://detail.zol.com.cn'+brand_url[x],headers=self.headers).text
            brand_html= etree.HTML(brand_data)
            page_url=brand_html.xpath('//*[@id="J_PicMode"]/li/a/@href')
            L1=L1+page_url
#        print(L1)
        for i in range(len(L1)):
            self.url_queue.put(self.url_head+L1[i])

定义addpage_queue方法来从URL队列中取出URL并执行以获得响应页面,再把这些响应数据放入页面队列中。

    def addpage_queue(self):
        url=self.url_queue.get()
#        print(url)
        resp=requests.get(url,headers=self.headers)
        self.page_queue.put(resp.text)
        self.url_queue.task_done()

定义adddata_queue方法,从页面队列中取出响应数据进行解析,把解析后的数据存储到数据队列中。中间三个注释掉的for循环是我准备转换格式用的,当初因为通过lxml模块解析后的数据都是’lxml.etree._ElementUnicodeResult’格式,无法存入到数据库中,在网上查了资料后就用了for循环,结果出现了字符串拼接超出范围的问题,于是我索性直接将最后的结果利用str函数转换,没想到还成功了。

    def adddata_queue(self):
        page=self.page_queue.get()
#        print(page)
        page_html=etree.HTML(page)
        #品牌的名字
        name=page_html.xpath('/html/body/div/h1/text()')
#        for i in range(len(name)):
#            name=name[i]
#            name.encode('utf-8')
#        name_list=list(name)
#        print(name)
        #价格
        price=page_html.xpath('/html/body/div/div/div/div/span/b[2]/text()')
#        for i in range(len(price)):
#           price=price[i]
#            price.encode('utf-8')
#        price_list=list(price)
        #参数
        parameter=page_html.xpath('/html/body/div/div/div/div/ul/li/p/text()')
#        for i in range(len(parameter)):
#            parameter=parameter[i]
#            parameter.encode('utf-8')
#        parameter_list=list(parameter)

        data=name+price+parameter
        data=[str(i)for i in data]
#        print(type(data[0]),type(data[1]),type(data[2]),type(data[3]),type(data[4]),type(data[5]),type(data[6]),type(data[7]),type(data[8]),type(data[9]),)
        self.data_queue.put(data)
        self.page_queue.task_done()

定义sava_mysql方法从数据队列中取出数据存储到MySQL数据库中,如果想直接在代码中创建数据库和表格要加上几个判断(该数据存在直接创建表格,或者数据库表格都存在直接写入数据,考虑到多次执行该程序会报错。)

    def sava_mysql(self):
        data=self.data_queue.get()
        print(data)
        db=pymysql.connect(host='localhost', user='root', password='XXXXXXXX', port=3306, db='comeputerdata') 
        cursor= db.cursor()
#        time.sleep(2)
#        sql1=sql='''CREATE TABLE IF NOT EXISTS com_chart(Brand VARCHAR(255) NOT NULL,Price INT NOT NULL,ScreenSize VARCHAR(255) NOT NULL,ScreenResolution VARCHAR(255) NOT NULL,CPUmodel VARCHAR(255) NOT NULL,Core VARCHAR(255) NOT NULL,GPU VARCHAR(255) NOT NULL,MemoryCapacity VARCHAR(255) NOT NULL,BatteryLife VARCHAR(255) NOT NULL,Endurance VARCHAR(255) NOT NULL,PRIMARY KEY (Brand))'''
#        cursor.execute(sql1)
        time.sleep(2)
        sql2='''INSERT INTO com_chart(Brand,Price,ScreenSize,ScreenResolution,CPUmodel,Core,GPU,MemoryCapacity,BatteryLife,Endurance)values(%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)'''
#        try:
        cursor.execute(sql2,data)
        db.commit() 
#        except:
#            print('数据写入失败') 
#            db.rollback()
        db.close()
        self.data_queue.task_done()

定义run函数开启线程执行上面几个方法,join方法则是判断当队列为空时则执行别的操作。

    def run(self):
        list1=[]
        list2=[]
        list3=[] 
        # 开启线程执行上面的几个方法 
        url_t=threading.Thread(target=self.addurl_queue) 
        # url_t.setDaemon(True) 
        url_t.start()
        for i in range(500):
            t=Thread(target=self.addpage_queue()) 
            list1.append(t)
            t.start()

        for i in range(500):
            t=Thread(target=self.adddata_queue()) 
            list2.append(t)
            t.start()

        for i in range(500):
            t=Thread(target=self.sava_mysql()) 
            list3.append(t)
            t.start()         
 
        # 使用队列join方法,等待队列任务都完成了才结束 

        for t in list1:
            t.join()

        for t in list2:
            t.join()

        for t in list3:
            t.join()

三、源代码

好了,几个重要的地方提出来说了一下,下面我们直接上源代码:

#coding=utf-8
import requests #请求模块
from lxml import etree #解析模块
#import re #正则表达式(备用)
#import os #操作访问系统的模块(此处用于创建文件夹)
#import xlsxwriter #操作xls文件
import pymysql #操作数据库模块
#import smtplibfrom email.mime.text 
#import MIMEText #此模块可以用于发送正文
#from email.mime.multipart import MIMEMultipart #此模块用于发送带附件的邮件
#from concurrent.futures import ThreadPoolExecutor, wait, ALL_COMPLETED, FIRST_COMPLETED 
import time
from queue import Queue 
from threading import Thread
import threading





class comSpider(object):
    """docstring for comSpider"""
    def __init__(self):
        super(comSpider, self).__init__()
        self.headers={'User-Agent':'Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36'}
        self.url_head='http://detail.zol.com.cn'
        #url队列
        self.url_queue=Queue()
        #单个页面队列
        self.page_queue=Queue()
        #数据队列
        self.data_queue=Queue()


    def addurl_queue(self):
        L1=[]
        home_url='http://detail.zol.com.cn/notebook_index/subcate16_list_1.html'
        home_data=requests.get(home_url,headers=self.headers).text
        home_html = etree.HTML(home_data)
        brand_url=home_html.xpath('//*[@id="J_ParamBrand"]/a/@href')
        for x in range(len(brand_url)):
            brand_data=requests.get('http://detail.zol.com.cn'+brand_url[x],headers=self.headers).text
            brand_html= etree.HTML(brand_data)
            page_url=brand_html.xpath('//*[@id="J_PicMode"]/li/a/@href')
            L1=L1+page_url
#        print(L1)
        for i in range(len(L1)):
            self.url_queue.put(self.url_head+L1[i])


    def addpage_queue(self):
        url=self.url_queue.get()
#        print(url)
        resp=requests.get(url,headers=self.headers)
        self.page_queue.put(resp.text)
        self.url_queue.task_done()


    def adddata_queue(self):
        page=self.page_queue.get()
#        print(page)
        page_html=etree.HTML(page)
        name=page_html.xpath('/html/body/div/h1/text()')
#        for i in range(len(name)):
#            name=name[i]
#            name.encode('utf-8')
#        name_list=list(name)
#        print(name)
        price=page_html.xpath('/html/body/div/div/div/div/span/b[2]/text()')
#        for i in range(len(price)):
#           price=price[i]
#            price.encode('utf-8')
#        price_list=list(price)
        parameter=page_html.xpath('/html/body/div/div/div/div/ul/li/p/text()')
#        for i in range(len(parameter)):
#            parameter=parameter[i]
#            parameter.encode('utf-8')
#        parameter_list=list(parameter)

        data=name+price+parameter
        data=[str(i)for i in data]
#        print(type(data[0]),type(data[1]),type(data[2]),type(data[3]),type(data[4]),type(data[5]),type(data[6]),type(data[7]),type(data[8]),type(data[9]),)
        self.data_queue.put(data)
        self.page_queue.task_done()

    def sava_mysql(self):
        data=self.data_queue.get()
        print(data)
        db=pymysql.connect(host='localhost', user='root', password='XXXXXXXX', port=3306, db='comeputerdata') 
        cursor= db.cursor()
#        time.sleep(2)
#        sql1=sql='''CREATE TABLE IF NOT EXISTS com_chart(Brand VARCHAR(255) NOT NULL,Price INT NOT NULL,ScreenSize VARCHAR(255) NOT NULL,ScreenResolution VARCHAR(255) NOT NULL,CPUmodel VARCHAR(255) NOT NULL,Core VARCHAR(255) NOT NULL,GPU VARCHAR(255) NOT NULL,MemoryCapacity VARCHAR(255) NOT NULL,BatteryLife VARCHAR(255) NOT NULL,Endurance VARCHAR(255) NOT NULL,PRIMARY KEY (Brand))'''
#        cursor.execute(sql1)
        time.sleep(2)
        sql2='''INSERT INTO com_chart(Brand,Price,ScreenSize,ScreenResolution,CPUmodel,Core,GPU,MemoryCapacity,BatteryLife,Endurance)values(%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)'''
#        try:
        cursor.execute(sql2,data)
        db.commit() 
#        except:
#            print('数据写入失败') 
#            db.rollback()
        db.close()
        self.data_queue.task_done()


    def run(self):
        list1=[]
        list2=[]
        list3=[] 
        # 开启线程执行上面的几个方法 
        url_t=threading.Thread(target=self.addurl_queue) 
        # url_t.setDaemon(True) 
        url_t.start()
        for i in range(500):
            t=Thread(target=self.addpage_queue()) 
            list1.append(t)
            t.start()

        for i in range(500):
            t=Thread(target=self.adddata_queue()) 
            list2.append(t)
            t.start()

        for i in range(500):
            t=Thread(target=self.sava_mysql()) 
            list3.append(t)
            t.start()         
#        self.run_use_more_task(self.adddata_queue, 2) 
#        self.run_use_more_task(self.sava_mysql, 2) 
        # 使用队列join方法,等待队列任务都完成了才结束 
#        self.url_queue.join() 
#        self.page_queue.join() 
#        self.data_queue.join()

        for t in list1:
            t.join()

        for t in list2:
            t.join()

        for t in list3:
            t.join()

if __name__ == '__main__':
    qbs=comSpider() 
    qbs.run()

效果展示:

在这里插入图片描述
在这里插入图片描述

四、拓展代码

在这里插入图片描述
当然,中间我并没有加邮件发送,实在考虑到太麻烦了,有想实验的小伙伴可以自行操作一番,还是挺有趣的,下面我发一个邮件操作的简单代码:

import smtplib
from email.mime.text import MIMEText  #此模块可以用于发送正文
from email.mime.multipart import MIMEMultipart #此模块用于发送带附件的邮件


#邮件基础信息配置
smtpserver = "smtp.163.com" #发件服务器,qq邮箱为smtp.qq.com
port = 0    #端口,qq邮箱为465
sender = "XX@163.com" #发件箱
psw = " XXXX"  #qq邮箱为授权码
receiver = "XXXXXX@qq.com" #收件箱,多个邮箱为["XX@qq.com","XXXX@163.com"] 
msg=MIMEMultipart()
msg["from"]=sender
msg["to"]=receiver  #发给单个邮箱
#msg["to"]=";".join(receiver)  #发给多个邮箱
msg["subject"]="test"

body=MIMEText(mail_body,"html","utf-8")
msg.attach(body)                               #读取附件中的正文作为邮件正文

#附件信息的配置
file_path=r"D:\result.html"  #读取本地的一个一个测试报告
with open(file_path,"rb") as fp:
    mail_body=fp.read()
att=MIMEText(mail_body,"basse64","utf-8")
att["Content-Type"]="application/octet-stream"
att["Content-Disposition"]='attachment; filename="test_report.html"'  #将系统中的result.html重命名一下作为附件发送
msg.attach(att)

#兼容一般邮箱和qq邮箱
try:
    smtp = smtplib.SMTP()
    smtp.connect(smtpserver)  #连接服务器 ,一般邮箱,如163等
    smtp.login(sender,psw)    #登录
except:
    smtp=smtplib.SMTP_SSL(smtpserver,port) #连接服务器,qq邮箱
    smtp.login(sender,psw)    #登录
smtp.sendmail(sender,receiver,msg.as_string()) #发送
smtp.quit()    #关闭

五、问题与总结

1.首先是这个线程还是有点迷迷糊糊,没有学精,操作上面以及格式语法有很多错误。
2.在数据库中尽量不用汉字来命名字段,会有很多语法错误,还有在保存的时候大家可以看到有很多错位以及其他参数,因为在网页上的数据不太一致,所以还要利用其它方法筛选一下再保存。
3.其次就是执行线程函数中for循环的参数是表示开启多少个线程,也决定着你要爬取多少数据。
操作上略有粗糙,各位大佬勿怪,再次希望和我一样的小白共同努力。
在这里插入图片描述

  • 16
    点赞
  • 39
    收藏
    觉得还不错? 一键收藏
  • 8
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值