htmlunit 简介:
htmlunit 是一款开源的 java 页面分析工具,启动 htmlunit 之后,底层会启动一个无界面浏览器,用户可以指定浏览器类型:firefox、ie 等,如果不指定,默认采用 INTERNET_EXPLORER_7:
WebClient webClient = new WebClient(BrowserVersion.FIREFOX_3_6);

通过简单的调用:
HtmlPage page = webClient.getPage(url);
即可得到页面的 HtmlPage 表示,然后通过:
InputStream is = targetPage.getWebResponse().getContentAsStream()
即可得到页面的输入流,从而得到页面的源码,这对做网络爬虫的项目来说,很有用。
当然,也可以从 page 中得更多的页面元素。

很重要的一点是,HtmlUnit 提供对执行 javascript 的支持:
page.executeJavaScript(javascript)
执行 js 之后,返回一个 ScriptResult 对象,通过该对象可以拿到执行 js 之后的页面等信息。默认情况下,内部浏览器在执行 js 之后,将做页面跳转,跳转到执行 js 之后生成的新页面,如果执行 js 失败,将不执行页面跳转。

htmlunit 执行 js 的大致过程如下:

从图中可以看出,htmlunit 执行js时,会将整个页面 download 下来,而很多时候,我们执行 js,只是因为需要执行后后生成的 url,不必要的频繁页面 download 不但会增加程序运行时长,也会加重网络负载。有下面两种方案可以完成这个需求:

1). 第一种方法是拿到这个 url 之后,将其返回,但代码的调用层次较深,如果修改源码的话,需要修改的地方可能较多,实现起来可能有一定的复杂性和难度。
2). 第二种方法是,生成一个伪 response,而不是去真正获取页面的 response,用来构造所有的新 page。该方法具有代码改动小,实现方便的特点。

因第一种方法对源码的修改大,实现起来也比较困难,这里给出第二种方法的实现:

查看源码可以发现,在:

com.gargoylesoftware.htmlunit.javascript.host.location.java 类中,有这样一个方法:

public void jsxSet_href(final String newLocation) throws IOException {
        final HtmlPage page = (HtmlPage) getWindow(getStartingScope()).getWebWindow().getEnclosedPage();
        if (newLocation.startsWith(JavaScriptURLConnection.JAVASCRIPT_PREFIX)) {
            final String script = newLocation.substring(11);
            page.executeJavaScriptIfPossible(script, "new location value", 1);
            return;
        }
        try {
            final URL url = page.getFullyQualifiedUrl(newLocation);
            final URL oldUrl = page.getWebResponse().getWebRequest().getUrl();
            if (url.sameFile(oldUrl) && !StringUtils.equals(url.getRef(), oldUrl.getRef())) {
                // If we're just setting or modifying the hash, avoid a server hit.
                jsxSet_hash(newLocation);
                return;
            }

            final WebWindow webWindow = getWindow().getWebWindow();
            webWindow.getWebClient().download(webWindow, "", new WebRequest(url), "JS set location");            
        }
        catch (final MalformedURLException e) {
            LOG.error("jsxSet_location('" + newLocation + "') Got MalformedURLException", e);
            throw e;
        }
    }

第 18 行,就是去 download 页面,download 方法如下:


public void download(final WebWindow requestingWindow, final String target,
        final WebRequest request, final String description) {
        final WebWindow win = resolveWindow(requestingWindow, target);
        final URL url = request.getUrl();
        boolean justHashJump = false;
        if (win != null) {
            final Page page = win.getEnclosedPage();
            if (page instanceof HtmlPage && !((HtmlPage) page).isOnbeforeunloadAccepted()) {
                return;
            }
            final URL current = page.getWebResponse().getWebRequest().getUrl();
            if (url.sameFile(current) && !StringUtils.equals(current.getRef(), url.getRef())) {
                justHashJump = true;
            }
        }

        // verify if this load job doesn't already exist
        for (final LoadJob loadJob : loadQueue_) {
            if (loadJob.response_ == null) {
                continue;
            }
            final WebRequest otherRequest = loadJob.response_.getWebRequest();
            final URL otherUrl = otherRequest.getUrl();
            // TODO: investigate but it seems that IE considers query string too but not FF
            if (url.getPath().equals(otherUrl.getPath())
                && url.getHost().equals(otherUrl.getHost())
                && url.getProtocol().equals(otherUrl.getProtocol())
                && url.getPort() == otherUrl.getPort()
                && request.getHttpMethod() == otherRequest.getHttpMethod()) {
                return; // skip it;
            }
        }

        final LoadJob loadJob;
        if (justHashJump) {
            loadJob = new LoadJob(win, target, url);
        }
        else {
            try {
              final WebResponse response = loadWebResponse(request);
              loadJob = new LoadJob(requestingWindow, target, response);
            }
            catch (final IOException e) {
                throw new RuntimeException(e);
            }
        }
        loadQueue_.add(loadJob);
    }

第40, 41行,拿到一个页面的 response,然后根据该 response 生成一个 LoadJob 对象,放入loadQueue_ 队列,后续将从队列中取出该 LoadJob 对象,完成生成新页面并加载至浏览器的工作。我们只要修改这里 response 的生成方式,思路如下:


如果当前线程是第一次执行该 download 方法,就不对代码做修改,让其生成一个真正的 response,然后,将该 response 对象保存起来,待该线程后续再执行 js 进入该方法,不再生成 response 对象,而是将之前保存起来的 response 拿出来直接使用,并修改对应的 url 为执行 js 之后生成的 url 即可:

response.getWebRequest().setUrl(request.getUrl());

js 执行完成之后,返回的 ScriptResult  对应的 url ,就是执行 js 之后生成的 url 了,但如果去拿页面的源码的话,会得到 ”错误“ 的数据,这是因为我们每次都用了同一个 response,而不是 url 页面对应的 url 。因为我们的初衷就是得到正确的 url ,而不去 download 整个页面,所以这种 ”错误“ 不会影响我们的程序。