配置和运行 DHT 爬虫实战记录

2026-01-24 57 浏览 0 评论

在前面的文章中已经完成了运行环境的搭建,Node.js、依赖库和网络权限均已准备就绪。本文将继续深入,记录如何配置并运行一个完整的 DHT 爬虫系统 ,实现从全球 DHT 网络嗅探活跃资源,并提取 BT 种子的 metadata 信息,最终生成磁力链接并存入数据库。

需要特别说明的是: DHT 网络中包含大量未知来源的资源,其中可能存在敏感、侵权或非法内容。 请勿将爬取的数据公开分享或传播 ,否则责任自负。本文仅用于技术研究与分布式网络原理学习。


p2pspider 简介

p2pspider 是一个将 DHT 网络爬虫BT 客户端 融合的工具。 它的核心原理是:

  1. 加入 DHT 网络并维护节点池
  2. 监听网络中广播的 infohash
  3. 主动向远端 Peer 请求 metadata
  4. 解析种子信息并触发回调

最终我们得到完整的 torrent metadata,就可以计算文件列表、大小、类型,并生成磁力链接。

该项目本身非常轻量,但足够稳定,适合二次开发。

源码地址:

https://github.com/BruceDone/p2pspider

安装方式:

git clone https://github.com/Fuck-You-GFW/p2pspider

使用前请确保 Node.js >= 0.12.0(如果你使用的是现代 Node 版本则完全无压力)。


最基础运行配置

'use strict';

var P2PSpider = require('../lib');

var p2p = P2PSpider({
    nodesMaxSize: 200,   // DHT 节点池上限
    maxConnections: 400, // 最大并发连接数
    timeout: 5000        // 连接超时
});

p2p.ignore(function (infohash, rinfo, callback) {
    // false => 即使已存在也继续下载 metadata
    var theInfohashIsExistsInDatabase = false;
    callback(theInfohashIsExistsInDatabase);
});

p2p.on('metadata', function (metadata) {
    // 在这里处理并保存 metadata
    console.log(metadata);
});

p2p.listen(6881, '0.0.0.0');

这段代码已经可以加入 DHT 网络并开始抓取 metadata。 但仍然缺少几个关键模块:

  • 种子去重
  • 数据库存储
  • 种子热度统计
  • 无效种子过滤

接下来逐步改造。


ignore 与 metadata 的协作逻辑

p2p.ignore() 决定是否跳过某个 infohash。 如果 callback 返回 true ,则不会触发 metadata 事件。

因此逻辑应当是:

  • 若 hash 已存在数据库 → 忽略并顺便更新热度
  • 若 hash 不存在 → 下载 metadata 并入库

这正是 DHT 爬虫高效运行的关键。


连接数据库

这里使用 ali-rds 作为 MySQL 驱动:

npm i ali-rds -s

连接配置如下:

let db2 = new rds({
host: '45.6.218.6',
port: 3306,
user: 'root',
password: 'kxi?44J1',
database: 'p2pspider',
charset: 'utf8mb4',
});

utf8mb4 是必须的,因为种子文件名经常包含 emoji 或多语言字符。


hash 总表(去重核心)

所有见过的 infohash(无论是否成功入库)都记录在此表中。 只要 hash 存在,就不再重复请求 metadata,从而极大减少网络负载。

CREATE TABLE `hash` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`hash` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `hash` (`hash`) USING BTREE
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;

查询 hash 是否存在:

async function hashExists(infohash) {
const sql = `select count(*) count from hash where hash = '${infohash}'`;
const val = await db2.query(sql);
return !!val[0].count;
}

种子热度统计

如果某个 hash 被多次广播,说明该资源仍然活跃。 因此顺便累加下载次数并刷新更新时间:

async function updateHeat(infohash) {
try {
await db.query(`update torrent set download_count = download_count + 1,
update_date = '${date('Y-m-d H:i:s')}' where hash = '${infohash}'`);
} catch (err) {}
}

