尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

Python爬虫慢得像蜗牛?别让i9白等!异步源码救你命

发布时间:2026/9/3 6:31:42

资讯中心
01
ARTICLE

Python爬虫慢得像蜗牛?别让i9白等!异步源码救你命

Python爬虫慢得像蜗牛?别让i9白等!异步源码救你命
小李今天差点把电脑砸了。他编写了一个爬虫程序, 目的是要从数量多达一万个的网站之上抓取数据, 其代码呈现出一种较为简单的状态分别为请求给定的网址, 接着解析相应的内容, 随后将解析出来的内容存储进数据库之中, 运行操作持续了十分钟之久, 然而仅仅才抓取到三百个数据, 他把任务管理器打开进行查看之后, 发现CPU所对应的占用率仅仅才5%, 网络流量几乎接近于零。“我这电脑是i9啊怎么就这水平”哪里出现了问题呢? 他所编写的代码, 规规矩矩地一个一个依次等待着: 先是将请求发送出去, 接着等待服务器做出响应, 随后等待数据传送回来, 之后才会发送下一个请求。每一个请求耗费的时间是0.5秒, 一万个这样的请求就是5000秒, 也就是一个多小时。然而, CPU的大部分时间都处于闲置状态, 原因在于它正在等待网络。这就是典型的I/O密集型任务。代码在等但CPU不干活。小李心里琢磨着, 是不是能够不让它处于等待状态呢, 然后发出了十个请求之后, 会依据谁先返回就对谁进行处理?当然能。这就是“异步”。可是问题在于, 他所编写的代码是沿着同步的方式来写就的。要更改成为异步的形式? 那几十个函数全部都得进行变动, 还有一堆的库需要更换, 光是想一想就让人的脑袋感觉特别大。于是他想知道有没有办法让同步代码“假装”在异步执行先搞清楚同步和异步到底差在哪举个例子。你订了三份外卖,同步的做法是,处在第一家门店外头等候,取得第一份,接着到第二家去等,获取第二份,然后前往第三家等,要是第二家店铺忙碌,你就只能干巴巴等待,整个过程什么都做不了。三家店都下单, 之后回家坐着, 这是异步的做法, 谁做好了就会给你打电话, 这时你去拿, 在那之前你能够看电视, 也能打游戏, 甚至还可以再点上一份。于代码之中, “等”一般而言算得上是I/O操作, 这其中涵盖读文件, 发送HTTP请求, 查询数据库, 以及等待用户输入。这些操作所具备的特性是, 速度迟缓, 然而并非非常耗费CPU当同步代码碰到I/O时, 就会出现卡住的情况。而异步代码在遇到I/O时, 会去做其他的事情, 等I/O完成之后才回来接着继续做原本的事情。那么问题来了同步代码怎么异步执行里有个很直接的办法扔进线程池。简单来讲, 就是去开启若干个所谓的“小弟”, 然后让每个“小弟”都去执行一项同步任务。而主程序, 没有必要等候下去, 转而继续去做自身该做的事儿。看个例子timefrom .这是一个同步函数请求一个网址def fetch(url):print(f开始抓取 {url}) .get(url) # 这里会等print(f抓取完成 {url}).十个网址urls f{i} for i in range(1, 11)同步执行一个一个等start time.time()for url in urls:fetch(url)打印, 格式化输出, 同步耗时冒号, 时间戳减去开始时间的结果保留两位小数并加上秒字。异步执行用线程池start time.time()with (5) as : .map(fetch, urls)把时间戳起始值start, 与当前时间time.time()相减, 并保留两位小数, 然后用print以线程池耗时: { 具体数值 }秒这样的格式输出。来跑一跑, 你就会发觉, 同步版本大约是二十一, 每个请求等待两秒, 线程池这个呢, 仅仅差不多四秒钟便搞定。神奇不? 不神奇, 只是启动了5个线程, 每个线程去处理两个请求, 然后同时等待。不过呢, 这儿存在个陷阱, 那就是线程池适宜于I/O任务, 然而并不适宜CPU密集型任务。要是你开启10个线程去进行计算, 比如说循环一亿次, 反倒会由于线程切换所产生的开销而致使速度变慢。有没有更轻量的办法有即使线程池用起来挺方便, 可每一单个线程都得占用内存大约是8MB, 要是开启的数量过多就承受不起。并且线程进行切换也存在花销。3.4之后, 它被引入了, 其呈现出真正的异步特性, 并非依赖线程, 而是借助一个名为“事件循环”的事物。但存在的问题在于, 对你的函数所提出的要求是, 其必须具备异步性。这意味着什么呢就是说你要达成这样的转换, 把原本的time.sleep换成.sleep, 如此一来, 代码基本上就得重新编写了。那有没有办法让同步代码跑在里有。.。def (url):# 这是一个同步函数没法直接await return requests.get(url).status_codeasync def main():urls # 十个网址# 把同步函数扔到线程池里跑但用异步的方式等待 tasks [asyncio.to_thread(sync_fetch, url) for url in urls] results await asyncio.gather(*tasks) print(results).run(main())该方法的实质仍然是线程池, 然而其写法更为优雅, 能够与真正的异步代码进行混合使用。再深一层事件循环是怎么骗过你的如果你想知道“异步到底是怎么做到的”我们得聊聊事件循环。那事件循环, 宛如一座调度中心, 它手中维持着一份任务列表, 而每个任务, 要么正处于运行状态, 要么就是在等候某件事情, 像网络数据之类的, 一旦有任务宣称“我在等”, 事件循环便会将其挂起, 转而执行下一个任务。等待那个网络数据抵达, 事件循环接着将任务唤醒, 于刚才停下的位置持续。听闻起来繁杂, 然而的的确确已然为你进行了封装处理完毕。你仅仅只需将函数撰写成async def这样子, 其中运用await来表明是“此处需要予以等候”的意思。单问题在于, 我们手上存在着数量众多的同步代码, 根本没办法通通改写成为async def。有没有一个黑科技能把同步函数直接变成异步的存在然而并非十分完善。给出了一个循环, 从本质上来说依旧是线程池。真正达成“将同步代码转变为纯粹异步”是无法实现的, 因为要是同步代码之中存在时间睡眠十秒的情况, 那就是实实在在地致使线程阻塞, 谁都无法挽救你。实战给一个同步爬虫提速假设你写了一个爬虫大概是这样的def (url):# 发请求 r requests.get(url) # 解析 soup BeautifulSoup(r.text, html.parser) # 提取数据 title soup.find(title).text # 存数据库 db.insert({url: url, title: title}) return titledef (urls):for url in urls:.((url))要提速最简单的改动from . ,def (urls, 10):with () as :# 提交所有任务 future_to_url {executor.submit(crawl_one, url): url for url in urls} # 谁先完成就处理谁 for future in as_completed(future_to_url): url future_to_url[future] try: result future.result() results.append(result) print(f完成: {url}) except Exception as e: print(f失败: {url}, 错误: {e}) return results哪怕仅仅是这么不多的几行去进行改动, 速度提升近乎达到了倍的程度, 当然这也是受到了网络所处带宽以及对方服务器承受能力的相关限制的。可是要留意, 要是你在其中运用了数据库连接, 那就得以保证数据库连接是线程安全的才行。好多数据库驱动并非如此, 在这种情况下, 你或许需要为每个线程单独去创建连接。真正的异步怎么把同步库改成异步有时, 你没办法不直面一种情形, 那便是, 你想要运用的库, 仅有同步版本, 举例来说, 就像, redis - py旧版本状态下的。三个办法方法一线程池包装async def (url):loop .()await loop.(None, .get, url)这个代表使用默认线程池的None, 简单, 然而每个调用都会占用一个线程。方法二找异步替代品改代码是麻烦但一旦改完性能提升显著而且不占线程。方法三用anyio或trio这两个库给出了更具高级特性的抽象, 能够使得同步代码于异步环境里运行得越发自然, 然而学习曲线相对较为陡峭, 是不被建议新手去加以尝试举动的。一个容易踩的坑假异步很多人写异步代码写着写着就变成这样了async def fetch(url): .get(url) # 同步操作.textasync def main():tasks fetch(url) for url in urlsawait .(*tasks)你猜怎么着完全没有提速。之所以.get是同步阻塞的原因在于, 当你对一个任务进行await操作时, 要是这个任务在其内部出现了阻塞情况, 便会致使整个事件循环都被卡住。把握住这样一个说法: 异步所具备的那种传染性。一旦运用了async, 从调用链条起始之处一直到末尾, 所有牵涉到I/O的位置都必然得是异步的。要是其中夹杂了一个同步阻塞的调用, 那么整个异步状态就宣告失效了。查看方式极为简便: 于代码之中搜寻、time.sleep、以及open这些执行同步性质的操作, 瞧瞧它们是不是能够在async函数里面出现。有没有更激进的方案有但不太推荐方案一存在着这样一个第三方库, 它借助“打补丁”这种方式, 悄然将标准库里的那些同步I/O操作, 像time.sleep此类, 替换成异步版本。你无需去写async/await, 代码呈现出来的样子全然是同步的, 然而实际上却是异步执行的。from.() # 这行会替换标准库# 现在是异步的了from .pool Pooldef fetch(url):.get(url).pool Pool(10)urls pool.map(fetch, urls)看起来很美好但问题也不少方案二curio或trio这两个异步库, 相较于其他更为现代, 且使用起来更加便利。但在生态这一方面, 却不如别的, 第三方提供的支持数量稀少起来呀。实际项目里怎么选我见过很多团队纠结这个问题。给你一个决策树一个完整的例子混合方案假设你存在这样一种需求, 要从一千个应用程序编程接口抓取数据, 之后针对每一个数据开展一次中央处理器密集计算, 诸如图像处理之类的。混合方案最合适from . ,timeCPU密集函数def (data):# 假设这里有复杂的计算 time.sleep(0.1) # 模拟计算 return data * 2I/O密集函数def (url):.get(url).json()async def main():urls # 1000个URL# 用线程池处理I/O with ThreadPoolExecutor(max_workers50) as io_executor: loop asyncio.get_running_loop() fetch_tasks [ loop.run_in_executor(io_executor, fetch_data, url) for url in urls ] raw_data await asyncio.gather(*fetch_tasks) # 用进程池处理CPU密集任务 with ProcessPoolExecutor(max_workers8) as cpu_executor: process_tasks [ loop.run_in_executor(cpu_executor, process_data, data) for data in raw_data ] results await asyncio.gather(*process_tasks) return results.run(main())这个方案里总结一句话若要将同步代码实现异步执行, 比较简单的方式便是采用线程池。要是期望达成更高效且更优雅的效果, 那就得运用配合。不过需记住: 不存在能解决所有问题的唯一办法, 真正的异步执行需要你从基础层面开始进行修改。转而回到小李所编写的爬虫程序上, 其最终做出了选择, 对其中10行代码进行了修改, 如此这般之后其速度提升至原本的8倍, 尽管这般并非毫无瑕疵, 不过已然能够满足使用需求了。“够用就好”这四个字在工程里往往比“最优”更重要。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。