后端抓取动态网页内容

在网上搜集了一些资料:

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池的方案

  1. 使用对象池库:例如Apache Commons Pool,可以帮助你管理和维护WebDriver的池。
  2. 自定义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池管理。

额外注意事项

  1. 浏览器实例配置:确保每个WebDriver实例的配置一致,包括ChromeDriver路径、启动参数等。
  2. 资源清理:确保在关闭WebDriver池时,所有WebDriver实例都被正确关闭,释放资源。
  3. 并发控制:根据实际需求设置池的大小,避免过多的浏览器实例占用系统资源。

通过这些方法,你可以在生产环境中高效、稳定地使用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来进行爬取网页内容了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值