Python selenium 多处理
- 2025-03-25 08:47:00
- admin 原创
- 18
问题描述:
我用 python 编写了一个脚本,结合 selenium 从其登录页面抓取不同帖子的链接,最后通过跟踪指向其内页的 url 来获取每篇帖子的标题。虽然我在这里解析的内容是静态的,但我使用了 selenium 来查看它在多处理中的工作原理。
但是,我的目的是使用多处理进行抓取。到目前为止,我知道 selenium 不支持多处理,但看来我错了。
我的问题是:当使用多处理运行时,如何使用 selenium 减少执行时间?
This is my try (it's a working one)
:
import requests
from urllib.parse import urljoin
from multiprocessing.pool import ThreadPool
from bs4 import BeautifulSoup
from selenium import webdriver
def get_links(link):
res = requests.get(link)
soup = BeautifulSoup(res.text,"lxml")
titles = [urljoin(url,items.get("href")) for items in soup.select(".summary .question-hyperlink")]
return titles
def get_title(url):
chromeOptions = webdriver.ChromeOptions()
chromeOptions.add_argument("--headless")
driver = webdriver.Chrome(chrome_options=chromeOptions)
driver.get(url)
sauce = BeautifulSoup(driver.page_source,"lxml")
item = sauce.select_one("h1 a").text
print(item)
if __name__ == '__main__':
url = "https://stackoverflow.com/questions/tagged/web-scraping"
ThreadPool(5).map(get_title,get_links(url))
解决方案 1:
当使用多处理运行时,如何使用 selenium 减少执行时间
您的解决方案中,很多时间都花在为每个 URL 启动 Web 驱动程序上。您可以通过每个线程仅启动一次驱动程序来减少此时间:
(... skipped for brevity ...)
threadLocal = threading.local()
def get_driver():
driver = getattr(threadLocal, 'driver', None)
if driver is None:
chromeOptions = webdriver.ChromeOptions()
chromeOptions.add_argument("--headless")
driver = webdriver.Chrome(chrome_options=chromeOptions)
setattr(threadLocal, 'driver', driver)
return driver
def get_title(url):
driver = get_driver()
driver.get(url)
(...)
(...)
在我的系统上,这将时间从 1 分 7 秒缩短至 24.895 秒,提升了约 35%。若要自行测试,请下载完整脚本。
注意:ThreadPool
使用受 Python GIL 约束的线程。如果大部分任务受 I/O 限制,那么这样做没问题。根据您对抓取结果进行的后处理,您可能希望改用multiprocessing.Pool
。这将启动并行进程,这些进程作为一个组不受 GIL 约束。其余代码保持不变。
解决方案 2:
我认为巧妙的“每个线程一个驱动程序”解决方案的一个潜在问题是,它忽略了“退出”驱动程序的任何机制,因此留下了进程挂起的可能性。我会做以下更改:
而是使用类
Driver
来创建驱动程序实例并将其存储在线程本地存储中,但也有一个析构函数,quit
当线程本地存储被删除时,该驱动程序将析构:
class Driver:
def __init__(self):
options = webdriver.ChromeOptions()
options.add_argument("--headless")
self.driver = webdriver.Chrome(options=options)
def __del__(self):
self.driver.quit() # clean up driver when we are cleaned up
#print('The driver has been "quitted".')
create_driver
现在变成:
threadLocal = threading.local()
def create_driver():
the_driver = getattr(threadLocal, 'the_driver', None)
if the_driver is None:
the_driver = Driver()
setattr(threadLocal, 'the_driver', the_driver)
return the_driver.driver
最后,在您不再使用
ThreadPool
实例之后但在终止之前,添加以下几行以删除线程本地存储并强制Driver
调用实例的析构函数(希望如此):
del threadLocal
import gc
gc.collect() # a little extra insurance
解决方案 3:
我的问题:如何减少执行时间?
Selenium 似乎不适合进行网页抓取 - 尽管我很欣赏 YMMV,特别是当你需要模拟用户与网站的交互或者存在某些 JavaScript 限制/要求时。
对于不需要太多交互的抓取任务,我使用开源Scrapy Python 包进行大规模抓取任务取得了良好的效果。它具有开箱即用的多处理功能,可以轻松编写新脚本并将数据存储在文件或数据库中 - 而且速度非常快。
当作为完全并行的 Scrapy 蜘蛛实现时,你的脚本看起来会像这样(注意,我没有测试过这个,请参阅选择器的文档)。
import scrapy
class BlogSpider(scrapy.Spider):
name = 'blogspider'
start_urls = ['https://stackoverflow.com/questions/tagged/web-scraping']
def parse(self, response):
for title in response.css('.summary .question-hyperlink'):
yield title.get('href')
要运行,请将其放入blogspider.py
并运行
$ scrapy runspider blogspider.py
请参阅Scrapy 网站以获取完整教程。
请注意,得益于 @SIM 的提示,Scrapy 还通过scrapy-splash支持 JavaScript 。到目前为止,我还没有接触过它,所以除了它看起来与 Scrapy 的工作方式很好地集成在一起之外,我无法谈论它。