线程池的 map 每次只把一个条目交给工作线程。请把该条目做成完整任务,包含 URL 和已选代理,这样工作线程就不必从不断变化的全局列表中推断自己的路线。

对于数量有限的 urls,请提供非空的、经过验证的代理 URL 列表。公共 HTTP 候选可在HTTP 代理列表中获取:

from concurrent.futures import ThreadPoolExecutor
from itertools import cycle
import requests

def fetch(job):
    url, proxy_url = job
    proxies = {"http": proxy_url, "https": proxy_url}
    with requests.get(url, proxies=proxies, timeout=(5, 15)) as response:
        response.raise_for_status()
        return url, response.text

if not proxy_urls:
    raise ValueError("No proxy endpoints were supplied")

jobs = zip(urls, cycle(proxy_urls))
with ThreadPoolExecutor(max_workers=5) as executor:
    for url, body in executor.map(fetch, jobs):
        handle_result(url, body)

urls、proxy_urls 和 handle_result 由应用程序提供。协调器按重复顺序分配端点;每个工作线程拿到一个固定的配对。遍历结果时也会暴露工作线程的异常。

五个工作线程限制的是活跃任务数,而不是每个代理的活跃任务数。如果列表里有两个端点,多个任务可能同时使用同一个端点。需要时请另外添加按端点计算的容量规则。若要让每个端点 URL 只有一个活跃任务,同步队列租约可以预订并归还每个条目。

重试应为同一个 URL 创建新的、有界的一次尝试。避免使用会从共享列表中删除条目的递归重试调用:另一个工作线程可能正在从中选择,而耗尽的列表仍然需要明确的结果。

相关文章