爬虫框架Playwright在Java环境下的开发实践

简介: 爬虫框架Playwright在Java环境下的开发实践
<dependency>
    <groupId>com.microsoft.playwright</groupId>
    <artifactId>playwright</artifactId>
    <version>1.20.0</version>
</dependency>

官方文档

出现问题首要的是查看官方文档

简单的页面抓取

public static BrowserContext createContext(Browser browser) {
   
    BrowserContext context = browser.newContext(new Browser.NewContextOptions()
            .setIgnoreHTTPSErrors(true)
            .setJavaScriptEnabled(true)
            .setViewportSize(1920, 1080)
            .setUserAgent("Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.130 Safari/537.36"));
    return context;
}


public static Browser createBrowser(String name, Playwright playwright) {
   
    try {
   
        switch (name) {
   
            case "firefox":
                return playwright.firefox().launch();
            case "chromium":
                return playwright.chromium().launch(new BrowserType.LaunchOptions().setHeadless(false).setTimeout(120 * 1000));
            case "webkit":
                return playwright.webkit().launch();
        }
    } catch (Exception e) {
   
        e.printStackTrace();
    }
    return null;
}

public static Page initPage(){
   
    return initPage("chromium");
}

public static Page initPage(String browserName){
   
    Playwright playwright = Playwright.create();
    Browser browser = createBrowser(browserName, playwright);
    BrowserContext browserContext = createContext(browser);
    Page page = browserContext.newPage();
    return page;
}

抓取一个页面,获取内容

抓个使用了异步加载数据的页面,以 https://www.taobao.com/ 为例。

public static String getContent(String url){
   
    String trueUrl = DomainUtils.formatUrl(url);
    Page page = initPage();
    try {
   
        page.navigate(trueUrl);
        String content = page.content();
        return content;
    }catch (Exception e){
   
        e.printStackTrace();
    }finally {
   
        BrowserContext context = page.context();
        Browser browser = context.browser();
        page.close();
        context.close();
        browser.close();
    }
    return "";
}

有时候,页面会加载很久,然后超时,所以我们需要考虑超时的问题。

page.navigate(trueUrl, new Page.NavigateOptions().setTimeout(120 * 1000));

Page.navigate 官方地址 ,具体该函数的使用方法,参考官方文档的写法。

拦截请求和响应

public static void handleOn(Page page) {
   
    page.onRequest(request -> {
   
        System.out.println("请求 " + request.method() + " " + request.url());
    });
    page.onResponse(response -> {
   
        System.out.println("响应 " + response.request().method() + " " + response.request().url() + " " + response.status());
    });
}

将该函数放到调用 page.navigate 之前,得到的部分日志结果如下:
image.png
现在对handleOn函数进行函数化,使之能够传入函数作为参数。
函数式编程入门
修改后的handleOn函数如下:

public static void handleOn(Page page, Consumer<Request> requestConsumer, Consumer<Response> responseConsumer) {
   
    page.onRequest(request -> {
   
        requestConsumer.accept(request);
    });
    page.onResponse(response -> {
   
        responseConsumer.accept(response);
    });
}

调用方式如下:

handleOn(page,
                request -> {
   

                }, response -> {
   

                });

获取页面中所有的链接

  • 使用selector的方式抓取所有结构化数据中的链接
    • playwright支持的selector的方式:传送门
    • [href]
    • [src]
  • 利用响应拦截器,获取请求链接和响应头里面的链接
  • 对页面进行正则匹配解析

代码如下:

public static Set<String> getAllUrls(String url) {
   
    String trueUrl = DomainUtils.formatUrl(url);
    Page page = initPage();
    try {
   
        Set<String> urls = new HashSet<>();
        handleOn(page,
                request -> {
   
                    urls.add(request.url());
                }, response -> {
   
                    urls.add(response.request().url());
                    Map<String, String> allHeaders = response.allHeaders();
                    for (Map.Entry<String, String> header : allHeaders.entrySet()) {
   
                        List<String> tmpUrls = HtmlUtil.getUrls(header.getValue());
                        urls.addAll(tmpUrls);
                    }
                });
        page.navigate(trueUrl, new Page.NavigateOptions().setTimeout(120 * 1000));
        List<String> hrefUrls = evalAndGetValue(page, "[href]", PlaywrightFunc.HREF.getFuncStr());
        List<String> srcUrls = evalAndGetValue(page, "[src]", PlaywrightFunc.SRC.getFuncStr());
        String content = page.content();
        List<String> htmlUrls = HtmlUtil.getUrls(content);
        urls.addAll(hrefUrls);
        urls.addAll(htmlUrls);
        urls.addAll(srcUrls);
        return urls;
    } catch (Exception e) {
   
        e.printStackTrace();
    } finally {
   
        BrowserContext context = page.context();
        Browser browser = context.browser();
        page.close();
        context.close();
        browser.close();
    }
    return new HashSet<>();
}

