爬蟲是目前獲取數據的一個重要手段,而 python 是爬蟲最經常使用的語言,有豐富的框架和庫。最近在學習的過程當中,發現 nodjs 也能夠用來爬蟲,直接使用 JavaScript 來編寫,不但簡單,快速,並且還能利用到 Node 異步高併發的特性。下面是個人學習實踐。html
爬蟲的過程離不開對爬取網址的解析,應用到 Node 的 url 模塊。url 模塊用於處理與解析 URL。node
url.parse()
用於解析網址url.resolve()
把一個目標 URL 解析成相對於一個基礎 URLconst url = require('url')
const myUrl = url.parse('https://user:pass@sub.host.com:8080/p/a/t/h?query=string#hash');
console.log(myUrl)
// {
// protocol: 'https:',
// slashes: true,
// auth: 'user:pass',
// host: 'sub.host.com:8080',
// port: '8080',
// hostname: 'sub.host.com',
// hash: '#hash',
// search: '?query=string',
// query: 'query=string',
// pathname: '/p/a/t/h',
// path: '/p/a/t/h?query=string',
// href:'https://user:pass@sub.host.com:8080/p/a/t/h?query=string#hash'
// }
console.log(url.resolve('/one/two/three', 'four'))
// 解析結果爲 '/one/two/four'
console.log(url.resolve('http://example.com/', '/one'))
// 解析結果爲 'http://example.com/one'
console.log(url.resolve('http://example.com/one', '/two'))
// 解析結果爲 'http://example.com/two'
複製代碼
爬蟲須要發送網絡請求,這時須要根據 url 協議採用不一樣模塊,若是是 http 則採用 http 模塊,若是 https 協議則採用 https 模塊。請求須要用到模塊的 request 方法python
使用 http.request(options[, callback])
發出 HTTP 請求。http.request()
返回 http.ClientRequest 類的實例。git
ClientRequest 實例是一個繼承自 stream 的可寫流。表示正在進行的請求。在請求的時候可用 setHeader(name, value)
、getHeader(name)
或 removeHeader(name)
等函數改變請求頭。 實際的請求頭將與第一個數據塊一塊兒發送,或者當調用 request.end()
時發送。github
要調用
request.end()
後才能開始發送請求。api
發送 POST 請求瀏覽器
const querystring = require('querystring')
const http = require('http')
const postData = querystring.stringify({
'msg': 'Hello World!'
});
const options = {
hostname: 'nodejs.cn',
port: 80,
path: '/upload',
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': Buffer.byteLength(postData)
}
};
const req = http.request(options, (res) => {
console.log(`狀態碼: ${res.statusCode}`);
console.log(`響應頭: ${JSON.stringify(res.headers)}`);
res.setEncoding('utf8');
res.on('data', (chunk) => {
console.log(`響應主體: ${chunk}`);
});
res.on('end', () => {
console.log('響應中已無數據');
});
});
req.on('error', (e) => {
console.error(`請求遇到問題: ${e.message}`);
});
// 將數據寫入請求主體。
req.write(postData);
req.end();
複製代碼
經過封裝,把經常使用的請求方式封裝成一個函數,便於複用和管理代碼。bash
// 定義默認的請求頭
const _header = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36',
'Accept-Encoding': 'gzip, deflate, br' // 默認加載壓縮過的數據
}
複製代碼
請求時在請求頭添加User-Agent
,用於模擬瀏覽器請求;添加 'Accept-Encoding': 'gzip, deflate, br'
。請求通過 gzip 壓縮過的數據,減小消耗的流量和響應的時間。這樣在讀取完數據的時候須要用 zlib
模塊進行解壓。網絡
zlib.gzip(buffer[, options], callback)
壓縮數據zlib.gunzip(buffer[, options], callback)
解壓數據// 判斷請求頭裏是否有 gzip 字符串, 有則說明採用了 gzip 壓縮過
if(res.headers['content-encoding'] && res.headers['content-encoding'].split(';').includes('gzip')) {
// 解壓數據後返回數據
zlib.gunzip(result, (err, data) => {
if(err) {
reject(err)
} else {
resolve({
buffer: data,
headers: res.headers
})
}
})
}
複製代碼
封裝的函數傳入一個 options 參數,能夠只是一個字符串,也能夠是一個 object 對象,包含各類請求信息。併發
// 判斷 options 是不是字符串
if(typeof options === 'string') {
// 修改 options 格式爲對象
options = {
url: options,
method: 'GET',
header: {}
}
} else {
// 若是是對象,則給 options 對象添加默認屬性
options = options || {}
options.method = options.method || 'GET'
options.header = options.header || {}
}
複製代碼
函數返回的是一個 Promse 對象,利用 JavaScript 的異步特性發送請求,提升運行效率。以後在 Promise 中利用 url 模塊來解析請求的網址,根據 protocol 判斷請求網址使用的 協議。
// 解析 url
var obj = url.parse(options.url)
// 解析協議
let mode = null
let port = 0
switch(obj.protocol) {
// https 協議
case 'https:':
mode = require('https')
port = 443
break
// http 協議
case 'http':
mode = require('http')
port = 80
break
}
複製代碼
http.request
請求後 callback 回調函數裏,經過判斷 response 的 statusCode 是不是 200,來判斷請求是否成功。若是請求失敗,判斷是不是重定向,進行重定向處理。
if(res.statusCode!=200){
// 判斷是不是跳轉
if(res.statusCode==302 || res.statusCode==301){
// 更新 url 爲跳轉的網址
let location=url.resolve(options.url, res.headers['location']);
// 更新 options 的設置
options.url=location;
options.method='GET';
// 從新發起請求
_request(options);
}else{
// 返回 response
reject(res);
}
}
複製代碼
最終代碼 fetch.js
const assert = require('assert')
const url = require('url')
const zlib = require('zlib')
const querystring = require('querystring')
// 定義默認的請求頭
const _header = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36',
'Accept-Encoding': 'gzip, deflate, br' // 默認加載壓縮過的數據
}
module.exports = (options) => {
// 處理參數
if(typeof options === 'string') {
options = {
url: options,
method: 'GET',
header: {}
}
} else {
options = options || {}
options.method = options.method || 'GET'
options.header = options.header || {}
}
// 添加請求頭信息
for(let name in _header) {
options.header[name] = options.header[name] || _header[name]
}
// 封裝 post 的數據
if(options.data) {
options.postData = querystring.stringify(options.data)
options.header['Content-Length'] = options.postData.length
}
// 返回 Promise 對象
return new Promise((resolve, reject) => {
_request(options)
function _request(options) {
// 解析 url
var obj = url.parse(options.url)
// 解析協議
let mode = null
let port = 0
switch(obj.protocol) {
case 'https:':
mode = require('https')
port = 443
break
case 'http':
mode = require('http')
port = 80
break
}
// 封裝請求
let req_options = {
hostname: obj.hostname,
port: obj.port || port,
path: obj.path,
method: options.method,
headers: options.header
}
// 發送請求
let req_result = mode.request(req_options, (res) => {
// 判斷是否出錯
if(res.statusCode!=200){
// 判斷是不是跳轉
if(res.statusCode==302 || res.statusCode==301){
// 更新 url 爲跳轉的網址
let location=url.resolve(options.url, res.headers['location']);
options.url=location;
options.method='GET';
_request(options);
}else{
// 返回 response
reject(res);
}
} else {
// 處理數據
var data = []
res.on('data', chunk => {
data.push(chunk)
})
// 返回數據
res.on('end', () => {
// 處理數據
var result = Buffer.concat(data)
if(res.headers['content-length'] && res.headers['content-length'] != result.length) {
reject('數據加載不完整')
} else {
// 判斷是不是壓縮過的數據
if(res.headers['content-encoding'] && res.headers['content-encoding'].split(';').includes('gzip')) {
zlib.gunzip(result, (err, data) => {
if(err) {
reject(err)
} else {
resolve({
buffer: data,
headers: res.headers
})
}
})
} else {
// 直接加載數據
resolve({
buffer: result,
headers: res.headers
})
}
}
})
}
})
// 出錯返回
req_result.on('error', e=>reject(e));
// POST 時有數據則發送
if(options.postData) {
req_result.write(options.postData)
}
req_result.end();
}
})
}
複製代碼
接下來,利用封裝後的函數,來爬取豆瓣的電影數據,將收集的數據按照評分開始排序,最後輸出到 txt 文件上。
實戰代碼
const fetch = require('../fetch')
const fs = require('fs')
// 爬取豆瓣數據
var data = []
// 爬取100頁數據
getData(100)
// 爬取單頁數據
// 參數 time 爬取頁數
async function getData(time) {
var pageStart = 0
var pageLimit = 20
for(var i = 0; i < time; i++) {
var res = await fetch({
url: `https://movie.douban.com/j/search_subjects?type=movie&tag=%E8%B1%86%E7%93%A3%E9%AB%98%E5%88%86&sort=rank&page_limit=${pageLimit}&page_start=${pageStart}`
})
// 添加數據到 data
var newData = JSON.parse(res.buffer.toString())
data.push(...newData.subjects)
pageStart += pageLimit
}
// 對數據進行排序
data.sort((a, b) => b.rate - a.rate)
// 處理輸出到文檔的字符串
var res = data.reduce((str, item) => {
return str + item.title + ': ' + item.rate + '\n'
}, '')
// 保存數據到文件
fs.writeFile('./sort.txt', res, function(err) {
if (err) {
throw err;
}
});
}
複製代碼
最終 sort.txt 數據
是,大臣 1984聖誕特輯: 9.8
伊麗莎白: 9.6
霸王別姬: 9.6
肖申克的救贖: 9.6
控方證人: 9.6
莫扎特!: 9.5
辛德勒的名單: 9.5
美麗人生: 9.5
茶館: 9.4
這個殺手不太冷: 9.4
十二怒漢: 9.4
背靠背,臉對臉: 9.4
控方證人: 9.4
福爾摩斯二世: 9.4
十二怒漢: 9.4
燦爛人生: 9.4
阿甘正傳: 9.4
搖滾莫扎特: 9.4
羅密歐與朱麗葉: 9.4
新世紀福音戰士劇場版:Air/真心爲你: 9.4
千與千尋: 9.3
熔爐: 9.3
極品基老伴:完結篇: 9.3
盜夢空間: 9.3
銀魂完結篇:直到永遠的萬事屋: 9.3
巴黎聖母院: 9.3
城市之光: 9.3
...
複製代碼