爬虫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()

posted @ 2018-12-29 11:45  bitterz  阅读(324)  评论(0编辑  收藏  举报