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))

爬虫程序运行速度极快!

posted @ 2021-05-04 15:59  王陸  阅读(1146)  评论(1编辑  收藏  举报