2013年12月21日

更有效的全网页深度抓取(样式+图片)

作者 非鱼

通常情况下的网页采集一般是指我们想要抓取整个站的页面内容,从首页开始,然后分析整个页面里的a链接,然后把链接指向的页面再取出来,然后再往下一层去采集。这种情况下,只需要分析a元素或者链接的正则就可以了。但是还有一种情况,我们想保存一个完整的网页内容下来到本地,希望它能够还原整个访问的体验,以便在离线时也可以使用,这样就需要保存下完整的js和css以及图片等。但是有些网页使用的css做了多层import嵌套,而有些图片还是通过js等方式后加载的,如果要写出一个完整有效的分析代码然后能够层层获取,并不容易。

这时候有一个更简单有效的方式,就是让浏览器告诉你哪些内容是显示这个页面需要的,是你需要保存下来的。而有些css(甚至大部分)中出现的样式和图片,在你想要的这个页面里并没有被用到。怎么做呢?用proxy方式。

接下来就要用到nodejs出场了。用express建立一个简单的网站,express –sessions proxy1,然后这个默认模板下面的public, views, routes等目录可以全部删掉。在package里添加上依赖包request和mkdirp,这就够了。

然后修改app.js,我们要怎么做呢?先把端口改成80,然后下面的route部分,直接加一个app.get(‘*’, function(req, res){});,由它来接管所有的路径请求。然后在这个函数中,我们需要让站点起到代理的作用。此时过来的访问请求的路径在变量req.originalUrl中,我们要把这个请求转发到原网站上去。所以在这里把原网站的域名加上去,var url = ‘http://www.sample.com/’+req.originalUrl,然后把这个请求交给request包去获取。request(url)得到的是一个读取流,基于nodejs的流机制,你可以把它直接pipe给输出流。比如下面写上这样一句:request(url).pipe(res); 这样request就直接去获取这个远程URL并把取到的内容直接转交给了response输出。我们的代理就完成了。

那么怎么来测试这个功能呢?只要node app.js启动你的本地站点,用浏览器访问http://localhost/some/url,访问任何路径,域名后面的部分都会被转发到你要抓取的网站上对应的URL上去,页面的内容都被被nodejs取回来并转发回你的浏览器,理论上,你的浏览器看到的页面内容应该是跟直接访问对方网站没有任何区别的。而且,在这个过程中,浏览器会负责解析现在需要加载哪些js,包括js中的再引入,还有css中的import,以及css样式多层覆盖之后实际需要的背景图片之类的。

这个时候,我们只要稍加改造,就可以让这个proxy做点贡献了。我们只需要把request取到的东西先保存下来,再返回给浏览器就可以了。经过试验,这个pipe貌似并不能转发两次,比如先转发给一个文件写入流,再转发给一个response写入流。那么我们只好把过程分开了,

        var writestream = fs.createWriteStream(file);
        writestream.on('close', function (result) {
            var readstream = fs.createReadStream(file);
            readstream.pipe(res);
        });
        request(url).pipe(writestream);

这里的file参数,就是根据文件的URL,去除了路径后面的参数,加上你要保存的路径,拼起来的一个绝对路径。当然,在写入文件之前,你需要检查一下它的路径是否存在,不存在的话需要先创建路径,这时候就要用到mkdirp了,可以一次创建多层路径。

这个过程写完,再用浏览器访问你的代理服务器,这个页面需要的所有文件都保存在你的本地了,而且保持着相对路径。

剩下的还有一点小优化,比如有的网站页面里的链接、js、css引用的时候全部写绝对地址,那你用localhost访问的时候,只有第一个页面的HTML会经过你的代理,后面的所有的请求都直接回到他的网站上去了,不经过代理了。这时候就要用到另外一台机器(比如虚拟机),把这个域名的IP直接指向你的这台机器的IP,再用浏览器访问他的绝对路径,所有的请求都会到你这儿,再由你的机器去访问真实的路径。为什么要用另一台机器呢,因为如果在你的这台机器上直接改了域名的IP指向,那你的代理服务器本身也访问不了他的网站了。

另外一个需要处理的是,在返回的内容中把可能存在的绝对路径替换掉。换成相对路径。这样保存在本地的所有内容才是可以原样打开的。

这样抓下来的页面,基本上是完整的。(除了ajax请求的部分和流媒体的部分)当然,ajax请求如果是本站内的请求,你也可以把路径替换成相对路径,然后在本地再打开的时候不是直接双击首页文件打开,而是通过nodejs建一个本地服务器来打开,其实应该也是支持的。