这篇《我为开源做贡献,网页正文提取——Html2Article》给了我很大启发。以下就是对基于文本密度提取算法的改进。
这个算法源于我们的两个观察:
- 正文所在的区域往往字符比较密集,排除掉 html 标签后。
- 正文文本往往连成一片。
所以,我们设想:在我们清洗掉 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(/( )+/, '')
.gsub(/( )+/, '')
.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(/( )+/, '')
.gsub(/( )+/, '')
.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)
嗯,效果好多了。
网友评论