
简介这份资源面向PHP开发者与对P2P文件分享感兴趣的技术人员提供一套将BT种子文件转换为磁力链接的完整实现方案帮助解决种子文件依赖中心服务器、分享不便的问题。压缩包共5个文件包含4个PHP源码与1个Markdown说明文档整体仅3KB体积轻巧便于快速阅读与二次开发。其中PHP文件分别承担种子解析、Bencode编解码与转换入口等职责说明文档则梳理整体思路与使用方式。核心逻辑围绕解析.torrent文件的info部分、计算SHA1得到Info Hash、按magnet:?xturn: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 或 NodeBT 种子的解析核心是 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.4Web 服务器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比如trudp://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.iniupload_max_filesize 20M post_max_size 20M max_execution_time 60改完后重启 Web 服务器或 PHP-FPM。如果用的是共享主机可能无法修改php.ini可以在项目根目录加.htaccessApache或.user.iniPHP-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 的小种子做冒烟测试确认基本流程通了再拿大种子和异常种子跑边界。这个习惯帮我省了不少调试时间。希望帮到你。本文还有配套的精品资源点击获取