Python异步爬取梁羽生小说网

爬取这个网站的小说,用异步的方式,输出txt文件,小说章节按顺序

写这个的目的是,我在百度搜的的小说爬虫,异步,或多线程,都是没有按照顺序输出,小说阅读体验极差
也为了让自己进一步理解异步
就是爬这个网站 其实爬那个都差不多 http://www.liangyusheng.net/
一般先放代码,然后在写一些,自己的理解

import os
import re
import time
import random
import asyncio
import aiohttp
from lxml import etree


"""
@Date   :   2021-4-13
@Author :   江户川_柯北
@Version:   v1.0
"""

class GetNovel(object):
    """
    这个类爬取www.liangyusheng.net网站的小说
    """

    def __init__(self,url):
        """
        self.url    :   是小说首页的url
        """    
        #记录开始时间
        self.start_time = time.time()

        #创建小说保存目录,并切换到此处
        try:
            self.path = './novel/'
            os.mkdir(self.path)
            os.chdir(self.path)
        except:
            os.chdir(self.path)
            print("创建文件夹失败,当前工作路径为{}".format(os.getcwd()))

        self.url = url
        self.headers = [
            "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:34.0) Gecko/20100101 Firefox/34.0",
            "Mozilla/5.0 (X11; U; Linux x86_64; zh-CN; rv:1.9.2.10) Gecko/20100922 Ubuntu/10.10 (maverick) Firefox/3.6.10",
            "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.71 Safari/537.36",
            "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.64 Safari/537.11",
            "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.133 Safari/534.16",
            "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.101 Safari/537.36",
            "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko",
            "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/21.0.1180.71 Safari/537.1 LBBROWSER",
            "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; QQDownload 732; .NET4.0C; .NET4.0E; LBBROWSER)",
            "Opera/8.0 (Windows NT 5.1; U; en)",
            "Mozilla/5.0 (Windows NT 5.1; U; en; rv:1.8.1) Gecko/20061208 Firefox/2.0.0 Opera 9.50",
            "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; en) Opera 9.50",
        ]

    #既然想用递归就要想办法设置递归深度,但现有技术不会设置全局递归深度,就用n来设置
    async def get_url_html(self,n=0):
        """
        @return :   text 返回小说首页的html
        """
        self.session = aiohttp.ClientSession()

        n += 1

        headers = {
            "User-Agent" : random.choice(self.headers),
            "Accept-Language" : "zh-CN,zh;q=0.9",
            "Accept-Encoding" : "gzip, deflate",
        }
        # print(headers)
        if n == 3:
            return ''
        else:
            async with self.session.get(self.url,headers = headers) as resp:
                if resp.status == 200:
                    print("首页请求成功")
                    return await resp.read()
                else:
                    return self.get_url_html(n)

    async def get_url_list_author(self):
        """
        @return: url_list[11:] 返回所有章节url数据
        self.name_author初始化,小说名称,以及小说作者
        """
        try:
            t = await self.get_url_html()

            try :t = t.decode('utf-8')
            except:pass#能执行到这里说明上面发生错误,也就是,他不是二进制的数据,那他就是正常文本,不需要操作

            screen = etree.HTML(t)
            url_list = screen.xpath('/html/body/div[@id="wrapper"]/div[@id="main"]/div[@class="box_con"]/div/dl/dd/a/@href')
            # print(url_list)

            t = t.replace(' ','')
            novel_author = re.findall('<h1>(.*?)</h1>[\s\S]+<p>(.*?)<a href="/author/\d+/">(.*?)</a></p>',t)[0]
            # print(novel_author)
            
            #这里是小说名称和作者名称
            self.name_author = novel_author
            return url_list[11:]
        except:
            self.name_author = ()
            return []

    def data_clear(self,text):
        """
        @return txt 返回小说章节标题+小说正文
        """
        try :text  = text.decode('utf-8')
        except:pass

        text = text.replace('&nbsp;',' ')

        #编译正则表达式
        cmp = re.compile('<h1>(.*?)</h1>[\s\S]+<div id="content" deep="3">[\s\S]+</p>([\s\S]+)<div align="center">')
        data = re.findall(cmp,text)[0]

        #data[0]是章节 data[1]是正文
        txt  = data[0] + data[1]
        txt = txt.replace('<br />','\n').replace('\n\n','\n').replace('\n\n','\n')
        txt += '\n\n'

        return txt

    async def get_text(self,url,num):
        """
        @return 字典数据,包含url,小说html,小说章节,小说排序号
        
        """
        headers = {
            "User-Agent":random.choice(self.headers),
            "Referer":self.url
        }

        async with self.async_num:
            try :
                await asyncio.sleep(1)
                async with self.session.get(url,headers = headers) as res:
                    if res.status == 200:
                        text = await res.read()

                        print("第{:0>5d}章,请求成功".format(num))
                        return {url:
                                    {
                                        "text":self.data_clear(text),
                                        "chapter":"第{:0>5d}章".format(num),
                                        "num" : num,
                                    }
                                }
                    else:
                        self.get_text(url,num)
            except aiohttp.ClientError as e:
                print(f"e:>>>{e}")
                self.get_text(url,num)

    def write_file(self,content):
        """
        写入文件
        @return None
        """
        with open("{}_{}{}.txt".format(self.name_author[0],self.name_author[1],self.name_author[2]),'a+') as f:
            f.write(content)

    async def task(self):
        """
        @return None
        设置任务
        """
        #设置并发数量,这里设置了10
        self.async_num = asyncio.Semaphore(10)
        url_list = await self.get_url_list_author()
        url_root = self.url[0:self.url.index('/',7)]

        #任务列表
        task1 = [asyncio.ensure_future(self.get_text(url_root+url_list[i],i)) for i in range(1,len(url_list))]
        resu = await asyncio.gather(*task1)#返回的数据,顺序不对,下面要排序

        dic_content = {}#上回老师说字典比列表快
        for i in resu:
            dic_content.update(i)
            '''{url1:
                    {
                        "text":'',
                        "chapter":"第{:0>5d}章".format(num),
                        "num":num,
                    },
                url2:
                    {
                        "text":'',
                        "chapter":"第{:0>5d}章".format(num),
                        "num":num,
                    },
                }

            '''
        await self.session.close()#一定记得关闭会话

        #接下来就是写文件,写文件之前要排序,不会异步写文件只能顺序写文件,那么就要排好序
        dic_content = dict(sorted(dic_content.items(),key = lambda item:item[1]['num']))
        for _,v in dic_content.items():
            self.write_file(v['text'])
        print("结束!!!---用时{:.2f}".format(time.time()-self.start_time))


    def main(self):
        loop = asyncio.get_event_loop()
        loop.run_until_complete(self.task())