这里使用到了 page.evalOnSelectorAll,我对此进行封装,封装的函数为evalAndGetValue,代码如下:

public static List<String> evalAndGetValue(Page page, String selector, String functionStr) {
   
    List<String> ans = (List<String>) page.evalOnSelectorAll(selector, functionStr);
    List<String> res = new ArrayList<>();
    for (String x : ans) {
   
        if (StringUtils.hasText(x)) {
   
            res.add(x);
        }
    }
    return res;
}

其实这个函数不封装页没啥问题,这里封装了一遍主要是为了去除为空的内容。该函数的目的是对该selector注入一个函数表达式进行执行,我这边目前积累的函数如下:

public enum PlaywrightFunc {
   
    HREF(1,"function do123(as) {\n" +
            "    var ans = [];\n" +
            "    for (var i =0;i<as.length;i++){\n" +
            "        ans.push(as[i].href);\n" +
            "    }\n" +
            "    return ans;\n" +
            "}","获取链接"),
    INNER_TEXT(2,"function do123(as) {\n" +
            "    var ans = [];\n" +
            "    for (var i =0;i<as.length;i++){\n" +
            "        ans.push(as[i].innerText);\n" +
            "    }\n" +
            "    return ans;\n" +
            "}","获取内容"),
    INNER_HTML(3,"function do123(as) {\n" +
            "    var ans = [];\n" +
            "    for (var i =0;i<as.length;i++){\n" +
            "        ans.push(as[i].innerHtml);\n" +
            "    }\n" +
            "    return ans;\n" +
            "}","获取里面的代码"),
    SRC(4,"function do123(as) {\n" +
            "    var ans = [];\n" +
            "    for (var i =0;i<as.length;i++){\n" +
            "        ans.push(as[i].src);\n" +
            "    }\n" +
            "    return ans;\n" +
            "}","获取链接"),
    DELETE_WEBDRIVER(5,"Object.defineProperties(navigator, {webdriver:{get:()=>undefined}});","去除webdriver属性"),

    ;

    PlaywrightFunc(Integer code, String funcStr, String info) {
   
        this.funcStr = funcStr;
        this.code = code;
        this.info = info;
    }

    private String funcStr;
    private String info;
    private Integer code;

    public String getInfo() {
   
        return info;
    }

    public Integer getCode() {
   
        return code;
    }

    public String getFuncStr() {
   
        return funcStr;
    }

    public void setInfo(String info) {
   
        this.info = info;
    }

    public void setCode(Integer code) {
   
        this.code = code;
    }

    public void setFuncStr(String funcStr) {
   
        this.funcStr = funcStr;
    }
}

再次对函数进行抽象

写到这里我发现每个地方都得写打开关闭浏览器的操作,这部分代码是重复代码,所以考虑使用函数式编程的方式,将这部分独立出来,代码如下:

public static void getAns(String url, BiConsumer<Page, String> a) {
   
    String trueUrl = DomainUtils.formatUrl(url);
    Playwright playwright = Playwright.create();
    Page page = initPage(playwright);
    try {
   
        a.accept(page, trueUrl);
    } catch (Exception e) {
   
        e.printStackTrace();
    } finally {
   
        BrowserContext context = page.context();
        Browser browser = context.browser();
        page.close();
        context.close();
        browser.close();
        playwright.close();
    }
}

不过这里目前没有设置返回值,所以需要外部的数据结构作为要存储结果的结构。那么刚才的方法可以改写为如下部分。

