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

通常情况下的网页采集一般是指我们想要抓取整个站的页面内容,从首页开始,然后分析整个页面里的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建一个本地服务器来打开,其实应该也是支持的。

在IIS里运行nodejs站点

这是一个很神奇的功能,可以让你的nodejs开发的web server程序直接运行在iis里面,由iis作为deamon程序,即可以解决nodejs本身的单进程单线程问题,又可以实现把nodejs站点托管到80端口上的功能。而且,nodejs的站点即可以作为一个独立的站点来运行,也可以作为已有的asp.net网站中的一个虚拟目录来运行,完全没有影响。微软对nodejs的社区还是提供了很大的支持的,比如官方提供了sql server的nodejs驱动。

要实现这个功能,基于这样一个项目:https://github.com/WindowsAzure/iisnode,它实现的是一个IIS Module,全局加载到IIS中以后,就可以在任意一个站点中,通过Web.config来指定把某些路径转交给node程序来解释执行,同时可以配置一些额外参数,比如启动多少个nodejs进程,每个进程最大允许多少个连接,允许多少个等待中的连接等等。而且这个module本身还有监视站点文件变化的功能,当你修改了某个js文件,它可以自动重启加载。

你可以下载项目的源代码,自己编译,也可以下载官方已经提供的安装程序。(必须使用安装程序,因为注册为IIS Module需要一些系统注册动作,因为一开始一直找不到最新版的官方的安装程序,就想自己编译一个,结果vs2013又不支持wix项目,只生成了两个dll,修改iis全局配置,搞了几个小时也没有生效,后来找到安装程序以后一下子就成功了。当然,也是因为一开始没有读清楚这个项目的页面上写的编译说明,没有安装http://wix.codeplex.com/这个东西,其实要自己编译生成安装程序还是比较容易的。)推荐windows server 2008及以上,IIS7以上。

首先需要在你的服务器上安装node,建议也使用安装程序吧,可以自动帮你设置path。安装了node以后再安装iisnode,你可以使用这个地址:iisnode-full-iis7-v0.2.7-x64-2.msi。安装完以后它的安装目录下有个www目录,还有个安装测试站点的bat脚本。如果有兴趣你也可以直接使用这个脚本,它会在你的IIS的默认站点下建立一个node的虚拟目录,指向这个www,这样就可以直接看到下面提供的五个测试站点了。

web.config里面只有一句比较重要,就是一个handler配置,你可以把站点中的启动文件,比如app.js,指定用iisnode这个module来托管。然后在浏览器中打开localhost/node/app.js,就可以看到执行结果(如果你的app.js里没有引用非node基本模块的话)。

如果你的项目使用了基本模块之外的依赖,那就需要先npm install一下,这个过程可能很顺利,也可能很痛苦,如果你要安装的东西不需要根据平台重新编译,安装就会直接完成,但是如果需要重新编译,那就比较麻烦,你需要安装visual studio。目前你可以选择在服务器上安装visual studio express 2012 for windows desktop,千万不要装错。中文版大概是一个600多M的iso文件,直接安装就可以。安装完成以后再运行npm install –msvs_version=2012(如果不加这个参数,默认是2010版的编译程序,应该会提示无法加载vcbuild.exe之类的错误)可能还会看到一些警告,不过正常来说应该能编译通过了。不过也有些还会继续有依赖,比如crypt,它需要openssl的头文件。你要下载一个16M的64位windows的完整版openssl,安装到C盘,再来运行npm install。另外也有一些模块依赖的头文件只有unix系统上有提供,那就只好完全放弃了。比如sleep,完全无法编译通过,只能放弃这个函数,改用setTimeout来解决。

做完这些以后,我的整个demo项目的代码就可以完全跑起来了(前提是你还安装好了mongodb的windows版并已经启动)。测试了一下,速度相当快。启动了两个node进程,只占用了150M内存。

另外,还有一点需要处理,就是URL映射。可以通过IIS的urlrewrite模块,把public下的静态文件直接rewrite到文件目录,由IIS来直接提供,不经过node,然后其它路径全部rewrite到app.js上。这时候在node里面看到的程序路径是当前访问的完整路径,所以如果你把这个项目放在一个虚拟路径下,那么在app.get(‘/’)这个url的时候,就要把虚拟目录这一层加进去才可以了。

这样,就你可以在整个asp.net的站点下,把一部分功能交给nodejs来完成。比如,nodejs最擅长的RESTFul形式的api,以及访问量巨大但单个请求负载不高的内容。

写了个node.js+express+jade+mongoose的项目模板

这两天对Node.js比较上瘾,结合前面翻译的两篇文章,加上学习mongoose的过程,对express的标准模板做了一些改进,形成了一个完整的项目模板,已经放到了github上,有兴趣的可以自己clone下来,在此项目基础上复制出自己的项目,然后修改其中的类和页面文件就可以了。

项目地址:https://github.com/unfish/expressjs-template

目前完成的改动和主要功能点:

  1. 改成MVC架构。默认的项目模板没有Model,所有的业务逻辑在routes目录下的js文件里,并且在app.js里一个路径一个路径的加载其对应的处理函数,极其原始。在这个模板里删除了routes目录,改成models, controllers, views三个目录,models下是每个数据类的定义文件,每个数据类包含自己的一部分处理逻辑(基于mongoose),包含静态方法和实例方法,可以基于mongoose的hook功能自我做字段完整性验证(已完成,示例中还加入了自定义验证方法,实现Email格式验证和不重复验证,直接赋值给对象并save就可以,所有验证会自动完成)。
  2. controllers目录中每块业务作为一个js文件,所有的js文件通过exports公开一个同名的controller函数,在app.js中通过fs组件直接遍历controllers目录,并对所有的js文件调用这个controller函数。这样每个js在内部添加自己的URL处理的route就可以了。业务间完全隔离。(不过要注意不同的controller里,不要把URL写重了,如果都有统一的一级目录,再大的项目应该也可以避免这个问题)。views目录下根据controller中的文件再分不同的子目录,同一块业务的view文件放在单独的子目录中。在controller中调用的时候前面加上路径名限制即可。在模板中使用extend的时候也可以加路径限定。
  3. 在user的数据类中实现了Cookie验证,在app.js里先加载一个全局middleware,在所有请求前判断登录cookie是否存在,如果存在就把当前用户的对象放进req.user中。这样所有的其它业务逻辑中,都可以直接使用req.user变量取到当前用户。(本来是想放到req.session.user中的,但是放进去以后这个user就会变成普通的js对象,不再是mongoose对象,所有的方法都不可用了)然后在user数据类中还定义了两个强制要求登录的middleware,在所有需要登录的页面的route前加上这个静态方法的名字即可,如果是GET方法,没登录的话会自动跳转到login页面,如果是POST,会统一返回请登录的错误提示。(以后还可以再加上跳转到登录页时记住当前的位置,登录后返回该页,以及在登录页对不同的来源显示不同的提示)
  4. 对user和topic,comment三种类在controller中给出了完整的增删改查的代码,和前端的完整的Ajax操作的实现方式。topic包含一个指向user的外键字段,同样给出了关联查询的语法的例子,关联查询后可以直接使用topic.author.username的方式来显示作者姓名。
  5. view端也给出了比较完整的示例,在原来的空白模板的基础上增加了jquery+bootstrap,所有的页面元素遵守bootstrap的样式定义。所有的post请求使用jquery的ajax操作,统一post过程中的等待提示和返回错误时的提示的处理方式。(这整套组件里我最喜欢的就是jade引擎,虽然据说它执行效率很低,比其它几个流行的引擎都低10倍,但是这种语法结构和页面展现出来的效果,实在是太赏心悦目了)
  6. 增加了文件上传的处理,前端使用PLUpload实现Ajax上传,后端使用gridfs-stream将收到的文件保存进mongodb GridFS,不使用本地文件。前端也实现了统一的文件上传控件的JS调用,在任何需要上传文件的地方调用一个这个js函数,就可以得到完整的上传功能。(这个功能真的搞了好久才搞定)

项目随时可以进行新的改动,加入学习到的新的东西,不过以目前这个模板来说,复制粘贴代码再做点修改,要实现了一个简单的CMS系统或者博客系统,或者是商品展示或者购物系统,应该都是很容易的了。

【翻译】Node.js+Express站点中的简易MVC架构

原文:http://timstermatic.github.io/blog/2013/08/17/a-simple-mvc-framework-with-node-and-express/

我喜欢框架。一旦我放弃了程序员的自我,学会了拥抱严格的约定,立即就在开发和部署的时间上感受到了好处。另一方面,我也喜欢了解引擎盖下是如何运转的,如果你看不到框架下面的东西,可能就会遇到危险。

这就是我为什么喜欢node.js和express。它们提供了一套框架样板,可以让我快速建立我自己的约定。

当然,使用express建立一个网站非常简单,而且,让express设置的更接近MVC也很简单。

继续阅读“【翻译】Node.js+Express站点中的简易MVC架构”

Javascript里要命的prototype (1)

var Person = function() {
    this.canTalk = true;
    this.greet = function() {
        if (this.canTalk) {
            console.log("Hi, I'm " + this.name);
        }
    };
};

var Employee = function(name, title) {
    this.name = name;
    this.title = title;
    this.greet = function() {
        if (this.canTalk) {
            console.log("Hi, I'm " + this.name + ", the " + this.title);
        }
    };
};
Employee.prototype = new Person();

var Customer = function(name) {
    this.name = name;
};
Customer.prototype = new Person();

var Mime = function(name) {
    this.name = name;
    this.canTalk = false;
};
Mime.prototype = new Person();

var bob = new Employee('Bob','Builder');
var joe = new Customer('Joe');
var rg = new Employee('Red Green','Handyman');
var mike = new Customer('Mike');
var mime = new Mime('Mime');
bob.greet();
joe.greet();
rg.greet();
mike.greet();
mime.greet();

输出:

Hi, I’m Bob, the Builder
Hi, I’m Joe
Hi, I’m Red Green, the Handyman
Hi, I’m Mike

Ubuntu+Nginx搭建全功能服务器

最终效果:nginx作为统一的前端服务器,处理所有的站点的静态页面,同时,将不同站点的脚本发送到不同的后端引擎,包括Django(Python),PHP,NodeJs,当然,同理可以继续增加Mono+.Net的站点,和ROR的站点配置。

以前一直在使用lighttpd,不过这服务器貌似不怎么更新了,传说中的2.0已经说了好几年了也不出来,只是过几个月偶尔放一个补丁版本出来修改一两个重要Bug,远不像nginx这么活跃。而且原来的lighttpd的PHP采用了fastcgi模式配置以后,进程里面5个PHP站点启动了十几个PHP的fastcgi进程,相当浪费内存。于是决定统一换到nginx。不过过程还是挺痛苦的。

安装nginx,直接apt-get install nginx搞定。配置文件也自动安装好了,自己编译安装的话当配置文件就得累个半死。/etc/nginx/目录下好多配置文件,其中的nginx.conf里面的连接数、压缩、缓存之类的参数自己可以改一改,其它的不用动。

在site-available目录下已经有一个默认的default站点配置,在site-enabled目录下是指向它的一个软链接。同样,把所有的可用的站点配置文件放在site-available目录下,把需要生效的站点链接到site-enabled下即可。可以删掉default。

继续阅读“Ubuntu+Nginx搭建全功能服务器”