java直接操作chrome谷歌浏览器 实现网页爬虫

java | 2020-03-14 21:37:18

我知道程序是可以调用chrome浏览器的,我以为很复杂需要安装除了chrome以外的driver程序才能驱动chrome,但是经过研究发现我错了,无论使用java或python或其他程序操作都一样简单,方法也一样。当然也有现成封装好的api,例如nodejs pyppeteer,前介绍过的Java操作Chrome浏览器的API库-cdp4j。但我还是觉得直接去操作chrome很直接方便。下面就介绍java如何直接操作chrome。

1.程序操作chrome浏览器原理

1.1Chrome DevTools Protocol 浏览器开发工具协议

使用程序操作chrome,你不需要干其他任何事,除了Chrome DevTools Protocol 。

官方文档:https://chromedevtools.github.io/devtools-protocol/。

                https://vanilla.aslushnikov.com/

打开官网可以看到协议有三个版本

The latest (tip-of-tree) protocol (tot) 最新版
v8-inspector protocol (v8) nodejs版
stable 1.2 protocol (1-2)  稳定版
stable 1.3 protocol (1-3) 稳定版

使用tot最新版一般没什么问题,稳定版可能功能没那么多,上线前测试好久行

Chrome DevTools Protocol按名称理解就是一个协议,我只要按照这个协议来操作chrome就可以了,无论什么程序都按照同样的这个协议来操作chrome.只要使用http和webSocket按照他的文档编写json来和chrome通信。

1.2那怎么能操作到chrome?

Chrome Debug API包括两个部分:标签管理和页面管理。首先我们介绍标签管理部分,它是Rest形式的API接口,而页面管理是webSocket+json就能和html页面通信,json的格式按照官方文档的格式。

首先你要安装chrome,参考:linux centos安装google chrome浏览器使用headless无头模式,然后你可以用命令启动 

chrome --headless --disable-gpu --user-data-dir= --remote-debugging-port=9222

--headless表示无头浏览器,不用看到界面,windows调试可以不用这个参数,正式使用的时候要使用这个,特别是linux服务器

--disable-gpu参数在未来可能不需要

--user-data-dir 指定不同目录,代表启动不同的实例,这个并发操作很有用。

--remote-debugging-port 远程调试模式打开浏览器,关键是这个

先把浏览器关掉,再用命令启动,先不使用--headless测试一下,浏览器起来后访问

浏览器访问http://127.0.0.1:9222/json

[ {
   "description": "",
   "devtoolsFrontendUrl": "/devtools/inspector.html?ws=127.0.0.1:9222/devtools/page/5FCC37E7406644DEEFB12F840E904419",
   "id": "5FCC37E7406644DEEFB12F840E904419",
   "title": "127.0.0.1:9222/json",
   "type": "page",
   "url": "http://127.0.0.1:9222/json",
   "webSocketDebuggerUrl": "ws://127.0.0.1:9222/devtools/page/5FCC37E7406644DEEFB12F840E904419"
}, {
   "description": "",
   "devtoolsFrontendUrl": "/devtools/inspector.html?ws=127.0.0.1:9222/devtools/page/F44194E32AEA4076FC645C86B0B66DFB",
   "faviconUrl": "http://bcxw.net/favicon.ico",
   "id": "F44194E32AEA4076FC645C86B0B66DFB",
   "title": "文章编辑_编程学问网",
   "type": "page",
   "url": "http://bcxw.net/user/articleEditor.html?id=703",
   "webSocketDebuggerUrl": "ws://127.0.0.1:9222/devtools/page/F44194E32AEA4076FC645C86B0B66DFB"
} ]

看到这个页面说明你已经成功了

chrome支持简单操作使用http+json

常见的访问命令如下:

  • http://127.0.0.1:9222/json :查看已经打开的Tab列表

  • http://127.0.0.1:9222/json/version : 查看浏览器版本信息

  • http://127.0.0.1:9222/json/new?http://www.baidu.com : 新开Tab打开指定地址

  • http://127.0.0.1:9222/json/close/92615aad-5862-48d5-983d-248468e9741a : 关闭指定Id的Tab页面

  • http://127.0.0.1:9222/json/activate/92615aad-5862-48d5-983d-248468e9741a : 切换到指定Id的Tab页面

