很久没有发一些有技术含量的文章了,最近发博文都有一种应付式的感觉,真对不起自己。感觉有时候是我沉醉于一样东西太长时间了,把我正常的生活节奏都打乱了,而却没有注意到这样子反而效率很低下。适时抽时间出来总结一下是蛮重要的!所以,以后决定每天都抽一个小时出来自我总结,觉得有所感想就写下来,有技术研究的,就给大家分享一下吧!
这两天在写下面的一个东西,用来搜集博客种子(Feed)的RSS或者Atom地址的。没有种子的博客不会被收录进来。因为只有Feed才对我有用!
网站地址:http://feed.readself.com/
我的需求
因为我最近在做一个读书网站,需要从互联网博客频道中获取文章。之前通过人工添加了将近200个博客的Feed地址,定期更新以获取文章。如今阅读的基本功能已经完成,现在亟需的是大量的文章以供各种实验性的测试,例如文章分类,全文索引等,属于数据挖掘和模式匹配的范畴。
之前已经在构思用何种方式搜集互联网上的Feed来源,李劼杰同学的“中国个人博客索引”给了我很大的灵感。采用博客的“友情链接”的方式进行扩散搜索,而我仿效其方法采用BFS搜集博客的Feed。虽然我们俩做的是不同的方向,但是没想到这么巧合,他所分享的技术东西正是我所要研究的。
话说回来,我要采集的是博客的Feed地址,如何找呢?
寻找Feed地址
最近研究得出的方法是,从博客页面(可以是首页或者是文章页面)的HTML中,遍历所有link标签,采集如下的href属性。
<link rel=”alternate” type=”application/rss+xml” title=”Xiaoxia[PG] » Feed” href=”http://xiaoxia.org/feed/” />
大部分博客的Feed都是按照上面的代码提供,RSS是其中一种聚合类型,还有另外一种常见的是Atom。而常见的RSS还有两种不同的版本,当然这个不需要关心,因为我现在使用FeedParser来帮我采集RSS或者Atom里的内容(这个开源的FeedPaser在处理不正确的RSS时经常出错,需要自己修正代码)。
另外需要注意的是,有点link标签并没有 ref=”alternate” 这个属性,同时还有可能把评论的Feed或者RPC的XML收录进来了。这个时候就需要加以区别了。
我目前采用一种评分的方法,即完全匹配标准格式的,打10分满分。遇到下列情况,则会被扣分:
1、没有 ref=”alternate” 这个属性。
2、href中含有comment字符。
3、href中含有rpc字符。
4、href中含有rsd字符。
在对所有含有RSS或者Atom的link进行评分后,选取最高分作为Feed的地址。
另外需要注意的问题是,href不一定是完整的URL地址,有可能是相对路径,所以需要对其补全。
寻找友情链接
既然过友情链接进行扩散搜集,就需要对每个博客的友情链接李劼杰同学采用的是固定的模板匹配的方法,这种方法的限制是只能识别支持的博客的友情链接,而面对未知的博客类型,就无法提取友情链接了。所以会影响总体搜集的友情链接的数量。而我采用的方法是,基于概率统计的方式,定位友情链接的容器标签。
首先,枚举所有可以存放友情链接的容器,我取了div、table、ul这三个,当然还可能有其他的我还没有考虑到,但这三个已经涵盖了大部分的博客的情况。
对枚举的每个容器,统计所有的链接(a标签)。判断是否为站外链接,如果是,则增加链接数量,以及增加链接文字数量(为何要这个?因为图片超链接将会被排除),否则增加站内链接数。如果该站外链接为新窗口打开,则有相应的奖励,我目前是多算一次链接数量。
然后,通过上述信息统计这个容器的的分数,分数越高,成为友情链接容器的可能性越大。我的统计方法如下:
1、原始分数 = (站外链接 – 站内链接) * 1000,如果原始分数 <= 0,直接否定。
2、链接文字所占比例 = 链接文字数 / 容器总文字数,用这个比例对原始分数进行正影响。
3、链接标签所占比例 = 链接标签数 / 容器总标签数,用这个比例对原始分数进行正影响。
4、如果最后得分少于1000,直接否定。(这样做是因为链接数太少,不考虑)
最后,对统计的所有容器的分数进行排序,得分最高的,选取为友情链接容器。
统计过程中,我所提及的正影响采用了下面的公式:
输出分数 = 输入分数 * 保持比例 + 输入分数 * (1 – 保持比例) * pow(输入比例,影响力)
为了方便大家理解,直接上代码:
def affect(points, keep_ratio, ratio, power): keep = points * keep_ratio if ratio >= 1.: return points return keep + (points - keep) * pow(ratio, power) def calc_link_points(host, ul): # simplified host 不要子域名部分! parts = host.split('.') if parts[-2] in ('com','edu','net','gov','org'): host = '.'.join(host.split('.')[-3:]) else: host = '.'.join(host.split('.')[-2:]) link_density = linktext_count = totaltext_count = 0.001 container_count = innerlink_count = 0.001 for a in ul.findAll('a'): href = a.get('href', '') # 内部链接 if not href or not href.lower().startswith('http') or host in href: innerlink_count += 1 continue # 层次太深 if urlparse(href)[2].strip('/').count('/') >= 1 or '?' in href: continue link_density += 1 linktext_count += len(a.text) if '_blank' == a.get('target'): link_density += 1 # 统计容器字数 for t in ul.recursiveChildGenerator(): if type(t) is NavigableString: totaltext_count += len(t) else: container_count += 1 points = (link_density - innerlink_count) * 1000 if points < 0: return 0 points = affect(points, 0.1, linktext_count / totaltext_count, 2.) points = affect(points, 0.1, link_density / container_count, 1.) if points < 1000: points = 0 return points
测试运行,提取 http://www.lijiejie.com 的友情链接。
root@xiaoxia-pc:~/project/reader/test# python urls.py http://www.lijiejie.com
Fetching http://www.lijiejie.com
Parsing 36827 bytes
Title 李劼杰的博客
Found feed http://www.lijiejie.com/index.php/feed/
12856.8536983 ul
0 div
0 div
0 div
……Found links 11
http://www.68flash.com/ 68flash
http://hi.baidu.com/d7hack D7Hack
http://www.rrgod.com/ Eddy Blog
http://hi.baidu.com/15108971 hu1s4
http://hi.baidu.com/282635791/ Xch40
http://xiaoxia.org/ Xiaoxia[PG]
http://www.fachun.net 中国博客索引
http://chinahacker.blog.163.com/ 叱诧冰子
http://www.lijiejie.cn 旺园阅览室
http://haipo.me/ 杨海坡
http://timepw.com 连文井
友情链接的容器是一个ul,得分很高。其他容器直接被否决了,没有分数出来。
下面测试 某熊 的 typecho,
root@xiaoxia-pc:~/project/reader/test# python urls.py http://44670.org/
Fetching http://44670.org/
Parsing 11802 bytes
Title 某熊[PG]
Found feed http://44670.org/index.php/feed/atom/
1649.51073438 ul
1363.34619279 div
0 div
0 div
0 ul
0 ul
……
Found links 7
http://sxkdz.0ginr.com/blog/ SXKDZ
http://twd2.me 万呆博客
http://iceboy.org/ iceboy[PG]
http://whitefirer.org whitefirer[PG]
http://hi.baidu.com/vi_orz vienna
http://pm.mafom.com/ 愤怒的泡面
http://newbiecoder.0ginr.com/blog NewbieCoder
有一个ul和一个div的分数很接近,为什么呢?因为ul是被那个div包含的,而div多了一些额外标签和文字,所以分数没有ul那么高。
对于常见的wordpress,zblog,pjblog等都能够准确定位友情链接的区域,那么非常用博客呢?例如 simple-is-better.com(python.cn),
root@xiaoxia-pc:~/project/reader/test# python urls.py http://simple-is-better.com/
Fetching http://simple-is-better.com/
Parsing 36281 bytes
Title python.cn(news, jobs)
Found feed http://feed.feedsky.com/simple-is-better
Abstract 200
34359.9883395 ul
28956.1843727 div
8202.2453766 div
0 div
0 div
0 div
0 div
0 div
……
Found links 26http://hi.baidu.com/limodou/ limodou 的 Blog
http://www.chenxiaoyu.org/ Smallfish 鱼哥
http://techparty.org/ 珠三角技术沙龙
http://www.autopart007.com/ China auto parts supplies
http://www.mvmap.com/ 名城指南
……
这个34359分的ul和28956分的div,无论选哪个,都是成功定位到友情链接,毫无压力!
写到这里,我的Feed爬虫(8个进程)已经使用这套方法,工作了9个多小时,搜集2万多个博客的Feed地址 🙂 因为burst的VPS内存才512MB,比较小,所以开8个进程就足够,还有留下内存跑mysql呢。如果是在一台性能好一点的机器上,开100多个进程同时抓取,不到1个小时就能抓2万个博客的Feed了。
提取网页摘要
对于网页摘要的提取,我没有花太多心思在这个上面。网页摘要是给 http://feed.readself.com 展示用的,对于我来说,没有太大的意义。但是简略的做了一个算法,提取文字最密集,超链接数目最小的容器标签,取其文本的前100个字。其中,用容器的超链接数目与网页总超链接数目的比例对统计文字得到分数进行负影响。我对文字进行了utf8编码是为了增加中文的权重,即一个中文字的权重等价于3个英文字符。
大致代码如下:
def find_text(body): candidates = [] total_links = len(body.findAll('a')) + 0.001 # 枚举文字容器 for tag in ('div', 'section', 'article', 'td', 'li', 'dd', 'dt'): for x in body.findAll(tag): if type(x) is not Tag: continue points = len(x.text[:100].encode('utf8')) * 1000 points = affect(points, 0.1, 1 - len(x.findAll('a')) * 1. / total_links, 1.) candidates.append((points, x)) # 排序,取分数最高的容器 candidates.sort(reverse = True) if candidates: return candidates[0][1] return body
例如,对雄哥的博客进行提取网页摘要,
root@xiaoxia-pc:~/project/reader/test# python urls.py http://ibaiyang.org
Fetching http://ibaiyang.org
Parsing 44596 bytes
Title 白 杨
Found feed http://www.ibaiyang.org/feed/
Abstract 在看这边文章前,你还记得你班主任的名字么?一身中,我们或许永远记住那些曾经影响我们命运的人,老师当然在其中,而有如今的我,也正是感谢这些老师的关怀和肯定。昨天晚上,我一个朋友告诉我,他今天和我们初中班
这种方法提取到的网页摘要,通常是一篇文章的正文内容,对大量的中文网页采集的结果来看,效果很不错!但是对英文网页来看,还需要进一步的改进。
夜已深,对于我目前搜集Feed的技巧就介绍到这里!主要是想跟大家交流一下,因为我在这方面的经验还不多,相当于刚入门,所以希望有人站出来,给一些意见或者一些好的提议 🙂
沙发………
很有意思,友情链接一般是一级域名,而且友链一般在同一个上一级标签下,看上去也可以用来打分。抓取网页是否用了多线程呢?
用了多进程!
如果是多线程的话,单个进程的内存会随着数据量增大而不断增大,这些内存很难回收。
而多进程的方式可以让单个进程处理到一定的请求数量之后,自动死亡,回收资源后,重启进程 🙂
可以考虑用协程和多进程结合,个人觉得gevent是个不错的并发库。
wow! 目前采用xmlrpc来做多进程通信。工作方式是多进程和多线程混合。多进程是模块化,多线程是提高效率。
好像不遵守robots.txt,有些标了Disallow的东西也收了。
另外索引好像太模糊,搜索不精确。
Hi,我也知道存在这个搜索不精确的问题 🙂
不过,现在已经改善了!欢迎再去测试。
对于robots.txt,改天研究一下,把这个规则加上去!
不过,其实我主要目的是为了搜集Feed,而不是搜集其网页内容,而Feed一般都是向外接公开的,所以现在还不是很重视这个东西。
太有借鉴价值了!
你对友情链接容器定位非常巧妙。
我在自己的程序中,也基本上都是定位的div标签。
然后在提取超链接的地方使用正则。
你的程序无论是在通用性,还是性能,或者准确性上,都是我的小程序远远不可及的。
稍后时间空闲了,我参考你的方法,做一些改进。
感谢分享! 🙂
pow∝payment 😀
嗯,这个可以用在搜索排名上~
小虾果然厉害呀!我的博客在第六层被找到了 ^_^
小虾为什么不把那个 urls.py 放出来呢?还是说我看漏了?
呃,核心代码贴出来即可,那个文件有数据库帐号什么的,就不打算放出来啦,本人也有点懒 🙂
为啥没收录我的博客呢?啊?
嗯?难道是你的博客太新了,还没有被友情链接过去?
还是你的博客所在的层次太深远,我的BFS爬虫还没有搜到呢!
( .σ _ζσ) 以前的百度空间level6被收录,话说小虾url.py开源吗?
!!
核心代码不都放出来了吗? 剩下的代码都是处理数据库和网页编码的了,跟这个文章内容没多大联系的啦。
可以将爬虫代码发出来参考下么
Hi, 迟点有时间整理一下,再发上来吧 🙂
好的,:)
可以抓取一个博客首页的所有链接,提取出主站URL,依此构造出Feed页面进取读取。这样不依赖于友链和特定结构的博客,理论上所有博客都可以抓取。
但是这样降低了抓取速度了。。。互联网的网站太多,而博客网站只是占其中一小部分。
嗯,确实,这里还涉及到如何识别一个网站是否是博客网站的问题。
期待文件打包放出来
最近还没时间整理这个服务器上的代码。
不过这半个月内,打算放出来!!!一定放 😀 ~~~
期待中。。。。。。
文件啥时候可以发出来呢?对这个你叫感兴趣
楼主把数据库相关信息去掉,打包发出来吧,好期待啊。
小虾,为嘛网站打不开了呀
hi, 昨天被服务器提供商停掉了,说我服务器的IO使用超出了限制。。。正在处理中
Pingback引用通告: Python与简单网络爬虫的编写 « Xiaoxia[PG]
Pingback引用通告: Thought this was cool: Python与简单网络爬虫的编写 « CWYAlpha
Pingback引用通告: Thought this was cool: Python与简单网络爬虫的编写 « CWYAlpha
友情连接中不都是blog 如何只获取blog 呢? 光凭feed 也不行, 大部分网站都能找到feed。
用beautifulsoup 解析的确很方便,之前还没用过呢。
可以考虑增加关键字评分。。。是blog一般有“blog”或者“博客”。。。 是论坛一般有“论坛”或“bbs”
最近我也想做一个小爬虫,自学了几天python,还没什么头绪,不知道小虾同学还会不会将这个代码开源呢?我想参考一下,谢谢。
Pingback引用通告: Python与简单网络爬虫的编写 « Unlearn to Learn
很高兴又看到一个喜欢技术的同届校友 :-) 你的博客很赞,加油!
大侠,有源码分享吗?
代码可能已丢失。
用的什么相机