news 2026/4/15 6:24:50

Python 爬虫实战:多线程爬虫提升爬取效率

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python 爬虫实战:多线程爬虫提升爬取效率

摘要

本文聚焦 Python 多线程爬虫技术,深入剖析其核心原理、实现方式及性能优化策略,通过实战案例演示如何基于threading模块和concurrent.futures库构建高效多线程爬虫,解决单线程爬取效率低下的问题。实战目标网站为豆瓣电影 Top250,读者可直接点击该链接进行爬取验证。文中包含完整可运行代码、输出结果解析、性能对比表格及常见问题解决方案,旨在帮助开发者掌握多线程爬虫的核心逻辑,显著提升数据爬取效率。

前言

在网络爬虫开发中,单线程爬虫因串行执行的特性,在面对大量网页请求时效率极低 —— 每一次网络请求的等待时间(IO 阻塞)都会导致整个程序停滞。多线程技术通过并发处理多个网络请求,能有效利用 IO 阻塞的空闲时间,大幅提升爬取效率。本文从原理到实战,系统讲解 Python 多线程爬虫的实现流程,对比单线程与多线程的性能差异,同时规避多线程开发中的常见陷阱(如线程安全、请求频率控制),为处理中等规模的爬取任务提供最优解。

一、多线程爬虫核心原理

1.1 线程与并发的基础概念

  • 线程:操作系统调度的最小单位,一个进程可包含多个线程,线程共享进程的内存空间。
  • IO 密集型任务:爬虫的核心操作(网络请求、文件读写)均属于 IO 密集型任务,此类任务的瓶颈在于等待外部资源响应,而非 CPU 运算。
  • 多线程优势:在 IO 阻塞期间,CPU 可切换至其他线程执行任务,避免资源闲置,从而提升整体执行效率。

1.2 Python 多线程实现方式

Python 中实现多线程主要依赖两个模块:

模块名称核心特点适用场景
threading底层线程模块,可手动控制线程创建、启动、销毁需精细化控制线程行为的场景
concurrent.futures.ThreadPoolExecutor高层封装的线程池,自动管理线程生命周期快速实现并发任务,无需手动管理线程

二、实战准备:环境与依赖

2.1 环境要求

  • Python 3.7+
  • 核心依赖库:requests(网络请求)、lxml(解析 HTML)、time(计时)、threading/concurrent.futures(多线程)

2.2 依赖安装

bash

运行

pip install requests lxml

三、单线程爬虫实现(对比基准)

首先实现单线程爬虫爬取豆瓣电影 Top250 的电影名称和评分,作为性能对比基准。

3.1 单线程代码实现

python

运行

import requests from lxml import etree import time # 豆瓣电影Top250基础URL BASE_URL = "https://movie.douban.com/top250" def get_html(url): """获取网页HTML内容""" headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" } try: response = requests.get(url, headers=headers, timeout=10) response.raise_for_status() # 抛出HTTP错误 response.encoding = response.apparent_encoding return response.text except Exception as e: print(f"请求失败:{e}") return None def parse_html(html): """解析HTML,提取电影名称和评分""" if not html: return [] tree = etree.HTML(html) movies = [] # 提取电影条目 movie_items = tree.xpath('//div[@class="item"]') for item in movie_items: title = item.xpath('.//span[@class="title"][1]/text()')[0] score = item.xpath('.//span[@class="rating_num"]/text()')[0] movies.append({"title": title, "score": score}) return movies def single_thread_crawl(): """单线程爬取豆瓣Top250所有页面""" start_time = time.time() all_movies = [] # 豆瓣Top250共10页,每页25条 for page in range(10): offset = page * 25 url = f"{BASE_URL}?start={offset}&filter=" print(f"单线程爬取第{page+1}页:{url}") html = get_html(url) movies = parse_html(html) all_movies.extend(movies) # 模拟轻微延迟,避免请求过快 time.sleep(0.5) end_time = time.time() print(f"\n单线程爬取完成,总耗时:{end_time - start_time:.2f}秒") print(f"爬取电影总数:{len(all_movies)}") # 输出前5条数据验证 print("前5条数据:") for i in range(5): print(all_movies[i]) return all_movies if __name__ == "__main__": single_thread_crawl()

3.2 单线程输出结果

plaintext

单线程爬取第1页:https://movie.douban.com/top250?start=0&filter= 单线程爬取第2页:https://movie.douban.com/top250?start=25&filter= ... 单线程爬取第10页:https://movie.douban.com/top250?start=225&filter= 单线程爬取完成,总耗时:18.76秒 爬取电影总数:250 前5条数据: {'title': '肖申克的救赎', 'score': '9.7'} {'title': '霸王别姬', 'score': '9.6'} {'title': '阿甘正传', 'score': '9.5'} {'title': '泰坦尼克号', 'score': '9.5'} {'title': '这个杀手不太冷', 'score': '9.4'}