复杂的操作就需要使用webSocket请求webSocketDebuggerUrl对应的地址来操作html页面,具体的访问参数参考上面官方文档,下面介绍如何通过程序和chrome通信

 

2.windows cmd 命令测试操作chrome

我这里主要用windows来演示,linux上面道理也一样,参考:linux centos安装google chrome浏览器使用headless无头模式

请先下载安装google chrome谷歌浏览器,我安装在C:\Users\Administrator\AppData\Local\Google\Chrome\Application

2.1配置环境变量

windows搜索编辑系统环境变量-点击环境变量-选择系统变量path-点击编辑-点击新建-把google浏览器的安装地址粘贴进去(C:\Users\Administrator\AppData\Local\Google\Chrome\Application)

然后重新打开cmd命令-直接输入chrome-发现打开弹出浏览器

2.2测试和chrome浏览器通信

2.2.1启动chrome debug模式

cmd命令输入(先不使用无头模式方便看效果,且指定了user-data-dir表示启动新的实例,不要和你当前打开的浏览器冲突了):

chrome --disable-gpu --user-data-dir=d:/test/chrome --remote-debugging-port=9222

linux的命令是:

google-chrome --headless --disable-gpu --user-data-dir=/test/chrome --remote-debugging-port=9222

输入命令回车发现马上弹出google浏览器,不要管理,最小化就可以

2.2.2 http和chrome浏览器通信

上面说到和chrome浏览器通信分为标签管理和页面管理,标签管理用http就能做到,而页面管理则要使用websocket.这里我们先测试http通信

浏览器打开地址:http://127.0.0.1:9222/json

页面会输出

[ {
   "description": "",
   "devtoolsFrontendUrl": "/devtools/inspector.html?ws=127.0.0.1:9222/devtools/page/33BFC127FDD36501A0DD0202D4FC9A01",
   "id": "33BFC127FDD36501A0DD0202D4FC9A01",
   "title": "欢迎使用 Chrome",
   "type": "page",
   "url": "chrome://welcome/",
   "webSocketDebuggerUrl": "ws://127.0.0.1:9222/devtools/page/33BFC127FDD36501A0DD0202D4FC9A01"
} ]

这就是你刚才命令打开的浏览器,说明你和浏览器通信成功

  • Id:页面的id信息

  • devtoolsFrontendUrl:开发工具Url,可以通过chrome访问这个url来实现内置的调试工具访问

  • type:当前进程的类型,只有类型为page的才是浏览器页面,其余类型的是后台背景进程,我们不需要操作这类对象

  • url:当前页面访问的地址

  • webSocketDebuggerUrl:当前页面的调试接口地址

2.2.3 websocket操作chrome浏览器

这里举一个websocket例子,进行页面跳转,来测试操作chrome浏览器,要用到webSocketDebuggerUrl这个属性,使用在线websocket工具来测试http://www.websocket-test.com/。

复制你的webSocketDebuggerUrl属性值,粘贴到工具里点击断开再点击连接,看是否能连接上。

找到文档中页面跳转的章节https://chromedevtools.github.io/devtools-protocol/tot/Page#method-navigate

按照文档的要求组装json

{"method":"Page.navigate","params":{"url":"http://bcxw.net"},"id":1}

复制到消息窗口,发送,发现刚才命令打开的浏览器页面跳转了,这下激动了吧。

需要注意的是除了官方协议文档要求的参数,我们还必须带id参数,这个参数是你自己用来表示你自己的指令,否则会报错

{error:{code:-32600,message:"Message must have integer 'id' property"}}

不论多复杂的操作,都能在文档里面找到,然后发websocket消息给浏览器,就能实现。好了我们已经会了如何操纵通信chrome浏览器,下面就是java的案例

3.java操作通信chrome浏览器

首先要安装google浏览器,配置环境变量,前面详细介绍了

在写代码前还是要多看看官方文档:https://chromedevtools.github.io/devtools-protocol/ 

因为我们会面临一个问题,就是浏览器browser-标签页target-页面page-子页面iframe,这是一个有层级的结构,我们调用到某一层级,得从上级拼接参数然后连接websocket,然后发起操作信息,这很不方便,我们希望得是扁平化的,一个通信地址传不同的参数就能操作不同的对象,chromedevtools官方页有这样的考虑,就是传sessionId,

