使用JRegex抽取网页信息
当网络爬虫将网页下载到磁盘上以后,需要对这些网页中的内容进行抽取,为索引做准备。一个网页中的数据大部分是HTML标签,索引肯定不会去索引这些标签。也就是说,这种信息是没有用处的信息,需要在抽取过程中过滤掉。另外,一个网页中一般会存在广告信息、锚文本信息,还有一些我们不感兴趣的信息,都被视为垃圾信息,如果不加考虑这些内容,抽取出来的信息不仅占用存储空间,而且在索引以后,为终端用户提供检索服务,用户检会索到很多无用的垃圾信息,势必影响用户的体验。
这里,针对论坛,采用配置模板的方式来实现信息的抽取。使用的工具可以到http://jregex.sourceforge.net上下载,JRegex是一个基于Java的正则库,可以通过在正则模板中指定待抽取信息的变量,在抽取过程中会将抽取到的信息赋给该变量,从而得到感兴趣的信息。而且,JRegex库支持多级分组匹配。
为了直观,假设,有一个论坛的一个网页的源代码形如:
<a id="anchor">标题</a> <cont> <author>a1</author> <time>2009</time> <post>p1</post> <author>a2</author> <time>2008</time> <post>p2</post> <author>a3</author> <time>2007</time> <post>p3</post> <author>a4</author> <time>2006</time> <post>p4</post> <author>2005</author> <time>t5</time> <post>p5</post> </cont>
将该网页代码文件保存为bbsPage.txt文件,准备进行处理。
现在,我们的目标是抽取标题、作者、时间、内容这些内容,当然,标题完全可以从TITLE标签中获得,但是一般网站的一个网页,会在标题文本的后面加上一些目录或者网站名称的信息,例如一个标题为“品味北京奥运中心_奥运加油站_我行我摄_XXX社区_XXX社区是最活跃的社区之一”,一些垃圾信息占了标题的大部分,所以我们不从TITLE标签中抽取标题。
接着,针对上面的网页文件创建信息抽取的正则模板,如下所示:
(?s)<a\sid="anchor">({title}.{1,100}?)\s*(.{1,10240}?)({name}.{1,100}?)\s*<time>({when}.{1,100}?)</time>\s*({content}.{1,100}?)
第一部分为(?s)<a\sid=”anchor”>({title}.{1,100}?)\s*(.{1,10240}?),包含两个组,第一个组名称为title,直接能够抽取到网页的标题文本,并存储到变量title中;而第二个组没有指定组的名称,表示在后面还存在子组,在子组中继续进行抽取。
第二部分为({name}.{1,100}?)\s*({when}.{1,100}?)\s* ({content}.{1,100}?),恰好是父组中未指定组名称的第二个组内中的一个循环。
上面两个模板之间使用一个空格字符隔开,保存到pattern.txt文件中。
可能,你已经观察到了,网页的标题只有一个,而对其他的信息正好能够构成一个循环组,单独从父组中分离出来继续进行抽取,结构很整齐。所以,在使用JRegex库进行编码抽取的时候,主要就是针对两个组进行的。
我基于上面思想和数据,实现了信息的抽取。
首先定义了一个键值对实体类,使用泛型,如下所示:
package org.shirdrn.test; public class Pair<K, V> { private K key; private V value; public Pair(K key, V value) { this.key = key; this.value = value; } public K getKey() { return key; } public void setKey(K key) { this.key = key; } public V getValue() { return value; } public void setValue(V value) { this.value = value; } }
进行信息抽取的核心类为InfomationExtraction,如下所示:
package org.shirdrn.test; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.List; import jregex.Matcher; import jregex.Pattern; public class InfomationExtraction { private String htmlString; private String patternString; private List<Pair<String, String>> dataList = new ArrayList<Pair<String, String>>(); public InfomationExtraction() { } public InfomationExtraction(String htmlFileName, String patternFileName) { this.htmlString = this.readString(htmlFileName); this.patternString = this.readString(patternFileName); } public Pattern[] getPatternArray() { Pattern[] pa = new Pattern[2]; String[] psa = this.patternString.split(" "); for (int i = 0; i < psa.length; i++) { Pattern p = new Pattern(psa[i]); pa[i] = p; } return pa; } public void extract(Integer sgIndex) { // 指定父组中第sgIndex个组需要在子组中继续进行抽取 Pattern[] pa = this.getPatternArray(); Pattern pBase = pa[0]; Matcher mBase = pBase.matcher(this.htmlString); if (mBase.find()) { for (int i = 0; i < mBase.groupCount(); i++) { String gn = pBase.groupName(i); if (gn != null) { String gv = mBase.group(i); this.dataList.add(new Pair<String, String>(gn, gv)); } } String subText = mBase.group(sgIndex); if (subText != null) { this.dataList.addAll(this.getSubGroupDataList(pa, subText)); // 调用使用子组正则模板进行抽取的方法 } } } public List<Pair<String, String>> getSubGroupDataList(Pattern[] pa, String subText) { // 使用子组正则模板进行抽取 List<Pair<String, String>> list = new ArrayList<Pair<String, String>>(); for (int i = 1; i < pa.length; i++) { Pattern subp = pa[i]; Matcher subm = subp.matcher(subText); while (subm.find()) { for (int k = 0; k < subm.groupCount(); k++) { String gn = subp.groupName(k); if (gn != null) { String gv = subm.group(k); list.add(new Pair<String, String>(gn, gv)); } } } } return list; } public String readString(String fileName) { InputStream in = this.getClass().getResourceAsStream("/" + fileName); BufferedReader reader = new BufferedReader(new InputStreamReader(in)); StringBuffer sb = new StringBuffer(); String line = null; try { while ((line = reader.readLine()) != null) { sb.append(line); } } catch (IOException e) { e.printStackTrace(); } return sb.toString(); } public List<Pair<String, String>> getDataList() { return this.dataList; } public static void main(String[] args) { InfomationExtraction ie = new InfomationExtraction("bbsPage.txt", "pattern.txt"); ie.extract(2); for (Pair<String, String> p : ie.getDataList()) { System.out.println("[" + p.getKey() + " " + p.getValue() + "]"); } } }
测试一下,如下所示:
[title 标题] [name a1] [when 2009] [content p1] [name a2] [when 2008] [content p2] [name a3] [when 2007] [content p3] [name a4] [when 2006] [content p4] [name 2005] [when t5] [content p5]
至于如何组织抽取到的信息,比如你可能使用Lucene的索引,需要构造Field和Document,那么你就要设计一个实体能够包含一个Document的所有的Field,比如一个Document包括:URL、标题、作者、发表时间、发表内容这五个项,非常容易就能做到。
使用JRegex库,可以非常灵活地配置模板,尤其是对多个组的设计,这要根据你的需要来考虑。
原文声明如下:本文基于署名-非商业性使用-相同方式共享 4.0许可协议发布,欢迎转载、使用、重新发布,但务必保留文章署名时延军(包含链接:http://shiyanjun.cn),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请与我联系。