爬虫1.4-多线程和队列
爬虫-多线程和队列
当我们实现了一个小爬虫之后,会自然而然的考虑如何提升爬虫的效率,因此,我们就需要借助多线程、多进程和数据结构的方法。本次笔记提供一个简单的生产者和消费者模式的框架,并给出了一个实战代码。
1. 生产者和消费者模式
这个模式可以从生活实际出发,想想我们去吃自助,生产者(厨师们)将各类肉和蔬菜切好摆盘,消费者(我们)只管去拿食物,置于菜是怎么做的我们就不用关心了,qia肉就完事了。这就是生产者和消费者模式,在爬虫中就可以体现为,多个线程负责解析页面,将需要的信息放在一个队列里,消费者负责将信息存储到文件即可。那么问题来了,什么是队列和多线程/多进程?
1.1 多线程和多进程
关于多线程和多进程的概念,以及其优缺点本次笔记不做累述,只写python多线程和多进程的实现方式
多线程方法一:将线程需要做的事情在写函数中,让线程进入函数执行。
import threading
def xxfunc(xx):
print(xx)
t = threading.Thead(target=xxfunc,args=(xxx, )) # 函数的
t.start()
多线程方法二:继承Thread类,重写run方法,再创建实例,直接使用.start()就能使用run()方法
import threading
class Temp(threading.Thread):
def run(self):
xxxxx
for i in range(5):
t = Temp()
t.start
多进程的三种实现方法可见我的csdn博客https://blog.csdn.net/qq_36937323/article/details/83539761
1.2 队列
队列就是一种数据结构,将数据排成一个队伍,先进先出,即第一个被存入队列的数据,将被第一个取出
python创建一个队列:
from queue import Queue
q = Queue(5) # 5代表这个队列最多存放五个数据
q.put(1025)
print(q.get())
>> 1025
而在python中,队列可以实现阻塞模式,即当一个队列数据存满之后,再次存入数据时会进入阻塞模式,只有当消费者从队列中取出了一个数据后,队列才会退出阻塞模式,将新的数据存入。
2 多线程与队列结合的生产者-消费者模式
因为有多个消费者,如果他们将队列中的数据取出并写入同一个文件,那么在windows环境中可能会报错,所以在多线程对同一个文件进行写入时,要注意加锁。
import threading
mutex = threading.Lock()
with open('xxx.csv', 'w', encodind='utf-8') as fp:
mutex.acquire() # 抢占式上锁
fp.write(xx)
mutex.release() # 解锁,其他线程acquire继续抢
# 文件指针fp可以提前打开,并在创建多线程时将fp和锁都传入参数中。
下面给的示例代码是下载多个图片的,所以不存在对同一个文件的写入操作,所以不需要加锁。如果遇到需要对同一个文件的操作,就要考虑加锁的情况。
# 写了一个不优雅的多线程爬虫,使用正则解析数据,使用队列暂存大量的图片URL
# 目标网站:www.doutula.com 一个表情包网站,可以抓取图片URL和标题
# 注意jpg png格式的图片与gif格式图片的URL尾部有点不同 所以下方加入了尾部过滤
# request.urlretrieve 用于下载图片
import threading, requests, re, os
from urllib import request
from queue import Queue
# 头部信息
HEADERS = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36',
'Referer': 'http://www.doutula.com/photo/list/?page=2',
'Cookie': 'xx', #cookie请自己填写
}
# 这里主要用于调用生产图片url和标题的函数
def produce_img_url(q_url, q_img_info): # 传入两个队列的引用
while True:
if q_url.empty(): # 当页面队列为空时直接break 结束线程
break
url = q_url.get()
get_info(url, q_img_info) # 调用获取图片url和标题的函数
def donwload_img(q_url, q_img_info):
while True:
if q_img_info.empty() and q_url.empty(): # 当两个队列都为空时,线程就可以结束了
break
img_url, filename = q_img_info.get() # 获取url和文件名
request.urlretrieve(img_url, 'images/' + filename) # 下载图片到images/文件下,没有images文件夹就新建一个
def get_info(url, q_img_info):
response = requests.get(url, headers=HEADERS)
text = response.content.decode('utf-8')
# 正则直接获取url和标题,得到列表,格式[(url,title), (url,title)。。。]
srcs_and_title = re.findall(r'<img src=.*? data-original="(.*?)" alt="(.*?)".*?>', text, re.S)
for i in srcs_and_title:
img_url, title = i
# 因为jpg png图片的url末尾有!dta 所以这里先去掉
img_url = re.sub(r'!dta', '', img_url)
# 标题中的特殊字符会干扰文件写入
title = re.sub(r'[、。??!!,,*/]', '', title)
# 使用os模块的分割函数 取末尾的.xxx
suffix = os.path.splitext(img_url)[1]
# 组合成title.png title.gif等格式
filename = title + suffix
# 放入队列
q_img_info.put((img_url, filename))
def get_url_list(x, q_url):
"""x means the numbers of pages"""
base_url = 'http://www.doutula.com/photo/list/?page={}'
for i in range(1, x + 1):
q_url.put(base_url.format(i))
def main():
# 创建两个队列,第一个用于存储页面url,第二个用于存储图片的url
q_page_url = Queue(100)
q_image_info = Queue(1000)
# 获取页面url并存入队列
get_url_list(10, q_page_url)
# 生产者和消费者各5个
for i in range(5):
t = threading.Thread(target=produce_img_url, args=(q_page_url, q_image_info))
t.start()
for x in range(5):
t = threading.Thread(target=donwload_img, args=(q_page_url, q_image_info))
t.start()
if __name__ == '__main__':
data = []
main()