Set<String> urls1 = new HashSet<>();
getAns(url, (Page page, String trueUrl) -> {
   
    Set<String> tmp = new HashSet<>();
    handleOn(page,
            request -> {
   
                tmp.add(request.url());
            }, response -> {
   
                tmp.add(response.request().url());
                Map<String, String> allHeaders = response.allHeaders();
                for (Map.Entry<String, String> header : allHeaders.entrySet()) {
   
                    List<String> tmpUrls = HtmlUtil.getUrls(header.getValue());
                    tmp.addAll(tmpUrls);
                }
            });
    page.navigate(trueUrl, new Page.NavigateOptions().setTimeout(120 * 1000));
    page.waitForTimeout(3*1000);
    List<String> hrefUrls = evalAndGetValue(page, "[href]", PlaywrightFunc.HREF.getFuncStr());
    List<String> srcUrls = evalAndGetValue(page, "[src]", PlaywrightFunc.SRC.getFuncStr());
    String content = page.content();
    List<String> htmlUrls = HtmlUtil.getUrls(content);
    System.out.println("1 - 1 " + hrefUrls.size());
    System.out.println("1 - 2 " + srcUrls.size());
    System.out.println("1 - 3 " + htmlUrls.size());
    System.out.println("1 - 4 " + tmp.size());
    tmp.addAll(hrefUrls);
    tmp.addAll(htmlUrls);
    tmp.addAll(srcUrls);
    urls1.addAll(tmp);
});

获取页面中所有图片资源

获取图片资源这个问题本质上是上一个问题的子集。我们以上面抽象的方法来实现这么一个方法。

public static Set<String> getAllImages(String url) {
   
    Set<String> ans = new HashSet<>();
    getAns(url, (Page page, String trueUrl) -> {
   
        handleOn(page, request -> {
   

        }, response -> {
   
            if (HtmlUtil.isImage(response.request().url())) {
   
                ans.add(response.request().url());
            }
        });
        page.navigate(trueUrl, new Page.NavigateOptions().setTimeout(120 * 1000));
        page.waitForTimeout(3 * 1000);
        List<String> hrefUrls = evalAndGetValue(page, "[href]", PlaywrightFunc.HREF.getFuncStr());
        for (int i=0;i<hrefUrls.size();i++){
   
            if (HtmlUtil.isImage(hrefUrls.get(i))){
   
                ans.add(hrefUrls.get(i));
            }
        }
        List<String> srcUrls = evalAndGetValue(page, "[src]", PlaywrightFunc.SRC.getFuncStr());
        for (int i=0;i<srcUrls.size();i++){
   
            if (HtmlUtil.isImage(srcUrls.get(i))){
   
                ans.add(srcUrls.get(i));
            }
        }
        String content = page.content();
        List<String> htmlUrls = HtmlUtil.getUrls(content);
        for (int i=0;i<htmlUrls.size();i++){
   
            if (HtmlUtil.isImage(htmlUrls.get(i))){
   
                ans.add(htmlUrls.get(i));
            }
        }
    });
    return ans;
}

滚动到底

对于淘宝这种页面,很有可能会需要滚动,所以我们需要实现一个滚动,由于滚动有可能是局部组件滚动,所以我们不能简单的使用窗口滚动的方式。

/**
     * 滚轮向下滚动
     * 需要调整滚动量 和 检测的频率范围
     * 400 8
     * 这个滚动下降率和检测频率范围应该是变化的
     * 这里采取Log函数来做下降算法
     *
     * @param cnt
     * @param page
     * @param selector
     */
