阅读 3691

多线程爬虫实现(上)

本文首发于知乎

爬虫主要运行时间消耗是请求网页时的io阻塞,所以开启多线程,让不同请求的等待同时进行,可以大大提高爬虫运行效率。

本文基于多线程(这里开启了10个线程),使用github的api,抓取fork cpython项目的所有5千多个项目信息,将数据存储到json文件中。

抓取github的这个内容,在上一篇文章中展示了不使用多线程的版本,这里就直接在那个的基础上进行改进。

爬虫所需技术

  • requests库请求网页,获取json格式数据,解析字典提取我们要的信息,存储json文件
  • 使用threading为网页请求部分设计多线程
  • 使用两个队列,分别存储待抓取url和解析后的结果数据
  • 拥有github账户,需要在代码中填入账号和密码
  • 了解装饰器(这里只是计算运行时间,不了解也没关系)

爬虫代码如下

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

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.qurl = Queue()
        self.data = list()
        self.email = '' # 登录github用的邮箱
        self.password = '' # 登录github用的密码
        self.page_num = 171
        self.thread_num = 10

    def produce_url(self):
        baseurl = 'https://api.github.com/repos/python/cpython/forks?page={}'
        for i in range(1, self.page_num + 1):
            url = baseurl.format(i)
            self.qurl.put(url) # 生成URL存入队列,等待其他线程提取

    def get_info(self):
        while not self.qurl.empty(): # 保证url遍历结束后能退出线程
            url = self.qurl.get() # 从队列中获取URL
            print('crawling', url)
            req = requests.get(url, auth = (self.email, self.password))
            data = req.json()
            for datai in data:
                result = {
                    'project_name': datai['full_name'],
                    'project_url': datai['html_url'],
                    'project_api_url': datai['url'],
                    'star_count': datai['stargazers_count']
                }
                self.data.append(result)

    @run_time
    def run(self):
        self.produce_url()

        ths = []
        for _ in range(self.thread_num):
            th = Thread(target=self.get_info)
            th.start()
            ths.append(th)
        for th in ths:
            th.join()

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

        print('Data crawling is finished.')

if __name__ == '__main__':
    Spider().run()
复制代码

读者只需要在Spider__init__中,指定自己的github邮箱和密码,即可运行爬虫。

爬虫说明如下

1.run_time函数是一个计算程序运行时间的装饰器,作用于Spider对象的run函数

2.Spider

  • __init__初始化一些常量
  • produce_url用于生产所有URL,存储到Queue队列qurl中。5千多个元素分布在171个页面之中,将这171个URL存入队列中等待请求解析。其实这里不需要多线程之间通信,所以使用list代替Queue队列也是可以的。
  • get_info网页的请求与解析,之后开启多线程就是多个这个函数同时运行。函数逻辑:只要qurl中还有元素,就每次从qurl中提取一个url进行请求解析,将结果存入data列表中。当队列中没有元素了即退出循环(爬虫结束)。
  • run调用函数,运行爬虫。首先调用produce_url产生待爬URL队列。然后开启指定数量的线程,每个线程都从qurl不断提取URL进行解析,将数据存入data列表中。等到URL队列被解析结束,将data中的数据存储入json文件中

爬虫结果

抓取结果展示如下

这个程序开启10个线程抓取171个页面用了33秒。在这篇文章中不使用多线程则使用了333秒。为了能更清晰地体会多线程运行效率的改进,读者可以自行尝试修改上面代码中的self.page_numself.thread_num

我这里做了一个实验,self.page_num值设为20,即总共抓取20页

  • 开2个线程运行 18.51 秒
  • 开5个线程运行 7.49 秒
  • 开10个线程运行 3.97 秒
  • 开20个线程运行 2.11 秒

一个问题

最后留一个问题给读者思考:在前面的这篇文章中,我们也实现了一个多线程爬虫,为什么当时的代码那么简单,而现在却复杂了这么多呢?

后续

多线程爬虫的下一篇文章会实现在翻页、抓取二级页面时使用多线程。

欢迎关注我的知乎专栏

专栏主页:python编程

专栏目录:目录

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

文章分类
后端