Python爬虫优化:用多线程替代循环爬取并合并排序结果
问题描述
我原本用以下循环方式爬取5个链接并对结果排序,代码能正常运行但速度极慢:
def getTable(): p = getLinksToScrape()#返回5个URL all = getLinksData(p[0]) starting = 1 l = 5 while starting < l: allStart = getLinksData(p[starting]) if allStart: all = all + allStart all = sorted(all, key=lambda x: float(x[6])) starting = starting + 1 return all
为了提速,我想同时爬取这5个URL,合并结果后再排序。尝试了以下多进程实现但没成功:
all = "" def getTable(url): global all all.append(getLinksData(url)) def main(): global all p = getLinksToScrape()#返回5个URL p1 = Process(target=getTable, args=(p[0],)) p2 = Process(target=getTable, args=(p[1],)) p3 = Process(target=getTable, args=(p[2],)) p4 = Process(target=getTable, args=(p[3],)) p5 = Process(target=getTable, args=(p[4],)) p1.start() p2.start() p3.start() p4.start() p5.start() p1.join() p2.join() p3.join() p4.join() p5.join() all = sorted(all, key=lambda x: float(x[6])) main()
请问用多线程实现该循环爬取任务的最佳方式是什么?
解决方案
先说说你多进程失败的核心问题
- 全局变量
all初始化为字符串"",字符串没有append方法,直接触发报错;就算改成列表,多进程的全局变量是各进程独立副本,修改不会同步到主进程,最终主进程的all仍是空值。 - 手动创建5个
Process的写法繁琐,且不利于后续URL数量调整。
多线程最佳实现(推荐用concurrent.futures.ThreadPoolExecutor)
爬虫属于网络IO密集型任务,多线程比多进程更轻量,无需处理复杂的进程间通信。用ThreadPoolExecutor可以简洁实现批量任务调度:
from concurrent.futures import ThreadPoolExecutor def getTable(): urls = getLinksToScrape() # 获取目标URL列表 all_data = [] # 定义单个URL的处理逻辑 def scrape_single_url(url): data = getLinksData(url) return data if data else [] # 空结果返回空列表,避免合并出错 # 开启线程池,最大线程数设为URL数量(可按需调整) with ThreadPoolExecutor(max_workers=len(urls)) as executor: # 批量提交任务并获取所有返回结果 results = executor.map(scrape_single_url, urls) # 合并所有非空结果 for res in results: all_data.extend(res) # 最后统一排序(原代码每次追加后就排序完全没必要,浪费性能) all_data.sort(key=lambda x: float(x[6])) return all_data
该方案的优势
- 无需手动管理线程生命周期,
ThreadPoolExecutor自动处理线程的创建、调度和销毁。 - 避免全局变量的线程安全问题,通过返回值收集结果,逻辑更清晰。
- 仅做一次排序,比原代码的多次排序效率提升明显。
- 代码扩展性强,后续URL数量变化时,无需修改线程调度逻辑。
手动线程实现(可选)
如果不想用ThreadPoolExecutor,也可以用threading模块结合线程安全的Queue收集结果:
import threading from queue import Queue def getTable(): urls = getLinksToScrape() result_queue = Queue() def scrape_single_url(url): data = getLinksData(url) result_queue.put(data if data else []) # 创建并启动线程 threads = [] for url in urls: t = threading.Thread(target=scrape_single_url, args=(url,)) threads.append(t) t.start() # 等待所有线程执行完毕 for t in threads: t.join() # 合并队列中的结果 all_data = [] while not result_queue.empty(): all_data.extend(result_queue.get()) all_data.sort(key=lambda x: float(x[6])) return all_data
注意事项
- 确保
getLinksData是线程安全的:如果用到请求会话(如requests.Session()),建议每个线程创建独立的会话实例,避免竞争问题。 - 线程数不要设置过大:过多线程会增加上下文切换开销,一般设置为与URL数量相当即可。
内容的提问来源于stack exchange,提问作者Kankan2000
相关产品推荐
相关产品推荐

