在爬虫基础之环境搭建与入门中,介绍了如何用Requests下载(爬取)了一个页面,并用BeautifulSoup这个HTML解析库来解析页面里面咱们想要的内容。html
显然,爬虫确定不是只让咱们爬取一个网页的,这样的工做,人也能够作。下面咱们来看:nladuo.cn/scce_site/这个页面。这个页面一共有10页,点击下一页以后能够看到在网页的url中多了个字段“2.html”,也就是当前页面时第二页的意思。python
也就是咱们若是要爬取下全部的新闻,只要爬取形如"nladuo.cn/scce_site/{…"的页面就行了。算法
这里使用一个for循环就能够完成所有页面的爬取。网络
import requests
from bs4 import BeautifulSoup
import time
def crawl_one_page(page_num):
resp = requests.get("http://nladuo.cn/scce_site/{page}.html".
format(page=page_num))
soup = BeautifulSoup(resp.content)
items = soup.find_all("div", {"class": "every_list"})
for item in items:
title_div = item.find("div", {"class": "list_title"})
title = title_div.a.get_text()
url = title_div.a["href"]
date = item.find("div", {"class": "list_time"}).get_text()
print(date, title, url)
if __name__ == '__main__':
t0 = time.time()
for i in range(1, 11):
print("crawling page %d ......." % i)
crawl_one_page(i)
print("used:", (time.time() - t0))
复制代码
经过上面的代码,咱们完成了一个顺序结构的爬虫。下面咱们来讨论如何爬取的速度瓶颈在哪里,从而提高爬取速率。多线程
这里介绍一下CPU密集型业务和I/O密集型业务。并发
(上述解释来自blog.csdn.net/youanyyou/a…)app
网络爬虫主要有两个部分,一个是下载页面,一个是解析页面。显然,下载是个长时间的I/O密集操做,而解析页面则是须要调用算法来查找页面结构,是个CPU操做。async
对于爬虫来讲,耗时主要在下载一个网页中,根据网络的连通性,下载一个网页可能要几百毫秒甚至几秒,而解析一个页面可能只须要几十毫秒。因此爬虫实际上是属于I/O密集型业务,其瓶颈主要在网络上面。性能
因此,提高爬虫的爬取速度,不是把CPU都跑满。而是要多开几个下载器,同时进行下载,把网络I/O跑满。url
在Python中,使用多线程和多进程均可以实现并发下载。然而在python多线程没法跑多核(参见:GIL),而多进程能够。
这里,咱们主要说一下python中多进程的使用。
python中调用多进程使用multiprocessing这个包就行了。下面建立了两个进程,每隔一秒打印一下进程ID。(这里的time.sleep能够理解为耗时的I/O操做。)
import multiprocessing
import time
import os
def process(process_id):
while True:
time.sleep(1)
print('Task %d, pid: %d, doing something' % (process_id, os.getpid()))
if __name__ == "__main__":
# 进程1
p = multiprocessing.Process(target=process, args=(1,))
p.start()
# 进程2
p2 = multiprocessing.Process(target=process, args=(2,))
p2.start()
复制代码
能够看到基本上是同时打印两句话。而在没用多进程前,咱们的代码会像下面的代码的样子。
while True:
time.sleep(1)
print 'Task 1, doing something'
time.sleep(1)
print 'Task 2, doing something'
复制代码
此时,咱们建立两个进程,一个进程爬取1-5页,一个进程爬取6-10页。再来试试,看看速度有没有提高一倍。
import multiprocessing
import requests
from bs4 import BeautifulSoup
import time
def crawl_one_page(page_num):
resp = requests.get("http://nladuo.cn/scce_site/{page}.html".
format(page=page_num))
soup = BeautifulSoup(resp.content)
items = soup.find_all("div", {"class": "every_list"})
for item in items:
title_div = item.find("div", {"class": "list_title"})
title = title_div.a.get_text()
url = title_div.a["href"]
date = item.find("div", {"class": "list_time"}).get_text()
print(date, title, url)
def process(start, end):
for i in range(start, end):
print("crawling page %d ......." % i)
crawl_one_page(i)
if __name__ == '__main__':
t0 = time.time()
p = multiprocessing.Process(target=process, args=(1, 6)) # 任务1, 爬取1-5页
p.start()
p2 = multiprocessing.Process(target=process, args=(6, 11)) # 任务2, 爬取6-10页
p2.start()
p.join()
p2.join()
print("used:", (time.time() - t0))
复制代码
像上面的方式,咱们建立了两个进程,分别处理两个任务。然而有的时候,并非那么容易的把一个任务分红两个任务。考虑一下把一个任务想象为爬取并解析一个网页,当咱们有两个或者多个进程而任务有成千上万个的时候,代码应该怎么写呢?
这时候,咱们须要维护几个进程,而后给每一个进程分配一个网页,如何分配,须要咱们本身定义。在全部的进程都在运行时,要保证有进程结束时,再加入新的进程。
import multiprocessing
import requests
from bs4 import BeautifulSoup
import time
def crawl_one_page(page_num):
resp = requests.get("http://nladuo.cn/scce_site/{page}.html".
format(page=page_num))
soup = BeautifulSoup(resp.content, "html.parser")
items = soup.find_all("div", {"class": "every_list"})
for item in items:
title_div = item.find("div", {"class": "list_title"})
title = title_div.a.get_text()
url = title_div.a["href"]
date = item.find("div", {"class": "list_time"}).get_text()
print(date, title, url)
if __name__ == '__main__':
t0 = time.time()
p = None # 进程1
p2 = None # 进程2
for i in range(1, 11):
if i % 2 == 1: # 把偶数任务分配给进程1
p = multiprocessing.Process(target=crawl_one_page, args=(i,))
p.start()
else: # 把奇数任务分配给进程2
p2 = multiprocessing.Process(target=crawl_one_page, args=(i,))
p2.start()
if i % 2 == 0: # 保证只有两个进程, 等待两个进程完成
p.join()
p2.join()
print("used:", (time.time() - t0))
复制代码
上面的代码实现了一个简单的两进程的任务分配和管理,但其实也存在着一些问题:好比进程2先结束,此时就只有一个进程在运行,但程序还阻塞住,没法产生新的进程。这里只是简单的作个例子,旨在说明进程管理的复杂性。
下面咱们说一说进程池,其实就是为了解决这个问题而设计的。
既然叫作进程池,那就是有个池子,里面有一堆公用的进程;当有任务来了,拿一个进程出来;当任务完成了,把进程还回池子里,给别的任务用;当池子里面没有可用进程的时候,那就要等待,等别人把进程归还了再拿去用。
下面咱们来看一下代码,让每一个进程每秒打印一下pid,一共打印两遍。
from multiprocessing import Pool
import time
import os
def do_something(num):
for i in range(2):
time.sleep(1)
print("doing %d, pid: %d" % (num, os.getpid()))
if __name__ == '__main__':
p = Pool(3)
for page in range(1, 11): # 10个任务
p.apply_async(do_something, args=(page,))
p.close() # 关闭进程池, 再也不接受任务
p.join() # 等待子进程结束
复制代码
运行代码后能够看到,咱们能够看到这里是三个三个的打印的,咱们成功完成了三并发。同时,进程池一共产生了三个进程:59650、5965一、59652,说明后面的全部任务都是使用这三个进程完成的。
下面,修改爬虫代码,用进程池实现并发爬取。
import requests
from bs4 import BeautifulSoup
from multiprocessing import Pool
import time
def crawl_one_page(page_num):
resp = requests.get("http://nladuo.cn/scce_site/{page}.html".
format(page=page_num))
soup = BeautifulSoup(resp.content, "html.parser")
items = soup.find_all("div", {"class": "every_list"})
for item in items:
title_div = item.find("div", {"class": "list_title"})
title = title_div.a.get_text()
url = title_div.a["href"]
date = item.find("div", {"class": "list_time"}).get_text()
print(date, title, url)
if __name__ == '__main__':
t0 = time.time()
p = Pool(5)
for page in range(1, 11): # 1-10页
p.apply_async(crawl_one_page, args=(page,))
# 关闭进程池, 等待子进程结束
p.close()
p.join()
print("used:", (time.time() - t0))
复制代码
到这里,多进程的讲解就结束了。