zoukankan      html  css  js  c++  java
  • Python网络爬虫 第四章 多线程+异步协程

    一、多线程抓取北京新发地菜价

    多线程、多进程和线程池等的概念,我单独成章了,算到Python基础知识里面,https://www.cnblogs.com/wkfvawl/p/14729542.html

    这里就直接开启练习,抓取菜价其实在第二章已经讲过了,那时候用的是bs4解析的网页,这里使用xpath配合多线程。

    注意到新发地网站菜价表格网页的url是按照序号递增的,像第一页是

    http://www.xinfadi.com.cn/marketanalysis/0/list/1.shtml

    第二页是

    http://www.xinfadi.com.cn/marketanalysis/0/list/2.shtml

    这样,只需要遍历构造url即可得到所有需要的网页链接,但如果是单线程一个个的执行必然效率会很低,那就可以试一试多线程。

    使用谷歌浏览器F12的功能,直接获取到表格的xpath。

    # 1. 如何提取单个页面的数据
    # 2. 上线程池,多个页面同时抓取
    import requests
    from lxml import etree
    import csv
    from concurrent.futures import ThreadPoolExecutor
    
    f = open("data.csv", mode="w", encoding="utf-8")
    csvwriter = csv.writer(f)
    
    
    def download_one_page(url):
        # 拿到页面源代码
        resp = requests.get(url)
        html = etree.HTML(resp.text)
        table = html.xpath("/html/body/div[2]/div[4]/div[1]/table")[0]
        # 去掉表头 下面两种方法都想
        # trs = table.xpath("./tr")[1:] # 从第1个开始 去掉第0个表头
        trs = table.xpath("./tr[position()>1]") # 位置大于1
        # 拿到每个tr
        for tr in trs:
            txt = tr.xpath("./td/text()") # tr中找td td中找文本
            # 对数据做简单的处理: \  / 去掉
            txt = (item.replace("\", "").replace("/", "") for item in txt)
            # 把数据存放在文件中
            csvwriter.writerow(txt)
        print(url, "提取完毕!")
    
    
    if __name__ == '__main__':
        # for i in range(1, 14870):  # 效率及其低下
        #     download_one_page(f"http://www.xinfadi.com.cn/marketanalysis/0/list/{i}.shtml")
    
        # 创建线程池 50个线程
        with ThreadPoolExecutor(50) as t:
            for i in range(1, 200):  # 199 * 20 = 3980
                # 把下载任务提交给线程池
                t.submit(download_one_page, f"http://www.xinfadi.com.cn/marketanalysis/0/list/{i}.shtml")
    
        print("全部下载完毕!")

    二、协程

    协程是并发编程里面很重要的概念,感觉如果要真正弄明白,可能需要完完整整写一章博客,这里就先简单介绍一些基本概念和应用。

    协程能够更加⾼效的利⽤CPU,其实, 我们能够⾼效的利⽤多线程来完成爬⾍其实已经很6了。但是,从某种⻆度讲, 线程的执⾏效率真的就⽆敌了么? 我们真的充分的利⽤CPU资源了么? ⾮也~ ⽐如, 我们来看下⾯这个例⼦。我们单独的⽤⼀个线程来完成某⼀个操作,看看它的效率是否真的能把CPU完全利⽤起来。

    import time
    def func():
     print("我爱黎明")
     time.sleep(3)
     print("我真的爱黎明")
    func()

    各位请看,在该程序中, 我们的func()实际在执⾏的时候⾄少需要3秒的时间来完成操作,中间的三秒钟需要让我当前的线程处于阻塞状态。阻塞状态的线程 CPU是不会来执⾏的,那么此时cpu很可能会切换到其他程序上去执⾏。此时, 对于你来说, CPU其实并没有为你⼯作(在这三秒内), 那么我们能不能通过某种⼿段, 让CPU⼀直为我⽽⼯作,尽量的不要去管其他⼈。

    我们要知道CPU⼀般抛开执⾏周期不谈,如果⼀个线程遇到了IO操作, CPU就会⾃动的切换到其他线程进⾏执⾏. 那么, 如果我想办法让我的线程遇到了IO操作就挂起, 留下的都是运算操作. 那CPU是不是就会⻓时间的来照顾我~.
    以此为⽬的, 伟⼤的程序员就发明了⼀个新的执⾏过程. 当线程中遇到了IO操作的时候, 将线程中的任务进⾏切换, 切换成⾮ IO操作. 等原来的IO执⾏完了. 再恢复回原来的任务中。

    这里来看一个协程程序

    import asyncio
    import time
    
    async def func1():
        print("你好啊, 我叫test1")
        time.sleep(3)  # 当程序出现了同步操作的时候. 异步就中断了
        print("你好啊, 我叫test1")
    
    
    async def func2():
        print("你好啊, 我叫test2")
        time.sleep(2)
        print("你好啊, 我叫test2")
    
    
    async def func3():
        print("你好啊, 我叫test3")
        time.sleep(4)
        print("你好啊, 我叫test3")
    
    
    if __name__ == '__main__':
        f1 = func1()
        f2 = func2()
        f3 = func3()
        # 任务列表
        tasks = [
            f1, f2, f3
        ]
        t1 = time.time()
        # 一次性启动多个任务(协程)
        asyncio.run(asyncio.wait(tasks))
        t2 = time.time()
        print(t2 - t1)

     运行的结果并没有如同协程定义那样,产生异步效果,反而是同步的?这是因为里面的time.sleep()是同步操作,导致异步中断了,正确的写法应该是这样:

    import asyncio
    import time
    
    async def func1():
        print("你好啊, 我叫test1")
        await asyncio.sleep(3)  # 异步操作的代码 await挂起
        print("你好啊, 我叫test1")
    
    async def func2():
        print("你好啊, 我叫test2")
        await asyncio.sleep(2)
        print("你好啊, 我叫test2")
    
    async def func3():
        print("你好啊, 我叫test3")
        await asyncio.sleep(4)
        print("你好啊, 我叫test3")
    
    async def main():
        # 第一种写法
        # f1 = func1()
        # await f1  # 一般await挂起操作放在协程对象前面
        # 第二种写法(推荐)
        # tasks = [
        #     func1(),
        #     func2(),
        #     func3()
        # ]
        tasks = [
            asyncio.create_task(func1()),  # py3.8以后加上asyncio.create_task()
            asyncio.create_task(func2()),
            asyncio.create_task(func3())
        ]
        await asyncio.wait(tasks)
    
    
    if __name__ == '__main__':
        t1 = time.time()
        # 一次性启动多个任务(协程)
        asyncio.run(main())
        t2 = time.time()
        print(t2 - t1)

     从程序运行时间上来看利用异步协程直接从9秒减少到了4秒。这里需要asyncio的支持。

    关于asyncio的介绍参考https://www.liaoxuefeng.com/wiki/1016959663602400/1017970488768640

    await关键词。异步io的关键在于,await io操作,此时,当前携程就会被挂起,时间循环转而执行其他携程,但是要注意前面这句话,并不是说所有携程里的await都会导致当前携程的挂起,要看await后面跟的是什么,如果跟的是我们定义的携程,则会执行这个携程,如果是asyncio模块制作者定义的固有携程,比如模拟io操作的asyncio.sleep,以及io操作,比如网络io:asyncio.open_connection这些,才会挂起当前携程。

    三、aiohttp模块应用

    前面我们使用asyncio来实现了异步协程,那我们该如何将异步协程应用到爬虫上呢?其实爬虫在连接到要爬取的网页上的过程,也是一个类似IO的过程,这里介绍一下aiohttp,是一个用于asyncio和Python的异步HTTP客户端/服务器。

    以第二章讲过的唯美壁纸网站为例。之前同步时候用的requests ,换成了异步操作的aiohttp。

    import asyncio
    import aiohttp
    
    urls = [
        "http://kr.shanghai-jiuxin.com/file/2020/1031/191468637cab2f0206f7d1d9b175ac81.jpg",
        "http://kr.shanghai-jiuxin.com/file/2020/1031/563337d07af599a9ea64e620729f367e.jpg",
        "http://kr.shanghai-jiuxin.com/file/2020/1031/774218be86d832f359637ab120eba52d.jpg"
    ]
    
    async def aiodownload(url):
        # 发送请求.
        # 得到图片内容
        # 保存到文件
        name = url.rsplit("/", 1)[1]  # 从右边切, 切一次. 得到[1]位置的内容
        # 加with 上下文管理器
        # s = aiohttp.ClientSession() <==> requests.session()
        async with aiohttp.ClientSession() as session:  # requests
            async with session.get(url) as resp:  # resp = requests.get()
                # 请求回来了. 写入文件
                # 可以自己去学习一个模块, aiofiles
                with open(name, mode="wb") as f:  # 创建文件
                    f.write(await resp.content.read())  # 读取内容是异步的. 需要await挂起, resp.text()
    
        print(name, "搞定")
    
    async def main():
        # tasks列表
        tasks = []
        for url in urls:
            tasks.append(aiodownload(url))
        await asyncio.wait(tasks)
    
    if __name__ == '__main__':
        asyncio.run(main())

    这个程序还有待改进空间的,创建文件写文件也是一个IO操作,也是可以异步的,要引入aiofiles这个后面会讲。

    四、利用协程下载小说

    这次我们下载百度小说上的《西游记》。http://dushu.baidu.com/pc/detail?gid=4306063500

     F12抓包,找到了每一章节的名称和cid

    http://dushu.baidu.com/api/pc/getCatalog?data={"book_id":"4306063500"}

    经历了之前的实践,是不是感觉这次的url优点奇怪?date后面是一个json?

    接着为了获取每个章节里面的内容,点开一章,发现内容存在于http://dushu.baidu.com/api/pc/getChapterContent?data={"book_id":"4306063500","cid":"4306063500|11348571","need_bookinfo":1}中

    通过更换cid我们就能很轻松的获取到其他章节的内容了。

     在编写程序之前,先要清楚我们需要做什么工作?

    其实这是一个同步异步相结合的工作

    • 1. 同步操作: 访问getCatalog 拿到所有章节的cid和名称
    • 2. 异步操作: 访问getChapterContent 下载所有的文章内容
    import requests
    import asyncio
    import aiohttp
    import aiofiles
    import json
    
    async def aiodownload(cid, b_id, title):
        data = {
            "book_id": b_id,
            "cid": f"{b_id}|{cid}",
            "need_bookinfo": 1
        }
        # 转成json
        data = json.dumps(data)
        url = f"http://dushu.baidu.com/api/pc/getChapterContent?data={data}"
    
        async with aiohttp.ClientSession() as session:
            async with session.get(url) as resp:
                dic = await resp.json()
    
                async with aiofiles.open(title, mode="w", encoding="utf-8") as f:
                    await f.write(dic['data']['novel']['content'])  # 把小说内容写出
    
    
    async def getCatalog(url):
        resp = requests.get(url)
        # 取json
        dic = resp.json()
        tasks = []
        for item in dic['data']['novel']['items']:  # item就是对应每一个章节的名称和cid
            title = './novel/' + item['title'] + '.txt'
            cid = item['cid']
            # 准备异步任务
            tasks.append(aiodownload(cid, b_id, title))
        await asyncio.wait(tasks)
    
    
    if __name__ == '__main__':
        b_id = "4306063500"
        url = 'http://dushu.baidu.com/api/pc/getCatalog?data={"book_id":"' + b_id + '"}'
        asyncio.run(getCatalog(url))

    爬虫程序运行速度极快!

    作者:王陸

    -------------------------------------------

    个性签名:罔谈彼短,靡持己长。做一个谦逊爱学的人!

    本站使用「署名 4.0 国际」创作共享协议,转载请在文章明显位置注明作者及出处。鉴于博主处于考研复习期间,有什么问题请在评论区中提出,博主尽可能当天回复,加微信好友请注明原因

  • 相关阅读:
    laravel MethodNotAllowedHttpException错误一个原因
    laravel查看执行sql的
    二维,多维数组排序array_multisort()函数的使用
    REMOTE_ADDR,HTTP_CLIENT_IP,HTTP_X_FORWARDED_FOR获取客户端IP
    学习正则笔记
    关于apidoc文档生成不了的一个原因
    laravel 表单验证 Exists 规则的基本使用方法
    laravel 500错误的一个解决办法
    关于laravel 用paginate()取值取不到的问题
    C语言寒假大作战02
  • 原文地址:https://www.cnblogs.com/wkfvawl/p/14729647.html
Copyright © 2011-2022 走看看