使用HtmlParser提取HTML文本块

听人介绍说HtmlParser(Java版本)在网页预处理方面做得不错,于是最近几日就研究了一番,虽说没有什么大的收获,但是难得能够让我一个对html标签一无所知的人,认识了其树状结构的玄机,并实现了通过文件目录提取html的标题,关键词,摘要信息,链接及其锚文本,以及主题型网页的正文部分。

以下仅就提取正文部分做个简单拙劣的介绍。在提取网页正文时,对于不同类型的网页应当采用不同的提取策略,而网页又可以分成几种类型呢?粗略地讲,网页就分成两种类型:主题型(topic)和hub型,这种分类有一个明显的差别,即主题型相对hub型网页正文要占可视文本的绝大多数。比如,baidu空间,这种博客类型的网页,多数情况下都是大段大段的文本块,而又如新华网首页,经过分析提取出来1,000多的出链,打开网页映入我们眼帘实际上是这些链接的锚文本。锚文本与正文文本块一个明显的差别就是短小,而门户网站的锚文本又多了一个性质:种类庞杂,条目众多。

基于这种分析,我们在提取网页正文之前,至少在代码中应该定义一个用于判别网页类型的方法/函数,目的就是针对每种类型,采取一种独特的正文提取策略。至于这个功能函数如何实现这种判别,可以参考相关的文献和网络资料具体深入的研究。当然有一种被很多人推崇的版本,那就是计算文本的信噪比,但是这种方法从概念上就足以让人望而却步,更甭提具体实现了,但我觉得可能这种方法也许并没有乍看那么困难吧!先夸下海口,但我还没有抽出足够的时间了解它,以后慢慢地研究吧。我在程序中就采取了一种十分粗糙简化的判别,即通过为网页出链的数目设置一个阈值,当出链数目超出这个阈值时,即可认为该网页属于hub型;低于这个阈值,即被归入主题型网页。明显地,这是一种低级的,没有任何技术含量的假设,还有待我们细化。

网页类型分析出来了,采取什么策略来提取正文呢?还是简化的方式:针对主题型,提取的是主体的大块文本段落;而对hub型,由于其大量的出链和锚文本,我们就直接忽略掉这个提取。说了大半天,竟然没有提取。话虽如此,实际上,hub型网页的文本内容都被我们通过锚文本的形式提取出来了,这也就是我采用HtmlParser提取正文的原因,一方面它可以实现针对性的提取链接,同时跳过script、style、remark标签,过滤掉迷惑性的图片链接等其他链接,单单提取txt/html类型的所有链接,重要的是还能够同时提取相应的锚文本,并能够保存其标签的起始位置。一个让人着迷的实现,从技术上来看,实际上也还是使用我们万能的正则表达式匹配功能实现的,从我来讲,这种匹配只是被包装起来了。其实,我们还是可以让他们pk一下的,比如过滤掉html中的无用标签对以及文本内容,如惹人厌烦的script和style、remark标签:

<1>使用简单的正则表达式匹配:

 public String cleanHtml(String html){
  String regex="" +
    "|"+
    "|";
  Pattern pattern=Pattern.compile(regex,Pattern.CASE_INSENSITIVE);
  Matcher match=pattern.matcher(html);
  html=match.replaceAll("");
  return html;
 }

输入的是原始的html字符串,输出的结果就将以上垃圾信息过滤掉了 :)

<2>使用HtmlParser过滤script和style标签信息以及文本信息:

 public String htmlInit(String htmlStr){
  NodeFilter scriptFilter=new NodeClassFilter(ScriptTag.class);
  NodeFilter styleFilter=new NodeClassFilter(StyleTag.class);
  NodeFilter[] filter={scriptFilter,styleFilter};
  OrFilter orFilter=new OrFilter(filter);
  try {
   htmlStr=ParserUtils.trimTags(htmlStr,orFilter,true,true);
  } catch (ParserException e) {
   e.printStackTrace();
  } catch (UnsupportedEncodingException e) {
   e.printStackTrace();
  }
  return htmlStr;
 }

这里使用的OrFilter让人倾倒,本质上无非就是一个数组型的过滤器,实现循环过滤罢了。

 小结:比较起来,从代码量上来看,显然使用正则表达式更为简略,而且实现效果比起后者,过滤的更为彻底。我一直在寻找捷径,其实捷径不一定就是别人走出来的。当然,使用正则表达式的一个缺憾就是损坏了原来html的完整的结构,从而破坏了定位标签位置的实现。还要注意的一点就是,使用Parser.parser(Filter filter)还要初始化parser的编码,以胜任对中文的处理,要知道,它默认的字符编码是ISO-8859-1,在处理中文的时候,如果没有parser.setEncoding("GB2312")这一步,可能看到的是乱码了,或者直接抛出编码转换的错误。

我们多次强调要保存标签位置信息,其实是有目的的,即在完成建立索引,向用户提供查询时,实现关键字飘红或者动态摘要生成的目的。现在,如果采取第一种过滤方式,就使我们丢弃了这种信息,但是还有补救方法,即我们在提供关键字飘红和动态摘要的生成,完全可以基于所有关键字集合来讲,而不是基于全文的。

下面,为了回馈社会,我将前面长篇大论的提取正文信息的代码贴上:

// 提取网页主要文本内容
 public String getContent(){
  content=(isHub())?getHubEntries():getTopicBlock();
  System.out.println(":");
  System.out.println("=========================");
  System.out.println(content);
  return content;
 }
// 提取Hub类网页文本内容,如yahoo,sina等门户网
 public String getHubEntries(){
  StringBean bean=new StringBean();
  bean.setLinks(false);
  bean.setReplaceNonBreakingSpaces(true);
  bean.setCollapse(true);
  try {
   parser.visitAllNodesWith(bean);
  } catch (ParserException e) {
   System.err.println("getHubEntries()-->"+e);
  }
  parser.reset();
  return bean.getStrings();
 }
 
// 获取主题性(Topical)网页文本内容:对于博客等以文字为主体的网页效果较好
 public String getTopicBlock(){
  
  HasParentFilter acceptedFilter=new HasParentFilter(new TagNameFilter("p"));
  NodeList nodes=null;
  try {
   nodes=parser.extractAllNodesThatMatch(acceptedFilter);
  } catch (ParserException e) {
   System.err.println("getTopicBlock"+e);
  }
  
  StringBuffer sb=new StringBuffer();
  SimpleNodeIterator iter=nodes.elements();
  while(iter.hasMoreNodes()){
   Node node=iter.nextNode();
   sb.append(node.getText()+"\n");
  }
  parser.reset();
  return sb.toString();
 }

说实话,具体效果还真不太好,细节问题还有一箩筐,比如要剔除广告信息怎么处理,如何利用版权声明中的"About us"或者"关于我们"的链接网页,提取信息补充关键词和摘要信息,同时将声明的其余部分毫不保留的过滤掉....

补充说一下,有人提出通过遍历html所有标签,统计其中的文本文字的比特数与标签的比率,根据到达正文尾部可以达到最大化的统计假设来实现,我觉着这种方法也许是正解,慢慢研究咯。

结语:到目前为止,我对HtmlParser的研究还是皮毛,谬误之处多多,还望大虾们多多指教。它让我产生一种冲动,即按照树的结构写出自己的API,努力一把,兴许还真有收获吧!目前正在尝试着搭建一个搜索引擎,学海无涯啊!

你可能感兴趣的:(.net)