htmlUnit和Selenium的区别以及使用BrowserMobProxy捕获网络请求

发布于:2025-05-13 ⋅ 阅读:(16) ⋅ 点赞:(0)

1. Selenium:浏览器自动化之王

核心定位
        跨平台、跨语言的浏览器操控框架,通过驱动真实浏览器实现像素级用户行为模拟

技术架构

核心特性

  • 支持所有主流浏览器(含移动端模拟)

  • 精确的DOM元素定位(XPath/CSS/ID)

  • 屏幕截图与视频录制功能

  • 分布式测试能力(Selenium Grid)

2. HtmlUnit:无头浏览器轻骑兵

核心定位
        纯Java实现的无界面浏览器引擎,专为服务端自动化场景优化。

技术架构

核心特性

  • 毫秒级页面加载速度

  • 线程安全设计

  • 内置基础AJAX支持

  • Cookie自动管理

3. BrowserMobProxy:网络流量手术刀

核心定位
基于Netty开发的HTTP代理服务器,专为Web流量监控与操控设计。

技术架构

核心特性

  • 实时流量镜像

  • 请求/响应内容篡改

  • 性能指标采集(TTFB等)

  • 支持HTTPS中间人攻击

能力对比矩阵

维度 Selenium HtmlUnit BrowserMobProxy
执行环境 真实浏览器进程 纯JVM环境 独立代理服务
JS支持 完整ES6+ ES5(Rhino引擎) 不涉及
网络延迟模拟 需扩展 原生支持 精确到毫秒级控制
跨域请求处理 受同源策略限制 自动绕过 全流量穿透
移动端调试 完整设备模拟 仅UA伪装 流量分析
典型应用场景 自动化测试 服务端爬虫 接口监控

安装浏览器:Google Chrome谷歌为例



4、selenium和BrowserMobProxy捕获网络请求实例

驱动下载:Chrome for Testing 的可用性(135后版本)chromedriver.storage.googleapis.com/index.html(旧版本驱动)
安装需记住安装位置,启动时需要设置驱动路径

代码实现

getDynamicCrawlersDocument方法为htmlunit的请求监控使用

getParamsByNodeUrl方法为selenium的请求实现,selenium需要驱动支持,可以获取到复杂的请求接口

依赖:

<dependency>
    <groupId>org.seleniumhq.selenium</groupId>
    <artifactId>selenium-java</artifactId>
    <version>4.10.0</version>
</dependency>
<!-- BrowserMob Proxy -->
<dependency>
    <groupId>net.lightbody.bmp</groupId>
    <artifactId>browsermob-core</artifactId>
    <version>2.1.5</version>
</dependency>
<!-- ChromeDriver -->
<dependency>
    <groupId>org.seleniumhq.selenium</groupId>
    <artifactId>selenium-chrome-driver</artifactId>
    <version>4.1.0</version>
</dependency>

代码工具类:

package com.zzkj.zei.utils;


import com.zzkj.zei.component.ServerConfig;
import lombok.extern.slf4j.Slf4j;
import net.lightbody.bmp.BrowserMobProxy;
import net.lightbody.bmp.BrowserMobProxyServer;
import net.lightbody.bmp.client.ClientUtil;
import net.lightbody.bmp.core.har.Har;
import net.lightbody.bmp.core.har.HarEntry;
import net.lightbody.bmp.core.har.HarRequest;
import net.lightbody.bmp.mitm.manager.ImpersonatingMitmManager;
import net.lightbody.bmp.proxy.CaptureType;
import org.apache.commons.lang3.ObjectUtils;
import org.htmlunit.BrowserVersion;
import org.htmlunit.FailingHttpStatusCodeException;
import org.htmlunit.ScriptException;
import org.htmlunit.WebClient;
import org.htmlunit.html.HtmlPage;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.Proxy;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.remote.CapabilityType;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.htmlunit.ProxyConfig;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.util.concurrent.TimeUnit;
import java.time.Duration;
import java.util.*;

/**
 * FileName: SeleniumUtils
 * Author: wzk
 * Date:2025/4/29 11:31
 */
@Component
@Slf4j
public class SeleniumUtils {

    private static String SELENIUM_PATH;

    @Value("${selenium.chromedriver_path}")
    private String seleniumPath; // 非静态变量接收注入

    @PostConstruct
    public void init() {
        SELENIUM_PATH = this.seleniumPath;
    }

    private final static List<String> UA_LIST = Arrays.asList(
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36",
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36",
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36",
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.0.0 Safari/537.36",
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.0.0 Safari/537.36",
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.0.0 Safari/537.36",
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.0.0 Safari/537.36",
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.0.0 Safari/537.36",
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.0.0 Safari/537.36",
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.0.0 Safari/537.36"
            , "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.0.0 Safari/537.36");

