周末两天在家闲着没事,于是整理了一下之前的的文档和一些琐碎的测试代码,居然发现了几个月前写的一个新闻类主题型网页正文文本自动抽取模块。当时 写的比较简单和粗糙,虽然抽取结果差强人意,但是也还勉强说得过去。于是清理一下代码上的灰尘,做了一个小Demo,分享一下。
作者写这篇文章的主要目的在于抛砖引玉,同时希望能够以此为契机,与诸位大牛讨论一下机器网页内容智能识别方面的。作者自知才疏学浅,文中如有描述不当之处,请不吝指正,感激不尽!
应该说,在WEB分块领域,已经有大量的研究工作。由于HTML语法的灵活性,目前大部分的网页都没有完全遵循W3C规范,这样可能会导致DOM树 结果的错误。更重要的是,DOM树最早引入是为了在浏览器中进行布局显示,而不是进行WEB页面的语义结构描述。某些文献中提到,根据标签把网页分成若干 内容块,这些分块方法流程简单,但面对日益复杂的网页和不规范的网页结果,其分块效果往往不能令人满意。
另一类方法从视觉特征对页面结构进行挖掘。典型的代表就是微软亚洲研究院提出的VIPS(Vision-based Page Segmentation)。它利用WEB页面的视觉提示,如背景颜色、字体颜色和大小、边框、逻辑块和逻辑块之间的间距等,结合DOM树进行语义分块, 并把它应用在了TREC2003的评测中,取得较好的效果。但是由于视觉特征的复杂性,如何保证规则集的一致性是一大难点,另外VIPS算法需要计算和保 存DOM树中所有节点的视觉信息,导致该算法在时间和内存上的消耗比较大,使得在处理含有大量DOM节点的网页时性能不高。
利用网页的视觉特征和DOM树的结构特性对网页进行分块,并采用逐层分块逐层删减的方法将与正文无关的噪音块删除,从而得到正文块。对得到的正文块运用VIPS算法得到完整的语义块,最后在语义块的基础上提取正文内容。试验表明,这种方法是切实可行的。
文本网页可分为两种类型:主题型网页、目录型网页。主题型网页通常通过成段的文字描述一个活多个主题,虽然主体性网页也会出现图片和超链接,但是这 些图片和超链接并不是网页的主体。目录型网页通常提供一组相关或者不相关的连接,本文所研究的正文信息提取指的是针对新闻类主题型网页中核心文本的提取, 理论上来讲,应用本文所述研究成果,将算法稍加改造,将同样适用于论坛类主题型网页的正文提取。
分块统计算法
该算法中首先将DOM树中的节点类型分为四类,分别是容器类、文本类、移除类和图片类。
- /**
- * 检查指定DOM节点是否为容器类节点
- * @param node
- * @return
- */
- protected boolean checkContainerType(DomNode node) {
- return (node instanceof HtmlUnknownElement || node instanceof HtmlFont || node instanceof HtmlListItem || node instanceof HtmlUnorderedList || node instanceof HtmlDivision || node instanceof HtmlCenter || node instanceof HtmlTable || node instanceof HtmlTableBody || node instanceof HtmlTableRow || node instanceof HtmlTableDataCell || node instanceof HtmlForm);
- }
- /**
- * 检查指定DOM节点是否为文本类节点
- * @param node
- * @return
- */
- protected boolean checkTextType(DomNode node) {
- return (node instanceof HtmlSpan || node instanceof DomText || node instanceof HtmlParagraph);
- }
- /**
- * 检查指定DOM节点是否为移除类节点
- * @param node
- * @return
- */
- protected boolean checkRemoveType(DomNode node) {
- return (node instanceof HtmlNoScript || node instanceof HtmlScript || node instanceof HtmlInlineFrame || node instanceof HtmlObject || node instanceof HtmlStyle || node instanceof DomComment);
- }
- /**
- * 检查指定DOM节点是否为图片类节点
- * @param node
- * @return
- */
- protected boolean checkImageType(DomNode node) {
- return (node instanceof HtmlImage);
- }
算法将文本类节点作为探测的直接对象,在探测过程中遇到移除类节点时直接移除,遇到图片类节点时不做统计,直接跳过,但是不会移除该类节点,遇到容器类节点时,直接探测其子节点。
- /**
- * 检查指定节点是否含有容器类节点,如果有则继续递归跟踪,否则探测该节点文本数据
- * @param nodeList
- * @return 是否含有容器类节点
- */
- protected boolean checkContentNode(List<DomNode> nodeList) {
- boolean hasContainerNode = false;
- if (nodeList != null) {
- for (DomNode node : nodeList) {
- if (checkRemoveType(node)) {
- node.remove();
- } else if (checkContainerType(node)) {
- List<DomNode> list = node.getChildNodes();
- if (list != null) {
- hasContainerNode = true;
- if (!this.checkContentNode(list)) {
- if (cleanUpDomNode(node)) {
- checkNode(node);
- }
- }
- }
- } else {
- if (node.isDisplayed() && checkTextType(node)) {
- checkNode(node);
- } else {
- cleanUpDomNode(node);
- }
- }
- }
- }
- return hasContainerNode;
- }
- /**
- * 清理指定节点内的无效节点
- * @param element
- * @return 该节点是否有效
- */
- protected boolean cleanUpDomNode(DomNode element) {
- if (element == null) {
- return false;
- }
- List<DomNode> list = element.getChildNodes();
- int linkTextLength = 0;
- boolean flag = false;
- if (list != null) {
- for (DomNode node : list) {
- if (checkTextType(node)) {
- continue;
- } else if (checkRemoveType(node)) {
- node.remove();
- flag = true;
- } else if (checkImageType(node)) {
- //图片类型节点暂时不作处理
- } else if (node instanceof HtmlAnchor) {
- String temp = node.asText();
- temp = encoder.encodeHtml(temp);
- int length = Chinese.chineseLength(temp.trim());
- if (length > 0) {
- linkTextLength += length;
- }
- } else if (checkContainerType(node)) {
- if (!cleanUpDomNode(node)) {
- node.remove();
- }
- flag = true;
- }
- }
- }
- String content = element.asText();
- content = encoder.encodeHtml(content);
- return (flag || Chinese.chineseLength(content.trim()) - linkTextLength > 50 || (content.trim().length() - Chinese.chineseLength(content) > 5 && !flag));
- }
当算法首次探测到有效文本块时,记录该文本块的父节点块,并推测其为该网页正文文本所在区域,稍后发现可能性更高的块时,直接替换前一个推测为网页正文区域的块。
- /**
- * 推测正文文本区域信息
- * @param node
- */
- protected void checkNode(DomNode node) {
- String temp = node.getTextContent();
- temp = encoder.encodeHtml(temp);
- int length = Chinese.chineseLength(temp.trim());
- temp = null;
- if (contentNode != null && contentNode.equals(node.getParentNode())) {
- maxLength += length;
- } else if (length > maxLength) {
- maxLength = length;
- contentNode = node.getParentNode();
- }
- }
这样的逻辑适合于新闻、博客等整个网页中只有一个文本区域的主题型网页,而不适合与论坛、贴吧等网页中有多个兄弟文本区域的主题型网页。这里可以稍加改造,对探测到的所有文本块,及其所在区域做一个联合统计和推测,即可适用于论坛主题的网页。
该步骤采用递归探测,探测结果为0个或1个DOM节点(如果是论坛类主题页面,识别结果就会是N个DOM节点)。
- /**
- * 在N个DOM节点中搜索正文文本所在的节点
- * @param nodeList
- * @return
- */
- public DomNode searchContentNode(List<DomNode> nodeList) {
- checkContentNode(nodeList);
- if (cleanUpDomNode(contentNode)) {
- return contentNode;
- } else {
- return null;
- }
- }
机器学习
当智能识别
模块识别某个网页正文文本成功时,其将自动保存该网页的相关信息和探测结果数据(目前仅仅记录探测到的正文区域的XPath,稍后可能会保存一个支持序列
化和反序列化的对象来存储更多探测结果),下次再探测该网页或探测来自该网页所在domain的其他网页时,将首选从知识库中取出之前的经验,来抽取正
文,如果抽取失败,则继续尝试智能识别。目前每一个domain只支持保存一条经验,但是稍后可以扩展成一个列表,并按照其成功应用的次数来排序,甚至于
在该模块内添加一个过滤链,在智能识别前首先将任务通过过滤链,如果在过滤链内根据以往经验抽取成功,则直接返回,否则再由智能识别模块探测正文文本。将
来也有可能会将机器学习和人工指导结合起来,既可以自主积累知识,也可以从数据源中加载认为添加的知识。
以下代码并不能构成真正的机器学习,实际上,这里仅仅只做了一个演示。
- /**
- * 在网页中搜索正文文本
- * @param html
- * @return 识别成功后,将封装一个PageData对象返回<br/>
- * 之所以返回一个对象,是考虑到将来可能加强该算法后,能够从页面中抽取更多数据,PageData对象能够更好的封装这些结果
- */
- public static PageData spotPage(HtmlPage html) {
- if (html == null) {
- return null;
- }
- String domain = URLHelper.getDomainByUrl(html.getUrl());
- String contentXPath = contentXPathMap.get(domain);
- PageData pageData = null;
- HtmlEncoder encoder = HtmlEncoderFactory.createHtmlEncoder(html.getPageEncoding());
- AutoSpot2_2 spoter = new AutoSpot2_2(encoder);
- List<DomNode> nodeList = null;
- if (contentXPath != null) {
- log.debug("在经验库中找到站点[" + domain + "]的相关数据,正在尝试应用. . .");
- nodeList = (List<DomNode>) html.getByXPath(contentXPath);
- } else {
- log.debug("第一次遇到站点[" + domain + "],正在尝试智能识别. . .");
- }
- if (nodeList == null || nodeList.isEmpty()) {
- nodeList = html.getBody().getChildNodes();
- log.debug("经验库中关于站点[" + domain + "]的相关数据无法应用于当前网页,正在尝试重新识别. . .");
- }
- if (nodeList != null) {
- DomNode node = spoter.searchContentNode(nodeList);
- if (node != null) {
- pageData = new PageData();
- pageData.setContent(encoder.encodeHtml(node.asXml()));
- pageData.setTitle(html.getTitleText());
- contentXPathMap.put(domain, node.getCanonicalXPath());
- }
- }
- return pageData;
- }
Demo运行截图
第一次识别网页用时50毫秒
第二次识别,使用之前的探测结果,直接在页面中抽取数据,用时更短
抽取结果--网页正文的HTML代码,当然,结果也可以是纯文本。
就目前而言,该算法的探测结果还是勉强能够让人接受的,通过对来自百度新闻的新闻页面进行跟踪和识别,识别成功率超过90%,准确率则低很多,不到80%。