簡介
使用 Node 爬取信息和其他語言幾乎步驟相同,都同樣是以下幾點
- 發起請求
- 解析內容
- 避免反爬蟲
- 爬蟲策略更新
注意:爬正規網站可能會有法律風險,但是那些小站,甚至自身就有問題的那種,總不怕啥問題。
發起請求
舉個例子,筆者隨手找了一個種子搜索站。發送下圖請求,返回的是一個html頁面
接着我們分析頁面html代碼找到列表第一項的資源的超鏈接為 '/0AA61E5C1B7B665BC02BCCAF55F3EF7837AFA4F0.html',加上此站域名從而發送下圖請求
具體解析頁面html代碼抓取到想要的文本的方法,可以很粗暴的選擇正則表達式。當抓取完畢資源,應該存儲到本地,並且開始重新發送請求再來一遍。
Demo 代碼如下:
var http = require('http');
// http.request(options, callback);
http.get('http://bt2.bt87.cc/search/SMD31_ctime_1.html', function(res) {
var data = '';
res.setEncoding("utf8");
res.on('data', function(chunk) {
data += chunk;
}).on('end', function() {
console.log(data)
});
});
這里的data就是我們抓去到的html片段大概長這樣
第二幅圖里的 magnet:xxxxxxxx,這種格式就是我們要的資源鏈接,迅雷可用。
調整代碼如下:
var http = require('http');
var count = 31;
var start = function (id) {
http.get('http://bt2.bt87.cc/search/SMD'+ id + '_ctime_1.html', function(res) {
var data = '';
res.setEncoding("utf8");
res.on('data', function(chunk) {
data += chunk;
}).on('end', function() {
//var href = 第一個ul里的第一個第一個a標簽的href屬性
http.get('http://bt2.bt87.cc' + href, function(res1) {
var data1 = '';
res1.setEncoding("utf8");
res1.on('data', function(chunk) {
data1 += chunk;
}).on('end', function() {
//var magnet = 正則匹配帶有magnet關鍵字的信息
/* fs.appendFile(path, content, function (err){}) */
//重新開始請求
start(id + 1);
});
});
});
});
};
start(count);
代碼優化
上面的代碼陷入了回調地獄里,十分難看,並且也不健壯。任何一個環節出差錯都會導致后面代碼不執行而停止循環請求。
解決辦法是,我們可以使用 ES6 的 Promise 語法,畢竟 Node 自 8 后,完全支持 Promise。改造我們的請求函數和文件操作函數。
得到了爬取內容后,就得解析,解析 HTML 可以用 cheerio,類 JQuery 語法。但簡單點直接正則吧,代碼如下:
//第一個請求,請求資源列表
var getResourceUrl = function (url) {
return new Promise(function (resolve, reject) {
http.get(url, function(response) {
var html = '';
response.on('data', function(data) {
html += data;
});
response.on('end', function() {
var ul = html.match(/<ul class="media-list media-list-set">[\s\S]*<\/ul>/);
if (ul) {
resolve(ul[0]);
} else {
reject('can not match ul dom');
}
});
}).on('error', function() {
reject('getResourceUrl failed');
});
});
};
//第二個請求,請求具體的某個資源
var getMagnet = function (url) {
return new Promise(function (resolve, reject) {
http.get(url, function(response) {
var html = '';
response.on('data', function(data) {
html += data;
});
response.on('end', function() {
var magnet = html.match(/magnet:\??[^"|<]+/);
if (magnet) {
resolve(html);
} else {
reject('can not match magnetReg');
}
});
}).on('error', function (res) {
reject(res);
});
});
};
//追加文件
var appendFile = function (path, content) {
return new Promise(function (resolve, reject) {
fs.appendFile(path, content, {flag:'a'}, function (err) {
if (err) {
reject('append ' + path + ' failed');
} else {
resolve('append ' + path + ' success');
}
});
});
};
然后我們的調用的代碼就成了這樣
//開始函數
var start = function () {
getResourceUrl(url);
.then(function (html) {
//var href = 第一個ul里的第一個第一個a標簽的href屬性
return getMagnet('http://bt2.bt87.cc' + href);
}, function (res) {
return Promise.reject(res);
})
.then(function (resArr) {
//var magnet = 正則匹配帶有magnet關鍵字的信息
return appendFile('./SMD.txt', magnet);
}, function (res) {
console.log(res);
return Promise.reject(res);
})
.then(function (resArr) {
console.log('writeFile success!');
start();
}, function (res) {
console.log(res);
start();
});
};
簡單又粗暴,而且某個環節掉了鏈子,比方說第一次請求匹配不到我們要的鏈接,也能把錯誤傳遞到最后的then里而重新 start() 一個請求,不會中斷。
內容解析
具體怎么匹配到我們想要的資源,正則是一個王道的辦法,比如下面代碼
//匹配magnet磁力鏈接
var magnetReg = /magnet:\??[^"|<]+/;
//匹配ul標簽
var ulReg = /<ul class="media-list media-list-set">[\s\S]*<\/ul>/
//匹配a標簽
var aReg = /<a class="title".* href="\/\w+\.html")/g;
但是這里可以有更簡便的辦法,就是利用cheerio庫來DOM結構的html文本。
var cheerio = require('cheerio');
...
getResourceUrl(url);
.then(function (html) {
//var href = 第一個ul里的第一個第一個a標簽的href屬性
var $ = cheerio.load(html);
var $body = $('.media-body');
var href = $body.eq(0).find('.title').attr('href');
return getMagnet(href);
}, function (res) {
return Promise.reject(res);
});
就是這么容易,第二個請求也是如法炮制,最后輸出到 SMD.txt 文件里的就是這種格式
避免反爬蟲
筆者曾經在爬取妹子圖網站上的圖片的時候曾經遇到過,爬蟲返回 403,這表示網站采用了防爬技術,反爬蟲一般會采用比較簡單即會檢查用戶代理(User Agent)信息。再請求頭部構造一個User Agent就行了。也可能會檢測Referer請求頭,還有cookie等。高級的反爬蟲會統計一個 ip 在一小時內請求量是否超過限制,達到則封鎖 ip,這樣的方案就需要加上代理,下面代碼演示了一個偽造 User Agent 頭並且連代理的最基本例子
var http = require('http');
var opt = {
//代理服務器的ip或者域名,默認localhost
host: '122.228.179.178',
//代理服務器的端口號,默認80
port: 80,
//path是訪問的路徑
path: 'http://www.163.com',
//希望發送出去的請求頭
headers: {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.86 Safari/537.36',
}
};
http.get(opt, function(res) {
var data = '';
res.setEncoding("utf8");
res.on('data', function(chunk) {
data += chunk;
}).on('end', function() {
console.log(data)
});
});
如果目標網站封鎖了我方的IP地址的話,我們只要改變options參數里的host就能解決,這個代理ip只要在搜索引擎上輸入“免費代理ip”就有了,比方說這個網站。不過不是每個免費代理ip都能用,難免有些失效了,所以狡猾的程序員會事先抓取網站提供的免費代理ip用它發送請求,如果能發送的了則證明ip可用。可用的一堆ip當作ip池,在爬蟲的時候不停輪換使用。誠可謂道高一尺魔高一丈。
爬蟲策略
加了 IP 能突破多數的反爬設置,但 IP 並非無限的,若短時間發的太多,還是可能被數據投毒,或者直接封禁。故而需要一些策略。
舉個簡單的例子是爬取一陣,休息一兩分鍾再繼續,並且控制爬取速度。
思考題
源自本人的一次面試,面試官問:如何寫一個多線程的爬蟲。
提示:Node 里多線程是沒辦法,但是可以用多進程模式,關注一下 Node cluster 模塊。
結尾
獻上我的源碼一份,望不吝點贊。