多进程+协程方案处理高IO密集,提升爬取效率
# coding=utf-8 import gevent from gevent import monkey # monkey.patch_all() gevent.monkey.patch_all(thread=False, socket=False, select=False) # 协程gevent库和多进程,进程池冲突,需要关闭thread # 如不关闭, 代码会卡至创建进程池处. import requests import time # import sys from requests.adapters import HTTPAdapter from bs4 import BeautifulSoup # import multiprocessing from multiprocessing import Pool # sys.setrecursionlimit(10000) rs = requests.Session() rs.mount('http://', HTTPAdapter(max_retries=30)) rs.mount('https://', HTTPAdapter(max_retries=30)) # 设置最高重连次数 # import threading # 测试之后决定放弃多线程使用 # 在爬取数据上. # 相比较多进程下多线程 # 多进程下协程更具有性能优势. # class MyThread(threading.Thread): # """重写多线程,使其能够返回值""" # def __init__(self, target=None, args=()): # super(MyThread, self).__init__() # self.func = target # self.args = args # # def run(self): # self.result = self.func(*self.args) # # def get_result(self): # try: # return self.result # 如果子线程不使用join方法,此处可能会报没有self.result的错误 # except Exception: # return None # lock = threading.Lock() # 获取小说内容 def extraction_chapter(id, chapter_url, threads_content): """获取小说内容""" res = rs.get(chapter_url, timeout=(5, 7)) # print(result) # res.encoding = "gbk" # print (res) soup = BeautifulSoup(res.text, 'lxml') # print(soup) # title = soup.select('div.txtbox > h1')[].text title = soup.select('#txtbox > h1') content = soup.select('#content') # con = title + content title_str = str(title[0]) content_str = str(content[0]) # print(content_str) title_re = title_str.replace('<h1>', '') title_re = title_re.replace('</h1>', '\n') content_re = content_str.replace('<div id="content">', '') content_re = content_re.replace('<p>', '\n\t') content_re = content_re.replace('</p>', '') content_re = content_re.replace('</div>', '') make_sign = "\n\n\t_____(ฅ>ω<*ฅ)喵呜~~~_____\n\n\n" # 小mark con = title_re + content_re + make_sign threads_content[id] = con # 此处通过字典输入内容 # 获取小说每章网址(已分进程) def extraction(novel_url, ): # print("+") res = rs.get(novel_url, timeout=(3, 5)) # 输入小说总页面 # 获取元素 soup = BeautifulSoup(res.text, 'lxml') start_time = time.time() # 寻找书名 novel_title = soup.select('#bookinfo-right>h1') novel_title = str(novel_title[0]) novel_title = novel_title.replace('<h1>', '') novel_title = novel_title.replace('</h1>', '') print("开始: >>>"+novel_title+"<<< ") chapter_all = soup.select('#list>ul>li>a') # 获取章节所在元素,a标签 # chapter = str(chapter[0].attrs["href"]) # 获取a标签href属性 # print(type(chapter_all)) file_name = novel_title + '.txt' with open(file_name, 'w', encoding='utf-8') as f: f.write('') # content_con = "" id = 0 g_list = [] threads_content = {} # 遍历拼接每章网址 for chapter in chapter_all: chapter = str(chapter.attrs["href"]) # 获取a标签href属性 chapter_url = novel_url + chapter # 完成拼接 # print("协程创建+") # charpter_con = extraction_chapter(chapter_url) # 调用子方法, 萃取每章内容. # 使用协程提高效率 # charpter_con = gevent.spawn(extraction_chapter, chapter_url) # charpter_con.join() g = gevent.spawn(extraction_chapter, id, chapter_url, threads_content) id += 1 g_list.append(g) # 等待所有协程任务完成 gevent.joinall(g_list) # 遍历所有线程,等待所有线程都完成任务 # for t in threads: # t.join() # print(content_con) # 遍历线程字典, 导入内容 # i = 0 # value = "" # while i <= len(threads_content): # value = value + threads_content[i] # i += 1 # con_content = "" threads_content_key = sorted(threads_content.keys()) # 字典排序, 按照key值从小到大排列 for i in threads_content_key: # lock.acquire() with open(file_name, 'a', encoding='utf-8') as f: f.write(threads_content[i]) # lock.release() # con_content += threads_content[i] # 存储为字符串, 遍历完之后一次写入.[测试时间204] # threads_content.clear() # with open(file_name, 'a', encoding='utf-8') as f: # f.write(con_content) # # del con_content # 清除 end_time = time.time() elapsed = str( float('%.2f' % (end_time - start_time)) ) with open('console.log', 'a', encoding='utf-8') as f: f.write("Spend:["+ elapsed + "s]\t\t<<"+novel_title+">>\n") print("Spend:["+ elapsed + "s]\t\t<<"+novel_title+">>") # 完本页面网址 def end_book(end_url): res = rs.get(end_url, timeout=(3, 5)) # 连接超时和读取超时时间设置 # 输入小说总页面 # 获取元素 soup = BeautifulSoup(res.text, 'lxml') # 寻找书籍a元素 novel_name = soup.select('.bookimg>a') # print("准备创建进程") # 定义进程池, 默认为cpu核数 # print("创建进程池") # 默认进程数量为核心数量 po = Pool(8) # 使用八进程 # 使用协程后能效得到控制, 可根据总爬取数量进行更改. # ><><><测试><><><>< # 处理器:i5,3230M 四核, 内存8G # 爬取内容为同页,21本,每本约300章,30.9MB. 网络有浮动, 以下测试数据仅能作为参考 # >>效率对比<< # 4进程-协程,91s,118s,125s,131s,109s,100s <112.3> 四核CPU占用均约: 32% 内存最高占用:71.5% # 8进程-协程,74s,91s,89s,86s,89s,67s,80s,65s <80.12> 四核CPU占用均约: 45% 内存最高占用:77.9% # 12进程-协程,89s,96s,73s,81s,82s,78s,74s,69s <80.25> 四核CPU占用均约: 67% 内存最高占用:85.7% # <根据本数决定进程数> # 21进程-协程,82s,96s,89s,90s,85s <88.4> 四核CPU占用均约: 72% 内存最高占用:93.7% # <<>><><><> """ 总结: 计算密集型项目, 就只需使用多进程(核心数),能够达到最大效率,可跑满每颗核心.(核心数+1)可避免因为内存页缺失导致的计算资源浪费,可能造成一拖多现象,应根据具体情况调整. I/O 密集型项目, 则使用多进程,加线程或协程.(大部分爬虫项目,协程比多线程更有效率.) 在I/O密集型任务当中,多进程+协程的解决方案,应该适当变动进程数量. 决定因素有: 1.硬件性能. CPU: CPU尚未跑满,则尚有提升空间,可适当增加进程(N*核心数,N<=3). 内存: 一旦写满未能及时释放进程占用,则崩溃, 应减少进程. (硬盘写入门槛在小项目中很难触碰. 尤其是爬虫类,在使用协程时可不考虑) 2.网络. 自身带宽: 爬虫项目中, 带宽上限应为最终门槛.获取数据达到带宽上限, 代码可不必再进行优化. 遗憾的是此项目中, 抓取效率最高为800+Kb/s,远远未达到目标. 网页载入: 爬虫项目中最重要的限制, 页面的载入速度越快,获取数据越快,则进程应越少. 页面载入越慢, 则进程应越多才可提升效率,减少一拖多成本. 3.项目总量. 项目体量过大的时候, 应当仔细计算I/O时间与计算时间 公式应为: (IO时间+计算时间)/(计算时间+进程数*调度消耗) ***** 此公式另贴细表 ***** 项目体量不大的时候, 就根据具体的项目数量决定进程数 此项目中, 因为分页, 每页的21本书进行多进程操作.所以进行了一下这种非常规测试. 虽然此处效率并不是很理想, 但是这种因地制宜进程数必定有可取之处. """ # print("准备创建进程+") for name in novel_name: # 获取每个元素的网址 # print("进程创建") novel_url = name.attrs["href"] # print(novel_url) # extraction(novel_url) # 把 网址传入方法. # 进程池方式进行,把进程放入进程池 # p = multiprocessing.Process(target=extraction, args=(novel_url,)) po.apply_async(extraction, (novel_url,)) # p.start() # p_list.append(p) po.close() po.join() # 为避免抓取中断, 进程池设置, 本页数据抓取完毕之后再抓取下一页. 牺牲了一些性能, 可酌情更改 def book(index_url, start, end): num = start while num <= end: start_time = time.time() index = '/index_' + str(num) + '.html' if num == 1: index = "/" # 全本书索引页面 index_con = index_url + index print(index_con) # 输出网址 # 调用全本方法, 并传入参数 end_book(index_con) end_time = time.time() # 传入耗时参数 elapsed = str(float('%.2f' % (end_time - start_time))) localtime_end = time.asctime(time.localtime(time.time())) with open('console.log', 'a', encoding='utf-8') as f: f.write( '\n' + '*' * 50 + '\n'+ index +"\t"+ '消耗时间=\t' + elapsed + "\n" + localtime_end + "\n"+ '*' * 50+'\n\n') num += 1 if __name__ == '__main__': # 输入网址 url = "https://www.xxxxx.com/quanben" # 此处输入小说总网址 page_start = 1 # 开始页数 page_end = 96 # 结束页数 # 开始时间 start_time = time.time() localtime = time.asctime(time.localtime(time.time())) with open('console.log', 'w', encoding='utf-8') as f: f.write('<=====Start=====>\n\n' + localtime + '\n\n'+'-'*50+'\n\n') book(url, page_start, page_end) # 结束时间 end_time = time.time() # 耗时 elapsed = str( float('%.2f' % (end_time - start_time)) ) localtime_end = time.asctime(time.localtime(time.time())) with open('console.log', 'a', encoding='utf-8') as f: f.write('\n'+'-'*50+'\n'+'消耗时间=====' + elapsed + "\t\t" + "\n\n"+ localtime_end+"\n\n<=====Start=====>") print('消耗时间:' + elapsed)
总结:
计算密集型项目, 就只需使用多进程(核心数),能够达到最大效率,可跑满每颗核心.(核心数+1)可避免因为内存页缺失导致的计算资源浪费,可能造成一拖多现象,应根据具体情况调整.
I/O 密集型项目, 则使用多进程,加线程或协程.(大部分爬虫项目,协程比多线程更有效率.)
在I/O密集型任务当中,多进程+协程的解决方案,应该适当变动进程数量.
决定因素有:
1.硬件性能.
CPU: CPU尚未跑满,则尚有提升空间,可适当增加进程(N*核心数,N<=3).
内存: 一旦写满未能及时释放进程占用,则崩溃, 应减少进程.
(硬盘写入门槛在小项目中很难触碰. 尤其是爬虫类,在使用协程时可不考虑)
2.网络.
自身带宽: 爬虫项目中, 带宽上限应为最终门槛.获取数据达到带宽上限, 代码可不必再进行优化. 遗憾的是此项目中, 抓取效率最高为800+Kb/s,远远未达到目标.
网页载入: 爬虫项目中最重要的限制, 页面的载入速度越快,获取数据越快,则进程应越少. 页面载入越慢, 则进程应越多才可提升效率,减少一拖多成本.
3.项目总量.
项目体量过大的时候, 应当仔细计算I/O时间与计算时间
公式应为: (IO时间+计算时间)/(计算时间+进程数*调度消耗)
***** 此公式另贴细表 *****
项目体量不大的时候, 就根据具体的项目数量决定进程数
此项目中, 因为分页, 每页的21本书进行多进程操作.所以进行了一下这种非常规测试.
虽然此处效率并不是很理想, 但是这种因地制宜进程数必定有可取之处.
为闺中密友系列加了个书目