PHP实现BT种子转磁力链接:bencode解码与SHA1哈希实战

发布时间:2026/10/23 17:02:22
PHP实现BT种子转磁力链接:bencode解码与SHA1哈希实战

简介:这份资源面向PHP开发者与对P2P文件分享感兴趣的技术人员,提供一套将BT种子文件转换为磁力链接的完整实现方案,帮助解决种子文件依赖中心服务器、分享不便的问题。压缩包共5个文件,包含4个PHP源码与1个Markdown说明文档,整体仅3KB,体积轻巧便于快速阅读与二次开发。其中PHP文件分别承担种子解析、Bencode编解码与转换入口等职责,说明文档则梳理整体思路与使用方式。核心逻辑围绕解析.torrent文件的info部分、计算SHA1得到Info Hash、按magnet:?xt=urn:btih格式拼接磁力链接,并处理Tracker URL等关键环节展开,读者可据此理解DHT与磁力链接的定位原理。目前已有4098人学习下载,适合希望掌握种子转磁力实现细节、为分布式存储或文件分享功能做技术储备的开发者参考。

1. 从种子文件到磁力链接:一个 PHP 小工具的真实使用场景

手里攒了一堆.torrent种子文件,想分享给别人的时候才发现——直接发文件太麻烦,对方还得下载、导入、再校验。这时候磁力链接的优势就出来了:一串字符,复制粘贴就能用,不依赖中心服务器,也不怕种子文件丢失。但问题是,手头这些种子怎么批量转成磁力链接?总不能一个个手动算哈希吧。

这个基于 PHP 实现的 BT 种子转磁力链接工具,解决的就是这个场景。它不需要数据库,不需要复杂环境,上传或指定一个.torrent文件,解析出里面的 info 字典,做一次 SHA1 哈希,拼上 tracker 列表,就能输出标准的磁力链接。适合谁用?手里有种子文件需要批量转换的普通用户,以及想研究 BT 协议里 bencode 编码和磁力链接生成逻辑的 PHP 开发者。整个项目就是一个轻量的 Web 工具,扔到任意支持 PHP 的环境里就能跑。

2. 项目结构与技术选型:为什么用 PHP 做种子解析

2.1 目录结构一览

拿到源码包后,解压看到的目录结构大致是这样的:

bt2magnet/ ├── index.php # 入口文件,处理上传和展示 ├── parse.php # 核心解析类,负责 bencode 解码和磁力链接生成 ├── uploads/ # 临时存放上传的种子文件 ├── assets/ │ ├── style.css # 页面样式 │ └── script.js # 前端交互(复制磁力链接等) └── README.md # 简单说明

这个结构很直白,没有引入 Composer 依赖,也没有用框架。入口文件负责接收上传的.torrent文件,调用parse.php里的解析类,把生成的磁力链接返回给前端展示。uploads/目录用来暂存文件,转换完成后可以清理掉。

2.2 为什么选 PHP 而不是 Python 或 Node

BT 种子的解析核心是 bencode 解码。bencode 是一种简单的编码格式,只有四种类型:字符串、整数、列表、字典。用 PHP 实现完全够用,而且 PHP 的字符串处理函数(substr、strpos)配合递归,写起来很直接。

更关键的是部署成本。PHP 环境几乎无处不在,虚拟主机、共享空间、甚至路由器上都能跑。用户不需要装 Python 解释器,也不需要 Node 运行时,上传一个文件夹就能用。对于这种「用完即走」的小工具,PHP 的零依赖部署优势很明显。

常见做法是直接用 PHP 的hash_file('sha1', ...)或者sha1()函数处理二进制数据。但要注意,磁力链接里的哈希是对种子文件里 info 字典的原始 bencode 编码做 SHA1,不是对整个文件做哈希。所以不能直接对文件算哈希,必须先解析出 info 字典的字节范围。

2.3 核心依赖:没有第三方库,纯手写 bencode 解码

项目没有引入任何第三方库,bencode 解码器是手写的。这既是优点也是缺点:优点是零依赖,缺点是如果 bencode 数据有异常(比如嵌套过深、格式错误),需要自己处理边界情况。

一个合格的 bencode 解码器需要处理:

  • 字符串:<长度>:<内容>,比如4:spam表示字符串 "spam"
  • 整数:i<数字>e,比如i42e表示整数 42
  • 列表:l<元素>e,比如l4:spami42ee表示 ["spam", 42]
  • 字典:d<键值对>e,键必须是字符串且按字典序排列

解析种子文件时,最关键的是找到info键对应的值在原始字节流中的起始和结束位置。因为磁力链接的哈希是对这段原始字节做 SHA1,而不是对解码后的数据结构重新编码。重新编码可能因为键顺序、整数表示方式等细节导致哈希不一致。

3. 核心代码拆解:bencode 解码与磁力链接生成