    public static void main(String[] args) {
//        List<interfaceNodeData> interfaceNodeDataList = getParamsByNodeUrl(ce,true);
        List<interfaceNodeData> interfaceNodeDataList = getDynamicCrawlersDocument(gfwj1, 1000, true);
        for (interfaceNodeData interfaceNodeData : interfaceNodeDataList) {
            log.info("方法:{}  链接:{}   请求参数:{}" ,interfaceNodeData.getMethod(),interfaceNodeData.getUrl(), interfaceNodeData.getParams());
        }
    }

    public static List<interfaceNodeData> getDynamicCrawlersDocument(String url, Integer waitTime, boolean javaScriptEnabled) {
        List<interfaceNodeData> interfaceNodeDatas = new ArrayList<>();
        // 1. 启动BrowserMob代理
        BrowserMobProxy proxy = new BrowserMobProxyServer();
        proxy.start(0); // 自动分配端口
        int proxyPort = proxy.getPort();
        try {
            // 2. 配置HtmlUnit使用代理
            WebClient browser = new WebClient(BrowserVersion.CHROME);
            browser.getOptions().setProxyConfig(new ProxyConfig("localhost",proxyPort,"http"));
            // 启用HTTPS支持(忽略证书验证)
            browser.getOptions().setSSLInsecureProtocol("ssl");

            //解决动态页面抓取不到信息问题
            browser.getOptions().setCssEnabled(false);
            browser.getOptions().setJavaScriptEnabled(javaScriptEnabled);
            browser.getOptions().setThrowExceptionOnScriptError(false);
            browser.getOptions().setUseInsecureSSL(true);
            // 设置自定义的错误处理类
            browser.setJavaScriptErrorListener(new JsoupHtmlUintUtils.MyJSErrorListener());

            // 开始捕获请求
            proxy.newHar("zzjk");

            HtmlPage page = null;
            page = browser.getPage(url);
            // 等待后台脚本执行时间
            browser.waitForBackgroundJavaScript(waitTime);

//            String pageAsXml = page.asXml();
//            document = Jsoup.parse(pageAsXml.replaceAll("\\<\\?xml.*?\\?>", ""));
//            document.setBaseUri(url);
            // 5. 获取并分析HAR数据
            Har har = proxy.getHar();

            processHarEntries(har, url, interfaceNodeDatas);

        } catch (ScriptException e) {
            log.error("getDynamicCrawlersDocument页面:{}     JavaScript 异常:{}", url, e.getMessage());
        } catch (UnknownHostException e) {
            log.error("getDynamicCrawlersDocument页面:{}     无法解析或找到指定的主机名:{}", url, e.getMessage());
        } catch (FailingHttpStatusCodeException e) {
            log.error("getDynamicCrawlersDocument页面:{}     HTTP 状态异常:{}", url, e.getStatusCode());
        } catch (Exception e) {
            log.error("getDynamicCrawlersDocument页面:{}    获取页面异常:{}", url, e.getMessage());
        } finally {
            // 6. 清理资源
            proxy.stop();
        }
        return interfaceNodeDatas;
    }