public static void scroll(AtomicInteger cnt, Page page, String selector) {
   
    int lastCnt = cnt.get();
    int flag = 0;
    int cntFlag = 2;
    for (int i = 0; i < 100; i++) {
   
        page.evalOnSelector(selector, "(node,i)=>{
   {\n" +
                "        node.scrollTo((i-1)*400,i*400)\n" +
                "    }}", i);
        page.waitForTimeout(1000);
        flag++;
        if (flag * 1.0 >= MathUtils.getLogAN(2, cntFlag) * 8.0) {
   
            int nowCnt = cnt.get();
            if (lastCnt == nowCnt) {
   
                break;
            }
            flag = 0;
            lastCnt = nowCnt;
            cntFlag++;
        }
    }
}

这里的实现原理是每次利用Log函数进行次数下降,然后判断滚动这么多次之后,是否产生了新的请求,如果没有产生新的请求,就代表滚动无效,有可能滚动到底了,如果有新的请求就说明滚动有效,就继续滚动。
使用的方式如下:

AtomicInteger cnt = new AtomicInteger(0);
// 然后在handleOn里面加上监听
handleOn(page, request -> {
   
                cnt.incrementAndGet();
            }, response -> {
   

            });
// 最后调用滚动方法即可
... ...
scroll(cnt, page, "html");

有了这些基础的知识点之后,我们就具备利用playwright实现web漏洞扫描器的前奏步骤的基础能力了。

目录
相关文章
|
1月前
|
数据采集 存储 缓存
PHP爬虫的使用与开发
本文深入探讨了PHP爬虫的使用与开发,涵盖基本原理、关键技术、开发实践及优化策略。从发送HTTP请求、解析HTML到数据存储,再到处理反爬机制,全面指导读者构建高效可靠的爬虫程序。
51 3
|
6天前
|
存储 安全 Java
Java 集合框架中的老炮与新秀:HashTable 和 HashMap 谁更胜一筹?
嗨,大家好,我是技术伙伴小米。今天通过讲故事的方式,详细介绍 Java 中 HashMap 和 HashTable 的区别。从版本、线程安全、null 值支持、性能及迭代器行为等方面对比,帮助你轻松应对面试中的经典问题。HashMap 更高效灵活,适合单线程或需手动处理线程安全的场景;HashTable 较古老,线程安全但性能不佳。现代项目推荐使用 ConcurrentHashMap。关注我的公众号“软件求生”,获取更多技术干货!
29 3
|
12天前
|
移动开发 前端开发 Java
Java最新图形化界面开发技术——JavaFx教程(含UI控件用法介绍、属性绑定、事件监听、FXML)
JavaFX是Java的下一代图形用户界面工具包。JavaFX是一组图形和媒体API,我们可以用它们来创建和部署富客户端应用程序。 JavaFX允许开发人员快速构建丰富的跨平台应用程序,允许开发人员在单个编程接口中组合图形,动画和UI控件。本文详细介绍了JavaFx的常见用法,相信读完本教程你一定有所收获!
Java最新图形化界面开发技术——JavaFx教程(含UI控件用法介绍、属性绑定、事件监听、FXML)
|
22天前
|
存储 JavaScript 前端开发
基于 SpringBoot 和 Vue 开发校园点餐订餐外卖跑腿Java源码
一个非常实用的校园外卖系统,基于 SpringBoot 和 Vue 的开发。这一系统源于黑马的外卖案例项目 经过站长的进一步改进和优化,提供了更丰富的功能和更高的可用性。 这个项目的架构设计非常有趣。虽然它采用了SpringBoot和Vue的组合,但并不是一个完全分离的项目。 前端视图通过JS的方式引入了Vue和Element UI,既能利用Vue的快速开发优势,
107 13
|
27天前
|
算法 Java API
如何使用Java开发获得淘宝商品描述API接口?
本文详细介绍如何使用Java开发调用淘宝商品描述API接口,涵盖从注册淘宝开放平台账号、阅读平台规则、创建应用并申请接口权限,到安装开发工具、配置开发环境、获取访问令牌,以及具体的Java代码实现和注意事项。通过遵循这些步骤,开发者可以高效地获取商品详情、描述及图片等信息,为项目和业务增添价值。
58 10
|
21天前
|
前端开发 Java 测试技术
java日常开发中如何写出优雅的好维护的代码
代码可读性太差,实际是给团队后续开发中埋坑,优化在平时,没有那个团队会说我专门给你一个月来优化之前的代码,所以在日常开发中就要多注意可读性问题,不要写出几天之后自己都看不懂的代码。
56 2
|
1月前
|
安全 Java 数据库连接
Java中的异常处理:理解与实践
在Java的世界里,异常处理是维护代码健壮性的守门人。本文将带你深入理解Java的异常机制,通过直观的例子展示如何优雅地处理错误和异常。我们将从基本的try-catch结构出发,探索更复杂的finally块、自定义异常类以及throw关键字的使用。文章旨在通过深入浅出的方式,帮助你构建一个更加稳定和可靠的应用程序。
33 5
|
30天前
|
JavaScript 安全 Java
java版药品不良反应智能监测系统源码,采用SpringBoot、Vue、MySQL技术开发
基于B/S架构,采用Java、SpringBoot、Vue、MySQL等技术自主研发的ADR智能监测系统,适用于三甲医院,支持二次开发。该系统能自动监测全院患者药物不良反应,通过移动端和PC端实时反馈,提升用药安全。系统涵盖规则管理、监测报告、系统管理三大模块,确保精准、高效地处理ADR事件。
|
1月前
|
安全 Java 程序员
Java内存模型的深入理解与实践
本文旨在深入探讨Java内存模型(JMM)的核心概念,包括原子性、可见性和有序性,并通过实例代码分析这些特性在实际编程中的应用。我们将从理论到实践,逐步揭示JMM在多线程编程中的重要性和复杂性,帮助读者构建更加健壮的并发程序。
|
Java Linux
java环境配置 linux
java环境配置 linux
79 0