庆祝 2018 国庆,制作了一个 Node.js 的种子下载器。爬取页面,根据页面的链接,破解另外一个网站,下载种子文件,同时使用 async 模块提高爬虫的并发量。项目比较简单,爬取页面没有使用任何爬虫框架。
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 代理async// 提高下载并发量
请求代理
网站是国外网站,需要使用梯子,否则不能爬取。代理传送门。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\")
request.get(
{
url: requestUrl,
agentClass: Agent,
agentOptions: COMMON_CONFIG.socks,
headers: {
\"User-Agent\": COMMON_CONFIG.userAgent
},
// 去掉默认 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) {
console.log(error)
//如果连续发出多个请求,即使某个请求失败,也不影响后面的其他请求
resolve(null)
}
}
)
解析页面
爬取页面后,之后就是解析页面中的 DOM 元素,得到自己想要的数据。
解析分类页面
解析分类,重要的字段就是 和 theme,分别代表该分类的入口页面,以及该分类下总共有多少贴子,根据该字段可以判断网站数据是否更新了。具体就是 cheerio 依赖包的使用,简单理解,该包就是 Node.js 端的 jQuery。
如 $("#cate_3 tr") 就是获取 id 为 cate_3 下面的所有 tr 标签,该网站比较古老,页面布局是 table 布局,解析 DOM 非常简单。
cheerio 详细说明请看官方说明。
// $ 就是 request 请求后,解码后的数据
parseHtml($) {
// 获取 DOM 的主题内容
let categoryDom = $(\"#cate_3 tr\")
categoryDom.each(function() {
let Dom = $(this)
.find(\"h3\")
.eq(0)
.find(\"a\")
// path. name 去掉链接中无用的字符
let = path. name( Dom.attr(\"href\") || \"\")
let = Dom.text() || \"分类名为空\"
let theme = ~~$(this)
.find(\"td\")
.eq(1)
.find(\"span\")
.text()
let article = ~~$(this)
.find(\"td\")
.eq(2)
.find(\"span\")
.text()
if ( && ) {
let temp = {
, // 链接
, // 标题
theme, // 主题 ,即总的列表数量
article, // 文章
endPage: ~~(theme / COMMON_CONFIG.pageSize)
}
categoryList[ ] = temp
}
})
}
下载并发量
解析列表页面与分类页面类似,不过列表页面有很多页面,需要不断的请求新的页面。为了提高下载的并发量,使用 async 模块,使用了其官方提供的例子,提高了下载的并发量。内部的递归调用,是为了防止内存爆栈。
recursionExecutive() {
let category s = .keys(categoryList)
if (this.categoryIndex >= category s.length) {
return false
}
let currentCategory = category s[this.categoryIndex]
this.jsonPath =
COMMON_CONFIG.tableList + \"/\" + currentCategory.split(\"?\").pop() + \".json\"
tableList = this.readJsonFile(this.jsonPath)
let table s = []
if (!tableList) {
if (parseAllCategory) {
this.categoryIndex++
this.recursionExecutive()
}
return false
}
let category = categoryList[currentCategory].
let parentDir = COMMON_CONFIG.result + \"/\" + category
this.generateDirectory(parentDir)
table s = .keys(tableList)
let totalLength = table s.length
try {
let requestUrls = table s.map(url => COMMON_CONFIG. Url + url)
let step = COMMON_CONFIG.maxDetail s
/**
* 递归请求,防止返回的数据太多,爆栈
* 这是因为 async 返回的结果,都会放在 results 数组中
* 而且每个返回的结果,是一个 html 文件,当返回的文件非常多时,内存占满了
* 使用递归可以解决这个问题
* 列表页面的递归请求是一样的
*/
// 重新赋值 this 否则递归函数找不到 this
let _this = this
function innerRecursion(arrayIndex) {
log(\"内部递归调用\")
let urls = requestUrls.slice(arrayIndex * step, (arrayIndex + 1) * step)
async.mapLimit(
urls,
COMMON_CONFIG.connectTasks,
async url => {
return await _this.requestPage(url)
},
(err, results) => {
if (!err) {
for (let i = 0; i < results.length; i++) {
let seed = table s[i + arrayIndex * step]
let result = results[i]
let directory = parentDir + \"/\" + seed.replace(///gi, \"_\")
if (result) {
_this.parseHtml(result, seed, directory)
}
}
}
if (urls.length < step) {
_this.categoryIndex++
_this.recursionExecutive()
} else {
innerRecursion(arrayIndex + 1)
}
}
)
}
innerRecursion(0)
} catch (error) {
this.requestFailure()
}
}
解析列表页面
重要字段是 endPage 和 字段,分页代表总页数和页面详情链接。 DOM 解析如下:
parseHtml($, currentCategory) {
let tableDom = $(\"#ajaxtable tr\")
tableDom.each(function() {
// 获取列表页分页的总页数
let endPage = ~~$(\"#main .pages\")
.eq(0)
.text()
.trim()
.match(/((.+?))/g)[0]
.slice(1, -1)
.split(\"/\")
.pop()
categoryList[currentCategory].endPage = endPage
// 详情页面链接
let = $(this)
.find(\"h3\")
.eq(0)
.find(\"a\")
.attr(\"href\")
let tdDom = $(this).find(\"td\")
let createTime = tdDom
.eq(5)
.find(\"a\")
.text()
.trim()
if ( ) {
let temp = {
reply: ~~tdDom.eq(3).text(), // 回复
popular: ~~tdDom.eq(4).text(), // 人气
createTime, // 创建时间
images: [], // 图片
torrents: [] // 种子
}
if (tableList[ ]) {
tableList[ ] = .assign(tableList[ ], temp)
} else {
tableList[ ] = temp
}
}
})
this.updateCategoryList()
this.updateTableList()
}
解析每个详情页面
详情页面重要的字段是 、 torrents 和 images ,分别对应详情页的标题,种子的链接以及图片的链接。DOM 解析如下:
parseHtml($, seed, directory) {
let torrents = []
/**
* 获取页面上的每一个图片链接和地址链接
* 不放过任何一个图片和种子,是不是很贴心!!
*/
$(\"body a\").each(function() {
let href = $(this).attr(\"href\")
if (href && COMMON_CONFIG.seedSite.some(item => href.includes(item))) {
torrents.push(href)
}
})
torrents = [...new Set(torrents)]
let images = []
$(\"body img\").each(function() {
let src = $(this).attr(\"src\")
if (src) {
images.push(src)
}
})
images = [...new Set(images)]
// 字段非空,可以在下次不用爬取该页面,直接下载种子文件
let =
$(\"#td_tpc h1\")
.eq(0)
.text() || \"已经爬取了\"
if (this.isEmpty(tableList[seed])) {
tableList[seed] = temp
} else {
tableList[seed]. =
tableList[seed].torrents = [...tableList[seed].torrents, torrents]
tableList[seed].images = [...tableList[seed].images, images]
}
this.updateTableList()
this.downloadResult(directory, torrents, images)
}
图片的下载非常简单,代码如下:
/**
* 下载文件
* @param {String} url 请求链接
* @param {String} filePath 文件路径
*/
downloadFile(url, filePath) {
if (!url || !filePath) {
return false
}
request
.get({
url,
agentClass: Agent,
agentOptions: COMMON_CONFIG.socks,
headers: {
headers: {
\"User-Agent\": COMMON_CONFIG.userAgent
}
}
})
.pipe(fs.createWriteStream(filePath))
}
但是种子得到的只是一个链接,需要破解该网站的种子下载。查看网站的种子下载方式,就是一个 post 请求,后台就会返会种子文件。刚开始的时候,不熟悉服务端的表单提交方式,导致文件一直得不到,后来详细查看了 request 的官文文档,发现是自己写错了。结合上面的图片下载,种子的下载方式自然就有了,代码如下:
/**
* 下载种子链接
* @param {String} childDir // 子目录
* @param {String} downloadUrl // 下载种子地址
*/
downloadTorrent(childDir, downloadUrl) {
// 解析出链接的 code 值
let code = querystring.parse(downloadUrl.split(\"?\").pop()).ref
if (!code || !childDir) {
return false
}
// 发出 post 请求,然后接受文件即可
request
.post({
url: COMMON_CONFIG.torrent,
agentClass: Agent,
agentOptions: COMMON_CONFIG.socks,
headers: {
\"User-Agent\": COMMON_CONFIG.userAgent
},
formData: {
code
}
})
.pipe(fs.createWriteStream(childDir + \"/\" + code + \".torrent\"))
}
数据库
本应用没有使用数据库,而是使用 json 文件存储结果,是为了省去 sql 配置(楼主懒)。个人喜好添加数据库,https://github.com/sindresorhus/awesome-nodejs 可以找到你需要的 Node.js 包。
面向对象
刚开始是使用面向过程的方式写的,后来发现代码太重复了,所以采用 OOP 改写了整个代码。详细了解请看阮一峰 ES6 class
总结
- 学习中文编码为乱码的解决方法
- 学习了
request的代理以及文件下载功能 - 破解种子网站的种子下载功能
- js 面向对象开发
- 爬虫并发量解决方法
感谢阅读!
继续阅读与本文标签相同的文章
js实现当鼠标滑入缩略图时,banner图片切换
智能撰写和离线模式已登录Gmail
-
数十万共享雨伞不翼而飞,创始人却高兴的要命!网友:赚翻了
2026-05-18栏目: 教程
-
滴滴 这是一见钟情的感脚
2026-05-18栏目: 教程
-
以实践的方式讨论:N-Gram原理与其应用
2026-05-18栏目: 教程
-
Hi拼团,第六代云服务器拼团购买更便宜,低至148元/年
2026-05-18栏目: 教程
-
汇编(五)栈、CPU提供的栈机制、push、pop指令
2026-05-18栏目: 教程
