「Python」爬虫-6.爬虫效率的提高

简介: > 本文主要介绍如何提高爬虫效率的问题,主要涉及到的知识为多线程、线程池、进程池以及协程等。

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第19天, 点击查看活动详情

本文主要介绍如何提高爬虫效率的问题,主要涉及到的知识为多线程、线程池、进程池以及协程等。

关于爬虫相关,欢迎先阅读一下我的前几篇文章😶‍🌫️😶‍🌫️😶‍🌫️:

「Python」爬虫-1.入门知识简介 - 掘金 (juejin.cn)

「Python」爬虫-2.xpath解析和cookie,session - 掘金 (juejin.cn)

「Python」爬虫-3.防盗链处理 - 掘金 (juejin.cn)

「Python」爬虫-4.selenium的使用 - 掘金 (juejin.cn)

「Python」爬虫-5.m3u8(视频)文件的处理 - 掘金 (juejin.cn)

参考文章:

进程和线程、协程的区别 - RunningPower - 博客园 (cnblogs.com)


首先对线程,进程,协程做一个简单的区分吧:

进程资源单位,每一个进程至少要有一个线程,每个进程都有自己的独立内存空间,不同进程通过进程间通信来通信。

线程执行单位,启动每一个程序默认都会有一个主线程。线程间通信主要通过共享内存,上下文切换很快,资源开销较少,但相比进程不够稳定容易丢失数据。

协程是一种用户态的轻量级线程, 协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。


了解了进程、线程、协程之间的区别之后,我们就可以思考如何用这些东西来提高爬虫的效率呢?

爬虫效率的提高

多线程

要体现多线程的特点就必须得拿单线程来做一个比较,这样才能凸显不同~

单线程运行举例:

 def func():
     for i in range(5):
         print("func", i)


 if __name__ == '__main__':
     func()
     for i in range(5):
         print("main", i)

运行结果如下:

# 单线程演示案例
result:
func 0
func 1
func 2
func 3
func 4
main 0
main 1
main 2
main 3
main 4

可以注意到在单线程的情况下,程序是先打印fun 0 - 4, 再打印main 0 - 4。

下面再举一个多线程的🌰:

需要实例化一个Thread类 Thread(target=func()) target接收的就是任务(/函数),通过.start()方法就可以启动多线程了。

代码提供两种方式:

# 多线程(两种方法)
# 方法一:
 from threading import Thread

 def func():
     for i in range(1000):
         print("func ", i)

 if __name__ == '__main__':
     t = Thread(target=func())  # 创建线程并给线程安排任务
     t.start()  # 多线程状态为可以开始工作状态,具体的执行时间由CPU决定  
     for i in range(1000):
         print("main ", i)
# two
class MyThread(Thread):
    def run(self): # 固定的  -> 当线程被执行的时候,被执行的就是run()
        for i in range(1000):
            print("子线程 ", i)


if __name__ == '__main__':
    t = MyThread()
    # t.run()  #方法调用 --》单线程
    t.start()  #开启线程
    for i in range(1000):
        print("主线程 ", i)

运行结果

image.png

子线程和主线程有时候会同时执行,这就是多线程吧

线程创建之后只是代表处于能够工作的状态,并不代表立即执行,具体执行的时间需要看CPU

感觉线程执行的顺序就是杂乱无章的。


接下来分享一下多进程:

多进程

进程的使用:Process(target=func())

先举一个🌰来感受一下多进程的执行顺序:

from multiprocessing import Process

def func():
    for i in range(1000):
        print("子进程 ", i)

if __name__ == '__main__':
    p = Process(target=func())
    p.start()
    for i in range(1000):
        print("主进程 ", i)

运行结果:

image.png

从结果中可以发出,所有的子进程按照顺序执行之后。就开始打印主进程0-999。进程打印的有序也表明线程是最小的执行单位。

开启多线程打印的时候,出现的数字并不是有序的。

线程池&进程池

在python中一般使用以下方法创建线程池/进程池:

with ThreadPoolExecutor(50) as t:
     t.submit(fn, name=f"线程{i}")