if __name__ == "__main__":
    print("只能爬取www.liangyusheng.net网站的小说")
    url = input("请输入目标小说首页url地址:").strip()
    if "www.liangyusheng.net" not in url:
        print("url不对")
        exit()
    GetNovel(url).main()

这个代码注释有点多哈。。
一点一点来看
1.在__init__()函数里面设置了开始时间,然后设置了工作目录,os.mkdir 是创建文件夹,os.chdir是切换工作目录。这里的工作目录就是最后小说保存位置。self.url就是小说首页的url,下面那个是U-A列表。
2.get_url_html()函数里面的n是处理一些异常的,我想着,他在这里假如没有请求到网页,就会返回异常,但是我想让他重复请求3次,若是,还请求不到就退出,就设置了n=0的默认值,然后response==200就正常返回,要是他不是200就重发请求,n+1,里面的self.session是创建一个异步请求的会话,这里就弄好,可以在后面调用,接下来就是async with 用with是因为方便,要不然还得手动关闭会话,然后就有了响应,resp.status 返回请求的状态码不是200就出现异常,然后resp.read()返回的是二进制数据,这里一会会处理编码。
3.get_url_list_author()这个函数,就是返回首页里面所有章节的连接,通过字符串拼接形成url可以获得文章正文,然后就是用try来处理二进制的编码,用utf-8。然后就用xpath得到url列表,用re得到小说名称和作者名称。(这里用正则是因为想练习一下正则),然后返回第12个以后的url,前12个url都是他最新章节的url,后面才从第一章开始,到最后。
4.data_clear()函数就是处理html得到标题和正文的函数了,这里面也用了re,用replace替换了一些垃圾字符,这些太影响阅读了,这里需要注意re.compile()这个是编译正则表达式(据网上传言说更快,这我也看不出来)。。。
5.get_text()这个函数,就是异步的到小说章节的html,也是异步,这里最最关键的是async with self.async_num 这个设置了异步的数量,以前在网上没找到,昨天偶然看见的记下了(不用这个并发太多,不给你爬)这个我设置在task()函数里面了一会讲。接下来就是停1秒,然后发送请求,等待所有响应结果,然后return 一个字典类型的数据,text字段是处理后的数据,num是他的排序号,有这个后面才能顺序输出,chaper字段没啥用,忘记删了。不管他。。。
6.write_file()这个没啥好说的,就是打开文件写入
7.task()这个函数就大有说道了,全局之关键。self.async_num设置了并发数量,asyncio.Semaphore(10)设置并发数为10。然后await等待url_list,字符串切割得到需要的前半截儿url,任务列表用列表生成式。,然后就是等待 asyncio.gather就是开始执行task1里面的任务了,返回值交给resu。然后创建一个字典,用for循环,把返回的列表里面每个小的字典,添加到dic_content里面,关闭异步会话,到这里异步操作结束然后就是lamba表达式排序,用的就是num字段,这样就有一个有顺序的字典了,然后for循环依次写入文件。
8.main()函数就是启动异步

最后希望大家能指正错误,谢谢

posted @ 2021-04-14 21:17  江户川_柯北  阅读(770)  评论(0)    收藏  举报