改造 ignore 逻辑

p2p.ignore(async (infohash, rinfo, callback) => {
const isExists = await hashExists(infohash);
if (isExists) updateHeat(infohash);
callback(isExists);
});

这里有个细节:

  • hashExists 必须 await,因为要等结果决定是否忽略
  • updateHeat 不需要 await,即使失败也不影响主流程

这样可以保证爬虫高并发下仍然保持吞吐。


收到 metadata 后的处理流程

当触发 p2p.on('metadata') 后,就进入真正的数据处理阶段。

主要工作包括:

  • 计算种子总大小
  • 提取文件列表
  • 判断资源类型
  • 过滤异常种子
  • 最终入库

下面逐步拆解。


计算种子总大小 length

let length = 0;
if (metadata.info.files) {
for (let i = 0; i < metadata.info.files.length; i++) {
length += metadata.info.files[i].length;
}
} else {
length = metadata.info.length;
}
length = length || 0;

BT 种子分为单文件模式和多文件模式,因此需要分别处理。


计算文件列表

const files = [];
if ('files' in metadata.info) {
// 多文件
for (let i = 0; i < metadata.info.files.length; i++) {
const name =
typeof metadata.info.files[i].path === 'string'
? metadata.info.files[i].path
: metadata.info.files[i].path.join('/');
if (/\.url$/.test(name.toString())) continue;
if (/\.mht$/.test(name.toString())) continue;
if (/如果您看到此文件/.test(name.toString())) continue;
if (/padding_file/.test(name.toString())) continue;
if (/readme\.txt/i.test(name.toString())) continue;

let fids = require('./iga.json');
if (new RegExp(fids.join('|')).test(name)) continue;

// 最多 50 个文件
if (files.length > 50) break;
files.push({
name: name.toString(),
length: metadata.info.files[i].length,
});
}
} else {
// 单文件
files.push({
name: metadata.info.name.toString(),
length: metadata.info.length,
});
}

这里顺便过滤掉:

  • 填充文件
  • 广告 readme
  • padding_file
  • 乱码标记文件

实际运行中,这一步能有效减少无意义种子。

仍有极少部分种子文件名乱码问题,这是 BT 协议中编码历史遗留问题,目前仍是业内难点。


计算资源类型

根据最大文件后缀判断类型:

files.sort((a, b) => b.length - a.length);
let type = 7;
for (let key in fileType) {
const ft = fileType[key].values.split(',');
for (let i = 0; i < ft.length; i++) {
if (new RegExp(ft[i] + '$', 'i').test(files[0].name)) {
type = fileType[key].type;
break;
}
}
if (type != 7) break;
}

类型字典如下:

// 类型 1 音频 2 视频 3 图片 4 文档 5 电子书 6 应用 7 其他
module.exports = {
ebook: {
type: 5,
values: 'EXE,TXT,HTML,HLP,CHM,LIT,PDF,WDL,CEB,ABM,PDG,UMD,JAR,TXT,EPUB,CAJ,UBD,WMLC,PDB,BRM,MOBI,AZW3',
},
audio: {
type: 1,
values: 'MP3,WAV,WMA,MP2,Flac,MIDI,RA,APE,AAC,CDA,MOV',
},
video: {
type: 2,
values: 'AVI,MP4,DAT,DVR,VCD,MOV,SVCD,VOB,DVD,DVTR,DVR,BBC,EVD,FLV,RMVB,WMV,MKV,3GP,MPG',
},
doc: {
type: 4,
values: 'DOC,XLS,PPT,DOCX,XLSX,PPTX',
},
image: {
type: 3,
values: 'JPG,PNG,PDF,TIFF,JPEG',
},
app: {
type: 6,
values: '.com,.exe,.bat,.txt,.htm,.obj,.sys,.bak,.dat,.bas,.pas,.c,.tmp,.ovl,.asm,.prg,.cpp,.cob,.img',
},
};

