在网上搜集了一些资料:
https://www.cnblogs.com/firstdream/p/5119730.html
https://blog.csdn.net/defence_/article/details/139602369
https://github.com/fanyong920/jvppeteer
最终采用selenium来实现抓取动态网页的内容
selenium本质上是启动一个chrome浏览器来实现的模拟正常发送请求
但是由于启动一个chrome实例十分的消耗资源,我们肯定不能在生产环境频繁的创建chrome实例,这会拖慢整个系统的运行
我们可以参考数据库连接池,线程池的思想,创建一批chrome并复用这些chrome实例。
基于这个想法,我们先问问AI给我们什么方案
AI提供的方案
在生产环境中使用Selenium执行网页抓取任务时,维护一个WebDriver池可以提高效率和稳定性。通过WebDriver池,你可以重用现有的WebDriver实例,而不是每次都创建新的实例,从而减少启动和关闭浏览器的开销。
使用WebDriver池的方案
- 使用对象池库:例如Apache Commons Pool,可以帮助你管理和维护WebDriver的池。
- 自定义WebDriver池:实现一个简单的WebDriver池来管理WebDriver实例。
使用Apache Commons Pool
Apache Commons Pool是一个常用的对象池库,可以用来实现WebDriver池。以下是一个简单的示例,演示如何使用Apache Commons Pool管理WebDriver实例。
添加Maven依赖
首先,在你的pom.xml
中添加Apache Commons Pool的依赖:
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.11.1</version>
</dependency>
实现WebDriver池
import org.apache.commons.pool2.PooledObject;
import org.apache.commons.pool2.impl.GenericObjectPool;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.apache.commons.pool2.BasePooledObjectFactory;
import org.apache.commons.pool2.impl.DefaultPooledObject;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
public class WebDriverPool extends BasePooledObjectFactory<WebDriver> {
@Override
public WebDriver create() {
// 设置ChromeDriver的路径
System.setProperty("webdriver.chrome.driver", "path/to/chromedriver");
return new ChromeDriver();
}
@Override
public PooledObject<WebDriver> wrap(WebDriver driver) {
return new DefaultPooledObject<>(driver);
}
@Override
public void destroyObject(PooledObject<WebDriver> p) {
p.getObject().quit();
}
public static void main(String[] args) {
GenericObjectPoolConfig<WebDriver> config = new GenericObjectPoolConfig<>();
config.setMaxTotal(5); // 设置池的最大大小
config.setMaxIdle(3); // 设置池的最大空闲实例数
config.setMinIdle(1); // 设置池的最小空闲实例数
GenericObjectPool<WebDriver> pool = new GenericObjectPool<>(new WebDriverPool(), config);
try {
WebDriver driver = pool.borrowObject();
// 使用WebDriver执行任务
driver.get("http://example.com");
System.out.println(driver.getTitle());
pool.returnObject(driver);
} catch (Exception e) {
e.printStackTrace();
} finally {
pool.close();
}
}
}
自定义WebDriver池
如果你不想引入外部依赖,可以实现一个简单的WebDriver池。以下是一个基本的示例:
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
public class SimpleWebDriverPool {
private final BlockingQueue<WebDriver> pool;
private final int maxSize;
public SimpleWebDriverPool(int maxSize) {
this.maxSize = maxSize;
this.pool = new LinkedBlockingQueue<>(maxSize);
for (int i = 0; i < maxSize; i++) {
pool.offer(createWebDriver());
}
}
private WebDriver createWebDriver() {
// 设置ChromeDriver的路径
System.setProperty("webdriver.chrome.driver", "path/to/chromedriver");
return new ChromeDriver();
}
public WebDriver borrowWebDriver() throws InterruptedException {
return pool.take();
}
public void returnWebDriver(WebDriver driver) {
pool.offer(driver);
}
public void shutdown() {
for (WebDriver driver : pool) {
driver.quit();
}
}
public static void main(String[] args) {
SimpleWebDriverPool pool = new SimpleWebDriverPool(5);
try {
WebDriver driver = pool.borrowWebDriver();
// 使用WebDriver执行任务
driver.get("http://example.com");
System.out.println(driver.getTitle());
pool.returnWebDriver(driver);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
pool.shutdown();
}
}
}
选择合适的方案
- Apache Commons Pool:适用于需要复杂管理和配置的场景,提供了丰富的对象池管理功能。
- 自定义WebDriver池:适用于简单场景,快速实现基础的WebDriver池管理。
额外注意事项
- 浏览器实例配置:确保每个WebDriver实例的配置一致,包括ChromeDriver路径、启动参数等。
- 资源清理:确保在关闭WebDriver池时,所有WebDriver实例都被正确关闭,释放资源。
- 并发控制:根据实际需求设置池的大小,避免过多的浏览器实例占用系统资源。
通过这些方法,你可以在生产环境中高效、稳定地使用WebDriver池来执行网页抓取任务。
具体实现
有了AI的启发,我决定自己弄一个自定义的chrome池
说干就干
SimpleWebDriverPool
代码
/**
* 简单 Web 驱动程序池
*
* @author CN016
* @date 2024/09/02 03:57:20
*/
@Component
@Slf4j
public class SimpleWebDriverPool implements ApplicationRunner {
@Value("${webdriver.chrome.driver}")
private String driverPath;
private static final int MAX_POOL_SIZE = 5;
private static final List<MyWebDriver> webDriverPool = new ArrayList<>(MAX_POOL_SIZE);
private static int index = 0;
private static int getAvailableDriverIndex() {
return index++ % MAX_POOL_SIZE; //轮训负载均衡
}
public synchronized WebDriver getWebDriver() {
int times = 0;
while (true) {
if (times > MAX_POOL_SIZE * 2) {
log.error("webDriverPool没有空闲的chrome实例");
throw new RuntimeException("网页服务异常");
}
if (times++ > MAX_POOL_SIZE){
log.error("webDriverPool当前没有空闲的chrome实例,等待一会~");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
}
}
MyWebDriver myWebDriver = webDriverPool.get(getAvailableDriverIndex());
if (!myWebDriver.isUsed) {
myWebDriver.isUsed = true;
return myWebDriver.chrome;
}
}
}
@Override
public void run(ApplicationArguments args) throws Exception {
// 设置ChromeDriver的路径
System.setProperty("webdriver.chrome.driver", driverPath);
for (int i = 0; i < MAX_POOL_SIZE; i++) {
webDriverPool.add(new MyWebDriver(new ChromeDriver(), false));
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
private static final class MyWebDriver implements WebDriver {
private ChromeDriver chrome;
private boolean isUsed;
@Override
public void get(String s) {
chrome.get(s);
}
@Override
public String getCurrentUrl() {
return chrome.getCurrentUrl();
}
@Override
public String getTitle() {
return chrome.getTitle();
}
@Override
public List<WebElement> findElements(By by) {
return chrome.findElements(by);
}
@Override
public WebElement findElement(By by) {
return chrome.findElement(by);
}
@Override
public String getPageSource() {
return chrome.getPageSource();
}
@Override
public void close() {
isUsed = false;
try {
chrome.get("chrome://version/");
} catch (Exception e) {
}
}
@Override
public void quit() {
this.close();
}
@Override
public Set<String> getWindowHandles() {
return chrome.getWindowHandles();
}
@Override
public String getWindowHandle() {
return chrome.getWindowHandle();
}
@Override
public TargetLocator switchTo() {
return chrome.switchTo();
}
@Override
public Navigation navigate() {
return chrome.navigate();
}
@Override
public Options manage() {
return chrome.manage();
}
}
}
解释
这是一个Spring bean,在启动的时候创建5个chrome实例放入一个list中,需要获取chrome实例时通过轮询的方式逐个给使用者返回chrome实例,这里我使用了synchronized
关键字修饰了获取方法,保证线程安全。读者们也可以选择使用乐观锁锁的队列来实现逐个功能,性能更高
然后我也封装了一下chrome实例,添加了一个是否正在被使用的标致,并且重写关闭逻辑,确保chrome实例不会被调用者主动关闭,而是指向另一个页面来释放内存
WebEngine
代码
@Component
@Slf4j
public class WebEngine {
@Autowired
private SimpleWebDriverPool simpleWebDriverPool;
public String getPageContent(String url) {
// 获取一个ChromeDriver实例
WebDriver driver = simpleWebDriverPool.getWebDriver();
try {
// 访问目标网页
driver.get(url);
// 使用MutationObserver来监控DOM变化
JavascriptExecutor js = (JavascriptExecutor) driver;
js.executeScript(
"var callback = function(mutationsList, observer) {" +
" window.domChanged = true;" +
"};" +
"var observer = new MutationObserver(callback);" +
"observer.observe(document.body, { attributes: true, childList: true, subtree: true });" +
"window.domChanged = false;" +
"window.domReady = false;" + // 初始化domReady
"setInterval(function() {" +
" if (window.domChanged) {" +
" window.domChanged = false;" +
" } else {" +
" observer.disconnect();" +
" window.domReady = true;" +
" }" +
"}, 500);"
);
// 等待DOM稳定
while (true) {
Boolean domReady = (Boolean) js.executeScript("return window.domReady || false");
System.out.println("DOM Ready: " + domReady);
if (domReady != null && domReady) {
break;
}
try {
Thread.sleep(100); // 每0.1秒检查一次
} catch (InterruptedException e) {
}
}
// 获取网页的body内容
// driver.get("http://localhost");
return driver.findElement(By.tagName("body")).getText();
} catch (Exception e){
log.error("getPageContent error", e);
return null;
}
finally {
// 关闭浏览器
driver.quit();
}
}
}
解释
这段代码的逻辑是从chrome池中获取一个chrome实例,然后根据调用者传入的url,在chrome打开这个url,并等待页面加载完成后获取这个页面的全部内容
现在调用者就只需要导入WebEngine就能使用chrome来进行爬取网页内容了