参考:https://github.com/aslushnikov/getting-started-with-cdp/blob/master/README.md,

如果不传sessionId,你拿browser的通信地址是操作不到page页面的。

下面写一个java操作浏览器的完整案例

3.1pom

    <dependency>
      <groupId>org.java-websocket</groupId>
      <artifactId>Java-WebSocket</artifactId>
      <version>1.4.0</version>
    </dependency>

    <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>fastjson</artifactId>
    </dependency>

3.2 java代码

    //浏览器标签Id
    static String targetId="";
    //页面sessionId
    static String sessionId="";
    public static void main(String[] args) {

        try {

            //命令启动浏览器
            StringBuffer strCmd=new StringBuffer();
            String os = System.getProperty("os.name");
            if(os.toLowerCase().startsWith("windows")){
                strCmd.append("chrome ");
            }else if(os.toLowerCase().startsWith("linux")){
                strCmd.append("google-chrome ");
            }
            //strCmd.append("--headless ");
            strCmd.append("--disable-gpu ");
            strCmd.append("--enable-automation ");
            strCmd.append("--user-data-dir=chromeHome22 ");
            //port=0 表示自动分配端口并返回启动信息
            strCmd.append("--remote-debugging-port=0 ");

            Process process=Runtime.getRuntime().exec(strCmd.toString());

            //获取启动返回信息
            InputStreamReader isr=new InputStreamReader(process.getErrorStream());
            BufferedReader br=new BufferedReader(isr);

            long startTime=new Date().getTime();
            String browserWs="";
            while(true){
                if(new Date().getTime()-startTime>=5000){
                    break;
                }
                browserWs=br.readLine();
                if(browserWs!=null&&browserWs.startsWith("DevTools")){
                    browserWs=browserWs.substring(browserWs.indexOf("ws:"));
                    break;
                }else{
                    Thread.sleep(100);
                }
            }
            if(StringUtils.isEmpty(browserWs)){
               throw new RuntimeException("初始化chrome失败");
            }
            System.out.println(browserWs);


            //websocket 通信
            URI uri=new URI(browserWs);
            WebSocketClient webSocketClient=new WebSocketClient(uri) {
                @Override
                public void onOpen(ServerHandshake serverHandshake) {
                    System.out.println("已经连接");
                }

                @Override
                public void onMessage(String s) {
                    System.out.println("接受到消息:"+s);
                    JSONObject message=JSON.parseObject(s);
                    if(message.getIntValue("id")==1){
                        //如果是 获取 target的回调
                        targetId=message.getJSONObject("result").getJSONArray("targetInfos").getJSONObject(0).getString("targetId");
                    }else if(message.getIntValue("id")==2){
                        //如果是 获取 target的回调
                        sessionId=message.getJSONObject("result").getString("sessionId");
                    }
                }

                @Override
                public void onClose(int i, String s, boolean b) {

                }

                @Override
                public void onError(Exception e) {

                }
            };
            webSocketClient.connect();

            //我这里先简单停留方法获取消息返回值,实际可以使用 sleep和notify来实现
            Thread.sleep(2000);


            //发送消息 获取target 我在消息接受里面简单处理了一下
            webSocketClient.send("{\"method\":\"Target.getTargets\",\"id\":1}");
            Thread.sleep(2000);
            //获取sessionId
            webSocketClient.send("{\"id\":2,\"method\":\"Target.attachToTarget\",\"params\":{\"flatten\":true,\"targetId\":\""+targetId+"\"}}");
            Thread.sleep(2000);
            //使用sessionId 操作页面
            webSocketClient.send("{\"sessionId\":\""+sessionId+"\",\"method\":\"Page.navigate\",\"params\":{\"url\":\"http://bcxw.net\"},\"id\":3}");

            Thread.sleep(10000);



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


    }

这已经是一个很完善的案例,只不过实际开发要把代码结构化,任何操作都可以从这个案例出发来找解决方案,前面的介绍文档部分花时间最多,很有价值,后面还会有更多复杂操作的案例!

登录后即可回复 登录 | 注册
    
关注编程学问公众号