美文网首页我爱编程
40 行代码搞定网页文本抽取

40 行代码搞定网页文本抽取

作者: Pope怯懦懦地 | 来源:发表于2018-06-21 20:33 被阅读166次

这篇《我为开源做贡献,网页正文提取——Html2Article》给了我很大启发。以下就是对基于文本密度提取算法的改进。

这个算法源于我们的两个观察:

  1. 正文所在的区域往往字符比较密集,排除掉 html 标签后。
  2. 正文文本往往连成一片。

所以,我们设想:在我们清洗掉 html 标签后,会不会得到这样一种画面:

横轴是「行号」,纵轴是「该行的字符数」

如果真是这样,我们只要找出这个「窗口」的范围,就可以大致提取出文本正文了。然后再上一些小措施,提高下识别率,想想也是~~

require "open-uri"
require "nokogiri"
require "json"
require 'pp'

url = "http://news.sohu.com/20131229/n392604462.shtml"
html = open(url).read.force_encoding('gbk').encode('utf-8')

strip_html = html.gsub(/<script.+?<\/script>/im, '')
                 .gsub(/<\/?.+?>/m, '')
                 .gsub(/^[\t\s]+/, '')
                 .gsub(/[\t\s]+$/, '')
                 .gsub(/(&nbsp;)+/, '')
                 .gsub(/(&#160;)+/, '')
                 .gsub(/https?:\/\/[\w\/\.]+/, '')
                 .gsub(/\r\n/, "\n")

line_sizes = []
strip_html.each_line do |line|
    puts "#{line.size}:\t>>#{line}"
end
左边是「该行的字符数」,「>>」之后是清洗过的原文。正文是 27~81 行。

好吧,还是画张图吧。

MB,骗子🤥!!!说好的驼峰呢

别急别急,我们先做个平滑处理,看能不能把曲线弄得好看(特征明显)一点:

平滑处理 逐渐放大平滑窗口的效果

好吧,好吧,单独把「平滑窗口 = 7」的曲线拎出来。

k = 7

现在可以清晰地看到,曲线在 21 附近陡然爬升,又在 81 附近骤然下降,而这段区间正好对应正文的位置( 27 ~ 81 行)。

下面怎么把这两个边界提取出来呢?很自然地想到了看看「斜率」或是「曲率」:

呃~~,谁说的「求导试试」的?!你出来,我保证不打死你!

但仔细一看,也不是全无用处嘛。毕竟在 21 和 81 附近挣扎得也挺卖命的嘛。有没有什么办法,即可以展示出坡度的变化,又能展示出值在高位徘徊?要不加个当前曲线值试试?

取边界

如果 x_i-1 和 x_i 很接近,边界指示值就接近 0 ;而如果两者相差很大(不管谁大),其值都不会小。好像很有道理的样子🤔

呃~~,谁说「绝对好使,信我」的?!你出来,我保证不打死你!

等等,等等,好像 21 和 81 附近有两处小起伏,要不乘个「放大系数」再试试:

我擦!好像成嘞!!!其实上不上「放大系数」,只对人眼有意义,对机器毫无意义。 凑巧把原文全覆盖了

完整代码:

require "open-uri"
require "nokogiri"
require "json"

url = "http://news.sohu.com/20131229/n392604462.shtml"
html = open(url).read.force_encoding('gbk').encode('utf-8')

strip_html = html.gsub(/<script.+?<\/script>/im, '')
                 .gsub(/<\/?.+?>/m, '')
                 .gsub(/^[\t\s]+/, '')
                 .gsub(/[\t\s]+$/, '')
                 .gsub(/(&nbsp;)+/, '')
                 .gsub(/(&#160;)+/, '')
                 .gsub(/https?:\/\/[\w\/\.]+/, '')
                 .gsub(/\r\n/, "\n")

line_sizes = []
strip_html.each_line { |line| line_sizes << line.size }

def smooth(vs, k)
    w = []
    vs[0..-k].each_with_index do |l, i|
        w << (vs[i..(i + k - 1)].sum / k.to_f)
    end
    w
end

alpha = 100
xs = smooth(line_sizes, 7)
ys = []
ys[0] = 0
(1...xs.size).each do |i|
    ys[i] = alpha * (1 - xs[i - 1] / xs[i].to_f)
end

y_b = ys.index(ys.max)
y_e = ys.index(ys.min)

puts strip_html.split(/\n/)[y_b..y_e].join("\n")

其实还可以更简单

其实在想到这个算法之前,我就找到了这个:URL2io ,一个专做网页文本提取的服务。上去注册个账号,然后调接口就行了。

require "json"
require "faraday"

url = "http://news.sohu.com/20131229/n392604462.shtml"
con = Faraday.new 
res = con.get do |req| 
    req.url 'http://api.url2io.com/article'
    req.params['token'] = 'xxxxxxxxx'
    req.params['url'] = url
end

pp JSON.parse(res.body)

你知道,要在知道有现成服务的情况下,还去写完同样功能的算法,需要多大的毅力吗?!🤦‍♂️

改进

试了几个网页,效果还不错。但也有严重跑偏的,比如这个网页

分析其原因,没有考虑「文本密度的绝对大小」。尝试修改下公式:

def edge(xs, k = 9)
    alpha = 2
    epsilon = 0.00001
    ys = Array.new(k, 0)
    (k..(xs.size - k)).each do |i|
        if xs[i - 1] == 0 and xs[i] == 0
            ys[i] = 0
        else
            if xs[i - 1] < xs[i]
                beta_i = xs[i..(i + k - 1)].sum / k.to_f
            else
                beta_i = xs[(i - k + 1)..i].sum / k.to_f
            end
            ys[i] = alpha * beta_i * (1 - xs[i - 1] / (xs[i] + epsilon))
        end
    end
    ys
end

ys = edge(smooth(line_sizes, 7))
y_b = ys.index(ys.max)
y_e = ys.index(ys.min)

嗯,效果好多了。

相关文章

网友评论

    本文标题:40 行代码搞定网页文本抽取

    本文链接:https://www.haomeiwen.com/subject/lsfcyftx.html