使用ThreadPoolExecutor与Selenium多线程爬取航班价格异常问题咨询
Selenium多线程爬取航班价格问题修复方案
你的多线程爬取方案本身是可行的,现在拿不到数据和页面加载速度无关,是代码存在几个逻辑错误导致的:
现有代码的核心问题
- 最严重的错误:
driver.get(url)方法没有返回值,你写的driver = driver0.get(url)实际是把None赋值给了driver变量,后续WebDriverWait传入空对象直接报错,根本不可能找到元素。 - 靠线程名绑定driver实例的逻辑非常脆弱:线程池的线程命名规则随Python版本、执行场景可能变化,一旦匹配失败直接拿不到可用driver。
- Selenium的WebDriver实例本身不是线程安全的,全局提前创建3个driver跨线程调用,本身就存在偶发崩溃的风险。
- 写死的绝对路径XPath容错性极差:谷歌航班的前端DOM是动态生成的,标签层级、序号只要随版本小幅变动,哪怕页面完全加载也定位不到元素。
- 额外加的
time.sleep(20)是无效冗余:你已经配置了显式等待,固定等待只会平白增加爬取耗时。
修正后的可运行实现
正确的多线程Selenium写法是给每个工作线程绑定独立的driver实例,复用实例处理分配到的URL,避免跨线程调用driver,同时替换不稳定的元素定位逻辑:
import pandas as pd from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from concurrent.futures import ThreadPoolExecutor import threading from selenium.common.exceptions import TimeoutException # 全局存储所有初始化的driver实例,方便最后统一关闭 all_drivers = [] # 线程本地存储,每个线程绑定自己的driver实例 thread_local = threading.local() def get_driver(): """获取当前线程专属的driver,不存在则初始化""" if not hasattr(thread_local, "driver"): options = webdriver.ChromeOptions() # 无头模式,不弹出窗口,大幅提升加载速度 options.add_argument("--headless=new") # 禁用图片、扩展、GPU加速,减少不必要的资源加载 options.add_argument("--blink-settings=imagesEnabled=false") options.add_argument("--disable-extensions") options.add_argument("--disable-gpu") options.add_argument("--no-sandbox") driver = webdriver.Chrome( executable_path='C:\Program Files (x86)\chromedriver.exe', options=options ) thread_local.driver = driver all_drivers.append(driver) return thread_local.driver def get_cost(url): driver = get_driver() driver.get(url) cost = "-" try: # 替换为更稳定的相对定位逻辑,不要用写死的绝对XPath # 英文界面把选择器里的"价格"替换为"price"即可 element = WebDriverWait(driver, 15).until( EC.visibility_of_element_located((By.CSS_SELECTOR, 'ul[role="list"] > li:first-child span[aria-label*="价格"]')) ) cost = element.get_attribute('textContent').strip() except TimeoutException: # 仅捕获超时异常,不要裸except吞掉所有错误 pass print(f"URL:{url}\n价格:{cost}\n") return {"url": url, "cost": cost} if __name__ == "__main__": urls = ['https://www.google.com/travel/flights?q=Flights%20to%20Paphos%20from%20Vienna%20on%202022-07-25%20one%20way', 'https://www.google.com/travel/flights?q=Flights%20to%20Paphos%20from%20Vienna%20on%202022-07-26%20one%20way', 'https://www.google.com/travel/flights?q=Flights%20to%20Paphos%20from%20Vienna%20on%202022-07-27%20one%20way', 'https://www.google.com/travel/flights?q=Flights%20to%20Paphos%20from%20Vienna%20on%202022-07-28%20one%20way', 'https://www.google.com/travel/flights?q=Flights%20to%20Paphos%20from%20Vienna%20on%202022-07-29%20one%20way', 'https://www.google.com/travel/flights?q=Flights%20to%20Paphos%20from%20Vienna%20on%202022-07-30%20one%20way', 'https://www.google.com/travel/flights?q=Flights%20to%20Paphos%20from%20Vienna%20on%202022-08-01%20one%20way', 'https://www.google.com/travel/flights?q=Flights%20to%20Paphos%20from%20Vienna%20on%202022-08-02%20one%20way', 'https://www.google.com/travel/flights?q=Flights%20to%20Paphos%20from%20Vienna%20on%202022-08-03%20one%20way'] result = [] # 线程数建议设置为2-8个,不要开太多避免触发反爬、内存占满卡顿 with ThreadPoolExecutor(max_workers=3) as exe: result = list(exe.map(get_cost, urls)) # 所有任务完成后统一关闭所有线程的driver for driver in all_drivers: driver.quit() # 结果转DataFrame存文件 # pd.DataFrame(result).to_csv("flight_price.csv", index=False)
进一步优化建议
- 如果URL规模达到十万甚至百万级,Selenium方案依然效率有限,优先用谷歌航班官方的API接口获取数据,稳定性和效率远高于页面爬取,也不需要维护元素定位规则。
- 爬取过程中如果出现大量超时、验证页面,说明已经触发反爬,需要降低并发数、搭配代理IP池使用,不要短时间内高频请求。
- 增加2-3次的失败重试逻辑,个别因为网络波动加载失败的URL自动重试,减少数据缺失。
内容的提问来源于stack exchange,提问作者max
相关产品推荐
相关产品推荐

