月度归档:2016年07月

Node.js Http实例

配置文件

module.exports = {
	server: '127.0.0.1',
	port: 9999,
	uas: [
		'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:46.0) Gecko/20100101 Firefox/46.0',
		'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.101 Safari/537.36',
		'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:43.0) Gecko/20100101 Firefox/43.0',
		'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.101 Safari/537.36',
		'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; Win64; x64; Trident/4.0; .NET CLR 2.0.50727; SLCC2; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E)',
		'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.101 Safari/537.36',
		'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/536.11 (KHTML, like Gecko) DumpRenderTree/0.0.0.0 Safari/536.11',
		'Opera/9.80 (Macintosh; Intel Mac OS X 10.6.8) Presto/2.10.289 Version/12.00',
		'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/534.57.2 (KHTML, like Gecko) Version/5.1.7 Safari/534.57.2',
		'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.114 Safari/537.36'
	]
}

以下是主文件:

var config = require('./config');

var http = require('http');
var https = require('https');
var querystring = require('querystring');
var url = require('url');
var zlib = require('zlib');
var cheerio = require('cheerio');

// 如果没有输出则list为空数组
var parseHtml = function(html, itemid) {
	var $ = cheerio.load(html);

	var trs = $("table:contains('Date of Purchase')").last().find("tr");

	var tr = trs.first();
	var hasVar = false;
	if(trs.first().find("th:contains('Variation')").length > 0) {
		hasVar = true;
	}

	var data = [];
	trs.each(function(){
		var row = $(this);
		var colmns = row.find("td");

		if(hasVar) {
			var variation = colmns.eq(2).text().replace(/^\s+|\s+$/,'');
			var price = colmns.eq(3).text().replace(/^\s+|\s+$/,'');
			var qty = colmns.eq(4).text().replace(/^\s+|\s+$/,'');
			var datetime = colmns.eq(5).text().replace(/^\s+|\s+$/,'');
		} else {
			var variation = '-';
			var price = colmns.eq(2).text().replace(/^\s+|\s+$/,'');
			var qty = colmns.eq(3).text().replace(/^\s+|\s+$/,'');
			var datetime = colmns.eq(4).text().replace(/^\s+|\s+$/,'');
		}
        if(!price || (price === 'Price')) {
            return true;
        }
		//console.log(variation + "|" + price.replace(/\s+/, ' ')+"|"+qty+"|"+datetime);
		data.push({'variation': variation, 'price': price, 'qty': qty, 'date_purchase': datetime});
	});

	return {'itemid': itemid, 'list': data};
};

var showMem = function () {
        var mem = process.memoryUsage();
        var format = function (bytes) {
            return (bytes / 1024 / 1024).toFixed(2) + ' MB';
        };

        console.log('Process: heapTotal ' + format(mem.heapTotal) + ' heapUsed ' + format(mem.heapUsed) + ' rss ' + format(mem.rss));
        console.log('-----------------------------------------------------------');
};

