因为负责一个新闻推荐系统的项目,需要采集大量的新闻数据作为文本分类的材料,通过自己的不断摸索,以知乎上某位大牛的框架为基础,完成了一个能采集几十万新闻数据的爬虫,并且健壮性非常强。仅以此文,将自己收获的见解与一些容易入的坑与大家分享。
开始
其实写爬虫的时候有个原则,如果该网站存在手机版本的网站,那么我们应该去爬取手机端的网站。为什么呢?看下面两张搜狐新闻pc网页版本和手机网页版本的图就明白了:
明显可以看到手机搜狐端的结构要比pc网页端的清晰,这样有利于我们分析网页的结构和提取重要的数据(还有一个原因是手机端比pc端的反爬虫措施要弱一点),因此我们应该选择手机端网页开始我们的分析。下一步,我们分析一下我们的入口url地址。由于我需要采集像体育,军事,娱乐等各个类别的新闻,在我第一次进行爬取的时候,其实手机搜狐网的网页结构每一级还是更加清晰工整的,非常有利于我进行大规模爬取的,第一次时候的做法是按三级结构依次进行爬取。如下图:
时隔几月,当我再次打开手机搜狐准备爬取的时候,发现页面结构有变化,按老方法做可能容易引入更多的噪声且解析起来更麻烦了。因此想着该升级一下方法了。
对付分级结构不明显的网站的时候,只能想到上BFS和DFS了(但是这样就给我带了一个弊端,后面在说)。我这次采用了BFS的思路,在页面解析出我们需要的有特征的的URL,然后放入一个阻塞队列,然后依次从阻塞队列里面取url,如此反复不断的进行。
但是入口地址不用变,打开手机搜狐拉到最底下有个导航,点进去之后可以看到有很全的类目,把这些地址抓下来作为队列里的初始url。
那我们这次就要分析一下拥有新闻正文内容的新闻url有什么特点,通过观察可以很容易的发现它的特点。
反之,我们不需要要的一些网页的url,可以看一些例子——
新闻是图片形式的网页url : https://m.sohu.com/p/783590/
小说内容的网页url : read.m.sohu.com/book/1212604/
总之,我们如果要文字内容的新闻,只要去匹配 http://m.sohu.comm/n/(此处是一串数字)/ 的特征的url就可以了,另外,我们发现像财经,体育这些类别入口的url是这样的——“https://m.sohu.com/c/5100/”,“https://m.sohu.com/c/5/”,“https://m.sohu.com/c/8/”。
所以说,不同内容和类别的网页url是有特征的,我们应根据自己的需要进行分析取舍。回到我的项目的需要,我在爬虫处理的时候,只爬取符合正则表达式http://m.sohu.com/n/[0-9]+/ 和 https://m.sohu.com/c/[0-9]+/的网页URL,把他们扔进队列,并且,如果是 http://m.sohu.com/n/[0-9]+/ 形式的url,我们知道存在我们需要的新闻正文内容,那么我们还要解析出我们需要的内容,否则,拿到类别入口URL的时候,先扔进队列里,等一下从队列里取出的时候再往深了爬。
下一步,拿到了有新闻内容的url,我们就该对我们要提取的内容做一下页面分析了。学爬虫的都知道用正则表达式来提取我们需要的内容,但你也应该知道了,用正则提取的鲁棒性实在不敢恭维,而且它又巨不好写,巨复杂(尽管它效率应该是最高的,任何事物都有两面性嘛)。所以我强烈推荐用css选择器或xpath的语法,配合java中的Jsoup使用简直是神器(python中有Beatifulsoup),而且它们语法简单,容易上手,定位准确,鲁棒性强。而且,在页面中检查一个元素的时候,点击右键直接可以获得它的css路径或者xpath路径。
只不过拷贝下来的路径,一般是不能直接用的,像一段新闻的内容都是用一个p标签括起来,比如 body > section.article-wrapper > article > p:nth-child(3),这样得到的是一个绝对路径,也就是你抽取内容的时候是精确的定位到这一段,如果要获得整个新闻的内容,就要获取所有同级路径下的p标签的内容然后加起来。
因此,必要的css和xpath语法还是要掌握,然后根据需要进行灵活组合使用。
在爬取的过程中,有一部分页面有异步加载的情况,需要点击展开剩余那个元素才能得到所有内容,这种情况其实好处理,但是我在第二次爬的时候手机搜狐又改版了。第一次,剩余的内容还真不在现在的页面里,需要我们重新发http请求去获取剩余内容,返回的是一个json字符串(如何知道请求的地址?跟上面分析url特征一样,只要先F12开启控制台,点击network,然后再去点展开剩余的标签,它就会向服务器发请求,多试几个,就能找到url的特征)。但是第二次的时候,其实剩余的内容已经在现在的页面中了,只是没展开,这样就更好办了,用同样的抽取内容的方法就能直接得到我们想要的了。
以上就是从分析到获取我们想要的内容的一个过程。如此只是实现了基本功能,如何构造一只健壮的爬虫呢?如何判断url有没有重掉呢?爬虫万一被封了怎么办?
对于鲁棒性来说,可以说就算使用了css筛选器和xpath语法也不能保证完全不在解析的时候出问题,我的办法是,尽量在定位元素和解析内容的时候多用 try catch 语句或者if else 语句,把所有有可能出现异常的地方都做异常处理。否则,就像我当时那样,报各种数组越界等异常。
去重算法的话知乎大神采用的是强大的bloomFilter算法,可以在海量数据的情况下依旧很高效的对字符串判重,原理就是四个hash函数的叠加使用。要了解的朋友可以自行上网搜索。
可以说,短时间内对同一个目标网站进行大量的请求肯定会被服务器封IP,那么我们想到的自然就是不断的换IP,有很多可用的提供代理IP的网站。针对这个问题,我还专门写了个miniSpider,在爬取代理IP的同时对该代理IP进行测试,因为不是所有的代理IP地址在我们的程序里换IP之后都是可用的,所以确认测试可用之后再存储到本地文件。测试IP是否可用的网址是 : http://ip.chinaz.com/getip.aspx 。
当然,要提高效率,就少不了多线程(想想一只蜘蛛和十只蜘蛛同时工作时效率的区别) 。多线程可以用一个线程池来维护,同时,再开启另一个监视线程池线程数量的线程,如果监测到线程池里的线程数少于一定数目了,就重新建立几个线程。如此,爬虫挂着几天几夜基本都挂不了了。
当然,如果你想暂停一下爬虫之后开启的时候爬取新闻URL仍能不重复,那么你可以把储存url队列的数据结构通过序列化的方式存储到本地,每次开启的时候再自动读入。
最终,我的爬虫爬了一个晚上之后,总共爬了40w来条新闻数据,但是后期检查的时候,发现严重的类别不平衡,某类别下的新闻总共有10w多条而其中一个类别却只有可怜的几百条。这样对我后期用来训练文本分类也会产生影响。原因也好理解,同一个类别的url在队列里总是凑在一块的,如果前面的队列没爬完的话,后面的就根本没机会。解决的办法现在想到的就是随机打乱队列,不过我还没去实现。
结语
其实写好爬虫是一个阶段性的过程,最重要的还是先掌握好http和html的知识。写爬虫并不要求熟练会写html,但是一定得看得懂。刚开始的时候不熟练,练得多了,也就是信手拈来的事情。
关于代码,下次我上传Github之后再来分享。
-------------------------------------------------------------------------------
2017.5.2 更新