全部文章

爬虫案例-对比单线程、多线程、多进程、异步编程的效率

我采用了单线程,多线程,多进程,异步编程来爬取菜鸟教程100例题

先说结论:

  • 单线程单进程耗时:0:00:21.835015
  • 多线程耗时:0:00:07.817000
  • 多进程耗时:0:00:05.293997
  • 异步编程:0:00:02.583346!!!!!!!!!!!!!!!!!!!!!!!!

实践总结:爬虫最耗时的地方在于网络请求(占总耗时的90%),其次才是文件写入;

  • 数据对比

    • 网络请求时间:假设每个请求耗时 0.5秒,100个请求总耗时约 50秒。
    • 文件写入时间:即使写入 100次,每次耗时仅几毫秒,总耗时不足 1秒。
    • 并发处理能力:多线程或多进程能否有效利用系统资源,减少总体等待时间。

实测数据对比分析

模型耗时(秒)相对速度提升关键瓶颈
单线程 21.83 1x(基准) 完全串行,无并发
多线程(16线程) 7.81 ~2.8x GIL竞争、线程切换开销
多进程(16进程) 5.29 ~4.1x 进程创建、IPC开销
异步协程 2.58 ~8.5x 网络延迟(理论极限)
  • 异步逼近理论极限:耗时主要取决于最慢的单个请求响应时间,而非并发数量。

  • 多线程/进程开销:线程切换、GIL竞争、进程间通信等额外成本拖慢整体速度。


在I/O密集型任务中异步编程为何比多线程/多进程更快?

1. 资源开销差异

模型线程/进程数内存占用上下文切换成本
多进程 16进程 高(独立内存) 高(内核级切换)
多线程 16线程 中等(共享内存) 中等(内核级切换)
异步协程 1线程 + N协程 极低 极低(用户态切换)
  • 异步模型:单线程内通过协程管理成千上万的并发任务,协程切换由事件循环在用户态完成,无需内核介入。

  • 多线程/进程:每次切换需陷入内核态,保存寄存器、堆栈等状态,开销显著。


2. I/O密集型任务特性

网络爬虫的核心耗时在于 等待远程服务器响应(I/O等待),而非本地CPU计算。

  • 异步编程:在等待某个请求响应时,事件循环会立即切换到其他就绪任务,实现“零等待”并发。

  • 多线程/进程:尽管可以并行发起多个请求,但每个线程/进程仍需阻塞等待自己的I/O完成,无法充分利用等待时间。

    3. Python的GIL限制

    • 多线程:受限于GIL,同一时刻仅一个线程能执行Python字节码。
      虽然I/O操作会释放GIL,但线程调度和锁竞争仍会引入额外开销。

    • 多进程:绕过GIL,但进程间通信(IPC)和数据序列化(Pickle)成本高。

    • 异步编程:完全避开GIL,单线程不存在GIL锁,协程间无GIL竞争。

    • 进一步解释

      1. 异步编程确实绕过了 GIL
        协程在单线程内运行,无需像多线程那样争夺 GIL,这是异步性能优势的来源。

      2. 协程间仍可能发生资源竞争
        当多个协程操作共享资源(如全局变量、文件、数据库连接)时,仍需同步控制(如锁)来保证数据一致性。

      3. 异步锁机制的特殊性
        异步锁(如 asyncio.Lock)的切换成本极低,且不会阻塞事件循环,与多线程/多进程的锁有本质区别。


    4. 连接管理与复用

    • 异步编程:通过 aiohttp 的连接池复用TCP连接,减少握手开销。
      示例代码中,所有请求共享一个 ClientSession,自动复用连接。

    • 多线程/进程:每个线程/进程独立管理连接,频繁创建/销毁连接增加延迟。


    5. 任务调度效率

    模型任务调度方式适用场景
    多进程 操作系统进程调度 CPU密集型任务
    多线程 操作系统线程调度 I/O密集型轻量级并发
    异步协程 用户态事件循环(高效轮询) 高并发I/O密集型任务
      • 异步事件循环:基于 epoll(Linux)或 kqueue(MacOS)实现高效I/O就绪通知,精确触发任务切换。

      • 操作系统调度:线程/进程切换由内核统一管理,无法针对应用特性优化。


