这篇文章的主要内容是,分享两种加快爬虫速度的方法。一个是多进程分布式的爬虫,一个是异步加载的爬虫。
分布式爬虫
我们可以利用python里的multiprocessing(多进程)和threading(多线程)实现简单的分布式爬虫。
它的原理就是:一般我们的程序都是单线程跑的, 也就是说程序当中的指令是一条条处理的, 执行完一条指令才能跳到下一条. 但在我们爬虫的程序中这样的方式有一个问题,就是大量的时间花费在下载网页上。所以如果下载一部分网页的时候就开始分析另一部分网页了, 又或者, 我们能同时下载多个网页, 同时分析多个网页, 这样就有种事倍功半的效用。分布式爬虫的体系有很多种, 处理优化的问题也是多样的. 这里有一篇博客可以当做扩展阅读, 来了解当今比较流行的分布式爬虫框架。
我是用了莫烦的思路:同时下载多个网页, 同时分析多个网页。大概的框架是这样的:
根据图,我们知道,爬虫分为两个步骤:第一步是打开网页,第二步就是解析网页的内容;
之前在爬虫成长日记-爬取图片里大概已经介绍过如何打开网页,并利用BeautifulSoup来解析网页了,这里就不赘述了。
#打开网页
def crawl(url):
html = urlopen(url).read().decode('utf-8')
return html
#解析网页
def parse(html):
soup = BeautifulSoup(html,features = 'lxml')
urls = soup.find_all('a',{'href':re.compile('^/.*?/$')})
title = soup.find('h1').get_text().strip()
page_urls = set([urljoin(base_url,url['href']) for url in urls])
#这个url是现在所爬取的网页的url
url = soup.find('meta',{'property':"og:url"})['content']
return title,page_urls,url
因为爬虫是不断打开网页上的URL,在不同的网页上可能存在相同的网页链接,为了避免重复爬取,我们需要记录一下哪些网站是爬取过的,哪些网站是没有爬取的。
#分别代表未爬取和已爬取的网页
unseen = set([base_url,])
seen = set()
做好这些准备工作,我们就可以准备让我们的爬虫开始工作了。这里我们采用了Pool(进程池)来并行“打开网页”和“解析网页”这两项工作。
#使用多进程进行爬虫来爬取网页
pool = mp.Pool(4)#创建4个进程池
count ,t1=1,time.time()
if base_url!='http://127.0.0.1:4000/':
restricted = True
else:
restricted = False
while len(unseen)!=0:
# if restricted_crawl and len(seen) > 20:
# break
crawl_jobs = [pool.apply_async(crawl, args=(url,)) for url in unseen]
htmls = [j.get() for j in crawl_jobs] # request connection
parse_jobs = [pool.apply_async(parse, args=(html,)) for html in htmls]
results = [j.get() for j in parse_jobs] # parse html
seen.update(unseen) # seen the crawled
unseen.clear() # nothing unseen
for title, page_urls, url in results:
print(count, title, url)
count += 1
unseen.update(page_urls - seen) # get new url to crawl
print('Total time: %.1f s' % (time.time()-t1, ))
以上就是一个简单的多进程分布式爬虫的实现。
多进程分布式的爬虫是通过利用计算机开辟多个进程来并行一些操作,从而使得运算速度加快。下面我们要介绍的是通过单线程就可以实现爬虫加速的效果,是不是感觉很神奇。不过在这之前,我们最好要了解一下python中协程的概念。
Python 提供了一个有力的工具, 叫做 asyncio. 这是一个仅仅使用单线程, 就能达到多线程/进程的效果的工具. 它的原理, 简单说就是: 在单线程里使用异步计算, 下载网页的时候和处理网页的时候是不连续的, 更有效利用了等待下载的这段时间. Python 官方解释 asyncio 的图(来源), 稍微复杂一点。
异步加载
在将asyncio应用于爬虫加速之前,我们先来了解一下asyncio库大概的用法,利用下面这个程序来热个身:
#不是异步的情况
import time
def job(t):
print('Start job',t)
time.sleep(t)
print('job takes ',t,'s')
t1 = time.time()
[job(t) for t in range(1,4)]
print("NO async total time : ", time.time() - t1)
同样的程序,用 asyncio来做:
import asyncio
async def job(t):
print('Start job',t)
await asyncio.sleep(t)
print('job takes ',t,'s')
async def main(loop):
tasks = [loop.create_task(job(t)) for t in range(1,4)]#1
await asyncio.wait(tasks)
t1 = time.time()
loop = asyncio.get_event_loop() #建立loop
loop.run_until_complete(main(loop))
loop.close()
print("Async total time:",time.time()-t1
运行结果是:
Start job 1
Start job 2
Start job 3
job takes 1 s
job takes 2 s
job takes 3 s
Async total time: 3.0082571506500244
可以看出使用异步IO可以确实加快了速度。那使用我们之前的进程池(多进程)效果会是什么样子的呢?
#使用进程池来做
import multiprocessing as mp
def job(t):
print('Start job',t)
time.sleep(t)
print('job takes ',t,'s')
def main():
t1 = time.time()
pool = mp.Pool(4)
res = pool.map(job,range(1,4))#1
print("Async total time:",time.time()-t1)
if __name__ == '__main__':
main()
运行结果如下:
Start job 2
Start job 1
Start job 3
job takes 1 s
job takes 2 s
job takes 3 s
Async total time: 3.022818088531494
看来asyncio确实可以仅仅使用一个单线程, 就能达到多线程/进程的效果。好奇的我就想把任务数加多,超过进程池的数量(4个)。修改了程序中(#1处)的任务数,改为range(1,8),运行程序得到这样的结果:
不过不同的情况结果也不同,只能说对于IO频繁的程序,异步IO确实可以提高效率。
有了这个背景以后,我们就可以用asyncio加速我们的爬虫。使用asyncio还要配合使用aiohttp。我们需要安装另一个牛逼的模块将
requests
模块代替成一个异步的 requests
, 这个牛逼的模块叫作 aiohttp
(官网在这). 下载安装特别简单. 直接在你的 terminal 或者 cmd 里面输入 “pip3 install aiohttp”那使用
asyncio
和aiohttp
以后的爬虫是一个什么样子的结构呢?又要盗图了-_-#可以看到,相比于分布式爬虫,我们只是把打开网页这一步进行了异步操作。而对于计算密集型的解析网页的操作,还是使用了原来的多进程并行操作。
具体代码如下:
def parse(html):
soup = BeautifulSoup(html, 'lxml')
urls = soup.find_all('a', {"href": re.compile('^/.+?/$')})
title = soup.find('h1').get_text().strip()
page_urls = set([urljoin(base_url, url['href']) for url in urls])
url = soup.find('meta', {'property': "og:url"})['content']
return title, page_urls, url
async def crawl(url,session):
r =await session.get(url)
html = await r.text()
return html
async def main(loop):
pool = mp.Pool(8)
async with aiohttp.ClientSession() as session:
count = 1
while len(unseen) != 0:
print('\nAsync Crawling...')
tasks = [loop.create_task(crawl(url, session)) for url in unseen]
finished, unfinished = await asyncio.wait(tasks)
htmls = [f.result() for f in finished]
print('\nDistributed Parsing...')
parse_jobs = [pool.apply_async(parse, args=(html,)) for html in htmls]
results = [j.get() for j in parse_jobs]
print('\nAnalysing...')
seen.update(unseen)
unseen.clear()
for title, page_urls, url in results:
print(count, title, url)
unseen.update(page_urls - seen)
count += 1
if __name__ == "__main__":
t1 = time.time()
loop = asyncio.get_event_loop()
loop.run_until_complete(main(loop))
loop.close()
print("Async total time: ", time.time() - t1)
运行结果是:完整代码见我的github :D