var server = http.createServer(function (req, res) {
	//showMem();

	/*
	这里的req是一个IncomingMessage对象,实现了Readable Stream接口(https://nodejs.org/dist/latest-v4.x/docs/api/stream.html#stream_class_stream_readable)

	对于客户端请求服务端,客户端的请求进来的数据是IncomingMessage,在服务端对外发起请求(组装数据),远程的响应类似于远程对服务端发起请求,那么这些响应信息就是一个IncomingMessage。

	从客户端读请求数据,还是从服务端读响应数据,都封装为对Readable Stream的读取。

	console.log(req.headers);
	{ 
		host: '127.0.0.1:9999',
		connection: 'keep-alive',
		'cache-control': 'max-age=0',
		accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*\/*;q=0.8',
		'user-agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.114 Safari/
		'accept-encoding': 'gzip,deflate,sdch',
		'accept-language': 'zh-CN,zh;q=0.8,en;q=0.6' 
	}
	console.log(req.httpVersion);		// http版本
	console.log(req.method);			// 请求方法,GET POST PUT
	console.log(req.rawHeaders);		// 原始头,headers中是去掉了重复头,这里是原始头
	console.log(req.rawTrailers);
	console.log(req.statusCode);		// http.ClientRequest.
	console.log(req.statusMessage);		// http.ClientRequest.
	console.log(req.url);				// http.Server.
	*/

	// 第二参数解析查询字符串
	var ur = url.parse(req.url, true);
	if(typeof ur.query.u !== 'undefined') {
		ur.query.u = req.url.replace(/[^=]+u\=/i, '');
	}
	/*
	{
	    protocol: null,
	    slashes: null,
	    auth: null,
	    host: null,
	    port: null,
	    hostname: null,
	    hash: null,
	    search: '?u=xxxx',
	    query: { u: 'xxxx' },
	    pathname: '/craw',
	    path: '/craw?u=xxxx',
	    href: '/craw?u=xxxx'
	}
	*/
	// 是否在线
    if(ur.pathname === '/ping') {
		res.write("online");
		res.end();
		return;
    }   

	// 只处理/craw
    if(ur.pathname !== '/craw') {
		res.statusCode = 404;
		res.statusMessage = 'Not found';
		res.end();
		return;
    }

	var ghost = '';
	var gpath = '/';
	var protocol = 'http';
	var itemid = '';
	// 传递了u参数,u=http://offer.xxx.com/ws/g.php?a=xxx
	if(typeof ur.query.u !== 'undefined') {
		var m = ur.query.u.match(/item=([0-9]+)/);
		if(typeof m[1] !== 'undefined') {
			itemid = m[1];
		} else {
			res.statusCode = 404;
			res.statusMessage = 'Not found';
			res.end();
			return;
		}

		var furl = url.parse(ur.query.u);

		if(furl.protocol && furl.host) {
			if(furl.protocol === 'https:') {
				protocol = 'https';
			}
			ghost = furl.host;
			gpath = ur.query.u;
		} else {
			res.statusCode = 404;
			res.statusMessage = 'Not found';
			res.end();
			return;
		}
	} else if(typeof ur.query.item !== 'undefined') {
		itemid = ur.query.item;
		var ghost = 'offer.ebay.com';
		var gpath = '/ws/eBayISAPI.dll?ViewBidsLogin&item='+ur.query.item+'&rt=nc&_trksid=p2047675.l2564';
	} else {
		res.statusCode = 404;
		res.statusMessage = 'Not found';
		res.end();
		return;
	}

	// 随机UA
	var ua = '';
	var uidx = Math.floor(Math.random() * config.uas.length + 1)-1;
	if((uidx >= 0) && (typeof config.uas[uidx] !== 'undefined')) {
		ua = config.uas[uidx];	
	}
	if(ua === '') {
		ua = 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:46.0) Gecko/20100101 Firefox/46.0';
	}
	//console.log(ua);

	// 发送POST时需要提交的数据
	//var postData = querystring.stringify({
	//	'msg' : 'xxxx'
	//});

	// 这里的options对应HTTP的请求行和请求头信息(请求的cookie是请求头的组成部分)
	// 请求体的设置是调用clientRequest对象的write方法(可写流)
	var options = {
			host: ghost,
			path: gpath,
			method: 'GET',
			headers:{
				'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
				'Accept-Encoding': 'gzip,deflate',
				'Accept-Language': 'en-US,en;q=0.8',
				'Cache-Control': 'max-age=0',
				'Connection': 'keep-alive',
				//'Referer': '',
				'User-Agent': ua
			}
	};
	var q = http;
	if(protocol === 'https') {
		options['port'] = 443;
		q = https;
	}

	//var html = '';
	// 内部对外请求
	// reqc是一个clientRequest对象,它是一个可写流
	// 而resc是一个IncomeMessage对象,它是一个可读流
	var reqc = q.request(options, function(resc){
		//resc.setEncoding('utf-8');
		var chunks = [];
		// 数据可能分块返回
		resc.on('data', function(data){
			//html += data;
			chunks.push(data);
		});

		// 数据返回结束
		// 发起请求时发送了gzip,deflate,服务器端返回压缩数据
		// 由于压缩,网络传输时间明显下降
		resc.on('end', function(){
			// 传递状态码,比如302,404等
			res.statusCode = resc.statusCode;
			var buffer = Buffer.concat(chunks);
			var encoding = resc.headers['content-encoding'];
			if (encoding == 'gzip') {
				zlib.gunzip(buffer, function(err, decoded) {
					if (!err) {
						var json = parseHtml(decoded.toString(), itemid);
						var rej = {"success": 1, "message": '', "data": json};
					} else {
						var rej = {"success": 0, "message": '', "data": {}};
					}
					res.setHeader('Content-Type', 'application/json;charset=UTF-8'); 
					res.write(JSON.stringify(rej));
					res.end();
				});
			} else if (encoding == 'deflate') {
				zlib.inflate(buffer, function(err, decoded) {
					if (!err) {
						var json = parseHtml(decoded.toString(), itemid);
						var rej = {"success": 1, "message": '', "data": json};
					} else {
						var rej = {"success": 0, "message": '', "data": {}};
					}
					res.setHeader('Content-Type', 'application/json;charset=UTF-8');
					res.write(JSON.stringify(rej));
					res.end();
				});
			} else {
				if (!err) {
					var json = parseHtml(decoded.toString(), itemid);
					var rej = {"success": 1, "message": '', "data": json};
				} else {
					var rej = {"success": 0, "message": '', "data": {}};
				}
				res.setHeader('Content-Type', 'application/json;charset=UTF-8');
				res.write(JSON.stringify(rej));
				res.end();
			}

			chunks = null;
		});
	});

	// 设置超时,当超时时执行回调,终止请求
	reqc.setTimeout(10000, function(t) {
		var timeout = (new Date()).toLocaleString() + " Timeout";
		console.log(timeout);

		// 触发error事件 -> socket hang up
		reqc.abort();
	});
	// 发生错误
	reqc.on('error', function(e) {
		var err = (new Date()).toLocaleString() + " Error: "+e.message;
		console.log(err);
		//res.write('error');
		res.statusCode = 599;
		res.statusMessage = err;
		res.end();
	});
	// 发送数据
	//reqc.write(postData);

	//
	reqc.end();

}).listen(config.port, config.server);

/////////////////////////////////////
server.timeout = 20000;
server.on('listening', function() {
    var msg = (new Date()).toLocaleString() + " Server Listening";
	console.log(msg);
});
server.on('close', function() {
	var msg = (new Date()).toLocaleString() + " Server Closed";
	console.log(msg);
});

