浏览 3696 次
锁定老帖子 主题: 信息采集模型思考
精华帖 (2) :: 良好帖 (0) :: 新手帖 (0) :: 隐藏帖 (0)
|
|
---|---|
作者 | 正文 |
发表时间:2011-08-12
最后修改:2011-08-12
以往,我们采集程序,第一印象是对方的网站结构易变,我们怎么能在面对对方眼花缭乱的网页元素中得到我们要的信息,即使我们疲于奔命,那么怎么能做个万金油似的采集方案,来应对各种网站呢,所以,在接到这个需求的时候,我花了一天去考虑怎样的模型是最适合的,下面列的,正是经过分析之后对自己这次任务的基本要求。 基本要求如下: 1,根据提供的网站,采集我们项目中所需要的各种信息,也就是我们需要的,尽可能在对方网站上扒下来。 2,当对方网站有一些变化的时候,我们能及时调整采集方案。 3,网站不限,按道理可以采集任意我们需要的网站,只要你提供基本信息。 4,保持数据的完整性和准确性,包括一些常用属性的转换(比如性别标识,年龄,是否婚配,户口,学校等各种),这些数据需要跟我们项目能有转换。 5,提供更多的接口,保持扩展性。 首先,很不幸我明白,万金油不存在,因为可变易变因素太多,但是对不可变的东西,我们得有一个提取。我觉得不可变的有以下几点, 1是连接方式不变 从连接到分析这个大过程不变 2是分析之后得有个转换成本项目数据 这个过程不变 3是,封装pojo 直到入库 这个不变。 很显然,可变的,就剩下网页的分析与提取了,网页是结构化的东西,我们在读取的时候得依照这种结构来读取,而不同网站的一些信息,有可能A网站在一个table里面,B网站就在div里面,C网站在一个嵌套table里面的div里面,这个可以大概概括为,你需要的读取主体(网页元素)不定,主体位置(嵌套或某个元素前后)不定。但是我们仍然能提取出共通点,那就是,你作为程序员,你肯定知道这样的结构变化,你所需要做的,就是能将这些变化放在可扩展的程序里面。 废话说了一大堆,先看看我的方案。连接+网页分析 我用的是httpclient+httpparser。 首先,我写了一个顶级的处理类,每个需要采集的网站需要实现这个类里面的方法。 public abstract class HtmlParserHandle { private HttpClient httpClient; /** * 采集的网站域名 */ public abstract String getWeb(); /** * 提交登录的url */ public abstract String submitUrl(); /** * 封装页面提交的表单 */ public NameValuePair[] formInputs(Map<String,String[]> inputValues){ Set<Entry<String, String[]>> set=inputValues.entrySet(); NameValuePair[] nvps=new NameValuePair[set.size()]; int index=0; for(Entry<String,String[]> ent:set){ String[] values=ent.getValue(); for(String value:values){ if(ent.getKey().equals("loginname")||ent.getKey().equals("password")){ System.out.print("value:"+value); value=URLDecoder.decode(value); System.out.println("---------"+value); } nvps[index]=new NameValuePair(ent.getKey(),value); index++; } } return nvps; } /** * http客户端连接 */ public HttpClient getHttpClient() { if (httpClient == null) { System.out.println("我初始化了httpClient"); httpClient = new HttpClient(); httpClient.getHostConfiguration().setHost(getWeb(), 80, "http"); // httpClient.getParams().setCookiePolicy( // CookiePolicy.BROWSER_COMPATIBILITY); } return httpClient; } /** * 登录后采集的地址 */ public abstract String parseUrl(); /** * 检查登录 * * @param location * 登录后返回的头部location * @return 返回登录是否成功 */ public abstract boolean checkLogin(PostMethod pm,int statusCode); /** * @return 解析的配置文件 */ public abstract String xml(); public abstract List<Object> parseHtml(NodeList nodelist) throws Exception; public abstract Convert convert(); } 上面的抽象类是每个网站处理需要继承实现的,由于我们采集的网站大多需要登录,所以加上了提交表单 模拟登录的处理。 xml()方法,返回网站的配置文件(每个网站对应一个配置文件) parseHtml()方法是暴露出的需要扩展的解析方法,这个待会讲。 convert方法返回每个网站对应的处理接口,之所以要这样做,是因为每个网站的数据格式都可能不同,比如A网站的性别--男可能就是1表示,而B网站是0,包括一些日期等等,都需要一个转换,包括和我们自己项目的转换,比如我们的性别--男就是1273521。 针对刚才说的,我把xml配置文件设计成与网页同结构,在需要获取值的时候做一个我设定的标识,并且可以提供网页的过滤接口,大家一看就应该明白。 <?xml version="1.0" encoding="UTF-8"?> <html> <beans> <bean id="baseinfo" class="org.entity.Baseinfo" /> <bean id="school" class="org.entity.School" /> <bean id="friend" class="org.entity.Friend" /> </beans> <body> <div id="mybaseinfo" filter="org.caiji.filter.BaseinfoFilter" params="class:div[class='mybase'];order:0"> <div>姓名</div><div axis="value">baseinfo.name</div> <div>性别</div><div axis="value" type="java.lang.Long" convert="db">baseinfo.sex</div> <div>出生日期</div><div axis="value" type="java.util.Date" convert="nianyueri yyyy-MM-dd" >baseinfo.csrq</div> <div>工作年限</div><div axis="value" type="java.lang.Long" convert="db">baseinfo.workexperience</div> <div>个人主页</div><div axis="value">baseinfo.grzy</div> <div>地址</div><div axis="valid" ></div> </div> <table id="jyjl" className="org.entity.ActeJyjl" filter="org.caiji.filter.BaseinfoFilter" params="css:div[id='Edu_edit'];child:table[class='weight780 weight780_mdf weight670']"> <tr> <td>时间</td><td axis="value">times</td> </tr> <tr> <td>学校</td><td axis="value" convert="blankone">zdyxxmc</td> </tr> <tr> <td>专业</td><td axis="value">zdyxxzy</td> </tr> <tr> <td>学历</td><td axis="value" type="java.lang.Long" convert="db">education</td> </tr> <tr> <td>专业描述</td><td axis="value" >description</td> </tr> <tr> <td>海外学习经历</td><td axis="valid">否</td> </tr> </table> </body> </html> 上面配置的bean,都是下面需要构造的pojo,会自动根据id来匹配。 比如说我现在发现,我要的信息在一个class=mybase的第一个div里面,那么设定符合我结构的参数, params="class:div[class='mybase'];order:0",传入org.caiji.filter.BaseinfoFilter这个过滤器 实现代码如下: public class BaseinfoFilter implements Filter { public NodeList filter(NodeList nodeList, Object params) { NodeList returnNodeList = new NodeList(); String[] myparams = ((String) params).split(";"); NodeFilter cssfilter = new CssSelectorNodeFilter(myparams[0].split(":")[1]); int order = Integer.parseInt((String) myparams[1].split(":")[1]); NodeList table_list = nodeList .extractAllNodesThatMatch(cssfilter, true); NodeList nl = table_list; returnNodeList.add(nl.elementAt(order)); return returnNodeList; } private NodeList parseParams(NodeList nodelist, String[] params) { NodeList returnNodeList = new NodeList(); returnNodeList.add(nodelist); for (String param : params) { String[] keyvalues = param.split(":"); NodeFilter filter = null; if ("css".equals(keyvalues[0])) { filter = new CssSelectorNodeFilter(keyvalues[1]); returnNodeList = returnNodeList .extractAllNodesThatMatch(filter, true); } if ("order".equals(keyvalues[0])) { int order = Integer.parseInt(keyvalues[1]); NodeList nl = new NodeList(); nl.add(returnNodeList.elementAt(order)); returnNodeList = nl; } if ("child".equals(keyvalues[0])) { filter = new CssSelectorNodeFilter(keyvalues[1]); returnNodeList = returnNodeList.extractAllNodesThatMatch( filter, true); } } return returnNodeList; } } 它实现了我设定的一个filter接口,假如我的这个基本过滤器不能满足需求,可以自行实现Filter接口并配置起来即可,其中filter方法是重写的方法,返回这个网页元素的NodeList进行操作。 此时,你就得到了这个div元素,进行下一步操作。大家可以看到我配置的div下面有这样的 <div>姓名</div><div axis="value">baseinfo.name</div> ,axis=value表明我需要采集这个div下面的值,并且放在baseinfo这个pojo的name属性里面,至于前面的“姓名”放这儿没太大作用,只是让我自己看的清楚。 还有比如 <div>性别</div><div axis="value" type="java.lang.Long" convert="db">baseinfo.sex</div>这样的,我本项目性别是Long型的,所以需要转换类型,并且与我数据库里面的相关数据转换,比如我本项目里面的性别是可控的,都是存在数据库的,那么我标识db,就与我数据库的字典进行匹配并自动转换,而转换器,就是最初的HtmlParserHandle里面的。 下面的table与此类似,不同点是多了一个className="org.entity.ActeJyjl",什么意思呢,比如说某个人的信息里面教育经历有很多,比如初中 高中 大学等,那么我们需要采集的对象就不只一个了,而是三个ActeJyjl对吧, 往往,这三个信息的结构是一样的,那么我只需要配置一个table就够了(假如我们设定教育经历是放在这样一个table里面),配置className,在采集的时候,自动重新创建n个ActeJyjl对象,并自动封装我需要的数据。而之前的baseinfo,这个信息只有一个,那么就保存在名为baseinfo的org.entity.Baseinfo对象里面,这几个预配置的对象都是在读取xml配置文件的时候缓存起来的,每个对象只缓存一个。 实际上,从上面的xml配置文件可看出,我的这种方式想法其实比较简单,就是同步读取信息,在我们需要的信息位置做一个标记并提供无数可能数据处理,其实想深入一点,我们完全可以在这个配置里面做更多的配置结构来处理我们的数据。 刚才所讲基本上是一些核心的想法实践,下面看看我是怎样通过filter来过滤读取信息并封装成pojo的。 我创建了一个共有的适配器类,如下: public class HtmlParseAdapter { private Map<String,String[]> inputValues=new HashMap<String, String[]>(); private HtmlParserHandle hph; private String html; Set<Object> objSet = new HashSet<Object>(); public String getHtml() { return html; } public HtmlParseAdapter(HtmlParserHandle hph,Map<String,String[]> inputValues) { this.hph = hph; this.inputValues=inputValues; } public boolean postSubmit() throws HttpException, IOException { HttpClient httpClient = hph.getHttpClient(); PostMethod pm = new PostMethod(hph.submitUrl()); NameValuePair[] data =hph.formInputs(inputValues); pm.setRequestBody(data); int statusCode = httpClient.executeMethod(pm); System.out.println("状态码: "+statusCode); boolean flag=hph.checkLogin(pm, statusCode); if(!flag) return false; /*查看 cookie 信息*/ CookieSpec cookiespec = CookiePolicy.getDefaultSpec(); Cookie[] cookies = cookiespec.match(hph.getWeb(), 80, "/", false, httpClient.getState().getCookies()); String cookie = ""; if (cookies!=null&&cookies.length != 0) { for (int i = 0; i < cookies.length; i++) { cookie = cookies[i].toString(); System.out.println(cookie); } } String url = hph.parseUrl(); GetMethod get = new GetMethod(url); httpClient.executeMethod(get); String html =get.getResponseBodyAsString(); /*File f = new File("D:\\test.html"); if (!f.exists()) { f.createNewFile(); } FileOutputStream fos = new FileOutputStream(f); FileWriter fw = new FileWriter(f); fw.write(new String(html)); fw.close(); fw = null; fos.close();*/ get.releaseConnection(); this.html = new String(html.getBytes("iso-8859-1")); return true; } public Set<Object> parseHtml(ServletContext sc) throws Exception { List<Entity> entityList = new ArrayList<Entity>(); Parser parser = Parser.createParser(html, "gb2312"); HtmlPage page = new HtmlPage(parser); parser.visitAllNodesWith(page); NodeList nodelist = page.getBody(); ReadXml rx = new ReadXml(hph.xml()); rx.parseXml(); List<Group> tables = rx.getTables(); for (Group group : tables) { Filter gf = group.getFilter(); NodeList node_list = gf.filter(nodelist, group.getParams()); String groupXml = group.getGroupString(); Parser groupParser = new Parser(groupXml); HtmlPage groupPage = new HtmlPage(groupParser); groupParser.visitAllNodesWith(groupPage); NodeList groupNodeList = groupPage.getBody(); TableTag gtable = (TableTag) groupNodeList.elementAt(0); for (int t = 0; t < node_list.size(); t++) { Object targetObj=null; String className=group.getClassName(); if(className!=null && !(className.equals(""))){ targetObj=Class.forName(className).newInstance(); } TableTag table = (TableTag) node_list.elementAt(t); for (int j = 0; j < table.getRowCount(); j++) { TableRow row = table.getRow(j); TableColumn[] columns = row.getColumns(); TableRow grow = gtable.getRow(j); if(grow==null){ break; } TableColumn[] gcolumns = grow.getColumns(); for (int k = 0; k < columns.length; k++) { Entity entity = new Entity(); String text = columns[k].toPlainTextString(); String axis = gcolumns[k].getAttribute("axis"); if ("value".equals(axis)) { text = text.trim().replaceAll(" ", ""); String regex = gcolumns[k].getAttribute("regex"); if (regex != null && !(regex.equals(""))) { Pattern pa = Pattern.compile(regex); Matcher matcher = pa.matcher(text); if (matcher.find()) { text = matcher.group(); } } String convert = gcolumns[k] .getAttribute("convert"); String type = gcolumns[k].getAttribute("type"); if (type == null || type.equals("")) { type = "java.lang.String"; } Object value=text; if (convert != null && !convert.equals("")) { value=hph.convert().convertValue(convert, text,sc); } if(targetObj==null){ entity.setType(Class.forName(type)); entity.setName(gcolumns[k].toPlainTextString()); entity.setValue(value); entityList.add(entity); }else{ Utils.callSetMethod(targetObj,gcolumns[k].toPlainTextString(),value,Class.forName(type)); } } } } if(targetObj!=null){ objSet.add(targetObj); } } } Map<String,String> beanMap = rx.getBeanMap(); Map<String,Object> objMap=new HashMap<String, Object>(); for (Entity ent : entityList) { String key = ent.getName(); String[] keys = key.split("\\."); Object obj=null; if(objMap.containsKey(keys[0])){ obj=objMap.get(keys[0]); }else{ obj=Class.forName(beanMap.get(keys[0])).newInstance(); } objMap.put(keys[0],obj); Utils.callSetMethod(obj, keys[1], ent.getValue(), ent.getType()); objSet.add(obj); } List<Object> listObj=hph.parseHtml(nodelist); if(listObj!=null && listObj.size()>0){ objSet.add(listObj); } return objSet; } } 因为这个类需要处理table div ul 这些不同的结构,篇幅较长,所以值保留了table的处理,还请各位耐心看一下,这个适配器接收传入的HtmlParserHandle(别忘了,就是我第一个介绍的接口 嘿嘿),其实就干了两件事儿, 1是postSubmit提交表单(对于有登录的来说) 2是根据配置文件处理各种提取要求来得到pojo,并且返回集合供后面程序使用(如插入db)。 其中这句代码 List<Object> listObj=hph.parseHtml(nodelist);就是保留给HtmlParseHandle来进行解析扩展的,并返回pojo,当然只要这个适配器足够强大,一般不需要,可直接返回null; 假如需要加一个网站的采集,步骤如下: 1,继承HtmlParserHandle,实现里面的方法,比如提供url,参数,xml路径,转换器等。 2,假如需要转换器的 实现Convert接口。 3,编写xml配置文件,注入filter,假如基本的不满足,可以扩展。 在应用层,我的调用过程如下: HtmlParserHandle hph=new HtmlParseXXX() Map<String,String[]> params=request.getParameterMap(); HtmlParseAdapter hpa=new HtmlParseAdapter(hph,params); if(hpa.postSubmit()){ Set objSet=hpa.parseHtml(); service.insertObjs(objSet) ; } 最后总结一下: 1,对于结构化文档的抓取,我的基本想法:采用同步读取,重点标识。可以满足在不同结构下仍然可以很精准的快速的调整策略,并且提供非常多的扩展办法,只要你愿意。 2,每个目标网站配置一个xml,可以摆平不同网站信息之间的差异化,比如我在A上面可以得到手机号,而在B网站上面只能得到姓名,那么A的配置文件可以配置手机号读取,而B网站的配置文件只配置一个姓名读取。再比如教育经历在A网站上可能只有最近教育信息,那么只需要配置一个预加载的对象,往这个对象里面塞入属性就可以;而B网站教育经历从小学到大学都有,那么配置成多个对象形式,即每次new一个对象来接收,而这种方式不但思维比较直观,而且能节约有效资源。实际上,在这种形式的配置中,按道理我可以抓取我任意想要的信息进入我的pojo的任意属性,不管是否多个。 3,缺点也很明显,就是假如出现某些重复信息 比如刚才说的教育经历,每个展现的结构不一样的话,我只能提取和我设定结构一样的数据信息,其他的会忽略。还有就是,目前多个同类信息(如教育经历)只能封装同一类对象,假如对方网站在这个教育经历的网页结构里面还有其他信息 比如学生作品,而这个学生作品在我的项目里面是另外一个对象里面的,那么我不能采集到,只能再配置一个结构用来专门抓取学生作品,然后放入我的学生作品对象。 做这个整体的模型还有很多辅助的工具包括一些细节代码我都略去,一是代码写的快,有一些白痴并且急需优化的代码,不拿出来也罢,二是我觉得可能各位iteyer有更好的方案,在提出我的方案的同时,急需更有经验人士提点,望大家各种评。 假如愿意做进一步沟通的 可以站内! 声明:ITeye文章版权属于作者,受法律保护。没有作者书面许可不得转载。
推荐链接
|
|
返回顶楼 | |
发表时间:2011-08-12
最后修改:2011-08-12
楼主这样做太复杂了,
你的xml配置文件 主要是做一个POJO到 html关键内容的映射 Mapping 用XPath 做应该比较好,例如 baseinfo.name=/html/body/div[1]/div[2] baseinfo.sex=/html/body/div[2]/div[2] 其他无用的元素完全可以不去管它,filter的功能也可以弱化的 |
|
返回顶楼 | |
发表时间:2011-08-12
guoapeng 写道 楼主这样做太复杂了,
你的xml配置文件 主要是做一个POJO到 html关键内容的映射 Mapping 用XPath 做应该比较好,例如 baseinfo.name=/html/body/div[1]/div[2] baseinfo.sex=/html/body/div[2]/div[2] 其他无用的元素完全可以不去管它,filter的功能也可以弱化的 你这个我也想过,但我觉得有这样几个问题: 1,我的需要采集的字段非常多,假如这样配置,每次都需要去过滤,不能尽可能在在对方网站的一个地方抓取更多信息 2,这样写仍然需要自己定义过滤规则,而且对于同类别(比如教育经历)多个信息的,这样写不太好标识 3,我的xml配置文件其实核心不是和html进行映射,其实更多的是结构的映射。 还有兄台,你能否分享一下你具体做的,有可能我上面的想法是多余的。 |
|
返回顶楼 | |
发表时间:2011-08-12
最后修改:2011-08-12
AngelAndAngel 写道 你这个我也想过,但我觉得有这样几个问题: 1,我的需要采集的字段非常多,假如这样配置,每次都需要去过滤,不能尽可能在在对方网站的一个地方抓取更多信息 当然不能只配一种类型的mapping关系,多配给个, 而且每一种mapping关系要用来处理那些页面也需要配的 用xml给你举例吧 举例 <mappings> <mapping> <pojo> <class>baseinfo</class> <attribute name="name" type="String" xpath=""> /html/body/div[1]/div[2] <attribute/> <attribute name="sex" type="String"> /html/body/div[2]/div[2] <attribute/> <attribute name="education" type="list" xpath="/html/body/div[2]/div[3]"> <list> /div/div* <list> <attribute/> </pojo> <url-mapping> http://xxxwebsite/*Student*.html; http://xxxwebsite/*Job*.html;<url-maping> </mapping> <mapping> 。。。。mappping 2 can be here </mapping> </mappings> baseinfo.name=/html/body/div[1]/div[2] baseinfo.sex=/html/body/div[2]/div[2] 2,这样写仍然需要自己定义过滤规则,而且对于同类别(比如教育经历)多个信息的,这样写不太好标识 上面的list你可以参考spring的list 怎么配的,我这些是伪代码,只是表达一下思路 3,我的xml配置文件其实核心不是和html进行映射,其实更多的是结构的映射。 还有兄台,你能否分享一下你具体做的,有可能我上面的想法是多余的。 没做过类似的,不知道你项目具体是什么样,提供一种思路仅供参考 |
|
返回顶楼 | |
发表时间:2011-08-12
另外XPath不必配绝对路径,
可以相对于某个关键Node去配 |
|
返回顶楼 | |
发表时间:2011-08-14
guoapeng 写道 另外XPath不必配绝对路径,
可以相对于某个关键Node去配 恩 你的方法我抽时间试试 XPath应该是表达能力比较丰富的(我这个方面经验不多)。 还有就是我这么做的另外一个目的是 可以同步读取,比如我的xml配置文件,配置的是html格式,那么我在采集目标页面时可以用同一种方式同时来读取这个配置文件,不需要另外一套读取配置文件的处理方式。 |
|
返回顶楼 | |
发表时间:2011-08-14
getHttpClient 方法没有加同步
|
|
返回顶楼 | |
发表时间:2011-08-14
看来楼主是想把招聘网站的信息抓下来
|
|
返回顶楼 | |
发表时间:2011-08-15
volking 写道 看来楼主是想把招聘网站的信息抓下来
当然不是 |
|
返回顶楼 | |
发表时间:2011-08-15
循环就嵌套了四次。
|
|
返回顶楼 | |