从HTML文件中抽取正文的简单方案

译者导读:这篇文章主要介绍了从不一样类型的HTML文件中抽取出真正有用的正文内容的一种有普遍适应性的方法。其功能相似于CSDN近期推出的“剪 影”,可以去除页眉、页脚和侧边栏的无关内容,很是实用。其方法简单有效而又出乎意料,看完后不免大呼原来还能够这样!行文简明易懂,虽然应用了人工神经 网络这样的算法,但由于FANN良好的封装性,并不要求读者须要懂得ANN。全文示例以Python代码写成,可读性更佳,具备科普气息,值得一读。
每一个人手中均可能有一大堆讨论不一样话题的HTML文档。但你真正感兴趣的内容可能隐藏于广告、布局表格或格式标记以及无数连接当中。甚至更糟的是,你但愿 那些来自菜单、页眉和页脚的文本可以被过滤掉。若是你不想为每种类型的HTML文件分别编写复杂的抽取程序的话,我这里有一个解决方案。
本文讲述如何编写与从大量HTML代码中获取正文内容的简单脚本,这一方法无需知道HTML文件的结构和使用的标签。它可以工做于含有文本内容的全部新闻文章和博客页面……
你想知道统计学和机器学习在挖掘文本方面可以让你省时省力的缘由吗?
答案极其简单:使用文本和HTML代码的密度来决定一行文件是否应该输出。(这听起来有点离奇,但它的确有用!)基本的处理工做以下:
  • 1、解析HTML代码并记下处理的字节数。
  • 2、以行或段的形式保存解析输出的文本。
  • 3、统计每一行文本相应的HTML代码的字节数
  • 4、经过计算文本相对于字节数的比率来获取文本密度
  • 5、最后用神经网络来决定这一行是否是正文的一部分。
仅仅经过判断行密度是否高于一个固定的阈值(或者就使用平均值)你就能够得到很是好的结果。但你也可使用机器学习(这易于实现,简直不值一提)来减小这个系统出现的错误。如今让我从头开始……

转换HTML为文本

你须要一个文本模式浏览器的核心,它应该已经内建了读取HTML文件和显示原始文本功能。经过重用已有代码,你并不须要把不少时间花在处理无效的 XML文件上。咱们将使用Python来完成这个例子,它的htmllib模块可用以解析HTML文件,formatter模块可用以输出格式化的文本。 嗯,实现的顶层函数以下:
def extract_text(html):
# Derive from formatter.AbstractWriter to store paragraphs.
writer = LineWriter()
# Default formatter sends commands to our writer.
formatter = AbstractFormatter(writer)
# Derive from htmllib.HTMLParser to track parsed bytes.
parser = TrackingParser(writer, formatter)
# Give the parser the raw HTML data.
parser.feed(html)
parser.close()
# Filter the paragraphs stored and output them.
return writer.output()
TrackingParser覆盖了解析标签开始和结束时调用的回调函数,用以给缓冲对象传递当前解析的索引。一般你不得不这样,除非你使用不被推荐的方法——深刻调用堆栈去获取执行帧。这个类看起来是这样的:
class TrackingParser(htmllib.HTMLParser):
"""Try to keep accurate pointer of parsing location."""
def __init__(self, writer, *args):
htmllib.HTMLParser.__init__(self, *args)
self.writer = writer
def parse_starttag(self, i):
index = htmllib.HTMLParser.parse_starttag(self, i)
self.writer.index = index
return index
def parse_endtag(self, i):
self.writer.index = i
return htmllib.HTMLParser.parse_endtag(self, i)
LinWriter的大部分工做都经过调用formatter来完成。若是你要改进或者修改程序,大部分时候其实就是在修改它。咱们将在后面讲述怎么为它加上机器学习代码。但你也能够保持它的简单实现,仍然能够获得一个好结果。具体的代码以下:
class Paragraph:
def __init__(self):
self.text = ''
self.bytes = 0
self.density = 0.0
class LineWriter(formatter.AbstractWriter):
def __init__(self, *args):
self.last_index = 0
self.lines = [Paragraph()]
formatter.AbstractWriter.__init__(self)
def send_flowing_data(self, data):
# Work out the length of this text chunk.
t = len(data)
# We've parsed more text, so increment index.
self.index += t
# Calculate the number of bytes since last time.
b = self.index - self.last_index
self.last_index = self.index
# Accumulate this information in current line.
l = self.lines[-1]
l.text += data
l.bytes += b
def send_paragraph(self, blankline):
"""Create a new paragraph if necessary."""
if self.lines[-1].text == '':
return
self.lines[-1].text += 'n' * (blankline+1)
self.lines[-1].bytes += 2 * (blankline+1)
self.lines.append(Writer.Paragraph())
def send_literal_data(self, data):
self.send_flowing_data(data)
def send_line_break(self):
self.send_paragraph(0)
这里代码尚未作输出部分,它只是聚合数据。如今咱们有一系列的文字段(用数组保存),以及它们的长度和生成它们所须要的HTML的大概字节数。如今让咱们来看看统计学带来了什么。