具体代码:

# 线程池:一次性开辟一些线程,我们用户直接给线程池提交任务,线程任务的调度交给线程池来完成
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor

def fn(name):
    for i in range(1000):
        print(name,i)

if __name__ == '__main__':
    # 创建线程池
    with ThreadPoolExecutor(50) as t:
        for i in range(100):
            t.submit(fn, name=f"线程{i}")
    # 等待线程池中的任务全部执行完毕,才继续执行(守护)
    print(123)

image.png

进程池的创建方法类似。


协程

协程:当程序遇见IO操作的时候,可以选择性的切换到其他任务上。

在微观上是一个任务一个任务的进行切换,切换条件一般就是IO操作

在宏观上,我们能看到的其实就是多个任务一起在执行,
多任务异步操作(就像你自己一边洗脚一边看剧一样~,时间管理带师(bushi

线程阻塞的一些案例:

🌰1:

time.sleep(30)    # 让当前线程处于阻塞状态,CPU是不为我工作的
# input()    程序也是处于阻塞状态
# requests.get(xxxxxx) 在网络请求返回数据之前,程序也是处于阻塞状态
# 一般情况下,当程序处于IO操作的时候,线程都会处于阻塞状态
# for example: 边洗脚边按摩
import asyncio
import time

async def func():
     print("hahha")

if __name__ == "__main__":
     g = func()  # 此时的函数是异步协程函数,此时函数执行得到的是一个协程对象
     asyncio.run(g) # 协程程序运行需要asyncio模块的支持

输出结果:

root@VM-12-2-ubuntu:~/WorkSpace# python test.py
hahha

🌰2:

 async def func1():
     print("hello,my name id hanmeimei")
     # time.sleep(3)  # 当程序出现了同步操作的时候,异步就中断了
     await asyncio.sleep(3)  # 异步操作的代码
     print("hello,my name id hanmeimei")


 async def func2():
     print("hello,my name id wahahha")
     # time.sleep(2)
     await asyncio.sleep(2)  # 异步操作的代码
     print("hello,my name id wahahha")


 async def func3():
     print("hello,my name id hhhhhhhc")
     # time.sleep(4)
     await asyncio.sleep(4)  # 异步操作的代码
     print("hello,my name id hhhhhhhc")


 if __name__ == "__main__":
     f1 = func1()
     f2 = func2()
     f3 = func3()
     task = [
         f1, f2, f3
     ]
     t1 = time.time()
     asyncio.run(asyncio.wait(task))
     t2 = time.time()
     print(t2 - t1)

运行结果:
image.png

注意到执行await asyncio.sleep(4)后,主程序就会调用其他函数了。成功实现了异步操作。(边洗脚边按摩bushi )

下面的代码看起来更为规范~

async def func1():
    print("hello,my name id hanmeimei")
    await asyncio.sleep(3)
    print("hello,my name id hanmeimei")


async def func2():
    print("hello,my name id wahahha")
    await asyncio.sleep(2)
    print("hello,my name id wahahha")


async def func3():
    print("hello,my name id hhhhhhhc")
    await asyncio.sleep(4)
    print("hello,my name id hhhhhhhc")


async def main():
    # 第一种写法
    # f1 = func1()
    # await f1 # 一般await挂起操作放在协程对象前面
    # 第二种写法(推荐)
    tasks = [
        func1(),   # py3.8以后加上asyncio.create_task()
        func2(),
        func3()
    ]
    await asyncio.wait(tasks)


if __name__ == "__main__":
    t1 = time.time()
    asyncio.run(main())
    t2 = time.time()
    print(t2 - t1)

再举一个模拟下载的🌰吧,更加形象啦:

async def download(url):
    print("准备开始下载")
    await asyncio.sleep(2) # 网络请求
    print("下载完成")

async def main():
    urls = [
        "http://www.baidu.com",
        "http://www.bilibili.com",
        "http://www.163.com"
    ]
    tasks = []
    for url in urls:
        d = download(url)
        tasks.append(d)

    await asyncio.wait(tasks)

if __name__ == '__main__':
    asyncio.run(main())
# requests.get()  同步的代码 => 异步操作aiohttp

import asyncio
import aiohttp

urls = [
    "http://kr.shanghai-jiuxin.com/file/2020/1031/191468637cab2f0206f7d1d9b175ac81.jpg",
    "http://i1.shaodiyejin.com/uploads/tu/201704/9999/fd3ad7b47d.jpg",
    "http://kr.shanghai-jiuxin.com/file/2021/1022/ef72bc5f337ca82f9d36eca2372683b3.jpg"
]


async def aiodownload(url):
    name = url.rsplit("/", 1)[1]  # 从右边切,切一次,得到[1]位置的内容 fd3ad7b47d.jpg
    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, "okk")
            # resp.content.read() ==> resp.text()
    # s = aiphttp.ClientSession <==> requests
    # requests.get()  .post()
    # s.get()  .post()
    # 发送请求
    # 保存图片内容平
    # 保存为文件


async def main():
    tasks = []
    for url in urls:
        tasks.append(aiodownload(url))
    await asyncio.wait(tasks)


if __name__ == '__main__':
    asyncio.run(main())

最后再来利用协程实现爬取异步小说的案例

案例:协程爬取一部小说

# 协程-爬取一部小说
# "https://dushu.baidu.com/api/pc/getCatalog?data={"book_id":"4305592439"}  => 所有章节的内容(名称,cid)
# 章节内部的内容
# https://dushu.baidu.com/api/pc/getChapterContent?data={"book_id":"4305592439","cid":"4305592439|10525845","need_bookinfo":"1"}

import requests
import os
import aiohttp
import aiofiles
import asyncio
import json

"""
1.同步操作:访问getCatalog 拿到所有章节的cid和名称
2.异步操作:访问getChapterContent 
"""


async def aiodownload(cid, b_id, title):
    data = {
        "book_id": b_id,
        "cid": f"{b_id}|{cid}",
        "need_bookinfo": 1
    }
    data = json.dumps(data)
    url = f"https://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:
                os.chdir(f"D:/python/workSpace/study_demo/base/book_demos/demo/novel")
                await f.write(dic['data']['novel']['content'])


async def getCatalog(url):
    resp = requests.get(url)
    dic = resp.json()
    tasks = []
    for item in dic['data']['novel']['items']:  # item 就是对应每一个章节的名称和cid
        title = item['title']
        cid = item['cid']
        # 准备异步任务
        tasks.append(aiodownload(cid, b_id, title))
    await asyncio.wait(tasks)


if __name__ == '__main__':
    b_id = "4305592439"
    url = 'https://dushu.baidu.com/api/pc/getCatalog?data={"book_id":"' + b_id + '"}'
    loop = asyncio.get_event_loop()
    loop.run_until_complete(getCatalog(url))
    # asyncio.run(getCatalog(url))

补充知识-json:

json.dumps将一个 Python数据结构转换为JSON

image.png
json.dump()和json.dumps()的区别

json.dumps() 是把python对象转换成json对象的一个过程,生成的是字符串。
json.dump() 是把python对象转换成json对象生成一个fp的文件流,和文件相关。

import json

x = {'name':'你猜','age':19,'city':'四川'}
#用dumps将python编码成json字符串
y = json.dumps(x)
print(y)
i = json.dumps(x,separators=(',',':'))
print(i)
"""
输出结果
{"name": "\u4f60\u731c", "age": 19, "city": "\u56db\u5ddd"}
{"name":"\u4f60\u731c","age":19,"city":"\u56db\u5ddd"}
"""
关于线程,进程,协程就写到这儿啦,如果对你有帮助的吧,就留下你的足迹吧~⛷️⛷️⛷️

---

往期好文推荐🪶

「MongoDB」Win10版安装教程

「Python」数字推盘游戏

「Python」sklearn第一弹-标准化和非线性转化

「Python」turtle绘制图形🎈

「Python」Pandas-DataFrame的相关操作二

相关文章
|
28天前
|
数据采集 存储 XML
Python爬虫定义入门知识
Python爬虫是用于自动化抓取互联网数据的程序。其基本概念包括爬虫、请求、响应和解析。常用库有Requests、BeautifulSoup、Scrapy和Selenium。工作流程包括发送请求、接收响应、解析数据和存储数据。注意事项包括遵守Robots协议、避免过度请求、处理异常和确保数据合法性。Python爬虫强大而灵活,但使用时需遵守法律法规。
|
11天前
|
数据采集 存储 XML
Python爬虫:深入探索1688关键词接口获取之道
在数字化经济中,数据尤其在电商领域的价值日益凸显。1688作为中国领先的B2B平台,其关键词接口对商家至关重要。本文介绍如何通过Python爬虫技术,合法合规地获取1688关键词接口,助力商家洞察市场趋势,优化营销策略。
|
29天前
|
数据采集 缓存 定位技术
网络延迟对Python爬虫速度的影响分析
网络延迟对Python爬虫速度的影响分析
|
1月前
|
数据采集 Web App开发 监控
高效爬取B站评论:Python爬虫的最佳实践
高效爬取B站评论:Python爬虫的最佳实践
|
1月前
|
数据采集 存储 JSON
Python网络爬虫:Scrapy框架的实战应用与技巧分享
【10月更文挑战第27天】本文介绍了Python网络爬虫Scrapy框架的实战应用与技巧。首先讲解了如何创建Scrapy项目、定义爬虫、处理JSON响应、设置User-Agent和代理,以及存储爬取的数据。通过具体示例,帮助读者掌握Scrapy的核心功能和使用方法,提升数据采集效率。
90 6
|
1天前
|
数据采集 存储 API
利用Python爬虫获取1688关键词接口全攻略
本文介绍如何使用Python爬虫技术合法合规地获取1688关键词接口数据,包括环境准备、注册1688开发者账号、获取Access Token、构建请求URL、发送API请求、解析HTML及数据处理存储等步骤,强调遵守法律法规和合理使用爬虫技术的重要性。
|
8天前
|
数据采集 JSON 开发者
Python爬虫京东商品详情数据接口
京东商品详情数据接口(JD.item_get)提供商品标题、价格、品牌、规格、图片等详细信息,适用于电商数据分析、竞品分析等。开发者需先注册账号、创建应用并申请接口权限,使用时需遵循相关规则,注意数据更新频率和错误处理。示例代码展示了如何通过 Python 调用此接口并处理返回的 JSON 数据。
|
13天前
|
XML 数据采集 数据格式
Python 爬虫必备杀器,xpath 解析 HTML
【11月更文挑战第17天】XPath 是一种用于在 XML 和 HTML 文档中定位节点的语言,通过路径表达式选取节点或节点集。它不仅适用于 XML,也广泛应用于 HTML 解析。基本语法包括标签名、属性、层级关系等的选择,如 `//p` 选择所有段落标签,`//a[@href=&#39;example.com&#39;]` 选择特定链接。在 Python 中,常用 lxml 库结合 XPath 进行网页数据抓取,支持高效解析与复杂信息提取。高级技巧涵盖轴的使用和函数应用,如 `contains()` 用于模糊匹配。
|
15天前
|
数据采集 XML 存储
构建高效的Python网络爬虫:从入门到实践
本文旨在通过深入浅出的方式,引导读者从零开始构建一个高效的Python网络爬虫。我们将探索爬虫的基本原理、核心组件以及如何利用Python的强大库进行数据抓取和处理。文章不仅提供理论指导,还结合实战案例,让读者能够快速掌握爬虫技术,并应用于实际项目中。无论你是编程新手还是有一定基础的开发者,都能在这篇文章中找到有价值的内容。
|
14天前
|
数据采集 JavaScript 前端开发
Python爬虫能处理动态加载的内容吗?
Python爬虫可处理动态加载内容,主要方法包括:使用Selenium模拟浏览器行为;分析网络请求,直接请求API获取数据;利用Pyppeteer控制无头Chrome。这些方法各有优势,适用于不同场景。