慶祝 2018 國慶,製作了一個 的種子下載器。爬取頁面,根據頁面的鏈接,破解另外一個網站,下載種子文件,同時使用 模塊提高爬蟲的併發量。項目比較簡單,爬取頁面沒有使用任何爬蟲框架。 的安裝請看我的另外一篇文章, "Node.js 的多版本安裝" 。 項目初始化 新建一個文件夾 ,在該文件夾下打開命 ...
Node.js 種子下載器
慶祝 2018 國慶,製作了一個 Node.js
的種子下載器。爬取頁面,根據頁面的鏈接,破解另外一個網站,下載種子文件。項目比較簡單,爬取頁面沒有使用任何爬蟲框架。項目源碼
Node.js
的安裝請看我的另外一篇文章,Node.js 的多版本安裝。
項目初始化
新建一個文件夾 FBIWarning
,在該文件夾下打開命令行 CMD
或者 git bash
。運行 npm init -y
,該文件夾會生成一個 package.json
文件。
安裝依賴包
安裝依賴包 cnpm install --save cheerio iconv-lite request socks5-http-client
。每個依賴包的功能如下:
cheerio
// 解析 DOMiconv-lite
// 解決中文亂碼的問題request
// http 請求,圖片和種子的下載socks5-http-client
// socks 代理
爬取網頁策略
網頁之間,是靠鏈接聯繫在一起的,符合數據結構裡面的圖狀結構。所以,對應有如下兩種爬取策略。
- 爬取所有列表頁面的鏈接後,再去爬取所有詳情頁面,對應圖演算法的廣度優先遍歷。
- 爬取一部分列表頁面,就去爬取詳情頁面。然後再去爬取列表頁面,爬取詳情頁面,迴圈進行,對應圖演算法的深度優先遍歷。
因為是國外網站,網路可能隨時斷開,所以採用第二種策略比較好。同時,也能很快得到種子文件。為了防止重覆爬取頁面,可以將爬取頁面的鏈接作為索引。
請求代理
網站是國外網站,需要使用梯子,否則不能爬取。代理傳送門。socks5-http-client
配合 reqeust
使用,可以解決代理的問題。但是,該代理只支持 socks
代理, http(s)
代理暫不支持。
解決中文亂碼的問題
目標網站的頁面編碼是 gbk
,而 request
依賴包的預設編碼是 UTF-8
,使用預設編碼解碼方式,會導致頁面的中文變成亂碼。所以得到返回數據前,去掉預設編碼,就是設置編碼為 encoding: null
,然後使用 iconv-lite
使用 gbk
方式解碼,這樣就可以解決中文編碼為亂碼的問題,代碼如下:
const request = require("request")
// 解析 dom
const cheerio = require("cheerio")
// 中文編碼
const iconv = require("iconv-lite")
// 代理
const Agent = require("socks5-http-client/lib/Agent")
const COMMON_CONFIG = require("./config")
/**
* 請求頁面
* @param {String} requestUrl 請求頁面
*/
function requestPage(requestUrl) {
try {
return new Promise((resolve, reject) => {
if (!requestUrl) {
resolve(false)
}
request.get(
{
url: requestUrl,
agentClass: Agent,
agentOptions: {
socksPort: 13838, // 代理埠
socksHost: "127.0.0.1" // 代理 Host
},
headers: {
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36"
},
// 去掉預設 utf-8 解碼,否則解碼會亂碼
encoding: null
},
function(err, response, body) {
// 防止解析報錯
try {
// 統一解決中文亂碼的問題
let content = iconv.decode(body, "gbk")
let $ = cheerio.load(content)
resolve($, err, response, body, content)
} catch (error) {
resolve(null)
}
}
)
})
} catch (error) {
//如果連續發出多個請求,即使某個請求失敗,也不影響後面的其他請求
Promise.resolve(null)
}
}
併發請求
分頁請求有很多個,可以使用遞歸來一個一個請求,但是寫法不太好看。所以,可以使用 ES7+
裡面的 async
函數,將同步過程變為非同步過程。async
要配合 await
使用,就可以將同步過程變為非同步過程。詳細瞭解 async
請看阮一峰 ES async
async function innerRecursion() {
for (let i = 1; i <= 100; i++) {
let requestUrl = "http://www.baidu.com?page=" + i // 事例網站,非爬取網站
let result = await this.requestPage(url)
}
}
一個一個請求比較慢,可以使用 Promise.all
實現併發請求。當然,也可以使用 async
模塊 提高下載的併發量,有需要的可以自己去瞭解。這個 async
模塊並非上面的 async
函數。
function innerRecursion() {
let requestUrls = []
for (let i = 1; i <= 100; i++) {
let requestUrl = "http://www.baidu.com?page=" + i // 事例網站,非爬取網站
requestUrls.push(requestUrl)
}
let promises = requestUrls.map(url => this.requestPage(url))
Promise.all(promises)
.then(results => {
// results 是一個數組,對應上面每個請求的結果
})
.catch(error => {
// 捕獲請求中可能發生的錯誤
console.log(error)
})
}
圖片下載
圖片的下載非常簡單,代碼如下:
/**
* 下載文件
* @param {String} url 請求鏈接
* @param {String} filePath 文件路徑
*/
function downloadFile(url, filePath) {
// try...catch 防止一個請求出錯,導致程式終止 種子的下載相同
try {
if (!url || !filePath) {
return false
}
request
.get({
url,
agentClass: Agent,
agentOptions: {
socksPort: 13838, // 代理埠
socksHost: "127.0.0.1" // 代理 Host
},
headers: {
headers: {
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36"
}
}
})
.pipe(fs.createWriteStream(filePath))
} catch (error) {
console.log(error)
}
}
破解網站種子下載
解析詳情頁面,只能得到類似 http://www.jandown.com?ref=VENU794
的鏈接,需要破解該網站的種子下載。查看網站的種子下載方式,就是一個 post
請求,後端就會返回種子文件。剛開始的時候,不熟悉服務端的表單提交方式,導致文件一直得不到,後來詳細查看了 request
的官文文檔,發現是自己寫錯了。結合上面的圖片下載,種子的下載方式自然就有了,代碼如下:
/**
* 下載種子鏈接
* @param {String} childDir // 子目錄
* @param {String} downloadUrl // 下載種子地址
*/
function downloadTorrent(childDir, downloadUrl) {
try {
// 解析出鏈接的 code 值
let code = querystring.parse(downloadUrl.split("?").pop()).ref
if (!code || !childDir) {
return false
}
// 發出 post 請求,然後接受文件即可
request
.post({
url: "http://www.jandown.com/fetch.php",
agentClass: Agent,
agentOptions: {
socksPort: 13838, // 代理埠
socksHost: "127.0.0.1" // 代理 Host
},
headers: {
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36"
},
formData: {
code
}
})
.pipe(fs.createWriteStream(childDir + "/" + code + ".torrent"))
} catch (error) {
console.log(error)
}
}
面向對象
剛開始是使用面向過程的方式寫的,後來發現代碼太重覆了,所以採用 OOP
改寫了整個代碼。詳細瞭解 javaScript Class
請看阮一峰 ES class
總結
- 學習中文編碼為亂碼的解決方法
- 學習了
request
的代理以及文件下載功能 - 破解種子網站的種子下載功能
- js 面向對象開發
- 爬蟲併發量解決
感謝閱讀!