Saturday, November 28, 2009

也谈网页正文提取[上]


看到这里,如果有看官不知道啥叫正文提取,那我只能说,大哥我真的没有忽悠您,我既没说"网页去噪",也没说互联网的"自动摘要",更没说海量互联网数据的"文本挖掘"。由此可见本博是个很厚道的人,会手把手教你如何完成这个看起来牛逼实则很简单的一件事情,绝对让你感到物超所值(阅读的时间)。

从字面意思上理解,网页的正文提取嘛,无非就是把网页当中对咱最有价值的那部分文章给取出来撒.有点编程经验的朋友肯定都知道,右键网页源文件,看看html代码,取出来有正则匹配一下也就几分钟而已的事情。更好一点的办法,那自然是用上像DOM或者XPath这样专门对付html的利器,多写几次估计一分钟也不要。如果本博也这么干的话,那还怎么体现您的慧眼如矩呢:)

上面说的方法,实际上在垂直搜索引擎的定向抓取中,给一个目标站点利用DOM建立抽取模板是一个很常用也很准确的办法。但是当问题域变得稍微大那么一点点,比方说吧,我觉着谷歌做的不错,我也想搞一个的话,那咋整呢?再利用上面的办法,机械的给每一个页面建立DOM,是会死人的哟XD

那么问题其实就变成了对于任意篇网页,有没有办法"聪明"点的法子,能识别出正文部分呢?
既然我们人是可以做到这一点的,那么就说明存在利用人工智能去解决这个问题的可能性.当然,我们现在不急,先从简单的做起.

网页里面除了可读文本就是链接,图片,视频,以及其他媒体类型.而后面这些东东在HTML里面都是用专有的标签来显示的,而且还是被标签所"夹住"的。那我们来看看一个文本块里,除了标签还剩下来的东西有多少.

通过Python内置的htmllib模块和formatter的配合,我们可以统计出网页中每一行文本中标签和正文的数量.代码如下:


#coding:utf-8

import htmllib,urllib2
import formatter,StringIO

class TrackParser(htmllib.HTMLParser):

    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)


class Para:

    def __init__(self):
        self.text = ''
        self.bytes = 0
        self.density = 0.0

class LineWirter(formatter.AbstractWriter):
    """
    a Formatter instance to get text in lines
    """

    def __init__(self):
        self.last_index = 0
        self.lines = [Para()]
        formatter.AbstractWriter.__init__(self)

    def send_flowing_data(self, data):
        t = len(data)
        self.index += t
        b = self.index - self.last_index
        self.last_index = self.index
        l = self.lines[-1]
        l.text += data
        l.bytes += b

    def send_paragraph(self,blankline):
        if self.lines[-1].text == '':
            return
        self.lines[-1].text += 'n'*(blankline+1)
        self.lines[-1].bytes += 2*(blankline+1)
        self.lines.append(Para())
      
    def send_literal_data(self,data):
        self.send_flowing_data(data)
  
    def send_line_break(self):
        self.send_paragraph(0)



def extract_text(html):

    writer = LineWirter()
    fmt = formatter.AbstractFormatter(writer)
    parser = TrackParser(writer,fmt)
    parser.feed(html)
    parser.close()
    return writer.lines


htmls = urllib2.urlopen("http://ent.hunantv.com/d/x/20091128/503722.html")
print map(lambda x:[x.bytes,len(x.text)],extract_text(htmls.read()))

看着飞速跑过的列表,你是不是恨不得把他给全部写入一个文件来看看结果?在文件尾部加入
q = open("e.csv","w+").writelines('\n'.join(["%s,%s"%(x[0],x[1]) for x in s]))

现在结果变成了一个csv文件鸟,来上个图看看:



上图清晰的表达了该网页的文本分布,根据与页面的比对,我们发现文本所在的区域与相对应的行域保持了某种关系.这似乎说明我们的思路是正确的.

实际上行文本字节数与行总字节数的比值被称为行文本密度.有了这个概念,我们就可以对网页全文扫描计算相应的文本密度,这里我们不妨做一个假设,文本密度在0.5以上的就是我们需要的文本部分,也就是说我们认定某行的文本至少和该行的标签一样多的话,他就可能是我们需要的文本区域.

修改上述代码的两个地方,我们来初试下身手,
在原有的LineWriter里加入:

    def output(self):
        self.compute_density()
        output = StringIO.StringIO()
        for l in self.lines:
            if l.density > 0.5: //这里就是我们设置的行文本密度
                output.write(l.text)
        return output.getvalue()

修改extract_text函数
def extract_text(html):
        writer = LineWirter()
        fmt = formatter.AbstractFormatter(writer)
        parser = TrackParser(writer,fmt)
        parser.feed(html)
        parser.close()
        return writer.output()

文件末尾改成
htmls = urllib2.urlopen("http://ent.hunantv.com/d/x/20091128/503722.html")
s = open('e.html','w+').write(extract_text(htmls.read()))

先透口气,然后平静的点开e.html,喔,你看见了什么!

激动之后,应该会有这么一个疑问,刚才我们设置的文本密度为0.5,这个数字到底是怎么来的?他具有普适性么?

其实这个文本密度是可以计算出来的:
设y为行文本集合,z为行标签集合,则文本密度p为:



设i代表任意行,分别用yi,zi代表任意行文本/标签的长度,设且均符合正态分布.uy,uz分别代表行文本和行标签的平均长度:




计算方差:





如果选择文本行的概率为p,那么标签行就为1-p.相应的各文本项平均长度为







最后得到p的估值:




将我们选用的网页实际情况带入以后,我们得到真实的文本密度p大约为0.53,和估计值很相近.学术界有针对行的做了大量实验,得出新闻资讯类网站的文本密度大约在0.4-0.6左右,sina和sohu的这个值大约都是0.6;博客类网站的文本密度大约在0.7-0.8之间.

作为这个话题上半部分的结束,谈谈这个文本密度的实际应用,除了本文所涉及到的文本抽取以外,文本密度现在被广泛应用于搜索引擎网页价值分析的预处理,同时我们也大体可以看出网站内容的大致分布.

下半部分,本博将引入ANN(神经网络)和FDR(错误控制)的相关方法继续探讨这个话题.敬请围观.


光風(gfn)

1 comment:

  1. 你好,这个代码报错self.compute_density()没有定义,请问怎么解决

    ReplyDelete