多线程爬虫实现(下)

1,298 阅读2分钟

本文首发于知乎

本文是上一篇文章的续篇,实现基于多线程的 翻页、抓取二级页面。使用豆瓣top250作为例子,为了防止请求过快ip被封,我们每页只抓取5个电影。

爬虫代码如下

import requests
import time
from threading import Thread
from queue import Queue
import json
from bs4 import BeautifulSoup

def run_time(func):
    def wrapper(*args, **kw):
        start = time.time()
        func(*args, **kw)
        end = time.time()
        print('running', end-start, 's')
    return wrapper


class Spider():

    def __init__(self):
        self.start_url = 'https://movie.douban.com/top250'
        self.qurl = Queue()
        self.data = list()
        self.item_num = 5 # 限制每页提取个数(也决定了二级页面数量)防止对网页请求过多
        self.thread_num = 10 # 抓取二级页面线程数量
        self.first_running = True

    def parse_first(self, url):
        print('crawling', url)
        r = requests.get(url)
        soup = BeautifulSoup(r.content, 'lxml')

        movies = soup.find_all('div', class_ = 'info')[:self.item_num]
        for movie in movies:
            url = movie.find('div', class_ = 'hd').a['href']
            self.qurl.put(url)

        nextpage = soup.find('span', class_ = 'next').a
        if nextpage:
            nexturl = self.start_url + nextpage['href']
            self.parse_first(nexturl)
        else:
            self.first_running = False

    def parse_second(self):
        while self.first_running or not self.qurl.empty():
            url = self.qurl.get()
            print('crawling', url)
            r = requests.get(url)
            soup = BeautifulSoup(r.content, 'lxml')
            mydict = {}
            title = soup.find('span', property = 'v:itemreviewed')
            mydict['title'] = title.text if title else None
            duration = soup.find('span', property = 'v:runtime')
            mydict['duration'] = duration.text if duration else None
            time = soup.find('span', property = 'v:initialReleaseDate')
            mydict['time'] = time.text if time else None
            self.data.append(mydict)

    @run_time
    def run(self):
        ths = []

        th1 = Thread(target=self.parse_first, args=(self.start_url, ))
        th1.start()
        ths.append(th1)

        for _ in range(self.thread_num):
            th = Thread(target=self.parse_second)
            th.start()
            ths.append(th)

        for th in ths:
            th.join()

        s = json.dumps(self.data, ensure_ascii=False, indent=4)
        with open('top_th1.json', 'w', encoding='utf-8') as f:
            f.write(s)

        print('Data crawling is finished.')

if __name__ == '__main__':
    Spider().run()

这里的整体思路和上一篇文章没有什么区别。分配两个队列,一个存储二级页面的URL,一个存储抓取到的数据。一级页面单独开一个线程,将二级页面URL不断填入队列中。解析二级页面URL时开启多个线程提高抓取速度。

除此之外,还需要说明一个地方

我们上一篇文章中,因为URL队列是事先产生的,而不是生产和消耗URL程序同时进行,因此队列一旦为空即结束爬虫。而这里的一级页面和二级页面的解析是同时进行的,也就是说二级页面URL是边生产边消耗的,这时我们就要保证

  • 所有页面解析结束可以退出所有线程(如果只是单纯while True,URL列表为空时,消耗线程就会永远等下去)
  • 不会因为二级页面URL消耗太快而使队列提前为空,提早退出爬虫

对于第二点,这里定义了self.first_running,它如果是True,则表示一级页面还没运行完成。此时即使二级页面的URL队列已经空了,也要继续等待一级页面解析后产生新的二级页面URL。

另外,由于这次URL队列是典型的生产消费者模式,因此如果不想自己实现Condition锁的话,就用Queue来代替list。

读者也可以试着更改self.thread_num看爬虫速度有什么改变。

欢迎关注我的知乎专栏

专栏主页:python编程

专栏目录:目录

版本说明:软件及包版本说明