Python3下如何高效发送10万次Web请求并优化URL状态码检测?
优化10万条同域名URL状态码检测的方案
针对你用线程+PyCurl处理10万条同域名URL时前期准备耗时久的问题,我整理了几个针对性的优化方向,结合你的代码给出具体改进方案:
一、核心问题分析
你当前的代码有两个关键瓶颈导致前期/整体效率低下:
- 无限制创建线程:10万条URL会瞬间创建10万个线程,系统线程调度、资源分配的开销极大,前期线程创建阶段会占用大量时间,甚至导致系统卡顿。
- 重复创建销毁PyCurl实例:Curl对象的初始化有不小的开销,每请求一次就新建、关闭一次,会累积大量额外耗时。
二、针对性优化方案
1. 用线程池控制并发+复用线程
用concurrent.futures.ThreadPoolExecutor代替手动创建线程,它会提前创建固定数量的线程并复用,避免大量线程创建/销毁的开销,同时能合理控制并发数(避免超出系统承载)。
2. 线程级复用PyCurl对象
通过threading.local()实现线程本地存储,让每个线程复用一个Curl实例,不用每次请求都重新初始化,大幅减少初始化开销。
3. 进阶:PyCurl MultiHandle(单线程异步)
PyCurl的MultiHandle支持单线程处理多个异步请求,比线程池更高效——完全没有线程切换的开销,再结合同域名TCP连接复用,能把请求效率拉到最高。
三、改进后的代码示例
方案1:线程池+复用Curl对象(易上手,兼容原有逻辑)
import pycurl import certifi from concurrent.futures import ThreadPoolExecutor import threading # 线程本地存储:每个线程复用一个Curl实例 thread_local = threading.local() def get_curl(): if not hasattr(thread_local, 'curl'): curl = pycurl.Curl() curl.setopt(pycurl.CAINFO, certifi.where()) curl.setopt(pycurl.WRITEFUNCTION, lambda x: None) curl.setopt(pycurl.CONNECTTIMEOUT, 5) # 开启TCP连接复用,同域名请求直接复用已建立的连接 curl.setopt(pycurl.TCP_KEEPALIVE, 1) curl.setopt(pycurl.TCP_KEEPIDLE, 30) curl.setopt(pycurl.TCP_KEEPINTVL, 10) thread_local.curl = curl return thread_local.curl def req(url, counter): curl = get_curl() try: curl.setopt(pycurl.URL, url) curl.perform() print(f"Requests: {counter} | URL: {url} | Status Code: {curl.getinfo(pycurl.HTTP_CODE)}") except pycurl.error as e: print(f"Requests: {counter} | URL: {url} | Error: {e}") if __name__ == "__main__": # 用生成器逐行读取URL,避免一次性加载10万条到内存 def load_urls(): with open("urllist.txt") as f: for line in f: yield line.strip() urls = load_urls() # 并发数根据系统配置调整,同域名建议100-200(避免触发目标站限流) max_workers = 150 counter = 0 with ThreadPoolExecutor(max_workers=max_workers) as executor: futures = [] for url in urls: counter += 1 futures.append(executor.submit(req, url, counter)) # 等待所有请求完成 for future in futures: future.result() # 最后清理线程内的Curl实例 if hasattr(thread_local, 'curl'): thread_local.curl.close()
方案2:PyCurl MultiHandle(单线程异步,效率最高)
这个方案完全避免线程开销,适合处理大量同域名请求:
import pycurl import certifi def check_urls(): multi = pycurl.CurlMulti() active_handles = [] max_concurrent = 200 # 同时处理的请求数 counter = 0 # 读取URL列表(如果文件极大,建议用生成器逐行处理) with open("urllist.txt") as f: urls = [line.strip() for line in f.read().splitlines()] remaining_urls = urls.copy() def add_request(url): nonlocal counter counter += 1 curl = pycurl.Curl() curl.setopt(pycurl.CAINFO, certifi.where()) curl.setopt(pycurl.WRITEFUNCTION, lambda x: None) curl.setopt(pycurl.CONNECTTIMEOUT, 5) curl.setopt(pycurl.URL, url) # 开启TCP连接复用 curl.setopt(pycurl.TCP_KEEPALIVE, 1) # 存储额外信息,方便后续打印 curl.url = url curl.counter = counter multi.add_handle(curl) active_handles.append(curl) # 先填充一批初始请求 for _ in range(min(max_concurrent, len(remaining_urls))): add_request(remaining_urls.pop(0)) # 启动异步处理循环 while active_handles: # 执行请求 ret, num_handles = multi.perform() if ret != pycurl.E_CALL_MULTI_PERFORM: break # 等待活动连接就绪 while True: ret, num_handles = multi.select(1.0) if ret != pycurl.E_CALL_MULTI_PERFORM: break # 处理完成的请求 while True: queued, ok_list, err_list = multi.info_read() # 处理成功的请求 for curl in ok_list: print(f"Requests: {curl.counter} | URL: {curl.url} | Status Code: {curl.getinfo(pycurl.HTTP_CODE)}") multi.remove_handle(curl) active_handles.remove(curl) curl.close() # 添加下一个请求(如果还有剩余) if remaining_urls: add_request(remaining_urls.pop(0)) # 处理失败的请求 for curl, errno, errmsg in err_list: print(f"Requests: {curl.counter} | URL: {curl.url} | Error: {errno} - {errmsg}") multi.remove_handle(curl) active_handles.remove(curl) curl.close() if remaining_urls: add_request(remaining_urls.pop(0)) if queued == 0: break if __name__ == "__main__": check_urls()
四、额外优化建议
- TCP连接复用:两个方案都开启了
TCP_KEEPALIVE,同域名请求会复用已建立的TCP连接,大幅减少握手时间,这对同域名大量请求的提升非常明显。 - 并发数控制:不要设置过高的并发数,避免触发目标网站的限流机制,同时也避免本地系统资源耗尽。同域名建议控制在100-200之间。
- 结果写入文件:如果10万条请求的打印会拖慢速度,可以把结果写入本地文件而不是打印到控制台,进一步提升效率。
- URL预校验:提前过滤掉格式无效的URL,避免无效请求浪费资源。
内容的提问来源于stack exchange,提问作者神瀬来未
相关产品推荐
相关产品推荐

