0%

爬虫系列(三)——多进程爬虫

承接上文,实现多任务的方法一般有三种,今日来实现其中的一种:多进程爬虫。而剩余的多线程并发和协程暂时没时间搞了,一方面是要先学习下Python的yield和send如何使用,其次要准备毕设的中期答辩和被老师安排了去读源代码,日后会补上的。但仅仅是多进程,就将IO密集的任务从1153秒提升到了105秒,且极高的提升了资源利用率。

还有一点,本文的代码可能只适用于Next主题,但方法思路是通解,只要改改参数适应你的主题,代码同样能用。结果展示:本站热门

爬虫

如果对爬虫不太了解,可以参考我之前的几篇文章。如果你计算机功底还不错,那么实现爬虫入门,完成简单的爬虫任务还是可以的:

这次的爬取任务很简单,之前看到了别人博客有『热榜』这个东西,很是眼馋,查询了一下发现实现热榜这个东西得经过很多的配置,但是我懒不想配置。于是准备自己动手爬虫,爬取每篇博客的访问量,以此作为博客每排文章的热度,说干就干,开工。

爬虫思路

  1. 本次爬虫需要request、BeautiulSoup和selenium联合使用,只有其中的一方完不成任务。

  2. 因为每篇博客的访问量并不在外部显示,必须点击到文章内部才难加载,所以,这次爬虫必须使用selenium控制浏览器模拟点击,之后进入对应网页,以此来获取不蒜子统计的浏览数量。

  3. 首先打开网页的根域名,按F12发现每篇文章的链接的类是post-title-link,以此来获取第一篇文章的名称,点击,进入第一篇博客。部分代码如下所示:

    # 寻找第一篇文章
    first_page = body.find('a', {'class', 'post-title-link'})
    # 记录第一篇文章的标题
    first = ""
    for i in first_page:
    first = i
    # 进入第一篇文章
    driver.find_element_by_xpath("//a[contains(text(),'{}')]".format(first)).click()
  4. 本来想的是,进入网页后,使用requests库直接请求,然后解析出不蒜子统计那个标签的数量。但是,requests网页后发现不蒜子标签下数量根本不显示。意思是,仅仅依靠requests和beautifulsoup的爬虫是爬取不到不蒜子统计的。

  5. 所以,还得靠selenium。再次按一下F12,发现不蒜子统计的id是busuanzi_value_page_pv。利用selenium的id定位法,定位到不蒜子统计元素,然后输出定位元素里的文本。部分代码如下所示:

    view = driver.find_element_by_id("busuanzi_value_page_pv")
    # num 就是每篇文章的访问量
    num = int(view.text)
  6. 而后,在每篇博客的末尾,都有指向下一篇博客的链接,点击既能进入下一篇博客。如下图所示:

  7. 而selenium定位到链接的文字又很费劲,不如用selenium获取当前网页的URL,然后使用requests和beautifulsoup库跟去获取的URL,进一步解析出指向下一篇博客链接的文字。而后把解析出的文字传给selenium的链接定位法,让selenium按照文字去点击链接,进入下一篇博客。如此循环往复,直到最后一篇博客。

  8. 因某些博客的图片数量过多,导致加载过慢,如果5秒内加载不完,则直接进入下一篇文章。中间为防止爬虫过猛被屏蔽,其中使用了一些延时函数放缓进度。

  9. 最终的结果要把文章的名称、文章的网址和文章的访问量都记录下来。考虑使用字典这种结构,将文章名称和网址作为key,访问量作为value。

  10. 将字典保存为json文件,而后将json文件中的结果稍微清洗一下,做成markdown文件,放到博客里,热榜就做成了。

  11. 最后,对爬虫任务的整体时间进行记录,对比单进程和多进程的效率。

单进程

import requests
from bs4 import BeautifulSoup

home_url = "https://muyuuuu.github.io/"

# 申请当前页面
r = requests.get(home_url)
soup = BeautifulSoup(r.text, "lxml")
body = soup.body