数据分析

幸运的是,数据里老是存在一些模式。从下面的原始输出你能够发现有些文本须要大量的HTML来编码,特别是标题、侧边栏、页眉和页脚。
虽然HTML字节数的峰值屡次出现,但大部分仍然低于平均值;咱们也能够看到在大部分低HTML字节数的字段中,文本输出却至关高。经过计算文本与HTML字节数的比率(即密度)可让咱们更容易明白它们之间的关系:
密度值图更加清晰地表达了正文的密度更高,这是咱们的工做的事实依据。

过滤文本行

过滤文本行的最简单方法是经过与一个阈值(如50%或者平均值)比较密度值。下面来完成LineWriter类:
    def compute_density(self):
"""Calculate the density for each line, and the average."""
total = 0.0
for l in self.lines:
l.density = len(l.text) / float(l.bytes)
total += l.density
# Store for optional use by the neural network.
self.average = total / float(len(self.lines))
def output(self):
"""Return a string with the useless lines filtered out."""
self.compute_density()
output = StringIO.StringIO()
for l in self.lines:
# Check density against threshold.
# Custom filter extensions go here.
if l.density > 0.5:
output.write(l.text)
return output.getvalue()
这个粗糙的过滤器可以获取大部分正确的文本行。只要页眉、页脚和侧边栏文本并不很是长,那么全部的这些都会被剔除。然而,它仍然会输出比较长的版本 声明、注释和对其它故事的概述;在图片和广告周边的比较短小的文本,却被过滤掉了。要解决这个问题,咱们须要更复杂些的启发式过滤器。为了节省手工计算需 要花费的无数时间,咱们将利用机器学习来处理每一文本行的信息,以找出对咱们有用的模式。

监督式机器学习

这是一个标识文本行是否为正文的接口界面:所谓的监督式学习就是为算法提供学习的例子。在这个案例中,咱们给定一系列已经由人标识好的文档——咱们 知道哪一行必须输出或者过滤掉。咱们用使用一个简单的神经网络做为感知器,它接受浮点输入并经过“神经元”间的加权链接过滤信息,而后输后另外一个浮点数。 大致来讲,神经元数量和层数将影响获取最优解的能力。咱们的原型将分别使用单层感知器(SLP)和多层感知器(MLP)模型。咱们须要找些数据来供机器学 习。以前的LineWriter.output()函数正好派上用场,它使咱们可以一次处理全部文本行并做出决定哪些文本行应该输出的全局结策。从直觉和 经验中咱们发现下面的几条原则可用于决定如何过滤文本行:
  • 当前行的密度
  • 当前行的HTML字节数
  • 当前行的输出文本长度
  • 前一行的这三个值
  • 后一行的这三个值
咱们能够利用FANN的Python接口来实现,FANN是Fast Artificial Neural NetWork库的简称。基本的学习代码以下:
from pyfann import fann, libfann
# This creates a new single-layer perceptron with 1 output and 3 inputs.
obj = libfann.fann_create_standard_array(2, (3, 1))
ann = fann.fann_class(obj)
# Load the data we described above.
patterns = fann.read_train_from_file('training.txt')
ann.train_on_data(patterns, 1000, 1, 0.0)
# Then test it with different data.
for datin, datout in validation_data:
result = ann.run(datin)
print 'Got:', result, ' Expected:', datout
尝试不一样的数据和不一样的网络结构是比较机械的过程。不要使用太多的神经元和使用太好的文本集合来训练(过拟合),相反地应当尝试解决足够多的问题。使用不一样的行数(1L-3L)和每一行不一样的属性(1A-3A)获得的结果以下:
有趣的是做为一个猜想的固定阈值,0.5的表现很是好(看第一列)。学习算法并不能仅仅经过比较密度来找出更佳的方案(第二列)。使用三个属性,下 一个 SLP比前两都好,但它引入了更多的假阴性。使用多行文本也增进了性能(第四列),最后使用更复杂的神经网络结构比全部的结果都要更好,在文本行过滤中减 少了80%错误。注意:你可以调整偏差计算,以给假阳性比假阴性更多的惩罚(宁缺勿滥的策略)。

结论

从任意HTML文件中抽取正文无需编写针对文件编写特定的抽取程序,使用统计学就能得到使人惊讶的效果,而机器学习能让它作得更好。经过调整阈值, 你可以避免出现鱼目混珠的状况。它的表现至关好,由于在神经网络判断错误的地方,甚至人类也难以断定它是否为正文。如今须要思考的问题是用这些“干净”的 正文内容作什么应用好呢?
相关文章
相关标签/搜索