记得知乎上有人把这个当做练习题发出来过,正好自己也进行过这方面的尝试,在这里把自己的思路写下来,抛砖引玉。希望大家一起讨论。

提取正文这件事可以很简单,也可以很复杂,跟你对它的要求直接有关,要不要提取其中的图片?要不要保留格式?这个程序是只针对一个网站还是要针对大部分乃至所有你想提取正文的网站?如果你只想开发针对一个网站的程序,那其实不管你对正文内容的要求有多高相对也是比较容易的,BeautifulSoup、css选择器、xpath……只要分析一下你的目标网站,按照它的格式规律把各个部分提取出来即可,我们接下来用BeautifulSoup举例,虽然BeautifulSoup的速度很慢,但就我目前的经验来看,它也目前我接触到的解析器里相对性能最强的。

比如你想提取新浪的正文,首先当然我们要提取整个页面,比如我们要提取这个页面:

import requests
from bs4 import BeautifulSoup
url = 'http://mil.news.sina.com.cn/china/2017-04-05/doc-ifycwymx3854291.shtml'
page = requests.get(url)
page.encoding = 'utf-8'

#编码……恐怕是Python里最让人头疼的问题了

soup = BeautifulSoup(str(page.text), 'html.parser')

我们用Chrome自带的开发者工具(网页上右键,检查)分析一下这个网页,轻而易举的发现,这些内容都在一个class是'content',id是'artibody'的元素内,接下来就很简单了,我们通过选择id也好,选择class也好,不过一句话的事。

article = soup.select('.content')[0].text

#或者

article = soup.select('#artibody')[0].text

#Select返回一个list,[0]选中其中第一个元素(其实在这个网页里,这个list只会有一个元素)

就可以得到正文,就是这么简单。


我们来数一下代码,1,2,3……一共就用了5行,我要是再丧心病狂点,还可以更少。

但是这种方法可以说,不出意外的话,基本只能适用于新浪这一个网站,谁知道你正文用的是content还是article呢,我们看看网易。


果然,class name变成了'post_text',id则是'endText',当然,其实内容值得看的网站也就那么几个,就算你针对它们一个个的去分别设立不同的规则也不亏啊,毕竟就算设10个网站,也才5x10 = 50行而已。噗,其实也没有那么简单拉,但是总的来说,这种方法是可以考虑的。

但是当我在写这个程序的时候,我也不知道就哪来一股轴劲,一定想写出来一种能在所有有文章的页面通用的正文提取规则,还要把图片也都按顺序弄下来。事实上我当时自己用WordPress在自己的台式机做的服务器上搭了一个网站,我的目标就是从网站上把需要的文章弄下来,然后贴到自己的网站上(当然不是为了盈利,也在文章开头著名了原文链接)。

于是我就开始想办法了,就像我之前写的那样,我最开始考虑的也是提取正文文字的规则,因为正文那一块看起来最好找嘛。

虽然没法从元素的类别或名字等信息来识别出哪个元素里是文章的正文了,但是看看下面这段正文附近的源代码,我们还是不难发现一个很显著的特征:在正文部分,html代码的密度瞬间低到了极点,形成这样现象的原因有多个,但其实这样的排版方式并不是必须的,理论上说,只要一个网站想,它完全可以让正文部分的html代码的密度几乎不低于其他部分,而在前台的显示效果并不会受到影响。但很幸运的是,我至今还没有碰到过刻意这样做的网站,或许也是因为这样做的成本太过高于所得的收益了吧。

总之,我们发现,我们可以通过判断元素内容内中文文字的占比来判断这个元素内是不是正文。思路有了,我们撸起袖子就开干。

首先,我们知道,html使用div来将网页区分成不同的区域。所以,除非网站非常非常的特立独行,用了一种无比诡异的实现方法实现了网页的显示(估计也是成本远大于收益的事,所以我目前还没见过有人去做。),一般来说文章肯定是放在某个div里,对于一个程序来说,刚拿到一个网页,不可能马上就知道哪个是含有正文的那个div,所以我们选中所有div元素。

part = soup.select('div')

