美文网首页python机器学习爬虫Python待看科技
Python爬虫——异步爬虫,两百四十多万字,六百章的小说20秒

Python爬虫——异步爬虫,两百四十多万字,六百章的小说20秒

作者: 白巧克力LIN | 来源:发表于2021-09-04 10:09 被阅读0次

    大家好!我是霖hero。

    相信很多人喜欢在空闲的时间里看小说,甚至有小部分人为了追小说而熬夜看,那么问题来了,喜欢看小说的小伙伴在评论区告诉我们为什么喜欢看小说,今天我们手把手教你使用异步协程20秒爬完两百四十多万字,六百章的小说,让你一次看个够。

    在爬取之前我们先来简单了解一下什么是同步,什么是异步协程?

    同步与异步

    同步

    同步是有序,为了完成某个任务,在执行的过程中,按照顺序一步一步执行下去,直到任务完成。

    爬虫是IO密集型任务,我们使用requests请求库来爬取某个站点时,网络顺畅无阻塞的时候,正常情况如下图所示:



    但在网络请求返回数据之前,程序是处于阻塞状态的,程序在等待某个操作完成期间,自身无法继续干别的事情,如下图所示:



    当然阻塞可以发生在站点响应后的执行程序那里,执行程序可能是下载程序,大家都知道下载是需要时间的。

    当站点没响应或者程序卡在下载程序的时候,CPU一直在等待而不去执行其他程序,那么就白白浪费了CPU的资源,导致我们的爬虫效率很低。

    异步

    异步是一种比多线程高效得多的并发模型,是无序的,为了完成某个任务,在执行的过程中,不同程序单元之间过程中无需通信协调,也能完成任务的方式,也就是说不相关的程序单元之间可以是异步的。如下图所示:



    当请求程序发送网络请求1并收到某个站点的响应后,开始执行程序中的下载程序,由于下载需要时间或者其他原因使处于阻塞状态,请求程序和下载程序是不相关的程序单元,所以请求程序发送下一个网络请求,也就是异步。

    • 微观上异步协程是一个任务一个任务的进行切换,切换条件一般就是IO操作;
    • 宏观上异步协程是多个任务一起在执行;

    注意:上面我们所讲的一切都是在单线程的条件下实现。

    请求库

    我们发送网络请求一定要用到请求库,在Python从多的HTTP客户端中,最常用的请求库莫过于requests、aiohttp、httpx。

    在不借助其他第三方库的情况下,requests只能发送同步请求;aiohttp只能发送异步请求;httpx既能发送同步请求,又能发送异步请求。

    接下来我们将简单讲解这三个库。

    requests库

    相信大家对requests库不陌生吧,requests库简单、易用,是python爬虫使用最多的库。

    在命令行中运行如下代码,即可完成requests库的安装:

    pip install requests
    

    使用requests发送网络请求非常简单,

    在本例中,我们使用get网络请求来获取百度首页的源代码,具体代码如下:

    import requests
    headers={
        'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36'
    }
    response=requests.get('https://baidu.com',headers=headers)
    response.encoding='utf-8'
    print(response.text)
    

    运行部分结果如下图:



    首先我们导入requests库,创建请求头,请求头中包含了User-Agent字段信息,也就是浏览器标识信息,如果不加这个,网站就可能禁止抓取,然后调用get()方法发送get请求,传入的参数为URL链接和请求头,这样简单的网络请求就完成了。

    这里我们返回打印输出的是百度的源代码,大家可以根据需求返回输出其他类型的数据。

    需要注意的是:

    百度源代码的head部分的编码为:utf-8,如下图所示:



    我们利用requests库的方法来查看默认的编码类型是什么,具体代码如下所示:

    import requests
    url = 'https://www.baidu.com'
    response = requests.get(url)
    print(response.encoding)  
    

    运行结果为:ISO-8859-1

    由于默认的编码类型不同,所以需要更改输出的编码类型,更改方式也很简单,只需要在返回数据前根据head部分的编码来添加以下代码即可:

    response.encoding='编码类型'
    

    除了使用get()方法实现get请求外,还可以使用post()、put()、delete()等方法来发送其他网络请求,在这里就不一一演示了,关于更多的requests网络请求库用法可以到官方参考文档进行查看,我们今天主要讲解可以发送异步请求的aiohttp库和httpx库。

    asyncio模块

    在讲解异步请求aiohttp库和httpx库请求前,我们需要先了解一下协程。

    协程是一种比线程更加轻量级的存在,让单线程跑出了并发的效果,对计算资源的利用率高,开销小的系统调度机制。

    Python中实现协程的模块有很多,我们主要来讲解asyncio模块,从asyncio模块中直接获取一个EventLoop的引用,把需要执行的协程放在EventLoop中执行,这就实现了异步协程。

    协程通过async语法进行声明为异步协程方法,await语法进行声明为异步协程可等待对象,是编写asyncio应用的推荐方式,具体示例代码如下:

    import asyncio
    import time
    async def function1():
        print('I am Superman!!!')
        await asyncio.sleep(3)
        print('function1')
    
    async def function2():
        print('I am Batman!!!')
        await asyncio.sleep(2)
        print('function2')
    
    async def function3():
        print('I am iron man!!!')
        await asyncio.sleep(4)
        print('function3')
        
    async def Main():
        tasks=[
            asyncio.create_task(function1()),
            asyncio.create_task(function2()),
            asyncio.create_task(function3()),
        ]
        await asyncio.wait(tasks)
        
    if __name__ == '__main__':
        t1=time.time()
        asyncio.run(Main())
        t2=time.time()
        print(t2-t1)
    

    运行结果为:

    I am Superman!!!
    I am Batman!!!
    I am iron man!!!
    function2
    function1
    function3
    4.0091118812561035
    

    首先我们用了async来声明三个功能差不多的方法,分别为function1,function2,function3,在方法中使用了await声明为可等待对象,并使用asyncio.sleep()方法使函数休眠一段时间。

    再使用async来声明Main()方法,通过调用asyncio.create_task()方法将方法封装成一个任务,并把这些任务存放在列表tasks中,这些任务会被自动调度执行;

    最后通过asyncio.run()运行协程程序。

    注意:当协程程序出现了同步操作的时候,异步协程就中断了。

    例如把上面的示例代码中的await asyncio.sleep()换成time.time(),运行结果为:

    I am Superman!!!
    function1
    I am Batman!!!
    function2
    I am iron man!!!
    function3
    9.014737844467163
    

    所以在协程程序中,尽量不使用同步操作。

    好了,asyncio模块我们讲解到这里,想要了解更多的可以进入asyncio官方文档进行查看。

    aiohttp库

    aiohttp是基于asyncio实现的HTTP框架,用于HTTP服务器和客户端。安装方法如下:

    pip install aiohttp
    

    aiohttp只能发送异步请求,示例代码如下所示:

    import aiohttp
    import asyncio
    headers={
        'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36'
    }
    async def Main():
        async with aiohttp.ClientSession() as session:
            async with session.get('https://www.baidu.com',headers=headers) as response:
                html = await response.text()
                print(html)
    loop=asyncio.get_event_loop()
    loop.run_until_complete(Main())
    

    运行结果和前面介绍的requests网络请求一样,如下图所示:



    大家可以对比requests网络请求发现,其实aiohttp.ClientSession() as session相当于将requests赋给session,也就是说session相当于requests,而发送网络请求、传入的参数、返回响应内容都和requests请求库大同小异,只是aiohttp请求库需要用async和await进行声明,然后调用asyncio.get_event_loop()方法进入事件循环,再调用loop.run_until_complete(Main())方法运行事件循环,直到Main方法运行结束。

    注意:在调用Main()方法时,不能使用下面这条语句:

    asyncio.run(Main())
    

    虽然会得到想要的响应,但会报:RuntimeError: Event loop is closed错误。

    我们还可以在返回的内容中指定解码方式或编码方式,例如:

    await response.text(encoding='utf-8')
    

    或者选择不编码,读取图像:

    await resp.read()
    

    好了aiohttp请求库我们学到这里,想要了解更多的可以到pypi官网进行学习。

    httpx请求库

    在前面我们简单地讲解了requests请求库和aiohttp请求库,requests只能发送同步请求,aiohttp只能发送异步请求,而httpx请求库既可以发送同步请求,又可以发送异步请求,而且比上面两个效率更高。

    安装方法如下:

    pip install httpx
    

    httpx请求库——同步请求

    使用httpx发送同步网络请求也很简单,与requests代码重合度99%,只需要把requests改成httpx即可正常运行。

    具体示例代码如下:

    import httpx
    headers={
        'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36'
    }
    response=httpx.get('https://www.baidu.com',headers=headers)
    print(response.text)
    

    运行结果如下图所示:



    注意:httpx使用的默认utf-8进行编码来解码响应。

    httpx请求库——同步请求高级用法

    当发送请求时,httpx必须为每个请求建立一个新连接(连接不会被重用),随着对主机的 请求数量增加,网络请求的效率就是变得很低。

    这时我们可以用Client实例来使用HTTP连接池,这样当我们主机发送多个请求时,Client将重用底层的TCP连接,而不是为重新创建每个请求。

    with块用法如下:

    with httpx.Client() as client:
        ...
    

    我们把Client作为上下文管理器,并使用with块,当执行完with语句时,程序会自动清理连接。

    当然我们可以使用.close()显式关闭连接池,用法如下:

    client = httpx.Client()
    try:
        ...
    finally:
        client.close()
    

    为了我们的代码更简洁,我们推荐使用with块写法,具体示例代码如下:

    import httpx
    headers={
        'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36'
    }
    with httpx.Client(headers=headers)as client:
        response=client.get('https://www.baidu.com')
        print(response.text)
    

    其中httpx.Client()as client相当于把httpx的功能传递给client,也就是说示例中的client相当于httpx,接着我们就可以使用client来调用get请求。

    注意:我们传递的参数可以放在httpx.Client()里面,也可以传递到get()方法里面。

    httpx请求库——异步请求

    要发送异步请求时,我们需要调用AsyncClient,具体示例代码如下:

    import httpx
    import asyncio
    headers={
        'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36'
    }
    async def function():
        async with httpx.AsyncClient()as client:
            response=await client.get('https://www.baidu.com',headers=headers)
            print(response.text)
    if __name__ == '__main__':
        loop = asyncio.get_event_loop()
        loop.run_until_complete(function())
    

    运行结果为:



    首先我们导入了httpx库和asyncio模块,使用async来声明function()方法并用来声明with块的客户端打开和关闭,用await来声明异步协程可等待对象response。接着我们调用asyncio.get_event_loop()方法进入事件循环,再调用loop.run_until_complete(function())方法运行事件循环,直到function运行结束。

    好了,httpx请求库讲解到这里,想要了解更多的可以到httpx官方文档进行学习,接下来我们正式开始爬取小说。

    实战演练

    接下来我们将使用requests请求库同步和httpx请求库的异步,两者结合爬取17k小说网里面的百万字小说,利用XPath来做相应的信息提取。

    Xpath小技巧

    在使用Xpath之前,我们先来介绍使用Xpath的小技巧。

    技巧一:快速获取与内容匹配的Xpath范围。

    我们可以将鼠标移动到我们想要获取到内容div的位置并右击选择copy,如下图所示:



    这样我们就可以成功获取到内容匹配的Xpath范围了。

    技巧二:快速获取Xpath范围匹配的内容。

    当我们写好Xpath匹配的范围后,可以通过Chrome浏览器的小插件Xpath Helper,该插件的安装方式很简单,在浏览器应用商店中搜索Xpath Helper,点击添加即可,如下图所示:



    使用方法也很简单,如下图所示:



    首先我们点击刚刚添加的插件,然后把已经写好的Xpath范围写到上图2的方框里面,接着Xpath匹配的内容将出现在上图3方框里面,接着被匹配内容的背景色全部变成了金色,那么我们匹配内容就一目了然了。

    这样我们就不需要每写一个Xpath范围就运行一次程序查看匹配内容,大大提高了我们效率。

    获取小说章节名和链接

    首先我们选取爬取的目标小说,并打开开发者工具,如下图所示:




    我们通过上图可以发现,<div class="Main List"存放着我们所有小说章节名,点击该章节就可以跳转到对应的章节页面,所以可以使用Xpath来通过这个div来获取到我们想要的章节名和URL链接。

    由于我们获取的章节名和URL链接的网络请求只有一个,直接使用requests请求库发送同步请求,主要代码如下所示:

    async def get_link(url):
        response=requests.get(url)
        response.encoding='utf-8'
        Xpath=parsel.Selector(response.text)
        dd=Xpath.xpath('/html/body/div[5]')
        for a in dd:
            #获取每章节的url链接
            links=a.xpath('./dl/dd/a/@href').extract()
            linklist=['https://www.17k.com'+link for link in links]
            #获取每章节的名字
            names=a.xpath('./dl/dd/a/span/text()').extract()
            namelist=[name.replace('\n','').replace('\t','') for name in names]
            #将名字和url链接合并成一个元组
            name_link_list=zip(namelist,linklist)
    

    首先我们用async声明定义的get_text()方法使用requests库发送get请求并把解码方式改成'utf-8',接着使用parsel.Selector()方法将文本构成Xpath解析对象,最后我们将获取到的URL链接和章节名合并成一个元组。

    获取到URL链接和章节名后,需要构造一个task任务列表来作为异步协程的可等待对象,具体代码如下所示:

    task=[]
    for name,link in name_link_list:
        task.append(get_text(name,link))
    await asyncio.wait(task)
    

    我们创建了一个空列表,用来存放get_text()方法,并使用await调用asyncio.wait()方法保存创建的task任务。

    获取每章节的小说内容

    由于需要发送很多个章节的网络请求,所以我们采用httpx请求库来发送异步请求。

    主要代码如下所示:

    async def get_text(name,link):
        async with httpx.AsyncClient() as client:
            response=await client.get(link)
            html=etree.HTML(response.text)
            text=html.xpath('//*[@id="readArea"]/div[1]/div[2]/p/text()')
            await save_data(name,text)
    

    首先我们将上一步的获取到的章节名和URL链接传递到用async声明定义的get_text()方法,使用with块调用httpx.AsyncClient()方法,并使用await来声明client.get()是可等待对象,然后使用etree模块来构造一个XPath解析对象并自动修正HTML文本,将获取到的小说内容和章节名传入到自定义方法save_data中。

    保存小说内容到text文本中

    好了,我们已经把章节名和小说内容获取下来了,接下来就要把内容保存在text文本中,具体代码如下所示:

    async def save_data(name,text):
        f=open(f'小说/{name}.txt','w',encoding='utf-8')
        for texts in text:
            f.write(texts)
            f.write('\n')
            print(f'正在爬取{name}')
    

    老规矩,首先用async来声明save_data()协程方法save_data(),然后使用open()方法,将text文本文件打开并调用write()方法把小说内容写入文本中。

    最后调用asyncio.get_event_loop()方法进入事件循环,再调用loop.run_until_complete(get_link())方法运行事件循环,直到function运行结束。具体代码如下所示:

    url='https://www.17k.com/list/2536069.html'
    loop = asyncio.get_event_loop()
    loop.run_until_complete(get_link(url))
    

    结果展示


    image

    好了,异步爬虫爬取小说就讲解到这里了,感谢观看!!!

    相关文章

      网友评论

        本文标题:Python爬虫——异步爬虫,两百四十多万字,六百章的小说20秒

        本文链接:https://www.haomeiwen.com/subject/dmfrwltx.html