为何16核CPU未让多进程碾压异步?

    1. 网络延迟非CPU问题
      爬虫性能瓶颈在于远程服务器响应速度和网络传输速度,而非本地CPU计算。
      即使有16核,若每个请求需等待100ms响应,并行16个请求总耗时仍至少100ms。
      异步模型可同时发起数百请求,总耗时≈最慢请求的响应时间。

    2. 资源争用与调度成本

      • 多进程:16进程需分配独立内存,进程间通信(如序列化数据)消耗CPU。

      • 异步:单进程内协程共享内存,无序列化开销。

    3. 连接数限制
      多进程/线程受操作系统限制(如最大文件描述符数),异步模型可轻松管理上千连接。


代码层面的关键优化点

异步版本优势:

async with aiohttp.ClientSession() as session:  # 全局复用连接池
    tasks = [process_href(href, session) for href in hreflist]
    await asyncio.gather(*tasks)  # 一次性提交所有任务

多线程/进程版本缺陷:

# 多线程:每个线程独立创建连接
def process_href(href):
    response = requests.get(url)  # 无连接复用
    # ...

# 多进程:进程间无法共享连接池
with Pool(16) as p:
    p.map(process_href, urls)  # 每个进程独立发起请求

总结

  • 异步编程在I/O密集型任务中凭借极低的开销和高效的调度机制,能够实现 数倍于多线程/进程的吞吐量

  • 多进程适合CPU密集型任务(如数值计算),但I/O场景下资源消耗和调度成本成为瓶颈。

  • 多线程受限于GIL和内核调度,性能介于异步与多进程之间。


最终选择建议

