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

如何提升爬虫的性能

如果你使用过爬虫框架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)的。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,612评论 5 471
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,345评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,625评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,022评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,974评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,227评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,688评论 3 392
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,358评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,490评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,402评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,446评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,126评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,721评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,802评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,013评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,504评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,080评论 2 341