    public static List<interfaceNodeData> getParamsByNodeUrl(String url,Boolean isTime){
        long stat = new Date().getTime();
        System.setProperty("webdriver.chrome.driver", SELENIUM_PATH);   //设置chrome驱动程序的路径

        BrowserMobProxy proxy = new BrowserMobProxyServer();
        proxy.start(0); // 自动选择端口
        // 获取Selenium的Proxy对象
        Proxy seleniumProxy = ClientUtil.createSeleniumProxy(proxy);

        ChromeOptions opt = new ChromeOptions();
        opt.addArguments();
        opt.addArguments(
                "--headless",               // 开启无界面模式
                "--disable-gpu",            // 禁用gpu
                "--remote-allow-origins=*", // 允许所有源访问
                "--ignore-certificate-errors", // 忽略证书错误
                "--user-agent=" + UA_LIST.get(0), // 设置请求头
                "--no-sandbox",             // 禁用沙盒,减少权限检查
                "--disable-dev-shm-usage",  // 避免共享内存问题
                "--log-level=3",            // 禁用 Chrome 日志
                "--blink-settings=imagesEnabled=false", // 禁止图片加载
                "--disable-extensions",     // 禁用扩展
                "--disable-javascript",     // 禁用 JavaScript(如果目标页面不需要 JS)
                "--disable-css",            // 禁用 CSS 渲染(按需)
                "--disable-fonts",          // 禁用字体加载
                "--dns-prefetch-disable",    // 禁用 DNS 预解析
                "--disk-cache-size=0",       // 禁用 缓存
                "--disable-cache"            // 禁用 缓存
        );
        opt.setCapability(CapabilityType.PROXY, seleniumProxy);

        WebDriver driver = new ChromeDriver(opt);   //初始化一个chrome驱动实例,保存到driver中

        try {
            driver.manage().window().maximize();
            driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS); // 启用隐式等待

            return seleniumGetDocument(driver,proxy,url,isTime);

        } catch (Exception e) {
            log.info("错误URL: " + driver.getCurrentUrl());
            e.printStackTrace();
        } finally {
            driver.quit();  // 自动清理Cookies和会话
            proxy.stop();
            long end = new Date().getTime();
            log.info("selenium参数获取时间" + (end - stat));
        }
        return null;
    }


    public static List<interfaceNodeData> seleniumGetDocument(WebDriver driver, BrowserMobProxy proxy, String url,Boolean isTime) {
        List<interfaceNodeData> dataList = new ArrayList<>();
        try {
            // 启用MITM抓取HTTPS
            proxy.setMitmManager(new ImpersonatingMitmManager.Builder()
                    .trustAllServers(true)
                    .build());

            proxy.setHarCaptureTypes(CaptureType.REQUEST_CONTENT, CaptureType.RESPONSE_CONTENT);

            proxy.newHar("zzkj");

            // 访问页面并等待
            driver.get(url);

            // 等待页面加载完成
            new WebDriverWait(driver, Duration.ofSeconds(30)).until(
                    webDriver -> ((JavascriptExecutor) webDriver).executeScript("return document.readyState").equals("complete")
            );

            // 验证请求是否稳定
            validationAll(proxy);

            if (isTime){
                Thread.sleep(10000); // 休眠10秒
            }

            // 处理HAR数据
            Har har = proxy.getHar();

            processHarEntries(har, url, dataList);

        } catch (Exception e) {
            e.printStackTrace();
        }
        return dataList;
    }

    private static void validationAll(BrowserMobProxy proxy) throws InterruptedException {
        int retries = 0;
        int stableCount = 0;
        int lastEntrySize = 0;
        while (retries < 30 && stableCount < 3) { // 最多等30秒,稳定3次
            Thread.sleep(1000); // 每秒检查一次
            int currentSize = proxy.getHar().getLog().getEntries().size();
            if (currentSize == lastEntrySize) {
                stableCount++;
            } else {
                stableCount = 0;
                lastEntrySize = currentSize;
            }
            retries++;
        }
        if (retries >= 30) {
            log.info("----------------------- 请求验证稳定超时 -----------------------");
        }
    }

    // 处理 HAR 条目并过滤
    private static void processHarEntries(Har har, String baseUrl, List<interfaceNodeData> interfaceNodeDatas) {
        har.getLog().getEntries().forEach(entry -> {
            HarRequest request = entry.getRequest();
            String method = request.getMethod();
            String toUrl = request.getUrl();
            log.info("检测链接:{}",toUrl);
            // 过滤
            boolean isStaticResource = toUrl.matches(".*\\.(css|js|png|jpg|jpeg|gif|ico|woff|woff2|svg|mp4|mp3)(\\?.*)?$");
            boolean isStaticPath = toUrl.contains("/material/") ||
                    toUrl.contains("/fonts/") ||
                    toUrl.contains("/script/") ||
                    toUrl.contains("/login/") ||
                    toUrl.contains("/images/");

            if (("POST".equalsIgnoreCase(method) || "GET".equalsIgnoreCase(method)) &&
//                    !filterOutsideUrl(baseUrl, toUrl) &&
                    isCurrentNodeUrl(baseUrl,toUrl) &&
                    !isStaticResource &&
                    !isStaticPath) {
                interfaceNodeData interfaceNodeData = new interfaceNodeData();
                interfaceNodeData.setUrl(toUrl);
                interfaceNodeData.setMethod(method);
                interfaceNodeData.setData(entry.getResponse().getContent().getText());
                interfaceNodeData.setParams(ObjectUtils.isEmpty(request.getPostData()) ? "" : request.getPostData().getText());
                interfaceNodeDatas.add(interfaceNodeData);
            }
        });
    }

    private static boolean isCurrentNodeUrl(String sourceUrl, String targetUrl) {
        // 移除协议、转为小写、处理末尾斜杠和index.html
        String normalizedSource = normalizeUrl(sourceUrl);
        String normalizedTarget = normalizeUrl(targetUrl);

        // 判断目标URL是否以当前节点URL开头
        return !normalizedSource.contains(normalizedTarget);
    }

    /**
     * 标准化URL处理
     */
    private static String normalizeUrl(String url) {
        // 移除协议头并转为小写
        String normalized = url.replaceAll("^(http://|https://)", "").toLowerCase();

        // 移除末尾的 "/" 和 "index.html"
        normalized = normalized.replaceAll("/+$", "")
                .replaceAll("/index\\.html$", "");

        return normalized;
    }

}
package com.zzkj.zei.utils;

import lombok.Data;

/**
 * FileName: interfaceNodeData
 * Author: wzk
 * Date:2025/4/30 17:00
 */
@Data
public class interfaceNodeData {
    String url;
    String data;
    String method;
    String params;
    interfaceNodeData(){

    }
    interfaceNodeData(String url,String method,String data,String params){
        this.url = url;
        this.method = method;
        this.data = data;
        this.params = params;
    }
}


 


网站公告

今日签到

点亮在社区的每一天
去签到