用scrapy镜像网站

近期接了个需求,有一个域名要下线(释放出服务器资源),但是又不希望损失SEO。

本文适合已经开始接触scrapy的朋友,可能会帮助到你。

问题分析

大概想了2个思路:

  • 从内部入手,直接用view层渲染出所有页面。
    • 缺点:需要梳理所有的页面,枚举出所有的URL,这个工作量不小。
  • 从外部入手,直接抓网站的所有页面和依赖资源下来,静态化到本地。

我选择了后者,虽然要做爬虫,但是这样最容易将URL覆盖完整,工作量也仅仅是开发爬虫自身而已。

技术选型

其实镜像网站这个事情,有很多现成的工具,比如wget就支持递归镜像一个网站到本地文件。

还有一些开源项目,就不一一列举了。

我个人试了一下这些工具,最终还是决定自己来研发,它们真的不是很好用,太不可控了。

框架选择python scrapy,这个框架堪称经典,以前也了解过它的架构,而且python语言我还算熟悉,所以就直接入手它了。

制作流程

scrapy上手还是蛮简单的,大家参照中文版的scrapy文档自学即可,我一共花了2天时间就基本搞定了爬虫,相信大家也差不多。

启动项目使用”scrapy startproject 项目名”,就可以生成scrapy骨架代码了。

下面我仅记录一些特殊问题,解决大家可能碰到的疑惑。

安装依赖

除了安装scrapy框架,我还安装了beautifulsoup4,后者用于解析与修改HTML。

为什么要修改HTML?因为网站依赖了其他域下的js,css文件,需要静态化到本地磁盘,这样就需要修改HTML中的链接地址。

beautifulsoup4可以基于lxml库加速执行,lxml是一个高效解析HTML的C扩展,我们得确保它也被安装。

MIDDLEWARES

scrapy默认会引入N多个中间件,它们被定义为:

这些默认的中间件基本是核心必要功能,不需要我们去关闭。

如果你要关闭,就在settings.py中修改SPIDER_MIDDLEWARES和DOWNLOADER_MIDDLEWARES,将对应的中间件设置为None即可关闭。

parse函数

我们发起的request会指定回调parse函数,我们可以在parse函数中提取页面信息为item,也可以产生新的request请求。

parse函数应该是一个迭代器,它应该用yield返回下面任意一种东西,scrapy会持续迭代直到没有更多东西被yield:

  • yield要发起的后续request
  • yield要投递给pipeline的item

有一个比较好用的功能,就是request可以配置meta保存一些上下文信息,当parse回调时可以从response中取出meta信息恢复上下文,这样我们就很容易知道当时为何要发起这个request。

链接去重

scrapy默认会在内存中去重抓过的链接,链接会被签名为整形保存。

我在使用scrapy的过程中发现,有的URL始终没有被抓取下来,而且scapy日志也没有对应的报错信息,URL像是凭空消失了一样。

网上碰到类似问题的人不在少数,没有搜到正确答案。

我直接跟了scrapy源码,最后定位了原因:因为scrapy默认将待抓URL存储在一个LIFO队列中,意思就是last in first out,所以当有源源不断的后链进入LIFO时,较老的URL始终无法弹出队列,无法得到及时抓取。(if you find scrapy lost urls and there’s no error logs, you need to change the crawling queue type from LIFO to FIFO in settings.py, this may solve your problem)

解决这个问题只需要在settings.py中配置一下队列类型为FIFO(first in first out)即可:

页面修改

scrapy自带了一些HTML的选择器,可以很方便的找到想要的DOM节点。

但是scrapy不支持修改DOM,所以我用了beautifulsoup4这个经典的类库。

beautifulsoup4也支持css选择器,并且可以直接对DOM做修改,使用起来很方便。

思路方面

因为我要镜像某个域名的所有页面,所以我在follow后链的时候,除了下载js和css允许离开本域,其他的后链则要求必须属于本域,否则不会继续follow下载。

当我从HTML提取外链其他域名的js与css的时候,会进行URL链接改写。比如HTML中有一个外链<script src=”http://www.b.com/xx/yy.js”>,那么我下载后会保存在磁盘的$webroot/www.b.com/xx/yy.js中,并且将HTML中的链接改写为:<script src=”/www.b.com/xx/yy.js”>。

另外,对于类似/xx/yy这样没有文件后缀的URL,我会把内容保存在磁盘的/xx/yy/index.html中。

本域下的图片我也会下载,其他域(包括外链与CDN上的图片)的图片我会保留原始链接。

关于HTML改写的思路大概就是这些。

持久化

scrapy支持持久化抓取状态,我没有用到,大家可以根据官方文档试验一下。

文档:https://doc.scrapy.org/en/latest/topics/jobs.html

排查问题

执行”scrapy crawl spider名字”会打印很多信息,可以通过–nolog关闭日志输出,或者-L ERROR来控制只输出错误信息,方便排查问题。

部署问题

官方提到有一个scrapyd守护进程可以管理爬虫,我看了一下配置还挺麻烦,没啥必要。

可以配置一个crontab命令来搞定,例如:

通过flock文件锁来防止多进程并发,简单有效。

代码demo

在github我放了一份代码,大家可以简单参考:https://github.com/owenliang/scrapy

如果文章帮助您解决了工作难题,您可以帮我点击屏幕上的任意广告,或者赞助少量费用来支持我的持续创作,谢谢~