场景推荐模型
高频HTTP API调用 异步(asyncio + aiohttp
文件批量下载(CPU空闲) 多进程(multiprocessing
简单脚本(低并发) 多线程(concurrent.futures

掌握异步编程后,您可轻松应对高并发爬虫、实时数据处理等场景,充分榨干硬件性能!

 内核态和用户态

通俗解释

想象你是一名餐厅服务员(事件循环),需要同时服务多桌客人(协程)。

  • 传统多线程(内核态切换):每服务一桌客人,你必须跑到经理(操作系统内核)办公室登记当前状态,再切换到下一桌。

  • 协程(用户态切换):你直接在餐厅内(用户态)记下当前桌的进度,直接转向下一桌,无需外部审批。

用户态切换省去了频繁“跑办公室”的步骤,效率自然更高。


技术细节拆解

1. 协程(Coroutine)

  • 本质:用户级轻量级线程,由程序员控制切换时机。

  • 特点

    • 协程切换无需操作系统介入。

    • 协程的上下文(如变量、执行位置)保存在用户空间内存中。

2. 事件循环(Event Loop)

  • 职责

    1. 监听I/O事件(如网络响应到达、文件读取完成)。

    2. 调度协程:当某个协程等待I/O时,挂起它并执行其他就绪协程。

工作流程

while True:
检查哪些协程的I/O已就绪 → 执行这些协程 → 重复

3. 用户态 vs 内核态

对比项用户态内核态
权限 低权限,只能访问受限资源 高权限,可操作硬件和核心资源
切换成本 低(无需CPU模式切换) 高(需保存/恢复寄存器、权限切换)
典型操作 协程切换、应用逻辑 线程切换、系统调用(如文件读写)

协程切换过程(用户态)

  1. 挂起当前协程

    • 保存当前协程的上下文(如程序计数器、局部变量)到内存。

  2. 选择下一个协程

    • 事件循环检查哪些协程的I/O已就绪。

  3. 恢复下一个协程

    • 从内存加载目标协程的上下文,继续执行。

全程无需内核参与,所有操作在应用程序内完成。


对比多线程切换(内核态)

  1. 触发线程切换

    • 时间片用完、I/O阻塞、主动让出。

  2. 陷入内核

    • CPU从用户态切换到内核态。

  3. 保存上下文

    • 内核保存当前线程的寄存器、堆栈等信息。

  4. 调度新线程

    • 内核选择下一个线程,加载其上下文。

  5. 返回用户态

    • CPU切回用户态,执行新线程。

每次切换涉及数十微秒开销,高并发下累积成本巨大。


性能对比示例

假设有 1000个并发网络请求,每个请求需等待 100ms 的I/O:

  • 多线程(1000线程)

    • 线程切换开销:假设每次切换 5μs

    • 总切换开销:1000线程 × 5μs = 5ms

    • 实际总耗时:100ms(受限于I/O) + 5ms ≈ 105ms

  • 异步协程(单线程)

    • 协程切换开销:每次 0.1μs(纯内存操作)

    • 总切换开销:1000协程 × 0.1μs = 0.1ms

    • 实际总耗时:100ms(受限于I/O) + 0.1ms ≈ 100.1ms


总结

  • “用户态切换”:协程的调度完全由应用程序控制,无需通过操作系统内核,减少权限切换和数据保存的开销。

  • 优势

    • 极低的切换成本(纳秒级 vs 微秒级)。

    • 支持超高并发(轻松管理数万协程)。

  • 适用场景:I/O密集型任务(如爬虫、API调用、实时聊天)。

理解这一点,就能明白为何异步编程在高并发I/O场景下性能碾压多线程/多进程。


 

关于多进程为何比多线程更快的解释

(1) 网络请求与 CPU 解析的混合负载

  • 多线程的优势场景:纯 I/O 密集型任务(如仅发送请求,无后续解析)。

  • 多进程的优势场景:CPU 密集型任务(如大量 HTML 解析或计算)。

  • 你的任务特性
    实际任务包含 网络请求(I/O) + HTML 解析(CPU) 的混合负载。多进程能更好地并行处理 解析阶段 的 CPU 计算,而多线程受限于 GIL,解析时需串行执行。

(2) 线程/进程数量的影响

  • 多线程(16线程)

    • 高并发请求可能触发服务器反爬限速,反而降低效率。

    • 线程切换开销在混合负载中显著增加。

  • 多进程(16进程)

    • 进程独立运行,解析阶段完全并行,不受 GIL 限制。

    • 进程数接近 CPU 核心数时,资源利用率更高(假设你的机器有 8 核以上)。

(3) 测试环境的网络波动

  • 多轮测试中网络延迟的随机性可能导致结果偏差,尤其是多线程对瞬时网络波动更敏感。


各版本耗时分析

(1) 单线程(21.8秒)

  • 耗时组成:100次网络请求(约15秒) + 100次HTML解析(约6.8秒)。

  • 瓶颈:完全串行,无并发优化。

(2) 多线程(7.8秒)

  • 耗时组成

    • 网络请求:约3秒(16线程并发,但受服务器反爬限制)。

    • HTML解析:约4.8秒(受 GIL 限制,实际为伪并行)。

  • 瓶颈:GIL 导致解析阶段无法充分利用多核。

(3) 多进程(5.3秒)

  • 耗时组成

    • 网络请求:约2.5秒(16进程独立请求,分散反爬检测)。

    • HTML解析:约2.8秒(多核并行,无 GIL 限制)。

  • 优势:CPU 解析阶段完全并行,抵消了进程创建的开销。


为何多进程反而更快?

(1) 解析阶段的 CPU 并行化

  • Beautiful Soup 解析:涉及字符串处理、DOM 树构建等 CPU 操作。

    • 多进程可同时利用多核加速解析,而多线程因 GIL 串行执行。

(2) 反爬策略的规避

  • 多进程的独立 IP 和请求头模拟更分散,降低了被服务器限速的概率。

(3) 测试机器的多核配置

  • 若你的机器有 8 核或更多,16进程可充分占用 CPU 资源,而多线程受限于 GIL 的串行解析。


验证实验

(1) 调整线程/进程数

  • 多线程:将线程数降至 4-8,观察是否因减少切换开销而提速。

  • 多进程:将进程数设为 CPU 核心数(如 8),避免过多进程导致调度开销。

(2) 分离网络与解析阶段

  • 纯网络耗时测试:仅发送请求,不解析 HTML,观察多线程是否显著优于多进程。

  • 纯解析耗时测试:使用本地缓存的 HTML 内容,测试多进程解析速度是否碾压多线程

import os
from concurrent.futures.process import ProcessPoolExecutor
from concurrent.futures.thread import ThreadPoolExecutor
from datetime import datetime
from bs4 import BeautifulSoup
def readHtml(path):
        # print(f"执行任务{path}")
        soup = BeautifulSoup(open(path,"r",encoding="utf-8"), "lxml")
        lis = soup.find("div", id="s-hotsearch-wrapper").find("ul", class_="s-hotsearch-content").find_all("li")
        list=[]
        for li in lis:
            url = li.find("a").get("href")
            title = li.find("a").find("span", class_="title-content-title").get_text()
            list.append([url,title])
        return list
if __name__ == '__main__':
    path = r"D:\learn\Python-卢战士优选\learncode\01FirstProject\Test\htmltest"
    chdir = os.listdir(path)
    paths = [os.path.join(path, file) for file in chdir]
    time1 = datetime.now()
    print("*" * 20, time1.strftime('%Y-%m-%d %H:%M:%S'), "*" * 20)
    # 单线程调用三次耗时记录:0:00:05.779819,0:00:04.695048,0:00:04.848575
    # for path in paths:
    #     readHtml(path)
    #多线程调用三次耗时记录:0:00:05.114409,0:00:05.216166,0:00:05.280045
    # 从结果来看,多线程对于处理cpu计算密集型任务时,几乎没有什么优势
    with ThreadPoolExecutor(max_workers=16) as executor:
        list(executor.map(readHtml, paths))#list# 确保所有任务完成
    #多进程调用三次耗时记录:0:00:00.056828,0:00:00.056402,0:00:00.060979
    # with ProcessPoolExecutor(max_workers=16) as executor:
    #     # list(executor.map(readHtml, paths))#list# 确保所有任务完成
    time2=datetime.now()
    print("*"*20,time2.strftime('%Y-%m-%d %H:%M:%S'),"*"*20)
    print(time2 - time1)

 


优化建议

(1) 混合并发模型

  • 分工协作(该方法已经验证,感觉没啥用)

    • 使用多线程处理网络请求(I/O 密集型)。

    • 使用多进程处理 HTML 解析(CPU 密集型)。

(2) 请求速率控制

添加随机延时,避免触发反爬

import random, time
time.sleep(random.uniform(0.1, 0.5))  # 在每次请求前暂停

总结

你的测试结果反映了 混合负载任务(I/O + CPU) 在特定环境下的表现:

  • 多进程 因绕过 GIL 并充分利用多核,在解析阶段优势明显,整体更快。

  • 多线程 受限于 GIL 的串行解析,未能完全发挥并发潜力。

  • 单线程 无并发优化,自然最慢。

要进一步提升性能,需根据任务特性设计更精细的并发策略,而非简单依赖单一模型。

 单线程单进程代码

# user-agent:Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36
'''
100题首页地址:https://www.runoob.com/python/python-100-examples.html
'''
import datetime
import re
import threading

import requests
from bs4 import BeautifulSoup
time1=datetime.datetime.now()
print("*"*20,time1.strftime('%Y-%m-%d %H:%M:%S'),"*"*20)
url="https://www.runoob.com/python/python-100-examples.html"
rootUrl="https://www.runoob.com"
# 根据首页地址获取题目列表及连接
header={#模拟浏览器范围,防止被反爬
    "user-agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36"
}
response = requests.get(url, headers=header)
# 防止乱码
response.encoding=response.apparent_encoding
# 将网页内容转为bs4对象
soup = BeautifulSoup(response.text, "lxml")
# 查找题目链接
uls = soup.find("div",id="content").find_all("ul")
hreflist=[]
for ul in uls:
    lis = ul.find_all("li")
    for li in lis:
        hreflist.append(li.find("a").get("href"))

# 创建列表存储每一页所有信息
pageList=[]
for href in hreflist:
    # 根据每个连接获取例题子元素
    topicUrl=rootUrl+href
    response = requests.get(topicUrl, headers=header)
    response.encoding=response.apparent_encoding
    soup = BeautifulSoup(response.text, "lxml")
    # 一、获取题目所有说明文本
    allP = soup.find("div", id="content").find_all("p")
    # 创建字典,暂存每一页的各个板块信息
    dic={}
    # 使用list暂存所有说明
    noteList=[]
    '''
    0题目:有四个数字:1、2、3、4,能组成多少个互不相同且无重复数字的三位数?各是多少?
    1程序分析:可填在百位、十位、个位的数字都是1、2、3、4。组成所有的排列后再去掉不满足条件的排列。
    2程序源代码:
    3以上实例输出结果为:
    '''
    for p in allP:
        if not p.find("a"):#排除“返回上一页”
            noteList.append(p.get_text())
    dic["noteList"]=noteList
    # 二、获取实例代码
    try:
        code = soup.find("div", id="content").find("div",class_="example")
        if code:
            dic["code"]=code.get_text()
        else:
            pre=soup.find("div", id="content").find("pre")
            dic["code"] = pre.get_text()
    except Exception as e:
        pre = soup.find("div", id="content").find("pre")
        print(pre)
        print(f"第{hreflist.index(href)+1}题获取示例代码出错了:{e}")
    # 三、获取执行结果
    try:
        result = soup.find("div", id="content").find("pre")
        if result:
            dic["result"]=result.get_text()
        else:
            imgSrc = soup.find("div", id="content").find("img",attrs={"src":re.compile(r"^//")}).attrs["src"]
            dic["result"]=f"执行结果为非文本数据:数据链接为:https:{imgSrc}"
    except Exception as e:
        print(f"第{hreflist.index(href)+1}题获取执行结果出错了:{e}")
        dic["result"] = "无执行结果!"
    pageList.append(dic)
    # 写入文件
def wirteInFile():
    with open("../Test/python100例题.txt","w+",encoding="utf-8") as f:
    # with open("../Test/python100例题.doc","w+",encoding="utf-8") as f:
        for dic in pageList:
            noteList= dic["noteList"]
            code= dic["code"]
            result= dic["result"]
            try:
                if len(noteList)==4:
                    f.write(f"{pageList.index(dic)+1}"+noteList[0]+"\n")
                    f.write(noteList[1]+"\n")
                    f.write(noteList[2]+"\n")
                    f.write(code+"\n")
                    f.write(noteList[3]+"\n")
                    f.write(result+"\n")
                    f.write("*"*50+"\n")
                else:
                    f.write(f"{pageList.index(dic)+1}"+noteList[0]+"\n")
                    f.write(noteList[1]+"\n")
                    f.write(code+"\n")
                    f.write(noteList[2]+"\n")
                    f.write(result+"\n")
                    f.write("*"*50+"\n")
            except Exception as e:
                print(f"{pageList.index(dic)+1}出错了:{e}")
    print("写入完成!!!")
wirteInFile()
time2=datetime.datetime.now()
print("结束时间","*"*20,time2.strftime('%Y-%m-%d %H:%M:%S'),"*"*20)
print(time2-time1)

多进程代码

import datetime
import random
import re
import time
from concurrent.futures import ProcessPoolExecutor

import requests
from bs4 import BeautifulSoup
from multiprocessing import Pool
time1=datetime.datetime.now()
print("*"*20,time1.strftime('%Y-%m-%d %H:%M:%S'),"*"*20)

# 单个耗时任务
def process_href(args):
    # 解包参数
    href, rootUrl ,header= args  # 解包参数
    # 后续代码保持不变
    topicUrl = rootUrl + href
    # 爬取并处理单个链接的逻辑
    # 根据每个连接获取例题子元素
    topicUrl = rootUrl + href
    response = requests.get(topicUrl, headers=header)
    response.encoding = response.apparent_encoding
    soup = BeautifulSoup(response.text, "lxml")
    # 一、获取题目所有说明文本
    allP = soup.find("div", id="content").find_all("p")
    # 创建字典,暂存每一页的各个板块信息
    dic = {}
    # 使用list暂存所有说明
    noteList = []
    '''
    0题目:有四个数字:1、2、3、4,能组成多少个互不相同且无重复数字的三位数?各是多少?
    1程序分析:可填在百位、十位、个位的数字都是1、2、3、4。组成所有的排列后再去掉不满足条件的排列。
    2程序源代码:
    3以上实例输出结果为:
    '''
    for p in allP:
        if not p.find("a"):  # 排除“返回上一页”
            noteList.append(p.get_text())
    dic["noteList"] = noteList
    # 二、获取实例代码
    try:
        code = soup.find("div", id="content").find("div", class_="example")
        if code:
            dic["code"] = code.get_text()
        else:
            pre = soup.find("div", id="content").find("pre")
            dic["code"] = pre.get_text()
    except Exception as e:
        pre = soup.find("div", id="content").find("pre")
        print(pre)
        print(f"第{hreflist.index(href) + 1}题获取示例代码出错了:{e}")
    # 三、获取执行结果
    try:
        result = soup.find("div", id="content").find("pre")
        if result:
            dic["result"] = result.get_text()
        else:
            imgSrc = soup.find("div", id="content").find("img", attrs={"src": re.compile(r"^//")}).attrs["src"]
            dic["result"] = f"执行结果为非文本数据:数据链接为:https:{imgSrc}"
    except Exception as e:
        print(f"第{hreflist.index(href) + 1}题获取执行结果出错了:{e}")
        dic["result"] = "无执行结果!"
    return dic

# 写入文件
def wirteInFile(pageList):

    with open("../Test/python100例题.txt","w+",encoding="utf-8") as f:
    # with open("../Test/python100例题.doc","w+",encoding="utf-8") as f:
        for dic in pageList:
            noteList= dic["noteList"]
            code= dic["code"]
            result= dic["result"]
            try:
                if len(noteList)==4:
                    f.write(f"{pageList.index(dic)+1}"+noteList[0]+"\n")
                    f.write(noteList[1]+"\n")
                    f.write(noteList[2]+"\n")
                    f.write(code+"\n")
                    f.write(noteList[3]+"\n")
                    f.write(result+"\n")
                    f.write("*"*50+"\n")
                else:
                    f.write(f"{pageList.index(dic)+1}"+noteList[0]+"\n")
                    f.write(noteList[1]+"\n")
                    f.write(code+"\n")
                    f.write(noteList[2]+"\n")
                    f.write(result+"\n")
                    f.write("*"*50+"\n")
            except Exception as e:
                print(f"{pageList.index(dic)+1}出错了:{e}")
    print("写入完成!!!")

if __name__ == '__main__':
    url = "https://www.runoob.com/python/python-100-examples.html"
    rootUrl = "https://www.runoob.com"
    # 根据首页地址获取题目列表及连接
    header = {  # 模拟浏览器范围,防止被反爬
        "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36"
    }
    response = requests.get(url, headers=header)
    # 防止乱码
    response.encoding = response.apparent_encoding
    # 将网页内容转为bs4对象
    soup = BeautifulSoup(response.text, "lxml")
    # 查找题目链接
    uls = soup.find("div", id="content").find_all("ul")
    hreflist = []
    for ul in uls:
        lis = ul.find_all("li")
        for li in lis:
            hreflist.append(li.find("a").get("href"))
    # executor.map只能接受一个参数,所以将参数打包成一个
    arglist = [(href, rootUrl,header) for href in hreflist]
 # 使用多进程处理网络请求任务:标准库线程池【平均用时5.7秒】#     多进程写入,最快用时:0:00:04.949686
    with ProcessPoolExecutor(max_workers=16) as executor:

        pageList=executor.map(process_href, arglist)
        wirteInFile(list(pageList))
        time2 = datetime.datetime.now()
        print("结束时间", "*" * 20, time2.strftime('%Y-%m-%d %H:%M:%S'), "*" * 20)
        print(time2 - time1)
# 使用多进程处理网络请求任务:传统线程池【平均用时6.7秒】

    # with Pool(16) as p:
    #     # 创建列表存储每一页所有信息
    #     pageList =p.map(process_href, arglist)
    # wirteInFile(pageList)
    # time2=datetime.datetime.now()
    # print("结束时间","*"*20,time2.strftime('%Y-%m-%d %H:%M:%S'),"*"*20)
    # print(time2-time1)

多线程代码

import datetime
import re
from concurrent.futures import ThreadPoolExecutor

import requests
from bs4 import BeautifulSoup
import concurrent.futures
import random, time

time1=datetime.datetime.now()
print("*"*20,time1.strftime('%Y-%m-%d %H:%M:%S'),"*"*20)
url="https://www.runoob.com/python/python-100-examples.html"
rootUrl="https://www.runoob.com"
# 根据首页地址获取题目列表及连接
header={#模拟浏览器范围,防止被反爬
    "user-agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36"
}
response = requests.get(url, headers=header)
# 防止乱码
response.encoding=response.apparent_encoding
# 将网页内容转为bs4对象
soup = BeautifulSoup(response.text, "lxml")
# 查找题目链接
uls = soup.find("div",id="content").find_all("ul")
hreflist=[]
for ul in uls:
    lis = ul.find_all("li")
    for li in lis:
        hreflist.append(li.find("a").get("href"))


# 创建列表存储每一页所有信息
pageList=[]
# 单个耗时任务
def process_href(href):
    # 爬取并处理单个链接的逻辑
    # 根据每个连接获取例题子元素
    topicUrl = rootUrl + href
    response = requests.get(topicUrl, headers=header)
    response.encoding = response.apparent_encoding
    soup = BeautifulSoup(response.text, "lxml")
    # 一、获取题目所有说明文本

    allP = soup.find("div", id="content").find_all("p")
    # 创建字典,暂存每一页的各个板块信息
    dic = {}
    # 使用list暂存所有说明
    noteList = []
    '''
    0题目:有四个数字:1、2、3、4,能组成多少个互不相同且无重复数字的三位数?各是多少?
    1程序分析:可填在百位、十位、个位的数字都是1、2、3、4。组成所有的排列后再去掉不满足条件的排列。
    2程序源代码:
    3以上实例输出结果为:
    '''
    for p in allP:
        if not p.find("a"):  # 排除“返回上一页”
            noteList.append(p.get_text())
    dic["noteList"] = noteList
    # 二、获取实例代码
    try:
        code = soup.find("div", id="content").find("div", class_="example")
        if code:
            dic["code"] = code.get_text()
        else:
            pre = soup.find("div", id="content").find("pre")
            dic["code"] = pre.get_text()
    except Exception as e:
        pre = soup.find("div", id="content").find("pre")
        print(pre)
        print(f"第{hreflist.index(href) + 1}题获取示例代码出错了:{e}")
    # 三、获取执行结果
    try:
        result = soup.find("div", id="content").find("pre")
        if result:
            dic["result"] = result.get_text()
        else:
            imgSrc = soup.find("div", id="content").find("img", attrs={"src": re.compile(r"^//")}).attrs["src"]
            dic["result"] = f"执行结果为非文本数据:数据链接为:https:{imgSrc}"
    except Exception as e:
        print(f"第{hreflist.index(href) + 1}题获取执行结果出错了:{e}")
        dic["result"] = "无执行结果!"
    pageList.append(dic)
# 在多线程版本中,`pageList`是全局变量,多线程并发append可能导致性能问题,但此处可能影响较小。另外,网络请求的并发管理(如TCP连接数限制)也会影响效率。
#  `pageList.append(dic)`可能因多线程同时修改列表而导致竞争条件,【但Python的列表操作在CPython中是线程安全的(由于GIL)】。不过频繁的append操作仍可能导致性能下降,但整体影响可能不大。

# 写入文件
def wirteInFile():

    with open("../Test/python100例题.txt","w+",encoding="utf-8") as f:
    # with open("../Test/python100例题.doc","w+",encoding="utf-8") as f:
        for dic in pageList:
            noteList= dic["noteList"]
            code= dic["code"]
            result= dic["result"]
            try:
                if len(noteList)==4:
                    f.write(f"{pageList.index(dic)+1}"+noteList[0]+"\n")
                    f.write(noteList[1]+"\n")
                    f.write(noteList[2]+"\n")
                    f.write(code+"\n")
                    f.write(noteList[3]+"\n")
                    f.write(result+"\n")
                    f.write("*"*50+"\n")
                else:
                    f.write(f"{pageList.index(dic)+1}"+noteList[0]+"\n")
                    f.write(noteList[1]+"\n")
                    f.write(code+"\n")
                    f.write(noteList[2]+"\n")
                    f.write(result+"\n")
                    f.write("*"*50+"\n")
            except Exception as e:
                print(f"{pageList.index(dic)+1}出错了:{e}")
    print("写入完成!!!")

if __name__ == "__main__":
    # 多线程耗时0:00:07.817000
    with ThreadPoolExecutor(max_workers=16) as executor:
        executor.map(process_href, hreflist)

    wirteInFile()
    time2=datetime.datetime.now()
    print("结束时间","*"*20,time2.strftime('%Y-%m-%d %H:%M:%S'),"*"*20)
    print(time2-time1)

 异步代码

 

import asyncio
import datetime
import re
import aiohttp
import requests
from bs4 import BeautifulSoup


# 单个耗时任务
async def process_href(href, rootUrl, session):
    topicUrl = rootUrl + href
    async with session.get(topicUrl, headers=header) as res:
        soup = BeautifulSoup(await res.text(), "lxml")
        return await analysishtml(soup, href)


async def analysishtml(soup, href):
    # 一、获取题目所有说明文本
    allP = soup.find("div", id="content").find_all("p")
    # 创建字典,暂存每一页的各个板块信息
    dic = {}
    # 使用list暂存所有说明
    noteList = []
    for p in allP:
        if not p.find("a"):  # 排除“返回上一页”
            noteList.append(p.get_text())
    dic["noteList"] = noteList
    # 二、获取实例代码
    try:
        code = soup.find("div", id="content").find("div", class_="example")
        if code:
            dic["code"] = code.get_text()
        else:
            pre = soup.find("div", id="content").find("pre")
            dic["code"] = pre.get_text()
    except Exception as e:
        pre = soup.find("div", id="content").find("pre")
        print(pre)
        print(f"第{hreflist.index(href) + 1}题获取示例代码出错了:{e}")
    # 三、获取执行结果
    try:
        result = soup.find("div", id="content").find("pre")
        if result:
            dic["result"] = result.get_text()
        else:
            imgSrc = soup.find("div", id="content").find("img", attrs={"src": re.compile(r"^//")}).attrs["src"]
            dic["result"] = f"执行结果为非文本数据:数据链接为:https:{imgSrc}"
    except Exception as e:
        print(f"第{hreflist.index(href) + 1}题获取执行结果出错了:{e}")
        dic["result"] = "无执行结果!"
    return dic


# 写入文件
async def wirteInFile(pageList):
    with open("../Test/python100例题.txt", "w+", encoding="utf-8") as f:
        # with open("../Test/python100例题.doc","w+",encoding="utf-8") as f:
        for dic in pageList:
            noteList = dic["noteList"]
            code = dic["code"]
            result = dic["result"]
            try:
                if len(noteList) == 4:
                    f.write(f"{pageList.index(dic) + 1}" + noteList[0] + "\n")
                    f.write(noteList[1] + "\n")
                    f.write(noteList[2] + "\n")
                    f.write(code + "\n")
                    f.write(noteList[3] + "\n")
                    f.write(result + "\n")
                    f.write("*" * 50 + "\n")
                else:
                    f.write(f"{pageList.index(dic) + 1}" + noteList[0] + "\n")
                    f.write(noteList[1] + "\n")
                    f.write(code + "\n")
                    f.write(noteList[2] + "\n")
                    f.write(result + "\n")
                    f.write("*" * 50 + "\n")
            except Exception as e:
                print(f"{pageList.index(dic) + 1}出错了:{e}")
    print("写入完成!!!")


if __name__ == '__main__':
    url = "https://www.runoob.com/python/python-100-examples.html"
    rootUrl = "https://www.runoob.com"
    header = {
        "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36"
    }
    response = requests.get(url, headers=header)
    response.encoding = response.apparent_encoding
    # 将网页内容转为bs4对象
    soup = BeautifulSoup(response.text, "lxml")
    # 查找题目链接
    uls = soup.find("div", id="content").find_all("ul")
    # 定义内容目录链接列表
    hreflist = []
    for ul in uls:
        lis = ul.find_all("li")
        for li in lis:
            hreflist.append(li.find("a").get("href"))


    async def main():
        time1 = datetime.datetime.now()
        print("*" * 20, time1.strftime('%Y-%m-%d %H:%M:%S'), "*" * 20)
        async with aiohttp.ClientSession(headers=header) as session:
            worklist = [process_href(href, rootUrl, session) for href in hreflist]
            pageList = await asyncio.gather(*worklist)
        await wirteInFile(pageList)
        time2 = datetime.datetime.now()
        print("结束时间", "*" * 20, time2.strftime('%Y-%m-%d %H:%M:%S'), "*" * 20)
        print(time2 - time1)


    asyncio.run(main())

 

posted @ 2025-03-27 09:15  指尖下的世界  阅读(107)  评论(0)    收藏  举报