3.1 bencode 解码器的实现

先看parse.php里 bencode 解码的核心逻辑。这里用一个类来封装,维护一个位置指针,递归解析:

<?php class BencodeParser { private $data; private $pos; public function __construct($data) { $this->data = $data; $this->pos = 0; } // 解析入口,根据当前字符判断类型 public function decode() { $char = $this->data[$this->pos]; if ($char === 'i') { return $this->decodeInt(); } elseif ($char === 'l') { return $this->decodeList(); } elseif ($char === 'd') { return $this->decodeDict(); } elseif (ctype_digit($char)) { return $this->decodeString(); } else { throw new Exception("Invalid bencode at position {$this->pos}"); } } // 解析整数:i<数字>e private function decodeInt() { $this->pos++; // 跳过 'i' $end = strpos($this->data, 'e', $this->pos); if ($end === false) throw new Exception("Unterminated integer"); $num = substr($this->data, $this->pos, $end - $this->pos); $this->pos = $end + 1; return (int)$num; } // 解析字符串:<长度>:<内容> private function decodeString() { $colon = strpos($this->data, ':', $this->pos); if ($colon === false) throw new Exception("Missing colon"); $len = (int)substr($this->data, $this->pos, $colon - $this->pos); $this->pos = $colon + 1; $str = substr($this->data, $this->pos, $len); $this->pos += $len; return $str; } // 解析列表:l<元素>e private function decodeList() { $this->pos++; // 跳过 'l' $list = []; while ($this->data[$this->pos] !== 'e') { $list[] = $this->decode(); } $this->pos++; // 跳过 'e' return $list; } // 解析字典:d<键值对>e private function decodeDict() { $this->pos++; // 跳过 'd' $dict = []; while ($this->data[$this->pos] !== 'e') { $key = $this->decodeString(); $value = $this->decode(); $dict[$key] = $value; } $this->pos++; // 跳过 'e' return $dict; } }

这段代码的逻辑很清晰:decode()是分发器,根据当前字符决定调用哪个子解析方法。decodeInt()找到e结束符,把中间的数字转成整数。decodeString()先读长度,再读指定长度的内容。decodeList()和decodeDict()递归调用decode()处理嵌套结构。

参数说明:$this->data是完整的种子文件二进制内容,$this->pos是当前解析位置。每次解析完一个元素,$pos都会移动到下一个待解析字符的位置。这种基于位置指针的解析方式,比用正则或字符串分割更可靠,因为 bencode 是二进制安全的,字符串里可能包含任意字节。

3.2 提取 info 字典的原始字节

磁力链接的哈希计算需要 info 字典的原始 bencode 字节,而不是解码后的数组。所以需要在解析过程中记录 info 值的起始和结束位置:

// 在 decodeDict 中增加位置记录 private function decodeDict() { $this->pos++; // 跳过 'd' $dict = []; while ($this->data[$this->pos] !== 'e') { $keyStart = $this->pos; $key = $this->decodeString(); $valueStart = $this->pos; $value = $this->decode(); $valueEnd = $this->pos; $dict[$key] = $value; // 记录 info 键对应的原始字节范围 if ($key === 'info') { $this->infoStart = $valueStart; $this->infoEnd = $valueEnd; } } $this->pos++; // 跳过 'e' return $dict; } // 获取 info 字典的原始 bencode 字节 public function getInfoBytes() { if ($this->infoStart === null || $this->infoEnd === null) { throw new Exception("No info dict found"); } return substr($this->data, $this->infoStart, $this->infoEnd - $this->infoStart); }

这里的关键是$valueStart和$valueEnd。在解析 info 键的值之前,记录当前位置;解析完成后,再记录结束位置。两者之间的字节就是 info 字典的原始 bencode 编码。直接对这个字节串做 SHA1,就能得到磁力链接的哈希部分。

常见错误是直接对解码后的数组重新做 bencode 编码再算哈希。这样做在大多数情况下没问题,但如果原始种子里的 bencode 编码有非标准写法(比如整数有前导零、字典键顺序不标准),重新编码会改变字节序列,导致哈希不匹配。所以一定要用原始字节。

3.3 生成磁力链接

拿到 info 哈希后,拼装磁力链接就简单了:

function buildMagnetLink($infoHash, $torrentData) { // 解析出种子里的 tracker 列表和文件名 $parser = new BencodeParser($torrentData); $decoded = $parser->decode(); $params = ['xt' => 'urn:btih:' . $infoHash]; // 添加显示名称 if (isset($decoded['info']['name'])) { $params['dn'] = $decoded['info']['name']; } // 添加 tracker 列表 if (isset($decoded['announce'])) { $params['tr'] = $decoded['announce']; } if (isset($decoded['announce-list']) && is_array($decoded['announce-list'])) { foreach ($decoded['announce-list'] as $tier) { foreach ($tier as $tracker) { $params['tr'][] = $tracker; } } } // 拼装查询字符串,注意 tr 参数可能多个 $query = http_build_query($params, '', '&', PHP_QUERY_RFC3986); return 'magnet:?' . $query; }

参数说明:xt是 exact topic,固定为urn:btih:加 40 位十六进制哈希。dn是 display name,显示用文件名。tr是 tracker,可以出现多次。http_build_query会自动做 URL 编码,但要注意用PHP_QUERY_RFC3986模式,因为磁力链接里空格等字符需要编码成%20而不是+。

如果种子文件里没有announce或announce-list,生成的磁力链接就没有 tracker 参数。这种情况下,下载客户端会依赖 DHT 网络来寻找 peer,仍然可用,但初始连接速度可能慢一些。

4. 运行与部署:从上传到生成磁力链接的完整流程

4.1 环境要求与快速启动

这个项目对运行环境要求很低:

组件要求
PHP5.6 及以上,推荐 7.4+
Web 服务器Apache / Nginx / PHP 内置服务器
扩展无特殊扩展要求,hash和mbstring默认开启即可
磁盘上传目录需要写权限

用 PHP 内置服务器启动最快,在项目根目录执行:

php -S 0.0.0.0:8080 -t .

然后浏览器访问http://localhost:8080,就能看到上传页面。如果部署到 Apache 或 Nginx,把项目目录指向 web 根目录即可,不需要额外配置 rewrite 规则。

4.2 上传与转换流程

前端页面提供一个文件选择框,用户选中.torrent文件后提交。后端index.php的处理逻辑:

// index.php 核心处理 if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['torrent'])) { $file = $_FILES['torrent']; // 基本校验 if ($file['error'] !== UPLOAD_ERR_OK) { die('上传失败,错误码:' . $file['error']); } if ($file['size'] > 2 * 1024 * 1024) { die('种子文件过大,限制 2MB'); } // 读取二进制内容 $data = file_get_contents($file['tmp_name']); // 解析并生成磁力链接 try { $parser = new BencodeParser($data); $parser->decode(); $infoBytes = $parser->getInfoBytes(); $infoHash = sha1($infoBytes); $magnet = buildMagnetLink($infoHash, $data); // 输出结果 echo "<textarea readonly>{$magnet}</textarea>"; } catch (Exception $e) { echo "解析失败:" . $e->getMessage(); } }

这段代码做了几件事:检查上传错误码、限制文件大小、读取临时文件的二进制内容、调用解析器、计算哈希、生成磁力链接。注意file_get_contents读取的是原始二进制,不能用file()按行读,因为 bencode 数据里可能包含换行符。

4.3 批量转换的思路

如果要批量处理多个种子文件,可以改造成遍历上传目录:

$dir = __DIR__ . '/uploads/'; $files = glob($dir . '*.torrent'); foreach ($files as $file) { $data = file_get_contents($file); $parser = new BencodeParser($data); $parser->decode(); $infoHash = sha1($parser->getInfoBytes()); $magnet = buildMagnetLink($infoHash, $data); echo basename($file) . " => " . $magnet . "\n"; }

这个脚本可以放到命令行执行,也可以做成 Web 页面批量展示。注意glob返回的文件顺序不保证,如果需要按名称排序,加一个sort($files)。

提示:上传目录建议设置定期清理,避免种子文件堆积占用磁盘。可以在每次转换完成后删除临时文件,或者用 cron 定时清理超过一定时间的文件。

5. 避坑与常见问题:解析失败、哈希不匹配、中文乱码

5.1 解析时报 "Invalid bencode" 错误

现象:上传某些种子文件后,页面提示Invalid bencode at position X。

原因:种子文件不是标准的 bencode 格式,或者文件在传输过程中损坏。有些老种子可能包含非标准编码,比如字典键没有按字典序排列,或者字符串长度声明与实际内容不符。

解决:先用十六进制编辑器打开种子文件,检查开头是不是d开头、e结尾。如果是压缩包里的种子,确认解压完整。对于非标准种子,可以在解析器里放宽校验,比如不强制检查字典键顺序。但更稳妥的做法是提示用户文件格式异常,而不是强行解析。

5.2 生成的磁力链接下载时提示 "元数据获取失败"

现象:磁力链接能生成,但复制到下载客户端后一直卡在「获取元数据」阶段。

原因:种子文件里没有 tracker 信息,或者 tracker 已经失效。磁力链接只包含 info 哈希,下载客户端需要通过 DHT 网络或 tracker 来寻找持有该资源的 peer。如果种子本身没有 tracker,且 DHT 网络里没有其他 peer,就无法获取元数据。

解决:检查种子文件里是否有announce或announce-list。如果没有,生成的磁力链接只能依赖 DHT。可以手动在磁力链接后面追加公共 tracker,比如&tr=udp://tracker.example.com:6969。但要注意,tracker 地址需要是有效的,且不能违反当地网络使用规定。

5.3 中文文件名显示乱码

现象:磁力链接里的dn参数显示为乱码,或者下载后文件名变成问号。

原因:种子文件里的文件名可能是 GBK 编码,而磁力链接和现代下载客户端默认用 UTF-8。PHP 的http_build_query不会自动转码,直接把 GBK 字节当 UTF-8 处理就会乱码。

解决:在生成磁力链接前,检测文件名编码并转换:

$name = $decoded['info']['name']; if (!mb_check_encoding($name, 'UTF-8')) { $name = mb_convert_encoding($name, 'UTF-8', 'GBK'); } $params['dn'] = $name;

mb_check_encoding检查是否为合法 UTF-8,如果不是,用mb_convert_encoding从 GBK 转过来。常见做法是优先尝试 UTF-8,失败后再试 GBK 或 GB18030。

5.4 大文件上传失败

现象:上传几十 MB 的种子文件时,页面报错或空白。

原因:PHP 默认的upload_max_filesize和post_max_size通常是 2MB 或 8MB,超过限制的文件会被截断或拒绝。

解决:修改php.ini:

upload_max_filesize = 20M post_max_size = 20M max_execution_time = 60

改完后重启 Web 服务器或 PHP-FPM。如果用的是共享主机,可能无法修改php.ini,可以在项目根目录加.htaccess(Apache)或.user.ini(PHP-FPM)来覆盖部分配置。

5.5 哈希值与预期不一致

现象:生成的磁力链接哈希和某些工具算出来的不一样。

原因:最常见的是对 info 字典的字节范围提取错误。比如在解析字典时,$valueStart记录的是值的起始位置,但如果值本身是嵌套字典,decode()返回后$pos指向的是嵌套字典的结束符之后,而不是 info 值的结束位置。需要确保$valueEnd记录的是 info 值解析完成后的位置。

解决:在decodeDict里,解析完值之后立即记录$this->pos作为$valueEnd。不要等到循环下一轮再记录,否则会多包含一个字节。可以用一个简单的测试种子验证:手动用xxd查看 info 字典的字节范围,和代码提取的做对比。

6. 二次开发:从单文件工具到可扩展的种子解析服务

这个项目的代码量不大,但留出的扩展点不少。如果只是拿来用,上面几章已经够了。如果想改成更通用的种子解析服务,有几个方向可以动手。

第一个方向是支持更多输出格式。现在只输出磁力链接,但解析出来的 bencode 数据里其实包含了文件列表、文件大小、创建时间等信息。可以在parse.php里加一个方法,把解码后的数组转成 JSON 返回。这样前端就能展示种子里的文件树,用户不用下载种子就能看到内容。改造时注意二进制字符串的 JSON 编码问题,PHP 的json_encode遇到非 UTF-8 字节会返回 false,需要先做编码转换或 base64 处理。

第二个方向是加缓存。同一个种子文件重复转换时,没必要每次都重新解析。可以用文件哈希做键,把生成的磁力链接存到 SQLite 或文件缓存里。常见做法是用md5_file算种子文件的指纹,存到cache/目录下,下次直接读缓存。注意缓存要设过期时间,避免种子文件更新后读到旧结果。

第三个方向是做成 API。把index.php里的处理逻辑抽成一个独立的api.php,接收 POST 请求,返回 JSON 格式的结果。这样其他系统就能调用这个服务来批量转换种子。API 设计上,建议用POST /api/convert接收文件上传,返回{"magnet": "...", "info_hash": "...", "name": "..."}。错误时返回对应的 HTTP 状态码和错误信息。

第四个方向是处理异常种子的容错。现在的解析器遇到格式错误直接抛异常,整个流程中断。可以改成收集错误但继续解析,比如某个 tracker 地址格式不对就跳过,不影响磁力链接生成。具体做法是在decodeDict里对每个键值对做 try-catch,记录警告但继续循环。

验证改造是否成功,我一般会准备一组测试种子:标准的单文件种子、多文件种子、带中文文件名的种子、没有 tracker 的种子、以及一个故意损坏的种子。每次改完代码,把这几个种子都跑一遍,看输出是否符合预期。特别是哈希值,可以用已知正确的磁力链接做对比。

注意:处理用户上传的文件时,永远不要信任文件扩展名。有人可能把恶意文件改成.torrent上传。建议在读取内容后检查开头是否为d,并且限制解析深度,防止恶意构造的嵌套 bencode 导致递归过深、内存耗尽。

从那以后我每次改解析类,都会先用一个 1KB 的小种子做冒烟测试,确认基本流程通了,再拿大种子和异常种子跑边界。这个习惯帮我省了不少调试时间。希望帮到你。

本文还有配套的精品资源,点击获取