# 使用selenium点开每个页面,统计访问人数和评论数
from selenium import webdriver
driver = webdriver.Chrome(executable_path='/home/lanling/chromedriver_linux64/chromedriver')

import time

# 记录文章的数量
article = {}

# 先进入第一页
# 打开网页
driver.get(home_url)
# 寻找第一篇文章
first_page = body.find('a', {'class', 'post-title-link'})
# 记录第一篇文章的标题
first = ""
for i in first_page:
first = i
# 进入第一篇文章
driver.find_element_by_xpath("//a[contains(text(),'{}')]".format(first)).click()
time.sleep(3)
# 记录浏览数
dr = driver.find_element_by_id("busuanzi_value_page_pv")
# 标题名和域名作为字典的 key
string = "[" + first + "]" + "(" + driver.current_url + ")"
article[string] = int(dr.text)
print(article)

# 而后点击 每篇博客末尾的指向下一篇文章的链接 直到遍历完所有文章
# 记录开始时间
start = time.time()
# 捕获当前的 url
url = driver.current_url
data = requests.get(url)
data = BeautifulSoup(data.text, "lxml")
# 解析当前页面,获取下一页的按钮
next = data.find('div', {'class':'post-nav-next post-nav-item'})
try:
while next:
time.sleep(1)
# 点击下一页的链接 进入 下一个网页
driver.find_element_by_link_text(next.text.strip()).click()
time.sleep(5)
# 查看浏览数,图片较多加载缓慢,5秒内加载不出来跳过
try:
view = driver.find_element_by_id("busuanzi_value_page_pv")
except:
pass
# 获取当前页面的url
url = driver.current_url
time.sleep(1)
data = requests.get(url)
# 解析当前url
data = BeautifulSoup(data.text, "lxml")
# 当前文章的标题和域名传入字典
string = "[" + data.title.text + "]" + "(" + driver.current_url + ")"
# 如果没有加载到浏览数 就给一个负数
if view.text == "":
article[string] = -2
else:
article[string] = int(view.text)
print(string, int(view.text))
time.sleep(1)
# 寻找下一篇博客的链接名,然后点击,直到最后一篇文章。
next = data.find('div', {'class':'post-nav-next post-nav-item'})
except:
# 记录结束时间
end = time.time()

print(end - start)
driver.quit()

# 将博客按访问数排序
article_save = sorted(article.items(), key = lambda item:item[1])

import json
# encoding='utf-8',用于确保写入中文不乱码
with open('data.json','w',encoding='utf-8') as f_obj:
# ensure_ascii=False,用于确保写入json的中文不发生乱码
json.dump(article_save,f_obj,ensure_ascii=False, indent=4)

这个单进程的任务总共耗时1153秒左右(毕竟网络时延说不准),资源利用率很少,如下所示:

多进程

