识别网页上的电话号码,一个比较容易想到的方法就是,通过预先设计电话号码的正则表达式,对网页文本内容中电话号码进行匹配,抽取出对应的联系方式。然而,这种方法是假定电话号码都是按照比较理想的格式在网页上展示的,自然对于这样的识别精度会很高,但是同时也漏掉了很多电话号码。如果你没有深入分析处理过Web网页数据,你是想象不到互联网上网页的格式到底有多不规范。
这里,我们实现一种识别网页上电话号码的方法,不需要设计精确的正则表达式来匹配电话号码,而是通过电话号码最抽象的特征来考虑和设计。
电话号码一定是一个含有数字的序列,而且可能数字之间通过一些特殊或常见的字符来进行分隔,比如“逗号”、“短线”、“空格”、“字母”等等。我们通过对一个页面的文本内容进行分析,将放宽数字字符串的定义:
如果两个数字字符之间连续,则认为两个数字字符属于同一个序列;如果两个数字字符之间存在小于给定阈值限制个数的非数字字符,则认为这两个数字字符也属于同一个序列。这种观点的实质是,将距离比较近的数字字符串合并为一个独立的序列,这样,通过分析一个页面的文本内容就可以得到一个数字字符序列的集合。
然而,这样会把比较短的数字,如日期、年龄、序号等都分析出来。自然而然想到,通过过滤算法将其过滤掉。我们这里通过一种推荐模型,计算每个数字字符序列的相似度,然后根据相似度进行排序,再从排序靠前的数字字符串序列中筛选出电话号码。
下面,看看我们用Java实现这个思路,并观察一下结果。
定义一个序列推荐接口SequenceRecommendation,recommend方法是具体的实现逻辑,可以根据自己的需要去设计。
package org.shirdrn.webmining.recommend;
public interface SequenceRecommendation {
public void recommend() throws Exception;
}
下面,我们实现一个用来抽取数字字符串序列的算法,并计算相关度,从而进行排序推荐。基本思路如下:
1、清洗原生的网页:将HTML标签等等都去掉,得到最终的文本内容。
2、对文本内容进行分词:使用Lucene自带的SimpleAnalyzer分析器(未使用停用词过滤),之所以选择这个是因为,在数字字符序列附近(前面和后面)存在某些具有领域特定含义的词(如电话号码数字前面和后面可能存在一些词:phone、telephone等;Email地址附近可能存在一些词:email、email us等;等等),可能它是一个停用词(对StandardAnalyzer等来说),我们不希望过滤掉这些词。另外,我们记录了每个词的位置信息。
3、聚集数字字符序列,同时记录前向和后向指定数量的词(核心):这个应该是最核心的,需要精细地处理文本内容,和设计数据结构,得到一个我们能够方便地进行相关度计算的结果集。
4、根据一个样本集的计算结果,来建立领域模型(特征词向量),用于计算数字字符序列的相关度:我这里收集了一部分英文网页,通过英文网页的分析处理,提炼出一批特征词,为简单起见直接使用词频作为权重(注意:这样使用词频简单而且合理,也可以采用其他的方法进行权重的计算,或者补充其它属性权重的贡献)。我们这里使用了两个特征词向量,分别如下所示:
前向特征词向量(文件forwards_feature_vector):
email 9124
e 3368
mail 4767
e-mail 2183
email us at 178
fax 147
email address 146
email us 121
fx 115
or 113
email us 102
email or 95
email us at 76
or e-mail 67
后向特征词向量(文件backwards_feature_vector):
phone 27407
call 13697
free 13092
toll 10092
toll free 9012
tel 8710
call 5247
telephone 4052
call us 3108
ph 3067
t 2838
p 2830
contact us 2150
or call 1889
local 1477
f 1437
or 1362
abn 1257
call us at 1194
office 1183
call us today 1152
customer service 1101
call toll free 1080
我们的特征词向量是通过文件形式导入,在后面的测试用例中使用。
5、相关度排序,并进行推荐:这里排序后就可一目了然,排在前面的是电话号码的可能性最大。
下面是整个思想的设计及其实现,NumberSequenceRecommendation类的代码,如下所示:
package org.shirdrn.webmining.recommend;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.analysis.en.EnglishAnalyzer;
import org.apache.lucene.analysis.tokenattributes.OffsetAttribute;
import org.apache.lucene.analysis.tokenattributes.TermAttribute;
import org.apache.lucene.util.Version;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Node;
import org.jsoup.nodes.TextNode;
import org.jsoup.parser.Parser;
public class NumberSequenceRecommendation implements SequenceRecommendation {
private byte[] content;
private Charset charset;
private String baseUri;
/** Max count of non-number number sequence in a continual number sequence,
* on conditions of which we think the number sequence is continual.*/
private int maxGap = 5;
/** Max word count after or before a number sequence */
private int maxWordCount = 5;
private Pattern numberPattern = Pattern.compile("^\\d+$");
private String cleanedContent;