这个例子中,HTTP模块多数功能多已经使用到。类比其它语言,并不简单多少(甚至更加复杂),JS语法也不见得有何优势。实际上,之所以选择Node.js来做Http代理,原因只有一个,部署非常简单。

PhantomJS 模拟登录二

以下的模拟登录程序,流程跟直接操作浏览器一致,不相同的地方是一个是程序开做,一个是人来做。

"use strict";
var page = require('webpage').create();

page.onResourceRequested = function (req) {
    //console.log('requested: ' + JSON.stringify(req, undefined, 4));
};
page.onResourceReceived = function (res) {
    //console.log('received: ' + JSON.stringify(res, undefined, 4));
};
page.onConsoleMessage = function(message) {
    console.log(message);
};


// 访问需要登录的页面
var url = 'http://my.sandbox.ebay.com/ws/eBayISAPI.dll?MyEbay&gbh=1&CurrentPage=MyeBayAllSelling&ssPageName=STRK:ME:LNLK:MESX';

page.open(url, function (status) {  
    if (status === 'success') {
        page.includeJs("http://xxx:8888/js/jquery.min.js", function() {
           var res = page.evaluate(function() {
                var str = '';
                $("table.my_itl-iT tr.my_itl-itR td a.g-asm").each(function(){
                    str += $(this).text()+"\n";
                });
                return str;
           });
           
           console.log(res);
            
           phantom.exit();
        });
    } else {
        phantom.exit();
    }
});

/*
page.open('https://signin.sandbox.ebay.com/ws/eBayISAPI.dll', function (status) {
    if (status === 'success') {
        page.includeJs("http:/xxx:8888/js/jquery.min.js", function() {
            // evaluate里面是一个沙箱,主要的DOM操作地
            page.evaluate(function() {
                var form = $("#SignInForm");
                form.find("input[name=userid]").val("testuser_xxx");
                form.find("input[name=pass]").val("xxxx");
                
                form.trigger("submit");
            });
            // 等待5秒后收集结果
            window.setTimeout(function() {
                //console.log(page.content);
                
                var cookies = page.cookies;
                var saveData = '';
                for(var i in cookies) {
                    console.log(cookies[i].name + '=+' + cookies[i].value);
                    saveData += cookies[i].name+"="+cookies[i].value+"&";
                }
    
                // 1 
                // 如果供外部程序使用,这里把cookie送出
                var save = webpage.create();
                save.open('http://xxx:7070/i.php', 'POST', saveData, function (status) {
                    if (status === "success") {
                        console.log(save.content);
                    }
                    phantom.exit();
                });
                // 2 
                // 如果不需要供外部使用,成功登录后直接退出记录
                // Cookie记录到文件,这样仅需要在Cookie失效时重新登录即可
                // phantom.exit();
            }, 5000);
        });
    } else {
        phantom.exit();
    }
});
*/

登录成功后,需要访问登录保护的页,可以把Cookie发出出去,然后外部程序来完成,也可以继续让PhantomJs去访问,如果抓取内容不连续,那么就类似于用完后,关闭浏览器,下次用再次打开浏览器,载入浏览器是低效的,而且它会载入URL相关的所有页,所以应该使用外部程序完成抓取,程序一旦检测到Cookie失效(比如检测到了跳到登录页),那么马上启动浏览器,重新自动登录,然后取到最新Cookie,这个类似于数据库链接里面的断线重连,如果断线重连无法链接,那么就需要终止了。 需要注意的是PhantomJs并不会把会话Cookie写入到你指定的cookie文件,会话cookie仅在phantom存在时有效,这个跟真实浏览器一致,浏览器关闭,所有会话Cookie会被删除,因为会话cookie的过期时间为0,意味关闭则失效,所以如果这些会话Cookie是维持登录状态的,就需要主动发送出去或记录,而不能依赖cookie文件。

用一个真实的浏览器去登录,再牛逼的防机器登录机制都无效(不使用验证码的情况,防程序登录的机制无非是用JS产生Cookie, 在HTML渲染后根据环境动态添加表单域,这些在一般模拟登录中,几乎可以通杀,因为你不可能得到HTML渲染后的值,也取不到JS在客户端产生的Cookie)。

PhantomJS 模拟登录一

"use strict";

var webpage = require('webpage');
var page = webpage.create();

/// 登录页
page.open('https://signin.sandbox.ebay.com/ws/eBayISAPI.dll', function (status) {
	if (status === "success") {
		var cookies = page.cookies;
		for(var i in cookies) {
			console.log(cookies[i].name + '=' + cookies[i].value);
		}
		//console.log(page.content);
		page.close();

		/// 登录
                // 需要提取其它的表单字段
		var postData = 'userid=testuser_xxx&pass=xxx';

		var login = webpage.create();
		login.open('https://signin.sandbox.ebay.com/ws/eBayISAPI.dll?co_partnerId=2&siteid=0&UsingSSL=1', 'POST', postData, function (status) {
		  if (status === "success") {

			var cookies = login.cookies;
			var saveData = '';
			for(var i in cookies) {
				console.log(cookies[i].name + '=+' + cookies[i].value);
				saveData += cookies[i].name+"="+cookies[i].value+"&";
			}

			//console.log(login.content);
			login.close();

			///	成功登录后发送数据
			var save = webpage.create();
			save.open('http://120.24.42.192:7070/i.php', 'POST', saveData, function (status) {
			  if (status === "success") {

				console.log(save.content);
				save.close();

				///
			  }
			  phantom.exit();
			});
		  } else {
			 console.log("Login Failed...");
			 phantom.exit();
		  }
		  
		});
	} else {
		phantom.exit();
	}
});

