1. scrapy运行过程概述
scrapy是一个基于python的网络爬虫框架,它读取对指定域名的网页request请求,截取对应域名的返回体,开发者可以编写解析函数,从返回体中抓取自己需要的数据,并对数据进行清洗处理或存入数据库。
scrapy工程的爬虫全部存放于工程二级目录下的scrapy文件夹中,使用genspider命令可以构建新的爬虫脚本,使用crawl命令可以使指定爬虫脚本运行。下面简述scrapy爬虫代码的运行过程。
实际上,scrapy有一个被提前定义好的方法start_requests(self)。在没有被重构的情况下,该方法会读取start_urls列表中的url字符串,默认使用get请求方式访问这些url,并默认回调解析函数parse。parse函数以请求得到的返回体response为参数,进行对应的解析数据操作。采用genspider命令生成的scrapy爬虫初始状态如下:
2. scrapy的使用技巧和注意事项
2.1 对多个数据源进行不同的解析处理
显然,start_urls方法和parse方法都是可以被重构的。重构parse方法,就可以更改对网页返回体的处理方式。解析函数并不是唯一的,可以自行定义多个解析函数,也并不是必须命名为parse,重构start_requests函数就能指定使用哪个函数对数据进行处理,实例如下:
如图所示,重构start_requests方法后,对于带有baidu字符的url,callback指向第一个解析函数parse_baidu,否则指向第二个解析函数parse_jd,也就是说,start_urls中定义的两个url,会在爬虫执行的过程中经过不同的处理方式,这就是对多个不同的数据源采用不同解析方式的实现方法。
2.2 数据元素的定位
找到正确的网页元素位置是爬虫获取数据的关键。对于结构单一的静态网页,只需要采用devtools(快捷键F12)自带的元素检查功能,就能定位到元素所在的位置,根据其采用的网页设计样式,使用scrapy自带的selector进行筛选,常用的筛选器有xpath和css,这是scrapy爬虫曾经常用的传统方法。
但实际操作中,并非所有网页元素都可以通过元素检查功能进行正确定位,一些错误的理解会导致数据无法真正的定位。下面举一个例子:
如图所见,如果使用devtools的元素检查功能定位“总计费用”字段的80.00数值,可以发现,数值80.00被放在了标签为“em”的网页样式中。如果单纯地认为数据已经被正确定位到html中的某个标签下,那么就大错特错了,我们打开网页源码,查找一下80.00字符串:
可以看到,80.00在网页源码中并不存在。
这是为什么呢?实际上,元素检查功能会将静态页面下的网页版式和所有动态生成的数据展示为一个“页面”。这个页面上的源码实际上是多个不同的网页格式和网页脚本共同生成的,它的代码并不是真正的网页源码。这种不存在于html网页的数据,实际上就是一种“动态数据”。
时下比较常用的存储动态数据的方式,一般是通过javascript脚本生成,或使用ajax异步加载。在已知数据加载类型的前提下,使用devtools的过滤器进行元素检查是更好的方式。
如果数据通过ajax异步加载,则过滤器设置为XHR,如果数据采用javascript脚本直接生成,则过滤器设置为JS。还有一个比较常用的过滤器是DOC,它是用来选择静态页面的。一些数据量较小的网站可能会将页面数据直接写在html中,DOC过滤器主要应对这种小型的网站。
而我们要查找的80.00字段,它实际上是通过ajax异步加载的数据,既然是异步加载的数据,就可以通过异步请求得到,因此,反复点击页面的一些按钮,如果整个页面没有被刷新,只有一部分刷新,那么这些刷新出来的数据就可以通过设置过滤器为XHR找到。
经过查找可以发现,80.00这个数据是一个异步请求的返回体中携带的两个数据20和60的加和,如图所示:
它的返回体是一个json,并不是网页标签包含下的数据,因此处理方式不能使用selector,而是需要引用json库,按包含键值对的字典操作进行处理。而它的url也不是原网页html,而是下图中红框圈出的Request URL:
用户浏览网页时,被加载的网页会自动向其它url中获取数据,因此可以得到来自很多不同的url的数据。但是爬虫不能享受这样的待遇,因此爬虫的撰写者需要额外费一番心思查找数据,依靠反复搜索,甚至需要结合html编码的经验才行。 2.3 scrapy的并发性与程序逻辑
scrapy是并行爬取框架,在默认的情况下不能用来做指定时序的爬虫。
scrapy会瞬间将start_urls列表内的数个url放入爬取队列中,爬取队列最多同时处理8个url的request请求,哪个url的请求最先得到回应,哪个url的parse方法就最先被执行,(多个parse方法不会交叉执行)。在多个url的页面结构相近的前提下,哪个页面最先返回,几乎是随机的。因此,在start_urls中定义的列表顺序某种意义上是无效的。如果一定想要scrapy顺序爬取,该怎么办呢?实际上这是一个并不常见的需求,如下几个思路可以考虑一下:
思路一,可以在settings.py中指定并发上限CONCURRENT_REQUESTS_PER_DOMAIN,默认值为8,如果设置为1,则scrapy变为顺序爬虫,按start_urls列表内各个url的排布顺序爬取,失去并发性。其效果如下图所示:
图一是并发上限为8的情况下,反复多次对5个新闻标题的爬取结果,图二是并发上限为调整为1之后的结果,可以看到,并发上限设置为1时,新闻的多个标题爬取顺序是固定的,和start_urls的列表顺序一致。
这种方法虽然能让爬虫按照程序内的逻辑顺序执行爬取,但会使爬虫的效率降低。首先,scrapy并不擅长单页面的爬取。其次,如果将请求并发数置为1,一旦其中的某个页面由于各种原因被阻塞,则整个爬虫都会受到影响,效率大大降低。另外,settings.py是一个全局设置,更改了settings.py会使整个工程所有的爬虫发生变化。
思路二,可以手动定义多个解析函数,令这些解析函数互相显式调用。这种方法过于笨重,不推荐使用。思路三,引入其他能瞬时发出request请求的库(如requests),在解析函数中进行爬取逻辑设计。这种方法虽然看起来有点“犯规”,但不失为一种很有效的解决方式。只要不破坏scrapy框架本身的结构,进行这样的设计是完全没有问题的。
这些设计思路都是为了解决scrapy按顺序爬取多个数据源的问题,但是这也必然会引发“单通道阻塞”的传统问题。这是一个逻辑问题,与框架结构无关,因此不可避免。需要注意的是,scrapy依然是以多线程并行处理能力见长的爬虫框架,设计爬虫时要尽可能扬长避短。