<html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>你好世界</title> </head> <body> <script>document.write('你好,世界');</script> </body> </html>
对不起,常见的网页抓取器对此是无能为力的,它们执行不了javascript脚本,所以你也得不到脚本执行的结果。
但很幸运的是,我们可以使用海葵--这个全球顶尖的抓取工具。
海葵是什么?
海葵是全球第一款基于浏览器构建的顶尖的垂直搜索专用网页抓取服务器软件,运行于LINUX系统之上。 它不仅能抓取网页动态内容,还能将网页结构化为XML,垂直搜索引擎可以方便地通过DOM操作来提取所关心的数据了。
与海葵比较起来,一般的网页抓取器有以下重大缺陷:
一、动态内容很难抓到
一般的网页抓取器,不论是通过socket调用,还是通过httpclient等工具,抓的都是“死”的网页数据,其中的javascript脚本是不会 运行的。假如网页部分内容由javascript生成,那么这种抓取是很难抓到这类动态数据的。而在现今的互联网上,由于WEB2.0技术的流行, 用javascript生成内容的网页比比皆是,并越来越多,一般的网页抓取器对此只能是望洋兴叹了。
二、HTML格式编程分析不易
一般的网页抓取器得到的是HTML源码,而HTML规范不够严格,导致程序解析上的困难。有不少开源的HTML解析器,但效果不尽如人意, 更谈不上企业级的应用了。最好的解析器应该属IE/FIREFOX浏览器内置的解析器了,但不易利用。另外,想编程获取网页中某个位置的数据也不容易, 而这对于垂直搜索引擎是非常重要的。
三、不是通过真正的浏览器来抓取数据
一般的网页抓取器是通过模拟浏览器的行为来获取网页的,模拟达不到与浏览器完全相同的程度,因为不是通过真正的浏览器来抓取, 所以在抓取网页时会存在这样那样的问题,都需编程解决。
海葵完全克服了这些缺陷,它具备如下优点:
一、基于真正的浏览器构建
海葵基于功能强大的FIREFOX浏览器构建。它通过FIREFOX浏览器获取网页数据,其中javascript脚本是可执行的, 得到的网页数据是“鲜活”的。所有由javascript生成的内容可完全得到。
二、数据格式采用XML
XML是规范严格的标记语言,易于编程存取。海葵网页抓取返回的就是XML格式的结构化数据。这些数据可直接转为DOM(Document Object Model)对象。
利用XPATH,能对于这些数据进行绝对定位,存取十分方便。假如想得到文档的标题,执行XPATH命令/html/head/title即可。 假如想得到文档中全部链接,执行一下XPATH命令//a/@href即可。假如想得到文档中全部图片,执行XPATH命令//img/@src即可。
另外,若想得到整个文档的文本信息,也非常容易。利用dom4j的Document类的getStringValue()就可以得到。
这样一种领先的技术,对于垂直搜索引擎的数据抓取是非常重要的,能起到如虎添翼的作用。
三、网页信息完整
海葵返回的XML数据中,包含了javascript生成的内容,是完整的网页信息。
海葵还可以输出网页请求与回应数据,方便用户跟踪调试。
四、支持多种操作系统,后台多线程运行
由于采用了最新技术,海葵自6.0版开始,除能在linux上运行外,还能在windows上运行了。
海葵运行于后台,多线程运行,能同时接受多个抓取请求,工作效率很高。
五、抓取协议简单
海葵采用类似HTTP形式的协议,简单有效。发送一个GET指令即可得到指定URL的网页数据。
六、能抓取由javascript脚本实现翻页的下一个页面数据
有不少动态网站用javascript脚本来实现翻页的,一般抓取工具无法得到其下一个页面的数据。海葵在协议中增加了动态执行javascript 脚本的命令EXEC和保持连接的命令CONTINUE。再结合GET命令,用户可以持续获取多个动态页面的数据。
海葵还为javascript脚本提供了getNodeByXPath方法,用户能得到指定的DOM节点,并能模拟节点的点击操作,得到点击后的页面数据。 这样就不必取出该节点对应的脚本了,如直接执行EXEC getNodeByXPath('//input[1]').onclick()便可模拟点击网页第一个input按钮, 得到更新后的页面数据,相当简单。
自2.0版开始,海葵协议新增nodata指令,使用此指令后,海葵将不返回网页数据,方便在此基础上使用javascript抓取下一页面的数据,节省了时间。 这非常适合基于海葵的网络蜘蛛使用:种子由URL加javascript脚本构成,当javascript脚本为空时,让海葵抓取指定URL并返回结果数据,否则,让海 葵以nodata方式访问指定URL,加快响应速度,然后再执行javascript脚本,得到动态页面的数据。
七、能定制HTTP网页请求数据
如可以发送HTTP-HEADER user-agent: xxx使WEB服务器认为客户端是xxx程序等等。
八、可以让网页不执行javascript
如发送disable javascript,网页便不执行javascript脚本,这非常适合特殊情况下的网页分析。
注:海葵不是网络蜘蛛(spider),不能自行分析URL并抓取。它是一个单纯的网页抓取工具,给出一个URL链接,海葵抓取后 返回结构化的XML格式的网页数据,最后其它程序对此数据进行处理。因为是XML格式,所以利用XPATH,处理是相当简单的。 网络蜘蛛系统我们推荐您使用海蛛--领先的垂直搜索专用网络蜘蛛系统!
service seaflower start
service seaflower stop
service seaflower status
usage: seaflowertctl command where command is: 1) list list current settings 2) set [port|rcj|vxsmin|vxsmax|captureWaitTime] value set config 3) proxy [ on <IP> <PORT> | off ] set or clear proxy setting 4) help print this help info
例1: seaflowerctl list 列出当前配置信息。 例2: seaflowerctl set port 4444 设定海葵侦听4444端口, 在此端口中接收请求。 例3: seaflowerctl set rcj 4444-5555-6666 设置注册码为4444-5555-6666。 例4: seaflowerctl set vxsmin 5 设定最少启动5个虚拟浏览器。此数字决定了海葵能并发接受多少个抓拍请求。 例5: seaflowerctl set captureWaitTime 2 设定默认抓取等待时间为2秒。 例6: seaflowerctl proxy on 192.168.28.91 8080 设定使用代理服务器192.168.28.91,端口8080。 例7: seaflowerctl proxy off 清除代理设置,不使用代理了。 注:vxsmax暂无意义,系统保留。
默认侦听端口: 4050
客户端抓拍网页时请发送如下请求(每行以换行符<LF>结束,遇到空行表示请求结束)
GET <url> <LF> 或 EXEC <javascripts> <LF>
WAIT-TIME <n> <LF>
HTTP-HEADER <one-http-header><LF>
OUTPUT [http-headers]<LF>
CONTINUE <LF>
NODATA <LF>
DISABLE [javascript]<LF>
<LF>
注:<url>是你要抓取的网址;<javascripts>是要执行的javascript脚本,脚本需要在一行上; <n>是抓取前等待的时间(单位为秒),默认为0。 GET/EXEC为必选项。WAIT-TIME为抓取前的等待时间,有些网页需要等待一下数据才能抓取完整。 HTTP-HEADER可输入用户自定义的HTTP请求头<one-http-header>,如HTTP-HEADER user-agent: xxxx。此命令允许多次输入。 OUTPUT确定要输出些什么信息,http-headers为输出HTTP请求与回应信息。 CONTINUE表示还有下一步的抓取请求,要保持连接,这主要为了方便对那些用javascript进行翻页的页面数据进行抓取。 NODATA为可选项,表示不需要海葵返回数据,方便蜘蛛程序进行下一步的抓取工作。 DISABLE javascript可禁止页面运行javascript。
海葵回应信息(成功时)
SEAFLOWER/6.1 200 OK <LF>
Current-Location: <location><LF>
Current-Title: <title><LF>
Content-Length: <length><LF>
<LF>
<contents>
注: <location>是当前网址;<title>是当前网页标题;<length>是抓取到的网页数据长度;<contents>是抓取到的网页数据(XML格式)。
海葵回应信息(抓取失败时)
SEAFLOWER/6.1 <code> <error> <LF>
<LF>
注: <code>是出错ID,以4开头;<error>是出错信息。
package com.zhsoft88.commons; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStreamWriter; import java.io.StringReader; import java.net.Socket; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.List; import java.util.StringTokenizer; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.math.NumberUtils; /** * seaflower crawler * @author zhsoft88 * @since 2008-4-13 * @update 2009-6-20 */ public class Seaflower { public static final int PORT = 4050; /** * crawl result * @author zhsoft88 * @since 2008-4-13 */ public static class SeaflowerResult { private int status; private String title; private String location; private String contents; private long time; private List<String> requestHeaders; private List<String> responseHeaders; public SeaflowerResult() { } public int getStatus() { return status; } public String getTitle() { return title; } public String getLocation() { return location; } public String getContents() { return contents; } public long getTime() { return time; } public List<String> getRequestHeaders() { return requestHeaders; } public List<String> getResponseHeaders() { return responseHeaders; } protected void setStatus(int status) { this.status = status; } protected void setTitle(String title) { this.title = title; } protected void setLocation(String location) { this.location = location; } protected void setContents(String contents) { this.contents = contents; } protected void setTime(long time) { this.time = time; } protected void setRequestHeaders(List<String> requestHeaders) { this.requestHeaders = requestHeaders; } protected void setResponseHeaders(List<String> responseHeaders) { this.responseHeaders = responseHeaders; } @Override public String toString() { return "status="+status+",location="+location+",title="+title+",time="+time+",request-headers="+requestHeaders+",response-headers="+responseHeaders+",contents=["+contents+"]"; } } /** * crawl configuration * @author zhsoft88 * @since 2008-4-13 * @update 2008-12-16 */ public static class SeaflowerConf { private String url; private String exec; private int waitTime; private boolean cont; private boolean nodata; private List<String> httpHeaders; private boolean outputHttpHeaders; private boolean disableJavascript; public SeaflowerConf() { } public void addHttpHeader(String header) { if (httpHeaders==null) { httpHeaders = new ArrayList<String>(); } httpHeaders.add(header); } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public String getExec() { return exec; } public void setExec(String exec) { this.exec = exec; } public int getWaitTime() { return waitTime; } public void setWaitTime(int waitTime) { this.waitTime = waitTime; } public void setContinue(boolean cont) { this.cont = cont; } public boolean isNodata() { return nodata; } public void setNodata(boolean nodata) { this.nodata = nodata; } public boolean isContinue() { return cont; } public List<String> getHttpHeaders() { return httpHeaders; } public void setHttpHeaders(List<String> httpHeaders) { this.httpHeaders = httpHeaders; } public boolean isOutputHttpHeaders() { return outputHttpHeaders; } public void setOutputHttpHeaders(boolean outputHttpHeaders) { this.outputHttpHeaders = outputHttpHeaders; } public boolean isDisableJavascript() { return disableJavascript; } public void setDisableJavascript(boolean disableJavascript) { this.disableJavascript = disableJavascript; } } private Socket socket; public Seaflower() throws UnknownHostException, IOException { this("localhost"); } public Seaflower(String host) throws UnknownHostException, IOException { this(host,PORT); } public Seaflower(String host,int port) throws UnknownHostException, IOException { socket = new Socket(host,port); } private String readUTFLine(InputStream in) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); int c; while ((c=in.read())!=-1) { if (c=='\r') continue; if (c=='\n') break; baos.write(c); } return baos.toString("utf-8"); } /** * crawl * @param conf * @return * @throws Exception */ public SeaflowerResult crawl(SeaflowerConf conf) throws Exception { if (socket==null) { throw new Exception("socket closed"); } long t1 = System.currentTimeMillis(); BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); if (conf.getUrl()!=null) { bw.write("GET "+conf.getUrl()+"\r\n"); } if (conf.getExec()!=null) { bw.write("EXEC "+conf.getExec()+"\r\n"); } if (conf.getWaitTime()!=-1) { bw.write("WAIT-TIME "+conf.getWaitTime()+"\r\n"); } if (conf.getHttpHeaders()!=null) { for (String s : conf.getHttpHeaders()) { bw.write("HTTP-HEADER "+s+"\r\n"); } } if (conf.isOutputHttpHeaders()) { bw.write("OUTPUT http-headers\r\n"); } if (conf.isDisableJavascript()) { bw.write("DISABLE javascript\r\n"); } if (conf.isContinue()) { bw.write("CONTINUE\r\n"); } if (conf.isNodata()) { bw.write("NODATA\r\n"); } bw.write("\r\n"); bw.flush(); InputStream in = socket.getInputStream(); String line = readUTFLine(in); int status = -1; StringTokenizer st = new StringTokenizer(line," "); st.nextToken(); status = NumberUtils.toInt(st.nextToken()); String tagTitle = "Current-Title: "; String tagLocation = "Current-Location: "; String tagLength = "Content-Length: "; String title = null; String location = null; int length = 0; while ((line=readUTFLine(in))!=null) { if (line.length()==0) break; if (line.startsWith(tagTitle)) { title = line.substring(tagTitle.length()); } else if (line.startsWith(tagLocation)) { location = line.substring(tagLocation.length()); } else if (line.startsWith(tagLength)) { length = NumberUtils.toInt(line.substring(tagLength.length())); } } ByteArrayOutputStream baos = new ByteArrayOutputStream(length); byte[] ba = new byte[4096]; while (length>0) { int len = in.read(ba); baos.write(ba, 0, len); length -= len; } String contents = baos.toString("utf-8"); if (!conf.isContinue()) { socket.close(); socket = null; } long t2 = System.currentTimeMillis(); SeaflowerResult result = new SeaflowerResult(); result.setStatus(status); result.setTitle(title); result.setLocation(location); result.setTime(t2-t1); result.setContents(contents); List<String> requestHeaders = new ArrayList<String>(); List<String> responseHeaders = new ArrayList<String>(); if (conf.isOutputHttpHeaders()) { BufferedReader hbr = new BufferedReader(new StringReader(result.getContents())); boolean found = false; while ((line=hbr.readLine())!=null) { if (line.equals("<!-- @REQUEST-HEADERS [")) { found = true; break; } } if (found) { while ((line=hbr.readLine())!=null) { if (line.equals("] -->")) break; requestHeaders.add(line); } } found = false; while ((line=hbr.readLine())!=null) { if (line.equals("<!-- @RESPONSE-HEADERS [")) { found = true; break; } } if (found) { while ((line=hbr.readLine())!=null) { if (line.equals("] -->")) break; responseHeaders.add(line); } } result.setContents(IOUtils.toString(hbr)); } result.setRequestHeaders(requestHeaders); result.setResponseHeaders(responseHeaders); return result; } }
package com.zhsoft88.commons.tests; import com.zhsoft88.commons.Seaflower; import com.zhsoft88.commons.Seaflower.SeaflowerConf; import com.zhsoft88.commons.Seaflower.SeaflowerResult; public class TestSeaflower { /** * @param args */ public static void main(String[] args) throws Exception { SeaflowerConf conf = new SeaflowerConf(); conf.setUrl("http://www.google.com"); conf.addHttpHeader("user-agent: jiong/0.1"); conf.addHttpHeader("kkkkkkk: vvvvvvvvvvvvvvv"); conf.setOutputHttpHeaders(true); Seaflower sf = new Seaflower(); SeaflowerResult result = sf.crawl(conf); System.out.println(result.getRequestHeaders()); System.out.println(result.getResponseHeaders()); System.out.println(result.getContents()); } }
package com.zhsoft88.commons.tests; import java.util.List; import org.dom4j.Document; import org.dom4j.DocumentHelper; import org.dom4j.Node; import com.zhsoft88.commons.Seaflower; import com.zhsoft88.commons.Seaflower.SeaflowerConf; import com.zhsoft88.commons.Seaflower.SeaflowerResult; /** * Test of Seaflower: 抓取酷基金网站数据 * @author zhsoft88 * @since 2009-6-20 */ public class TestSeaflower4 { /** * @param args */ public static void main(String[] args) throws Exception { Seaflower s = new Seaflower(); { SeaflowerConf conf = new SeaflowerConf(); conf.setUrl("http://www.ourku.com/index_kfjj.html"); conf.setWaitTime(10); SeaflowerResult result = s.crawl(conf); Document doc = DocumentHelper.parseText(result.getContents()); List<Node> list = doc.selectNodes("//table[@id='ilist']//tr[@class='tr']"); System.out.println("total size="+list.size()); for (Node no : list) { for (int i=1;i<=8;i++) { System.out.print(no.selectSingleNode("td["+i+"]").getStringValue()); if (i!=8) System.out.print(", "); } System.out.println(); } } } }
package test; import org.dom4j.Attribute; import org.dom4j.Document; import org.dom4j.DocumentHelper; import com.zhsoft88.commons.Seaflower; import com.zhsoft88.commons.Seaflower.SeaflowerConf; import com.zhsoft88.commons.Seaflower.SeaflowerResult; public class TestSeaflower3Cont { /** * @param args */ public static void main(String[] args) throws Exception { Seaflower sf = new Seaflower(); { SeaflowerConf conf = new SeaflowerConf(); conf.setContinue(true); conf.setNodata(true); conf.setWaitTime(10); conf.setUrl("http://www.gap.com/browse/product.do?cid=8793&vid=1&pid=655053"); sf.crawl(conf); } for (char c='0';c<='4'; c++) { SeaflowerConf conf = new SeaflowerConf(); conf.setContinue(c!='4'); conf.setExec("document.getElementById('colorSwatch_"+c+"').onclick()"); SeaflowerResult result = sf.crawl(conf); Document doc = DocumentHelper.parseText(result.getContents()); Attribute a = (Attribute)doc.selectSingleNode("//img[@id='dragImg']/@src"); System.out.println(a.getValue()); } } }