目前主流的页面静态技术都是基于模板生成的,但是对于一些采用ajax+js渲染的页面,这种方法是无能为力的。要解决这个问题,首先要有一个能模拟浏览器的运行环境,其他问题都比较容易解决。能模拟浏览器的技术有好多,seleninum , htmlunit等。其中htmlunit是java开发用无界面的浏览器,速度和性能非常好,对html建模并且提供API来访问页面,点击链接等等,不需要任务驱动程序 ,提供javascript执行环境,现在很多支持ajax网络爬虫也是在它基础上实现的。
如何基于htmlunit实现ajax页面静态化呢?下面我用一个例子阐述吧,没什么比用代码更直接清楚。这个例子有个ajax渲染的页面,页面主要有两块内容,顶部是用户信息,下面是读取osc 首页的综合资讯,基本需求是综合资讯内容要静态化,用户信息不需要。
index.jsp页面代码
<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>Insert title here</title> </head> <body whitelist="/userServlet" > <div id="top"></div> <h1>osc综合资讯</h1> <div id="content"></div> <div> <button onclick="generateStaticHtml(this);">生成静态页面</button> <script type="text/javascript"> //渲染页面 (function renderPage(){ var xmlhttp = new XMLHttpRequest() ; xmlhttp.onreadystatechange=function() { if (xmlhttp.readyState==4 && xmlhttp.status==200) { document.getElementById("top").innerText=xmlhttp.responseText; } } xmlhttp.open("GET","${pageContext.request.contextPath }/userServlet",true); xmlhttp.send(); var xmlhttp2 = new XMLHttpRequest() ; xmlhttp2.onreadystatechange=function() { if (xmlhttp2.readyState==4 && xmlhttp2.status==200) { document.getElementById("content").innerHTML=xmlhttp2.responseText; } } xmlhttp2.open("GET","${pageContext.request.contextPath }/contentServlet",true); xmlhttp2.send(); })() ; function generateStaticHtml(btn){ btn.innerText = "在处理中,请稍后" var xmlhttp = new XMLHttpRequest() ; xmlhttp.onreadystatechange=function() { if (xmlhttp.readyState==4 && xmlhttp.status==200) { btn.innerText ="重新生成" ; window.open("${pageContext.request.contextPath }/index.html") ; } } xmlhttp.open("GET","${pageContext.request.contextPath }/generateStaticServlet",true); xmlhttp.send(); } </script> </div> </body> </html>
动态页面效果
注意上面图的两个ajax是加载动态内容触发,然后用javascript渲染到页面
点击"生成静态页面“按钮会触发后台调用静态组件生成静态页面(index.html)
/** * 触发生成静态页面 */ protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { new StaticHtml().process("http://127.0.0.1:8080/ajax/index.jsp", request.getServletContext().getRealPath("/index.html")); }
/** * 生成静态页面组件 * @author Wen * */ public class StaticHtml { //javascript 拦截 ajax 请求 private final static String ajaxInterceptJs = "(function(XHR) { " + "var open = XHR.prototype.open;" + "var send = XHR.prototype.send;" + "%s" + "XHR.prototype.open = function(method, url, async, user, pass) {" + " this._url = url;" + " open.call(this, method, url, async, user, pass);" + "};" + "XHR.prototype.send = function(data) {" + " if(XHR[this._url]){" + " this.abort() ;" + " delete XHR[this._url] ;" + " return ;" + " }" + " send.call(this, data);" + "}" + "})(XMLHttpRequest);"; public void process(String dynamicUrl, String staticPath) throws FailingHttpStatusCodeException, MalformedURLException, IOException { final WebClient webClient = new WebClient(); LogAjaxController logAjaxController = new LogAjaxController(); webClient.setAjaxController(logAjaxController); final HtmlPage page = webClient.getPage(dynamicUrl); //取出加载页面过程中触发的ajax url List<String> ajaxRequests = logAjaxController.getAjaxRequests(); //页面完整html(包含ajax动态获取) String htmlContent = page.asXml(); //页面还包含ajax加载代码,需要把这些jax请求拦截下来,但是有些情况是不须要拦截的就要添加到白名单 Document document = Jsoup.parse(htmlContent); String whitelistStr = document.body().attr("whitelist"); if (ajaxRequests.size() > 0 && whitelistStr != null) { String[] whitelist = whitelistStr.split(","); List<String> list = new ArrayList<String>(); for (String url : ajaxRequests) { boolean find = false; for (String wlUrl : whitelist) { if (url.indexOf(wlUrl) != -1) { find = true; break; } } if (!find) { list.add(url); } } ajaxRequests = list; } if( ajaxRequests.size() > 0 ){ Element script = new Element(Tag.valueOf("script"), ""); script.attr("type", "text/javascript"); StringBuilder sb = new StringBuilder() ; for(String url : ajaxRequests ){ sb.append("XHR['").append(url).append("']=true;") ; } script.text( String.format(ajaxInterceptJs, sb.toString()) ) ; document.head().prependChild(script);//注入拦截ajax js 保证拦截ajax的代码最先执行 } //写入文件 FileUtils.writeStringToFile(new File(staticPath), document.html() ,"utf-8"); webClient.closeAllWindows(); } /** * 记录所有ajax请求url * * @author Wen * */ static class LogAjaxController extends NicelyResynchronizingAjaxController { private List<String> ajaxRequests = new ArrayList<String>(); @Override public boolean processSynchron(HtmlPage page, WebRequest settings, boolean async) { ajaxRequests.add(settings.getUrl().getPath()); return super.processSynchron(page, settings, async); } public List<String> getAjaxRequests() { return Collections.unmodifiableList(ajaxRequests); } } }
静态页面效果
页面效果和动态的index.jsp是一样的,但此时只有一个ajax请求刷新用户信息及访问次数,综合资讯的内容已经被静态化的。基本算是实现了我的需要。需要说明有几个地方。
一、如何通htmlunit取得ajax请求的url
htmlunit提供了处理ajax请求接口,我们只要简单继承NicelyResynchronizingAjaxController这个类,把ajax请求的url记录下来就可以了
二、静态页面也包含ajax加载综合资讯代码,这请求是处理拦截下来的
实际上静态页面会包含有跟原来页面一模一样的ajax加载动态内容代码,这些代码对于静态页面来说没有用的,因为内容都被静态化,没必要再发请求加载。我们通在生成静态页面会有页面注入以下javascript,可以把没必要的请求拦截下来(只拦截一次)
三、不需要被拦截ajax要怎样设置
在index.jsp 代码的body标签有个whitelist属性可设置ajax白名单,注入拦截代码时会读取这个值过虑掉,默认会拦截掉页面渲染触发的所有ajax请求。
四、未解决的问题
htmlunit只能调page.asXml()取页面html内容,但是这个方法不是很完美,它只是返回标准的xml代码,会把html的DOCTYPE声明删除掉,这个会导致浏览解析css会出错,临时办法把<!--?xml version="1.0" encoding="UTF-8"?-->替换回原代码页面的DOCTYPE。查遍了htmlunit文档,都没有找到可以直接获取完整html源代码的方法,找到的同学可以告诉我。
完成的例子代码下载http://pan.baidu.com/s/15qyPr