3.3 单线程性能分析

单线程爬取 10 页数据耗时约 18-20 秒,核心耗时点在于:

  1. 每页请求的网络等待时间(IO 阻塞);
  2. 串行执行导致必须等待上一页爬取完成才能开始下一页。

四、多线程爬虫实现(ThreadPoolExecutor 版)

使用concurrent.futures.ThreadPoolExecutor实现线程池,自动管理线程,简化多线程开发。

4.1 多线程代码实现

python

运行

import requests from lxml import etree import time from concurrent.futures import ThreadPoolExecutor, as_completed # 复用之前的get_html和parse_html函数 BASE_URL = "https://movie.douban.com/top250" def get_html(url): headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" } try: response = requests.get(url, headers=headers, timeout=10) response.raise_for_status() response.encoding = response.apparent_encoding return response.text except Exception as e: print(f"请求失败:{e}") return None def parse_html(html): if not html: return [] tree = etree.HTML(html) movies = [] movie_items = tree.xpath('//div[@class="item"]') for item in movie_items: title = item.xpath('.//span[@class="title"][1]/text()')[0] score = item.xpath('.//span[@class="rating_num"]/text()')[0] movies.append({"title": title, "score": score}) return movies def crawl_page(page): """爬取单页数据,供线程池调用""" offset = page * 25 url = f"{BASE_URL}?start={offset}&filter=" print(f"线程{page}爬取第{page+1}页:{url}") html = get_html(url) movies = parse_html(html) time.sleep(0.5) # 避免请求过快 return movies def multi_thread_crawl(): """多线程爬取豆瓣Top250""" start_time = time.time() all_movies = [] # 创建线程池,设置最大线程数为5(避免请求过于密集) with ThreadPoolExecutor(max_workers=5) as executor: # 提交任务到线程池 future_to_page = {executor.submit(crawl_page, page): page for page in range(10)} # 遍历完成的任务,收集结果 for future in as_completed(future_to_page): page = future_to_page[future] try: movies = future.result() all_movies.extend(movies) except Exception as e: print(f"第{page+1}页爬取异常:{e}") end_time = time.time() print(f"\n多线程爬取完成,总耗时:{end_time - start_time:.2f}秒") print(f"爬取电影总数:{len(all_movies)}") # 输出前5条数据验证 print("前5条数据:") for i in range(5): print(all_movies[i]) return all_movies if __name__ == "__main__": multi_thread_crawl()

4.2 多线程输出结果

plaintext

线程0爬取第1页:https://movie.douban.com/top250?start=0&filter= 线程1爬取第2页:https://movie.douban.com/top250?start=25&filter= 线程2爬取第3页:https://movie.douban.com/top250?start=50&filter= 线程3爬取第4页:https://movie.douban.com/top250?start=75&filter= 线程4爬取第5页:https://movie.douban.com/top250?start=100&filter= 线程5爬取第6页:https://movie.douban.com/top250?start=125&filter= 线程6爬取第7页:https://movie.douban.com/top250?start=150&filter= 线程7爬取第8页:https://movie.douban.com/top250?start=175&filter= 线程8爬取第9页:https://movie.douban.com/top250?start=200&filter= 线程9爬取第10页:https://movie.douban.com/top250?start=225&filter= 多线程爬取完成,总耗时:4.89秒 爬取电影总数:250 前5条数据: {'title': '肖申克的救赎', 'score': '9.7'} {'title': '霸王别姬', 'score': '9.6'} {'title': '阿甘正传', 'score': '9.5'} {'title': '泰坦尼克号', 'score': '9.5'} {'title': '这个杀手不太冷', 'score': '9.4'}

4.3 多线程原理解析

  1. 线程池创建ThreadPoolExecutor(max_workers=5)创建包含 5 个线程的线程池,避免线程过多导致的系统开销。
  2. 任务提交:通过executor.submit(crawl_page, page)将 10 个爬取任务提交到线程池,线程池自动分配空闲线程执行任务。
  3. 结果收集as_completed(future_to_page)遍历已完成的任务,按任务完成顺序收集结果,而非提交顺序。
  4. IO 复用:当一个线程发起网络请求后进入等待状态,CPU 立即切换到其他线程执行任务,直至请求响应返回,最大化利用 CPU 资源。

五、性能对比与分析

5.1 性能对比表格

爬取方式爬取页数总耗时(秒)平均每页耗时(秒)效率提升比例
单线程1018.761.88-
多线程(5 线程)104.890.49约 284%

5.2 关键影响因素

  1. 线程数设置:线程数并非越多越好,过多线程会导致线程切换开销增大,甚至触发网站反爬机制;建议根据目标网站的反爬策略设置(通常 5-10 线程为宜)。
  2. 请求延迟:即使多线程,也需添加合理的time.sleep(),避免短时间内大量请求被网站封禁 IP。
  3. 线程安全:若多个线程同时写入同一文件 / 变量,需使用threading.Lock()保证线程安全(本文仅读取数据,无需加锁)。

