You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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()

请问用多线程实现该循环爬取任务的最佳方式是什么?

解决方案

先说说你多进程失败的核心问题

  1. 全局变量all初始化为字符串"",字符串没有append方法,直接触发报错;就算改成列表,多进程的全局变量是各进程独立副本,修改不会同步到主进程,最终主进程的all仍是空值。
  2. 手动创建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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.08 16:29:59