这里有一个很不幸的事情,一般来说只要IDE有限制控制台内显示的代码行数,你在执行这句指令之后得到的part的内容一定会超过这个行数,因为几乎所有的网页都会有div嵌套,在最终的网页上看来,只有最大的那个div会占用实实在在的空间,而里面的div占用的空间都是在其之内的,但这条指令会选中所有的div,也就是说,它不仅会选中包含了所有小div的最大div块,还会将每个较小的div都分别选中,并把其中的内容也记录下来。如果你把这些东西都写入文件里仔细比较,你会发现part的大小经常一下就膨胀到了soup的两到三倍。

不过对Python来说其实这都没差啦,接下来我们只要写一个循环,判断里面每个div的中文占比,剔除掉那些中文文字数占比很小的div就行啦(话说写到这里我才突然意识到我这个程序对英文网页无效,改天再研究一下怎么办)

统计中文我们可以用正则表达式,通过统计其中与中文Unicode范围相匹配的字符数,理论上匹配成功的次数就是中文的字数,实践上虽然这两个结果有一定差异,但是差异很小,基本不会影响判断,因为正文部分与其他元素中文占比的差距实在是太悬殊了,所以就算统计有一点误差,我们也有很大的定标准的空间。给一个参考的统计中文字数,并计算其在整个div字符中占比的函数:

def countchn(string):
pattern = re.compile(u'[\u1100-\uFFFDh]+?')
result = pattern.findall(string)
chnnum = len(result) #list的长度即是中文的字数
possible = chnnum/len(str(string)) #possible = 中文字数/总字数
return (chnnum, possible)

然后是遍历,当可能性大于一定程度,比如0.7,我们就决定是它了:

for paragraph in part:
chnstatus = countchn(str(paragraph))
possible = chnstatus[1]
if possible > 0.7:
article = str(paragraph)

但是!聪明的小朋友一定已经发现了,这段程序其实很脆弱。首先div既然是可以嵌套的,装着文章的最小div外面很可能还嵌套着别的div,是有可能同时出现好几个possible > 0.7的情况的,如果最后一个出现的不正好就是真正的正文div,最后解析出的结果就有可能带有其他无关的内容。

那我们应该再提高一点这个准入的门槛吗?似乎也不行,因为有些文章,尤其是图多的文章,由于图片的url会跟文章正文内容一起写在正文div里,极大的降低了div内中文字符的占比。要是possible的标准定的太高了,很多这类的文章会被误判的。

所以综合各种情况之后,我想了一种办法,这不一定是最优解,但经过我爬取各大门户网站页面至今的结果,这个方法的效果至少还是比较靠谱的,我在这里写在下面,也欢迎各路大神来打我的脸。

思路就是把possible标准调低一点,然后建立一个list,把所有符合标准的div都装进去。在里面再比较一次,统计每个div的字数,通过设立一个最低的字数门槛,把由于标准降低可能意外筛选进来的一些元素(比如列表项很多很密集的div)一个一个从list里删掉,一般来说,剩下的就是嵌套的几个包含着正文的div了,这时只要选其中最短的一个就好了。或许从一开始就采用最低字数、概率、长短综合考虑的比较方式会比较方便一点,但是我想我这种方法应该能快一些,毕竟并不总是需要比较三个参数。代码如下:

def findtext(part):
length = 50000000
l = []
for paragraph in part:
chnstatus = countchn(str(paragraph))
possible = chnstatus[1]
if possible > 0.15:
l.append(paragraph)
l_t = l[:]

#这里需要复制一下表,在新表中再次筛选,要不然会出问题,跟Python的内存机制有关

for elements in l_t:
chnstatus = countchn(str(elements))
chnnum2 = chnstatus[0]
if chnnum2 < 300:

#最终测试结果表明300字是一个比较靠谱的标准,低于300字的正文咱也不想要了对不

l.remove(elements)
elif len(str(elements))
length = len(str(elements))
paragraph_f = elements

不出意外的话,最终得出的paragraph_f就会是我们想要的包含html代码的正文div了,接下来只要像前面那样,.text一下,就能完成正文的提取了~

我也不知道为什么,在众多语言中,只有Python让我感觉看着很舒服,所以这也算是我第一个好好学习了的语言吧。改天有时间再写写我是怎么从原文中提取图片,并按原顺序发到我的网站上的吧,今天就写到这了。大家再见ヾ( ̄▽ ̄)/