多进程仅需在单进程的代码上做一点改动即可:(如果你不懂多进程,请参考我的上一篇文章,如何理解多进程

  1. 首先统计一下博客有几页,就创建几个进程。比如,我的博客目前有14页,就创建14个进程:

  2. 进程扔到一个进程池里,等待所有进程执行完毕即可。

  3. 多进程之间不共享变量,所以字典的创建方法需要更改。

  4. 同样,防止爬虫过猛,中间同样适用延时函数放缓进度。

  5. 记录多进程的耗时,与单进程对比。代码如下所示:

import requests
from bs4 import BeautifulSoup

home_url = "https://muyuuuu.github.io/"

r = requests.get(home_url)
soup = BeautifulSoup(r.text, "lxml")

# 有几个页面就创建几个进程
body = soup.body
page_number = body.find('nav', {'class' : 'pagination'})
a = page_number.find_all('a')
max_page_number = 0
for i in a:
for j in i:
if str(j).isdigit():
if int(j) > max_page_number:
max_page_number = int(j)
print(max_page_number)
process_num = max_page_number

# 扔进进程池统一管理 并计时
from multiprocessing import Pool
import time, random
from selenium import webdriver
import multiprocessing

# 进程的字典
article = multiprocessing.Manager().dict()

# 记录每个进程爬几次,就是一个页面有几篇文章,我懒的爬取了,手动设置下
per_page = 7

# 每个进程的任务
def respite_task(url_num):
num = 1
sub_url = ""
if url_num == 0:
sub_url = "https://muyuuuu.github.io/"
else:
sub_url = "https://muyuuuu.github.io/page/" + str(url_num) + "/"
# 每个进程启动一个 driver
print("here")
driver = webdriver.Chrome(executable_path='/home/lanling/chromedriver_linux64/chromedriver')
driver.get(sub_url)
time.sleep(random.randint(2, 6))
r = requests.get(sub_url)
soup = BeautifulSoup(r.text, "lxml")
body = soup.body
# 寻找第一篇文章
first_page = body.find('a', {'class', 'post-title-link'})
# 记录第一篇文章的标题
first = ""
for i in first_page:
first = i
# 进入第一篇文章
driver.find_element_by_xpath("//a[contains(text(),'{}')]".format(first)).click()
time.sleep(5)
# 记录浏览数
try:
view = driver.find_element_by_id("busuanzi_value_page_pv")
except:
pass
# 标题名和域名作为字典的 key
string = "[" + first + "]" + "(" + driver.current_url + ")"
# 如果没有加载到浏览数 就给一个负数
if view.text == "":
article[string] = -2
else:
article[string] = int(view.text)
print(string, int(view.text))
# 捕获当前的 url (已经进入了新页面)
url = driver.current_url
data = requests.get(url)
data = BeautifulSoup(data.text, "lxml")
# 解析当前页面,获取下一页的按钮
next = data.find('div', {'class':'post-nav-next post-nav-item'})
try:
while next and num < per_page:
num += 1
time.sleep(1)
# 点击下一页的链接 进入 下一个网页
driver.find_element_by_link_text(next.text.strip()).click()
time.sleep(5)
# 查看浏览数,图片较多加载缓慢,5秒内加载不出来跳过
try:
view = driver.find_element_by_id("busuanzi_value_page_pv")
except:
pass
# 获取当前页面的url
url = driver.current_url
time.sleep(1)
data = requests.get(url)
# 解析当前url
data = BeautifulSoup(data.text, "lxml")
# 当前文章的标题和域名传入字典
string = "[" + data.title.text + "]" + "(" + driver.current_url + ")"
# 如果没有加载到浏览数 就给一个负数
if view.text == "":
article[string] = -2
else:
article[string] = int(view.text)
print(string, int(view.text))
time.sleep(1)
# 寻找下一篇博客的链接名,然后点击,直到最后一篇文章。
next = data.find('div', {'class':'post-nav-next post-nav-item'})
except:
pass
finally:
driver.quit()

p = Pool(process_num)

for i in range (process_num + 1):
p.apply_async(respite_task, args=(i,))

print('waitting for all subprocess done')
start = time.time()
p.close()
p.join()
end = time.time()
print('All subprocesses done costs {} seconds'.format(end - start))

# 升序排列并保存
article_save = sorted(article.items(), key = lambda item:item[1])
import json
# encoding='utf-8',用于确保写入中文不乱码
with open('data_multiprocess.json','w',encoding='utf-8') as f_obj:
# ensure_ascii=False,用于确保写入json的中文不发生乱码
json.dump(article_save,f_obj,ensure_ascii=False, indent=4)

最终的结果和单线程的结果保持一致,但仅仅用了105秒就执行完毕,足足提升了10倍。因为每个进程都有一个主线程,将主线程分配给多个核,所以导致了资源利用率比单线程要高的多:

结语

这好像不是传说中的并行计算,毕竟不是计算密集的任务,但这个几个进程的任务确实在并行执行,『知识就是力量』还是有道理的。

明人不说暗话,如果你感觉我写的还可以对你有帮助的话,Could you buy me a yogurt? ,文末也可评分。如果这样不行的话,我,秦始皇,打钱。

欢迎订阅我的文章