多进程+协程方案处理高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本书进行多进程操作.所以进行了一下这种非常规测试.
                    虽然此处效率并不是很理想, 但是这种因地制宜进程数必定有可取之处.




为闺中密友系列加了个书目
 
 
posted @ 2019-08-18 13:59  Jrri  阅读(804)  评论(0编辑  收藏  举报