这样后期检索和分类都会非常高效。


获取种子名称

const name =
(metadata.info['name.utf-8'] ? metadata.info['name.utf-8'].toString() : '') ||
(metadata.info.name ? metadata.info.name.toString() : '');

优先使用 UTF-8 字段,兼容老版本编码。


过滤异常种子

// 包含多个空格
const name2 = name.replace(/ /g, '');
const sm = name.length - 12 < name2.length;
// 未下载完成的文件做种子
const un = !/\.bc!$/gi.test(name);
// TS 碎片文件
const sp = !/^[0-9]+?\.ts$/gi.test(name);
// 未下载完成
const ungod = !/emule\.td/gi.test(name);
const unerd = !/emule\.xltd/gi.test(name);
const unedv = !/bt\.xltd/gi.test(name);
const uneas = !/bt\.td|\.vgtt$/gi.test(name);
// TS 小文件
const c = !/([0-9a-z]+?)\.ts$/gi.test(name);

这一段过滤规则来自长期实战积累。 它能剔除:

  • 未完成下载的临时文件
  • 某些客户端残留碎片
  • 明显异常命名

能显著提高数据库干净度。


种子表结构

CREATE TABLE `torrent` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`hash` varchar(100) NOT NULL,
`name` varchar(255) DEFAULT NULL,
`length` bigint(24) NOT NULL DEFAULT '0',
`file_count` int(11) NOT NULL DEFAULT '1',
`download_count` int(11) NOT NULL DEFAULT '1',
`files` text,
`type` int(11) NOT NULL DEFAULT '6' COMMENT '类型 1 音频 2 视频 3 图片 4 文档 5 电子书 6 应用 7 其他',
`add_date` datetime NOT NULL COMMENT '添加时间',
`update_date` datetime DEFAULT NULL COMMENT '更新时间,一般是有下载的时候',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `hash` (`hash`) USING BTREE,
KEY `add_date` (`add_date`) USING BTREE,
KEY `update_date` (`update_date`) USING BTREE
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;

该表同时支持:

  • 按时间排序
  • 按热度排序
  • 按类型筛选

适合后续构建搜索系统。


写入数据库

let isSuccess = false;
if (name && sm && un && sp && ungod && unerd && unedv && uneas && c) {
const params = {
hash: metadata.infohash,
name,
length,
file_count: files.length,
download_count: 0,
files: JSON.stringify(files),
type,
add_date: new Date(),
};

// 繁体转换
params.name = fantiConvert(0, params.name);
params.files = fantiConvert(0, params.files);

try {
await db.insert('torrent', params);
isSuccess = true;
} catch (err) {}
} else {
isSuccess = true;
}

// 插入 hash 表
if (isSuccess) db2.insert('hash', { hash: metadata.infohash });

这里还有一个细节:

  • 即便某些种子被过滤掉
  • 仍然会写入 hash 表

这是为了防止未来重复抓取同一个无效 hash,从而节省网络资源。

另外加入 繁体转简体 ,是为了后续中文搜索体验一致。


最终效果

运行后,系统将持续:

  • 自动加入全球 DHT 网络
  • 实时捕获活跃 infohash
  • 下载 metadata
  • 分类、过滤、入库
  • 更新活跃热度

整套流程完全无人值守,可长期稳定运行。

当数据积累到一定规模后,即可搭建:

  • 磁力搜索
  • 资源热度排行
  • 分类索引站点

这也是许多知名磁力搜索引擎的基础原理。


总结

DHT 爬虫本质上是:

分布式网络监听 + 高并发 I/O + 数据清洗入库

p2pspider 提供了极好的基础能力,而真正决定系统质量的,是后续的:

  • 去重策略
  • 过滤规则
  • 数据结构设计
  • 热度维护逻辑

当这些细节全部打磨完成,一个稳定、高效、干净的数据源就自然诞生了。


发布评论

发布评论前请先 登录

评论列表 0

暂无评论