六、多线程爬虫进阶优化

6.1 线程安全的数据存储

若需将爬取结果写入文件,需加锁避免数据错乱:

python

运行

import threading # 初始化锁 lock = threading.Lock() def save_to_file(movies): """线程安全的文件写入""" with lock: # 自动获取和释放锁 with open("movies.txt", "a", encoding="utf-8") as f: for movie in movies: f.write(f"{movie['title']} - {movie['score']}\n") # 在crawl_page函数末尾调用 def crawl_page(page): # ... 原有逻辑 ... save_to_file(movies) return movies

6.2 异常重试机制

为关键请求添加重试逻辑,提升稳定性:

python

运行

from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry def get_html_with_retry(url): """带重试机制的请求函数""" headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" } # 创建会话,设置重试策略 session = requests.Session() retry = Retry( total=3, # 总重试次数 backoff_factor=0.5, # 重试间隔时间(0.5, 1, 1.5秒) status_forcelist=[429, 500, 502, 503, 504] # 触发重试的状态码 ) adapter = HTTPAdapter(max_retries=retry) session.mount("http://", adapter) session.mount("https://", adapter) try: response = session.get(url, headers=headers, timeout=10) response.raise_for_status() response.encoding = response.apparent_encoding return response.text except Exception as e: print(f"请求失败(含重试):{e}") return None

七、注意事项与反爬规避

  1. 遵守 robots 协议:爬取前查看目标网站的robots.txt(如豆瓣 robots.txt),避免爬取禁止访问的内容。
  2. 控制请求频率:即使多线程,也需通过time.sleep()或限速机制控制请求间隔,避免触发反爬。
  3. User-Agent 轮换:使用多个 User-Agent 避免被识别为爬虫。
  4. 避免高频爬取:对同一网站的爬取频率不宜过高,建议根据网站响应调整。

八、总结

本文通过豆瓣电影 Top250 的爬取案例,对比了单线程与多线程爬虫的性能差异,多线程方案将爬取耗时从约 19 秒降至约 5 秒,效率提升近 3 倍。核心要点如下:

  1. 多线程爬虫适用于 IO 密集型任务,可有效利用 CPU 空闲时间;
  2. ThreadPoolExecutor是 Python 实现多线程的高效方式,无需手动管理线程生命周期;
  3. 多线程开发需注意线程安全、请求频率控制和异常处理;
  4. 实际开发中需结合反爬策略,合理设置线程数和请求延迟。

掌握多线程爬虫技术,可显著提升中等规模数据爬取的效率,是 Python 爬虫工程师必备的核心技能之一。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/8 6:06:38

Python 爬虫实战:User-Agent 随机切换防封禁

前言 在网络爬虫的开发与应用过程中,反爬机制是绕不开的核心问题。其中,基于请求头中 User-Agent 字段的校验是网站最基础也是最常用的反爬手段之一。固定的 User-Agent 会被服务器快速识别为爬虫程序,进而触发 IP 封禁、请求限制等反爬措施…

作者头像 李华
网站建设 2026/4/12 0:40:02

一、地理探测器:是什么?

Geo Detector是 用Excel编制的地理探测器软件, 可从以下网址免费下载:http://www.geodetector.org/。地理探测器方法简介地理探测器(Geographical Detector)是一种用于识别空间分异特征及其驱动因素的统计分析方法,最早由王劲峰等学者提出&am…

作者头像 李华
网站建设 2026/4/13 0:13:46

Python 爬虫实战:aiohttp 实现异步高并发爬虫

前言 传统同步爬虫受限于 “请求 - 等待 - 响应” 的串行执行模式,在面对海量 URL 采集场景时,I/O 等待时间占比极高,采集效率难以满足业务需求。异步编程通过事件循环机制,可在单个线程内同时处理多个网络请求,最大化…

作者头像 李华
网站建设 2026/4/12 20:34:30

Python 爬虫实战:asyncio 异步爬虫任务调度

前言 在基于 aiohttp 实现的异步爬虫中,单纯依靠 asyncio.gather 批量执行协程虽能实现高并发,但面对复杂场景(如任务优先级调度、动态任务添加、任务失败重试、资源限流)时,缺乏灵活的任务调度能力。asyncio 作为 Py…

作者头像 李华
网站建设 2026/4/11 19:40:02

必学收藏|AI Agent架构全解析:从ReAct到LangGraph设计模式

本文全面介绍了AI Agent的五大架构类型(反应型、审议式、混合、神经符号和认知)及LangGraph中的三大设计模式(多Agent系统、规划Agent、反思批判)。详细阐述了各架构特点、应用场景和优缺点,从基础到高级展示了AI Agent构建方法,强调选择合适架构的重要性…

作者头像 李华