多线程爬虫与异步爬虫的性能测试

如何提升爬虫的性能

如果你使用过爬虫框架scrapy,那么你多多少少会惊异于她的并发和高效。
在scrapy中,你可以通过在settings中设置线程数来轻松定制一个多线程爬虫。这得益于scrappy的底层twisted异步框架。
异步在爬虫开发中经常突显奇效,因为他可以是单个链接爬虫不堵塞
不阻塞可以理解为:在A线程等待response的时候,B线程可以发起requests,或者C线程可以进行数据处理。
要单个爬虫线程不阻塞,python可以使用到的库有:

  • threading
  • gevent
  • asyncio

一个常规的阻塞爬虫

下面的代码实现了一个获取 猫眼电影top100 的爬虫,网站反爬较弱,带上UA即可。
我们给爬虫写一个装饰器,记录其爬取时间。

import requests
import time
from lxml import etree
from threading import Thread
from functools import cmp_to_key


# 给输出结果排序
def sortRule(x, y):
    for i in x.keys():
        c1 = int(i)
    for i in y.keys():
        c2 = int(i)
    if c1 > c2:
        return 1
    elif c1 < c2:
        return -1
    else:
        return 0


# 计算时间的装饰器
def caltime(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        func(*args, **kwargs)
        print("costtime: ", time.time() - start)

    return wrapper


# 获取页面
def getPage(url):
    headers = {
        'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36',
        # 'Cookie': '__mta=141898381.1589978369143.1590927122695.1590927124319.9; uuid_n_v=v1; uuid=EDAA8A109A9611EABDA40952C053E9B506991609A05441F5AFBA3872BEA6088C; _csrf=f36a7050eb60429b197a902b4f1d66317db95bde0879648c8bff0e8237e937de; Hm_lvt_703e94591e87be68cc8da0da7cbd0be2=1589978364; mojo-uuid=8b4dad0e1f472f08ffd3f3f67b75f2ab; _lxsdk_cuid=17232188c2f0-022085e6f29b1b-30657c06-13c680-17232188c30c8; _lxsdk=EDAA8A109A9611EABDA40952C053E9B506991609A05441F5AFBA3872BEA6088C; mojo-session-id={"id":"afcd899e03fe72ca70e34368fe483d15","time":1590927095603}; __mta=141898381.1589978369143.1590063115667.1590927111235.7; mojo-trace-id=10; Hm_lpvt_703e94591e87be68cc8da0da7cbd0be2=1590927124; _lxsdk_s=1726aa4fd86-ba9-904-221%7C%7C15',
    }
    try:
        resp = requests.get(url=url, headers=headers)
        if resp.status_code == 200:
            return resp.text
        return None
    except Exception as e:
        print(e)
        return None


# 获取单个页面数据
def parsePage(page):
    if not page:
        yield
    data = etree.HTML(page).xpath('.//dl/dd')
    for d in data:
        rank = d.xpath("./i/text()")[0]
        title = d.xpath(".//p[@class='name']/a/text()")[0]
        yield {
            rank: title
        }


# 调度
def schedule(url, f):
    page = getPage(url)
    for data in parsePage(page):
        f.append(data)


# 数据展示
def show(f):
    f.sort(key=cmp_to_key(sortRule))
    for x in f:
        print(x)


@caltime
def main():
    urls = ['https://maoyan.com/board/4?offset={offset}'.format(offset=i) for i in range(0, 100, 10)]
    f = []
    for url in urls:
        schedule(url, f)
    show(f)


if __name__ == '__main__':
    main()

成功爬取完top100平均花费2.8s左右。
这个爬虫程序总共有10个小的爬虫线程,每个爬虫线程爬取10条数据。当前面的线程未成功收到response时,后面所有的线程都阻塞了。
这也是这个爬虫程序低效的原因。因为线程之间有明确的先后顺序,后面的线程无法越过前面的线程发送请求。


threading打破线程的优先级?

接下来我们使用多线程打破这种优先顺序。修改main函数

def main():
    urls = ['https://maoyan.com/board/4?offset={offset}'.format(offset=i) for i in range(0, 100, 10)]
    threads = []
    f = []
    for url in urls:
        # schedule(url, f)
        t = Thread(target=schedule, args=(url, f))
        threads.append(t)
        t.start()
    for t in threads:
        t.join()
    show(f)

记得导入threading库

from threading import Thread

点击运行,发现时间缩短为0.4s,性能的提升还是很客观的。
threading的作用在于开启了多个线程,每个线程同时竞争GIL,当拿到GIL发出requests后。该线程又立即释放GIL。进入等待Response的状态。
释放掉的GIL又马上被其他线程获取...如此以来,每个线程都是平等的,无先后之分。看起来就好像同时进行着(实际并不是,因为GIL的原因)。
所以效率大大提升了。


gevent异步协程搞一波?

gevent是一个优先的异步网络库,可以轻松支持高并发的网络访问。我们现在试着把阻塞的爬虫加上gevent试试

@caltime
def main():
    threads = []
    urls = ['https://maoyan.com/board/4?offset={offset}'.format(offset=i) for i in range(0, 100, 10)]
    f = []
    for url in urls:
        threads.append(gevent.spawn(schedule, url, f))
    gevent.joinall(threads)
    show(f)

同样这里也要导入gevent库

import gevent
from gevent import monkey
monkey.patch_all()

点击运行,平均时间在0.45上左右,和多线程差不多。


新版异步库ascyncio搞一波?

ascyncion是python前不久刚推出的基于协程的异步库,号称最有野心的库。要使ascyncio支持我们的程序,必须对getPage做点修改:
因为requests是不支持异步的,所以我们这里使用aiohttp库替换requests,并用它来实现getPage函数。

# 异步requests
async def getPage(url):
    headers = {'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36'}
    async with aiohttp.ClientSession() as session:
        async with session.get(url, headers = headers) as resp:
            return await resp.text()

main函数也需要修改

@caltime
def main():
    urls = ['https://maoyan.com/board/4?offset={offset}'.format(offset=i) for i in range(0, 100, 10)]
    loop = asyncio.get_event_loop()
    f = []
    threads = []
    for url in urls:
        threads.append(schedule(url,f))
    loop.run_until_complete(asyncio.wait(threads))
    show(f)

记得导入相关库

import asyncio
import aiohttp

点击运行,平均时间在0.35左右,性能稍优于多线程和gevent一点。


结语

对于爬虫技术,其实有些比较新的东西是值得去了解一下的。比如:

  • 提升并发方面:asyncio, aiohttp
  • 动态渲染:pyppeteer(puppeteer的python版,支持异步)
  • 验证码破解:机器学习,模型训练

还有一些数据解析方面的工具性能大概如下:

  • re > lxml > bs4
  • 但是即便是同一种解析方法,不同工具实现的,性能也不一样。比如同样是xpath,lxml的性能略好于parsel(scrapy团队开发的数据解析工具,支持css,re,xpath)的。

你可能感兴趣的:(多线程爬虫与异步爬虫的性能测试)