这里的phantom可以看做是浏览器进程,调用exit()方法表示直接关闭浏览器。page相当于浏览器中的Tab(开一个tab,跟建一个page概念相同),Tab或叫page打开以后,需要给一个URL,然后把这些个内容加载回来,然后在浏览器中就可以点击了,而在PhantomJs中,需要通过接口去模拟点击,要操作这个page,可以玩的花样很多,比如往这个page中注入一段JS,包含一个JS文件,修改DOM的内容,提交表单等等。page的evaluate方法一定程度上,可以看做是

可编程浏览器 – PhantomJS

可编程浏览器,目前主要有两个
1 PhantomJs,打包Webkit,主要提供JS编程接口
2 SlimerJs,需要独立安装Firefox,依赖这个独立安装的Firefox内核,提供JS编程接口

这两个工具提供的JS编程接口较原始,可以使用CasperJS(http://casperjs.org/)来操作(Node.js模块)。

>>>开始
1 下载
http://phantomjs.org/download.html,直接下载二进制包。
2 构建(Build)
建议直接使用二进制包。
3 最新发布
http://phantomjs.org/releases.html
4 Release Names
5 REPL
PhantomJS 1.5起,提供一个交互模式(在命令行里面直接输入代码运行)

phantomjs.exe	//进入交互模式
phantomjs> phantom.version
{
   "major": 2,
   "minor": 1,
   "patch": 1
}
phantomjs> console.log("phantomjs");
phantomjs
undefined

phantomjs> window.navigator
{
   "appCodeName": "Mozilla",
   "appName": "Netscape",
   "appVersion": "5.0 (Windows NT 6.1; WOW64) AppleWebKit/538.1 (KHTML, like Gecko) PhantomJS/2.1.1 Safari/538.1",
   "cookieEnabled": true,
   "language": "zh-CN",
   "mimeTypes": {
      "length": 0
   },
   "onLine": true,
   "platform": "Win32",
   "plugins": {
      "length": 0
   },
   "product": "Gecko",
   "productSub": "20030107",
   "userAgent": "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/538.1 (KHTML, like Gecko) PhantomJS/2.1.1 Safari/538.1",
   "vendor": "Apple Computer, Inc.",
   "vendorSub": ""
}

CTRL+C或CTRL+D或输入phantom.exit()退出交互模式。支持自动完成和历史记录。

>>>学习
1 开速开始

console.log('Hello, world!');
phantom.exit();

//截屏
var page = require('webpage').create();
page.open('http://example.com', function(status) {
	console.log("Status:" + status);
	if(status === "success") {
		page.render('example.png');
	}
	phantom.exit();
});

//测试加载时间
//phantomjs.ext loadspeed.js http://blog.ifeeline.com
var page = require('webpage').create(),
  system = require('system'),
  t, address;

if (system.args.length === 1) {
  console.log('Usage: loadspeed.js <some URL>');
  phantom.exit();
}

t = Date.now();
address = system.args[1];
page.open(address, function(status) {
  if (status !== 'success') {
    console.log('FAIL to load the address');
  } else {
    t = Date.now() - t;
    console.log('Loading ' + system.args[1]);
    console.log('Loading time ' + t + ' msec');
  }
  phantom.exit();
});

// evaluate()方法,只能返回简单对象,不能包含函数
// 并且evaluate()不能访问page只能的内容
var page = require('webpage').create();
page.open(url, function(status) {
  var title = page.evaluate(function() {
    return document.title;
  });
  console.log('Page title is ' + title);
  phantom.exit();
});

// 在evaluate()方法内部的console信息是不能显示的
// 不过可以在page的onConsoleMessage来实现
// 同时也是一个提取信息的有用办法
var page = require('webpage').create();
page.onConsoleMessage = function(msg) {
  console.log('Page title is ' + msg);
};
page.open(url, function(status) {
  page.evaluate(function() {
    console.log(document.title);
  });
  phantom.exit();
});

//以下实例展示了跟踪请求 和 响应
var page = require('webpage').create();
page.onResourceRequested = function(request) {
  console.log('Request ' + JSON.stringify(request, undefined, 4));
};
page.onResourceReceived = function(response) {
  console.log('Receive ' + JSON.stringify(response, undefined, 4));
};
page.open(url);

2 自动化测试
3 截屏

// 支持png jpg  gif  pdf
var page = require('webpage').create();
page.open('http://github.com/', function() {
  page.render('github.pdf');
  phantom.exit();
});

4 网络监控
5 页面自动化

// 设置用户代理
var page = require('webpage').create();
console.log('The default user agent is ' + page.settings.userAgent);
page.settings.userAgent = 'SpecialAgent';
page.open('http://www.httpuseragent.org', function(status) {
  if (status !== 'success') {
    console.log('Unable to access network');
  } else {
    var ua = page.evaluate(function() {
      return document.getElementById('myagent').textContent;
    });
    console.log(ua);
  }
  phantom.exit();
});
输出:
The default user agent is Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/538.1 (KHTML, like Gecko) PhantomJS/2.1.1 Safari/538.1
Your Http User Agent string is: SpecialAgent

// 使用jQuery来操作页面
// 在page.includeJs中必须使用 phantom.exit()来退出
var page = require('webpage').create();
page.open('http://www.sample.com', function() {
  page.includeJs("http://ajax.googleapis.com/ajax/libs/jquery/1.6.1/jquery.min.js", function() {
    page.evaluate(function() {
      $("button").click();
    });
    phantom.exit()
  });
});

// 页面的内容
page.content
page.plainText
// 页面的cookies(包括页面在渲染是JS产生的cookie)
page.cookies
// 页面设置,当前只有一个值userAgent
page.settings.userAgent = ‘SpecialAgent’;
// 页面URL
page.url
// 页面视口大小(浏览器尺寸)
page.viewportSize
{
	width:1024,
	height:768
}
////////////////////////////////////
// 函数列表

    page.childFramesCount
    page.childFramesName
    page.close
    page.currentFrameName
    page.deleteLater
    page.destroyed
    page.evaluate
    page.initialized
    page.injectJs
    page.javaScriptAlertSent
    page.javaScriptConsoleMessageSent
    page.loadFinished
    page.loadStarted
    page.openUrl
    page.release
    page.render
    page.resourceError
    page.resourceReceived
    page.resourceRequested
    page.uploadFile
    page.sendEvent
    page.setContent
    page.switchToChildFrame
    page.switchToMainFrame
    page.switchToParentFrame
    page.addCookie
    page.deleteCookie
    page.clearCookies

// 回调列表

    onInitialized
    onLoadStarted
    onLoadFinished
    onUrlChanged
    onNavigationRequested
    onRepaintRequested
    onResourceRequested
    onResourceReceived
    onResourceError
    onResourceTimeout
    onAlert
    onConsoleMessage
    onClosing

6 进程间通信
写入到文件
对外HTTP请求(GET或POST数据,在页面中通过XMLHttpRequest),跨域是禁止的
来自外部的HTTP请求(提供WebServer模块)
路由PhantomJS的流量通过一个代理(比如fiddler)
7 命令行工具
phantomjs [options] somescript.js [arg1 [arg2 […]]]

// 最为有用的是指定cookies保存的路径
–cookies-file=/path/to/cookies.txt specifies the file name to store the persistent Cookies.
–local-to-remote-url-access=[true|false] allows local content to access remote URL (default is false). Also accepted: [yes|no].

Phantomjs 1.3后,可以通过–config=/config.json来指定配置:

config.json
{
	"cookiesFile": "cookies.text"
}

phantomjs.exe --cookies-file=cookies.txt cookie.js
phantomjs.exe --config=config.json cookie.js

>>> 探索
1 实例
2 最佳实践
推荐使用自动化测试框架,高层封装CasperJS
3 提示和技巧
4 Supported Web Standards
5 Buzz
6 Who’s using PhantomJS?
7 Related Projects
http://phantomjs.org/related-projects.html

Node.js 操作MySQL

var mysql = require("mysql");

var cnn = mysql.createConnection({
    host:'127.0.0.1',
	port:3306,
	database:'test',
	user:'root',
	password:''
});

//连接池
var pool = mysql.createPool({
    host:'127.0.0.1',
	port:3306,
	database:'test',
	user:'root',
	password:''
});
var pcnn = pool.getConnection((err, pcnn) => {
	console.log("使用连接池");
	var query = pcnn.query("SELECT * FROM datatables_demo");
	query.on('error', (err) => {
		console.log("数据读取错误");

		// 释放连接回到池
		pcnn.release();
	}).on('fields', (fields) => {
		//console.log(fields);
		fields.forEach((field) => {
			console.log(field.name);
		});
	}).on('result', (row) => {
		pcnn.pause();
		console.log(row.first_name+" "+row.last_name+" "+row.age);
		pcnn.resume();
	}).on('end', () => {
		console.log("读取完毕");
		// 释放连接回到池
		pcnn.release();
	});	
	console.log("使用连接池结束");
});

var pcnn = pool.getConnection((err, pcnn) => {
	console.log("使用连接池");
	var query = pcnn.query("SELECT * FROM datatables_demo");
	query.on('error', (err) => {
		console.log("数据读取错误");

		// 释放连接回到池
		pcnn.release();
	}).on('fields', (fields) => {
		//console.log(fields);
		fields.forEach((field) => {
			console.log(field.name);
		});
	}).on('result', (row) => {
		pcnn.pause();
		console.log(row.first_name+" "+row.last_name+" "+row.age);
		pcnn.resume();
	}).on('end', () => {
		console.log("读取完毕");
		// 释放连接回到池
		pcnn.release();
	});	
	console.log("使用连接池结束");
});

// 关闭整个池
//pool.end((err) => {
//	console.log("关闭连接池");
//});

///////////////////////////////////////////////////////////
cnn.connect(function(err){
	if(err) {
		console.log("数据库连接错误");
	} else {
		console.log("数据库连接成功");
	}	
});

// 连接丢失重连
cnn.on('error', function(err){
	if(err.code === 'PROTOCOL_CONNECTION_LOST') {
		console.log('连接丢失');
		setTimeout(function(){
			cnn.connetct((err)=>{
				console.log("数据库重连");
			});
		}, 10000);
	}
});

//cnn.query("INSERT INTO posts SET ?", {id:1, title:'Hello MySQL'});
//cnn.query("UPDATE posts SET title = :title", {title:'Hello MySQL'});
//cnn.query("SELECT * FROM users WHERE ID = ?", [userId]);

// 以流的方法读取数据,以行为单位
// 正确姿势
var query = cnn.query("SELECT * FROM datatables_demo");
query.on('error', (err) => {
	console.log("数据读取错误");
}).on('fields', (fields) => {
	//console.log(fields);
	fields.forEach((field) => {
		console.log(field.name);
	});
}).on('result', (row) => {
	cnn.pause();
	console.log(row.first_name+" "+row.last_name+" "+row.age);
	cnn.resume();
}).on('end', () => {
	console.log("读取完毕");
});

// 一次读整个结果,然后对结果循环
cnn.query("SELECT * FROM datatables_demo LIMIT 20", function(err, result){
	//console.log(result);
	for(row in result) {
		//console.log(result[row]);
		console.log(result[row].first_name+" "+result[row].last_name+" "+result[row].age);
	}
	console.log("------------------------------------------");
	result.forEach((row) => {
		 console.log(row.first_name+" "+row.last_name+" "+row.age);
	});
});


cnn.end(function(err){
	console.log("数据库关闭。");
});

Node.js HTTP服务器

> 创建HTTP服务器

var http = require('http');
var server = http.createServer(function(request, reponse){

}).listen('9999','127.0.0.1', function(){
	console.log('开始监听');
});

/////
var server = http.createServer();
server.on('request', function(request, reponse){

});
// listen方法第一参数为端口号,0表示随机端口
// 第三参数默认是511表示最大连接数量
server.listen('9999','127.0.0.1', function(){
	console.log('开始监听');
});
//listen方法将触发listening事件,如果不指定回调
server.on('listening', function(){
	console.log('开始监听');
});

// 显式关闭HTTP服务器,触发close事件,可以监听它做善后处理
server.close()
server.on('close', function(){});

// 如果端口冲突,将触发server的error事件
server.on('error', function(e){
	if(e.code == 'EADDRINUSE') {
	}
});
// 当有客户端连接进来时触发server的connection对象
// 回调参数为服务器端用于监听客户端请求的socket端口对象
server.on('connection', function(socket){
});

方法createServer()的回调中第一参数是http.IncomingMessage对象,此处代表一个客户端请求,第二个参数值为一个http.ServerResponse对象,代表一个服务器端响应对象。

方法createServer方法返回被创建的服务器对象,如果不在createServer方法中使用参数,也可以通过监听该方法创建的服务器对象的request事件(这种方式比较直观)

可以使用HTTP服务器的setTimeout方法来设置服务器的超时时间。当该超时时间超过之后,客户端不可继续利用本次与HTTP服务器建立的连接,下次向该HTTP服务器发出请求时必须重新建立连接

// 设置超时并给出超时时需要执行的回调
server.setTimeout(1000, function(socket) {
});
// 超时时会触发timeout事件 
server.on('timeout', function(socket){

});
// 也可以直接操作server的timeout属性
server.timeout = 1000;
console.log(server.timeout);

HTTP服务器拥有一个timeout属性,属性值为整数值,单位为毫秒,可用于查看或修改服务器的超时时间。

> 获取客户端请求信息
http.IncomingMessage用于读取客户端请求流中的数据,当从客户端请求流中读取到新的数据时触发data事件,当读取完客户端请求流中的数据时触发end事件。当该对象被用于读取客户端请求流中的数据时,有如下属性:

.method		GET POST PUT DELETE
.url		带请求查询的URL
.headers	客户端发送的请求头
.httpVersion
.trailers	客户端附加的HTTP头(end事件触发后才能读取)
.socket		socket对象

例子:

var http = require('http');

var server = http.createServer(function (req, res) {
	 //console.log(req);
	 console.log(req.method);
	 console.log(req.url);
	 console.log(req.headers);
	 // data触发主要用来接收客户端请求体的数据,比如POST的数据
	 req.on('data', function(data) {
		console.log(decodeURIComponent(data));
	 });

	 req.on('end', function(){
		console.log("客户端数据已经全部接收");
	 });
}).listen(9999, '127.0.0.1');

server.on('connection',function(socket){
	console.log('客户端连接已建立');
});

> 转换URL字符串与查询字符串
在Node.js中,提供了一个url模块与一个queryString模块,分别用来转换完整URL字符串与URL中的查询字符串。

querystring.parse('userName=Lulingniu&age=40&sex=male','&','=');
querystring.stringify({username:'vfeelit',age:'18',sex:'male'})
// 第二参数表示是否解析查询字符串为一个对象
// 内部实际使用querystring对象
url.parse('http:// user:pass@host.com:8080/users/user.php?userName=vv&age=18&sex=male#name1')
url.format(urlObj);
url.resolve(from, to); //可以简单看做是字符串替换

> 发送服务器响应
利用http.ServerResponse对象的writeHead方法来发送响应头

var http = require('http');
var server = http.createServer(function(req, res){
	res.writeHead(200, {'Content-Type':'text/plain', 'Access-Control-Allow-Origin':'http://127.0.0.1'});
	//也可以使用setHeader设置响应头 
	res.setHeader("Set-Cookie",["type=xx","language=xxxx"]);
	res.write("Hello");
	res.end();
});

使用了http.ServerResponse对象的setHeader方法设置响应头信息之后,可以使用http.ServerResponse对象的getHeader方法获取响应头中的某个字段值。使用http.ServerResponse对象的removeHeader方法删除一个响应字段。http.ServerResponse对象具有一个headersSent属性,当响应头已发送时,该属性值为true,当响应头未发送时,属性值为false。

HTTP服务器自动将服务器端当前时间作为响应头中的Date字段值发送给客户端。可以通过将http.ServerResponse对象的sendDate属性值设置为false的方法在响应头中删除Date字段。

可以通过http.ServerResponse对象的statusCode属性值获取或设置HTTP服务器返回的状态码。

可以使用http.ServerResponse对象的setTimeout方法设置响应超时时间。如果在指定时间内服务器没有做出响应,则响应超时,同时触发http.ServerResponse对象的timeout事件。

在http.ServerResponse对象的end方法被调用之前,如果连接中断,将触发http.ServerResponse对象的close事件。

> HTTP客户端
request方法返回一个http.ClientRequest对象,代表一个客户端请求。

当客户端请求获取到服务器响应流时,触发http.ClientRequest对象的response事件,可以不在request方法中使用callback参数,而是通过对http.ClientRequest对象的response事件进行监听并指定事件回调函数的方法来指定当获取到其他网站返回的响应流时执行的处理。

var http = require('http');
var option = {
	hostname: 'blog.ifeeline.com',
	port:80,
	path:'/',
	method:'GET'
};
//回调在reponse事件触发时执行
var req = http.request(options, function(res){
});
// http.request()方法返回一个http.clientRequest对象,调用end()方法将发起请求
// 数据返回后触发reponse事件,所以可以如下
req.on('reponse', function(res){
});
// res对象是一个http.IncomingMessage对象,表示发起请求的响应

// 可以调用http.clientRequest对象的write来发送数据,比如上次文件
// 这些内容实际就是HTTP请求中的请求体
req.write(chunk,[encoding])
// 必须调用end方法来结束请求(实际是发出请求)
// end中的参数(可空),类似最后执行一次write方法
req.end([chunk],[encoding])

使用http.ClientRequest对象的abort方法终止本次请求,如果在向目标网站请求数据的过程中发生了错误,将触发http.ClientRequest对象的error事件。在建立连接的过程中,当为该连接分配端口时,触发http.ClientRequest对象的socket事件。

如果使用GET方式向其他网站请求数据,也可以使用http模块中的get方法。

Node.js 操作文件系统

> 文件读写
完整读取一个文件时,可用readFile方法或readFileSync:

fs.readFile(filename, [options], callback)

var fs = require('fs');
fs.readFile('./t.txt', function(err, data){
	if(err) console.log("读文件发送错误")
	slse console.log(data);
});

注意,如果没有指定options,那么data就是二进制数据(Buffer对象,可调用toString()方法获取字符串),options设置:

flag		r, r+, w, w+, a, a+
encoding	utf8, ascii, base64

在options参数值中,可使用encoding属性指定使用何种编码格式来读取该文件,过程就是读取文件,然后转换成指定编码后存入Buffer对象,否则就是直接读取文件的原始二进制内容。如果在readFile函数中使用options参数并将encoding属性值指定为某种编码格式,则回调函数中的第二个参数值返回将文件内容根据指定编码格式进行编码后的字符串。

在使用同步方式读取文件时,使用readFileSync方法:

var data=fs.readFileSync(filename, [options])

在完整写入一个文件时,我们可以使用fs模块中的writeFile方法或writeFileSync方法:

fs.writeFile(filename,data,[options],callback)
fs.writeFileSync(filename,data, [options]);

var fs = require('fs');
fs.writeFile('./t.txt', "数据", function(err){
	if(err) console.log("写文件失败");
	else console.log("写文件成功");
});

// 向文件追加数据
var fs = require('fs');
fs.writeFile('./t.txt', "追加数据", {flag:'a'}, function(err){
	if(err) console.log("写文件失败");
	else console.log("写文件成功");
});

// base64读入图片,base64解码数据写入图片
// 读入的data是一个Buffer对象,toString后就是base64字符串
// 然后写文件时,指定base64解码字符串(得到二进制数据)
var fs=require('fs');
fs.readFile('./a.gif','base64',function(err,data){
	fs.writeFile('./b.gif',data.toString(), "base64", function(err){});
});

产生data可以是字符串也可以是一个Buffer对象。options设置:

flat		默认为w
mode		文件权限,默认0666
encoding	指定使用何种编码来写入文件,data是Buffer时被忽略

将一个字符串或一个缓存区中的数据追加到一个文件底部时,我们可以使用fs模块中的appendFile方法或appendFileSync方法。

>从指定位置开始读写文件

fs.open(filename, flags,[mode],callback)
fs.openSync(filename, flags,[mode])

//回调函数第二参数为打开的文件描述符
var fs=require('fs');
fs.open('./t.txt', 'r', function(err,fd) {

});

// 取到文件描述符后,使用read读取
fs.read(fd, buffer, offset, length, position, callback)

> 创建与读取目录

// mode默认为0777
fs.mkdir(path, [mode], callback);

// 读取目录,files是一个数组
var fs = require('fs');
fs.readdir('D:/', function(err, files){
    console.log(files);
});

> 查看文件或目录的信息

// 当查看连接文件时,必须使用lstat
fs.stat(path,callback);
fs.lstat(path, callback);

// stats是一个fs.Stats对象,有一系列方法和属性
// 比如是isFile isDirectory
fs.stat('./t.txt', function(err, stats){

});

在使用open方法或openSync方法打开文件并返回文件描述符后,可以使用fs模块中的fstat方法查询被打开的文件信息。

> 检查文件或目录是否存在

fs.exists(path, function(exists){});

> 获取文件或目录的绝对路径

fs.realpath(path, function(err, resolvedPath){});

> 修改文件访问时间及修改时间

fs.utime(path, atime, mtime, function(err){});
fs.utime(path, new Date(), new Date(), function(err){});

在使用open方法或openSync方法打开文件并返回文件描述符后,可以使用fs模块中的futimes方法修改文件的访问时间或修改时间。

> 修改文件或目录的读写权限

fs.chmod('./t.txt', 06000, function(err){});

> 文件或目录的其它操作

fs.rename(oldPath,newPath,callback)
fs.link(srcpath,dstpath,callback)
fs.unlink(path,callback)
fs.symlink(srcpath,dstpath,[type],callback)
fs.truncate(filename,len,callback)
fs.rmdir(path,callback)
fs.watchFile('./message.txt',function(curr, prev) {})

Node.js Buffer对象

> Buffer对象创建
在Node.js中,Buffer类是一个可以在任何模块中被利用的全局类,不需要为该类的使用而加载任何模块。

//1 
//size表示多少字节
var buf = new Buffer(size)		

// 2 数组值
var buf = new Buffer([1,2,3]);

// 3 用指定编码的字符串填充,编码默认utf8,一般有ascii base64 hex
var buf = new Buffer(str, [encoding]);

//用value进行填充,指定段
buf.fill(value, [offset], [end]);	

被创建的Buffer对象拥有一个length属性,属性值为缓存区大小。

>字符串长度与缓存区的长度
字符串长度以单个字符作为单位(可能占用多个字节),缓存区以字节为单位。可以使用下标语法取出字符串或缓存区中的内容,比如str[2],buf[2],一个是取字符,一个是取字节。

字符串对象有可用于搜索字符串的indexOf、match、search方法,但是Buffer对象没有,它有一个slice方法(是指字符串和Buffer都有),不过它取的数据是字节,并且不是值拷贝(引用原来数据),所以如果对slice的结果进行修改,对应的Buffer对象的值也会被修改。

> Buffer对象与字符串对象之间的相互转换
可用buf.toString([encoding],[start],[end])来转换为指定编码(默认utf8)的字符串。

要向已经创建的Buffer对象中写入字符串,这时可以使用该Buffer对象的write方法:

buf.write(string, [offset], [length], [encodeing]);

注意和fill方法的区别。

> Buffer类的类方法

// 判断是否是Buffer对象
Buffer.isBuffer(obj);

//使用byteLength方法计算一个指定字符串的字节数
Buffer.byteLength(string, [encode]);

//用于将几个Buffer对象结合创建为一个新的Buffer对象
Buffer.concat(list,[totalLength])

//检测一个字符串是否为一个有效的编码格式字符串
Buffer.isEncoding(encoding)

Node.js 创建模块

mkdir /censorify
cd censorify

#建立主文件
vi censortext.js
var censoredWords = ["sad", "bad", "mad"];
var customCensoredWords = [];

function censor(inStr) {
    for(idx in censoredWords) {
        inStr = inStr.replace(censoredWords[idx], "****"); 
    }    
    for(idx in customCensoredWords) {
        inStr = inStr.replace(customCensoredWords[idx], "****"); 
    } 
    return inStr;
}

function addCensoredWord(word) {
       censoredWords.push(word); 
}

function getCensoredWords() {
       return censoredWords.concat(customCensoredWords);
}

exports.censor = censor;
exports.addCensoredWord = addCensoredWord;
exports.getCensoredWords = getCensoredWords;

#建立package.json(main指定入口)
vi package.json
{
    "author": "vfeelit@qq.com",
    "name": "censorify",
    "version": "0.1.1",
    "description": "",
    "main": "censortext",
    "dependencies": {},
    "engines": {
        "node": "*"   
     }
}

#打包(生成censorify-0.1.1.tgz文件)
npm pack

#在其它应用中安装
cd ../readwords
npm install ../censorify/censorify-0.1.1.tgz

#使用引入的包
vi readwords.js
var censor = require("censorify");

console.log(censor.getCensoredWords());
console.log(censor.censor("Some very sad, bad and mad text."));

censor.addCensoredWord("gloomy");
console.log(censor.getCensoredWords());
console.log(censor.censor("A very gloomy day."));

#运行结果
node readwords.js 
[ 'sad', 'bad', 'mad' ]
Some very ****, **** and **** text.
[ 'sad', 'bad', 'mad', 'gloomy' ]
A very **** day.