在爬取大量数据时,由于有成千上万的数据,单线程爬虫显然不能满足我们的需求,这时候多线程爬虫就来了,本篇文章使用Threading和Queue简单介绍。
私信小编01即可获取大量Python学习资料
首先先了解多线程队列,生产消费模式的大致步骤。
1.主线程生成目标链接。
2.主线程开启子线程访问队列并爬取数据保存。
3.待队列目标为空时关闭线程。
示例代码
主要字段:
city={
'河北省':['石家庄','保定市','秦皇岛','唐山市','邯郸市','邢台市','沧州市','承德市','廊坊市','衡水市','张家口'],
'山西省':['太原市','大同市','阳泉市','长治市','临汾市','晋中市','运城市','晋城市','忻州市','朔州市','吕梁市'],
'内蒙古':['呼和浩特','呼伦贝尔','包头市','赤峰市','乌海市','通辽市','鄂尔多斯','乌兰察布','巴彦淖尔'],
'辽宁省':['盘锦市','鞍山市','抚顺市','本溪市','铁岭市','锦州市','丹东市','辽阳市','葫芦岛','阜新市','朝阳市','营口市'],
'吉林省':['吉林市','通化市','白城市','四平市','辽源市','松原市','白山市'],
'黑龙江省':['伊春市','牡丹江','大庆市','鸡西市','鹤岗市','绥化市','双鸭山','七台河','佳木斯','黑河市','齐齐哈尔市'],
'江苏省':['无锡市','常州市','扬州市','徐州市','苏州市','连云港','盐城市','淮安市','宿迁市','镇江市','南通市','泰州市'],
'浙江省':['绍兴市','温州市','湖州市','嘉兴市','台州市','金华市','舟山市','衢州市','丽水市'],
'安徽省':['合肥市','芜湖市','亳州市','马鞍山','池州市','淮南市','淮北市','蚌埠市','巢湖市','安庆市','宿州市','宣城市','滁州市','黄山市','六安市','阜阳市','铜陵市'],
'福建省':['福州市','泉州市','漳州市','南平市','三明市','龙岩市','莆田市','宁德市'],
'江西省':['南昌市','赣州市','景德镇','九江市','萍乡市','新余市','抚州市','宜春市','上饶市','鹰潭市','吉安市'],
'山东省':['潍坊市','淄博市','威海市','枣庄市','泰安市','临沂市','东营市','济宁市','烟台市','菏泽市','日照市','德州市','聊城市','滨州市','莱芜市'],
'河南省':['郑州市','洛阳市','焦作市','商丘市','信阳市','新乡市','安阳市','开封市','漯河市','南阳市','鹤壁市','平顶山','濮阳市','许昌市','周口市','三门峡','驻马店'],
'湖北省':['荆门市','咸宁市','襄樊市','荆州市','黄石市','宜昌市','随州市','鄂州市','孝感市','黄冈市','十堰市'],
'湖南省':['长沙市','郴州市','娄底市','衡阳市','株洲市','湘潭市','岳阳市','常德市','邵阳市','益阳市','永州市','张家界','怀化市'],
'广东省':['江门市','佛山市','汕头市','湛江市','韶关市','中山市','珠海市','茂名市','肇庆市','阳江市','惠州市','潮州市','揭阳市','清远市','河源市','东莞市','汕尾市','云浮市'],
'广西省':['南宁市','贺州市','柳州市','桂林市','梧州市','北海市','玉林市','钦州市','百色市','防城港','贵港市','河池市','崇左市','来宾市'],
'海南省':['海口市','三亚市'],
'四川省':['乐山市','雅安市','广安市','南充市','自贡市','泸州市','内江市','宜宾市','广元市','达州市','资阳市','绵阳市','眉山市','巴中市','攀枝花','遂宁市','德阳市'],
'贵州省':['贵阳市','安顺市','遵义市','六盘水'],
'云南省':['昆明市','玉溪市','大理市','曲靖市','昭通市','保山市','丽江市','临沧市'],
'西藏':['拉萨市','阿里'],
'陕西省':['咸阳市','榆林市','宝鸡市','铜川市','渭南市','汉中市','安康市','商洛市','延安市'],
'甘肃省':['兰州市','白银市','武威市','金昌市','平凉市','张掖市','嘉峪关','酒泉市','庆阳市','定西市','陇南市','天水市'],
'青海省':['西宁市'],
'银川省':['银川市','固原市','青铜峡市','石嘴山市','中卫市']
}
years=['2011','2012','2013','2014','2015','2016','2017','2018','2019','2020']
month=['01','02','03','04','05','06','07','08','09','10','11','12']
month_less=['01','02','03','04','05','06','07','08','09','10']
title=['日期','最高气温','最低气温','天气','风向']
导入所需要的包:
from lxml import etree
import csv
import time
import bs4
import random
import requests
from xpinyin import Pinyin
import os
import threading
from queue import Queue
创建队列
q=Queue()
因为爬取的是全国省市2011年至今的天气数据,所以本段代码创建了省会路径并想队列传递目标链接。
for province in city.keys():
path='./Thread_Test/{}'.format(province)
if os.path.exists(path):
pass
else:
os.mkdir(path)
for nano_city in city[province]:
path='./Thread_Test/{}/{}'.format(province,nano_city)
if os.path.exists(path):
pass
else:
os.mkdir(path)
p=Pinyin()
if '市' in nano_city:
str_city=nano_city.split('市')[0]
str_city=p.get_pinyin(u'{}'.format(str_city),'')
print(str_city)
else:
str_city=p.get_pinyin(u'{}'.format(nano_city),'')
for y in years:
path='./Thread_Test/{}/{}/{}'.format(province,nano_city,y)
if os.path.exists(path):
pass
else:
os.mkdir(path)
if y=='2020':
for m in month_less:
url='https://lishi.tianqi.com/{}/{}.html'.format(str_city,y+m)
info=[province,nano_city,y,m]
q.put([url,info])
else:
for m in month:
url='https://lishi.tianqi.com/{}/{}.html'.format(str_city,y+m)
info=[province,nano_city,y,m]
q.put([url,info])
print(q.qsize())
队列中共有31388条链接
创建线程任务方法:
def working():
while True: #需要使用while 否则线程执行完一次操作就关闭了
url = q.get() #默认队列为空时,线程暂停
doing(url)
q.task_done()#告诉队列本次取操作已经完毕
def doing(url): #线程在获取到链接后的行为
rsp=requests.get(url=url[0],headers=headers)
html_4=bs4.BeautifulSoup(rsp.text,'html.parser')
ul=html_4.find('ul',class_='thrui')
f=open('./Thread_Test/{}/{}/{}/{}.csv'.format(url[1][0],url[1][1],url[1][2],url[1][3]),'w',encoding='utf-8',newline='')
csv_writer=csv.writer(f)
csv_writer.writerow(title)
for i in ul.find_all('li'):
lis=[]
a=i.find_all('div')
for j in a:
if len(j.text.split())==0:
pass
else:
lis.append(j.text.split()[0])
print(url[1][0],url[1][1],url[1][2],url[1][3],'剩余',q.qsize())
csv_writer.writerow(lis)
f.close()
创建子线程
threads = []
for i in range(10): #开启十个子线程
t = threading.Thread(target=working) #线程的目标任务为working方法
threads.append(t)
开启子线程
for item in threads:
item.setDaemon(True)
item.start()
q.join() #在队列为空时才进行后面的语句,需要配合task_done()使用
基本的多线程爬虫就完成了。
还有一个更优的方法
生成一个新的队列来储存主线程创建的十个子线程获取到网页数据,子线程并不直接写入,而是交给主线程,主线程再生成新的子线程来写入队列里的数据,这样就减少了十个子线程等待写入文件的时间,专注于爬取数据。
由于数据量太大,测试仅选择了一个省的数据进行爬取。约1300条数据
此图为线程直接写入所需时间
此图为使用新线程专门处理写入数据所需时间
次图为单线程爬虫所需时间
可以看到使用新线程专门处理写入数据时,速度比边爬边写快了30秒=百分之二十,而单线程花费了恐怖的1405秒,为多线程的十倍之多,可见多线程在爬取大量数据时是非常有用的。
当然,这么快的速度肯定会遭到反扒处理,封ip之类的,所以要事先准备一个ip池,每个线程在爬取一段时间后就更换一个ip,线程不建议开太多,避免给目标网站服务器造成太大压力,做一个绅士爬虫!
这段代码还可以再继续优化:
1.爬虫的类库提取
2.线程的类库提取
3.存数据库的类库提取
4.main()函数优化
初上手多线程,对于锁什么的还不是很懂,若有错误的地方欢迎指明。