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

如何提升爬虫的性能

如果你使用过爬虫框架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)的。

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