资讯详情

PHP实现BT种子转磁力链接:Bencode解析与info hash计算

📅 2026/10/23 16:18:25 | 华诺云谱 👁 阅读
PHP实现BT种子转磁力链接:Bencode解析与info hash计算
简介这份资源面向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 哈希拼出标准的magnet:?xturn:btih:...链接。整个项目不依赖数据库不需要额外扩展扔到任何支持 PHP 的环境里就能跑。适合谁手里有种子批量处理需求的开发者、想给资源站加个转换接口的后端、以及想搞懂 BT 协议里 info hash 到底怎么算出来的同学。下面从目录结构开始拆。2. 项目结构与核心解析逻辑种子文件里到底藏了什么2.1 目录长什么样拿到压缩包解压后典型的目录结构是这样的bt2magnet/ ├── index.php # 入口处理上传和输出 ├── BtParser.php # 核心解析类 ├── functions.php # 辅助函数 ├── static/ │ └── style.css # 简单样式 └── uploads/ # 临时存放上传的种子文件这个结构很轻没有框架没有 Composer 依赖。index.php负责接收文件、调用解析、返回结果BtParser.php是真正干活的地方functions.php里放了一些字符串处理和编码转换的工具函数。uploads/目录需要可写权限转换完成后临时文件会被删掉。我一般会把BtParser.php单独拎出来看因为整个项目的技术含量全在这个文件里。下面逐段拆。2.2 Bencode 解码BT 种子的编码格式BT 种子文件用的是 Bencode 编码规则很简单整数写成i数字e字符串写成长度:内容列表写成l元素e字典写成d键值对e。解析器要做的就是把这串字节流还原成 PHP 数组。?php // BtParser.php 中的 Bencode 解码核心 class BtParser { private $data; private $pos 0; public function __construct($filePath) { // 读取原始字节不能用 file_get_contents 的文本模式 $this-data file_get_contents($filePath); if ($this-data false) { throw new Exception(无法读取种子文件); } } // 解码入口根据首字符判断类型 public function decode() { $this-pos 0; return $this-decodeElement(); } private function decodeElement() { $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(); } throw new Exception(Bencode 格式错误位置 . $this-pos); } }这段代码的关键点在于$this-pos是一个游标每读一个字符就往前推。decodeElement()根据当前字符决定走哪个分支。整数以i开头、以e结尾列表以l开头、以e结尾字典以d开头、以e结尾字符串则是长度:内容的形式。参数说明$filePath是种子文件的绝对路径。注意必须用二进制安全的方式读取PHP 的file_get_contents默认就是二进制安全的但如果你用了fopen加fread要确保没有做编码转换。2.3 字典与字符串的递归解析字典和列表是嵌套结构必须递归处理。字典的键永远是字符串值可以是任意 Bencode 类型。private function decodeDict() { $this-pos; // 跳过 d $dict []; while ($this-data[$this-pos] ! e) { $key $this-decodeString(); // 键必须是字符串 $value $this-decodeElement(); // 值递归解析 $dict[$key] $value; } $this-pos; // 跳过 e return $dict; } private function decodeString() { $colonPos strpos($this-data, :, $this-pos); $length intval(substr($this-data, $this-pos, $colonPos - $this-pos)); $this-pos $colonPos 1; $str substr($this-data, $this-pos, $length); $this-pos $length; return $str; }decodeString()先找到冒号的位置冒号前面的数字就是字符串长度然后从冒号后面截取指定长度的字节。这里有个容易翻车的地方如果种子文件里包含中文文件名字符串长度是按字节算的不是按字符数。用substr截取字节是正确做法千万别用mb_substr。decodeDict()循环读取键值对直到遇到e结束。注意字典的键在 Bencode 规范里要求按字典序排列但解析时不需要校验顺序直接读就行。2.4 提取 info 字段并计算 SHA1磁力链接的核心是btih也就是 info 字段的 SHA1 哈希值。info 字段是种子字典里的一个子字典包含文件名、文件大小、分片信息等。要算哈希不能把 info 字典重新编码后再哈希必须从原始字节流里截取 info 字段对应的那一段原始字节。public function getInfoHash() { // 先完整解码拿到 info 字典的内容 $decoded $this-decode(); if (!isset($decoded[info])) { throw new Exception(种子文件缺少 info 字段); } // 关键从原始数据中定位 info 字段的字节范围 $infoStart strpos($this-data, 4:info) 6; // 跳过 4:info $infoEnd $this-findInfoEnd($infoStart); $infoBytes substr($this-data, $infoStart, $infoEnd - $infoStart); // 对原始字节做 SHA1 return sha1($infoBytes); } private function findInfoEnd($start) { // 从 info 起始位置开始按 Bencode 规则找到匹配的结束位置 $depth 0; $pos $start; $len strlen($this-data); while ($pos $len) { $char $this-data[$pos]; if ($char d || $char l) { $depth; } elseif ($char e) { $depth--; if ($depth 0) { return $pos 1; // 包含结束的 e } } elseif (ctype_digit($char)) { // 跳过字符串内容 $colon strpos($this-data, :, $pos); $strLen intval(substr($this-data, $pos, $colon - $pos)); $pos $colon $strLen; } $pos; } throw new Exception(无法定位 info 字段结束位置); }这段是整个项目最核心的部分。strpos($this-data, 4:info)找到 info 键的起始位置加 6 跳过4:info这六个字节就得到了 info 值的起始位置。findInfoEnd()用深度计数的方式找到匹配的结束位置遇到d或l深度加一遇到e深度减一深度归零时就找到了完整的 info 字典。注意sha1()的第二个参数必须为false默认值返回 40 位十六进制字符串。如果传true会返回原始二进制拼出来的磁力链接是乱码。2.5 拼接磁力链接拿到 info hash 之后拼接就简单了public function toMagnet() { $hash $this-getInfoHash(); $decoded $this-decode(); $name isset($decoded[info][name]) ? $decoded[info][name] : ; $magnet magnet:?xturn:btih: . $hash; if ($name) { $magnet . dn . urlencode($name); } return $magnet; }dn参数是显示名称用urlencode处理一下避免中文或特殊字符导致链接断裂。有些场景还会加tr参数指定 tracker 服务器但现代 BT 网络里 DHT 已经足够tracker 不是必须的。3. 跑起来环境准备、上传接口与批量转换3.1 环境要求与部署步骤这个项目对运行环境的要求很低项目要求PHP 版本5.6 及以上7.x / 8.x 均可扩展无特殊扩展sha1和mbstring是内置的Web 服务器Apache / Nginx / PHP 内置服务器目录权限uploads/需要可写部署步骤# 1. 解压到 Web 根目录 unzip bt2magnet.zip -d /var/www/html/bt2magnet # 2. 设置上传目录权限 chmod 755 /var/www/html/bt2magnet/uploads # 3. 如果用 PHP 内置服务器测试 cd /var/www/html/bt2magnet php -S 0.0.0.0:8080浏览器打开http://localhost:8080就能看到上传界面。生产环境建议用 Nginx PHP-FPM把uploads/目录设成不可执行防止有人上传伪装成种子的脚本文件。3.2 上传接口与表单处理index.php里的上传逻辑大概长这样?php require_once BtParser.php; $result ; $error ; if ($_SERVER[REQUEST_METHOD] POST isset($_FILES[torrent])) { $file $_FILES[torrent]; // 基础校验文件大小和扩展名 if ($file[error] ! UPLOAD_ERR_OK) { $error 上传失败错误码 . $file[error]; } elseif ($file[size] 2 * 1024 * 1024) { $error 种子文件不能超过 2MB; } else { $tmpPath $file[tmp_name]; try { $parser new BtParser($tmpPath); $result $parser-toMagnet(); } catch (Exception $e) { $error 解析失败 . $e-getMessage(); } } }参数说明$_FILES[torrent]对应表单里nametorrent的文件输入框。$file[tmp_name]是 PHP 自动生成的临时文件路径脚本结束后会自动删除不需要手动清理。$file[size]限制 2MB 是因为正常种子文件很少超过这个大小超过的要么是包含大量文件的巨型种子要么是异常文件。提示如果你的服务器upload_max_filesize设得比较小需要在php.ini里调大否则大种子会上传失败。3.3 批量转换命令行模式除了 Web 上传这个项目还可以改成命令行批量处理。我一般会加一个cli.php?php // cli.php - 批量转换目录下所有种子文件 require_once BtParser.php; $dir $argv[1] ?? ./torrents; $files glob($dir . /*.torrent); foreach ($files as $file) { try { $parser new BtParser($file); $magnet $parser-toMagnet(); echo basename($file) . \t . $magnet . PHP_EOL; } catch (Exception $e) { echo basename($file) . \tERROR: . $e-getMessage() . PHP_EOL; } }运行方式php cli.php /path/to/torrents magnets.txt输出是制表符分隔的两列文件名和磁力链接。重定向到文件后可以直接导入数据库或发给别人。这个脚本处理几百个种子文件也就几秒钟比手动一个个传快得多。3.4 常见报错与排查报错一file_get_contents返回 false。检查文件路径是否正确以及 PHP 进程是否有读权限。如果是上传的临时文件确认upload_tmp_dir配置正常。报错二Bencode 格式错误位置0。说明文件第一个字节就不是合法的 Bencode 起始字符。大概率是上传了非种子文件或者文件在传输过程中被损坏。用十六进制编辑器打开看看头几个字节正常种子应该以d开头。报错三种子文件缺少 info 字段。有些老种子或者特殊工具生成的种子可能结构不标准。可以先打印$decoded的键名看看实际有什么。报错四磁力链接复制出来是乱码。检查sha1()的第二个参数必须是false或者不传。另外确认 PHP 文件的编码是 UTF-8 无 BOMBOM 头会污染输出。4. 避坑与常见问题那些年我踩过的种子解析坑4.1 中文文件名导致哈希算错现象转换出来的磁力链接能用但下载时文件名显示乱码或者干脆找不到资源。原因种子文件里的name字段可能是 GBK 编码的而你的 PHP 文件是 UTF-8。直接urlencode会得到错误的dn参数。更严重的是如果解析时对字符串做了编码转换info 字段的原始字节就被破坏了SHA1 自然算错。解决解析阶段绝对不要做任何编码转换保持原始字节。只在最后拼接dn参数时判断一下编码必要时用mb_convert_encoding($name, UTF-8, GBK)转一下再urlencode。info hash 的计算必须基于原始字节。4.2 大种子文件内存溢出现象上传几十 MB 的种子文件时PHP 报Allowed memory size exhausted。原因file_get_contents把整个文件读进内存Bencode 解码又生成一个完整的 PHP 数组内存占用是文件大小的好几倍。解决在php.ini里把memory_limit调到 256M 或更高。更好的做法是流式解析但这需要重写解析器对于一般种子文件几百 KB 到几 MB来说没必要。我一般会在上传前限制文件大小超过 10MB 的直接拒绝。4.3strpos定位 info 字段时匹配到错误位置现象某些种子文件解析出来的 info hash 和主流工具算出来的不一致。原因strpos($this-data, 4:info)可能在文件的其他位置也匹配到4:info这个字节序列比如某个文件名恰好包含这个字符串。解决不要用strpos全局搜索而是在解码过程中记录 info 字段的起始偏移量。修改decodeDict()当键为info时记录当前$this-pos作为 info 值的起始位置。这样定位是精确的不受文件内容干扰。4.4 上传目录权限与安全问题现象上传后提示“无法读取种子文件”或者服务器被上传了恶意脚本。原因uploads/目录权限不对或者没有禁止执行权限。解决chmod 755 uploads确保可写。在 Nginx 配置里加location /uploads { deny all; }或者用.htaccess禁止访问。更彻底的做法是上传后立即处理处理完马上删除临时文件不保留任何上传内容。4.5 PHP 版本兼容性现象在 PHP 5.6 上跑正常换到 PHP 8.0 报错。原因PHP 8.0 对字符串偏移量访问更严格$this-data[$this-pos]在$this-pos超出范围时会抛警告。另外curly brace语法在 PHP 8.0 里被移除了。解决在访问字符串偏移量之前加边界检查确保$this-pos strlen($this-data)。把$str{0}这种写法改成$str[0]。这个项目的代码本身比较老派迁移到 PHP 8 需要做一轮兼容性检查。5. 二次开发从单文件转换到接口服务与批量入库5.1 封装成 JSON 接口原始项目是表单提交后刷新页面显示结果如果要做成 API 给其他系统调用改造起来很简单。新建一个api.php?php header(Content-Type: application/json; charsetutf-8); require_once BtParser.php; $response [code 0, msg , data null]; if ($_SERVER[REQUEST_METHOD] ! POST) { $response[code] 405; $response[msg] 仅支持 POST; echo json_encode($response); exit; } $input json_decode(file_get_contents(php://input), true); $torrentPath $input[path] ?? ; if (!file_exists($torrentPath)) { $response[code] 404; $response[msg] 文件不存在; echo json_encode($response); exit; } try { $parser new BtParser($torrentPath); $response[data] [ magnet $parser-toMagnet(), hash $parser-getInfoHash(), ]; } catch (Exception $e) { $response[code] 500; $response[msg] $e-getMessage(); } echo json_encode($response, JSON_UNESCAPED_UNICODE);调用方式curl -X POST http://localhost:8080/api.php \ -H Content-Type: application/json \ -d {path:/data/test.torrent}返回的 JSON 里包含磁力链接和 info hash方便前端或其他服务消费。JSON_UNESCAPED_UNICODE保证中文文件名不会被转义成\uXXXX。5.2 批量入库与去重如果你有一个种子库需要批量转换并存入数据库可以这样改?php require_once BtParser.php; $pdo new PDO(mysql:host127.0.0.1;dbnametorrents, user, pass); $pdo-setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $stmt $pdo-prepare( INSERT IGNORE INTO magnets (info_hash, magnet, name, created_at) VALUES (?, ?, ?, NOW()) ); $files glob(/data/torrents/*.torrent); $ok 0; $fail 0; foreach ($files as $file) { try { $parser new BtParser($file); $hash $parser-getInfoHash(); $magnet $parser-toMagnet(); $decoded $parser-decode(); $name $decoded[info][name] ?? ; $stmt-execute([$hash, $magnet, $name]); $ok; } catch (Exception $e) { $fail; error_log(解析失败: $file - . $e-getMessage()); } } echo 成功: $ok, 失败: $fail\n;数据库表结构CREATE TABLE magnets ( id INT AUTO_INCREMENT PRIMARY KEY, info_hash CHAR(40) NOT NULL UNIQUE, magnet TEXT NOT NULL, name VARCHAR(255) DEFAULT , created_at DATETIME, INDEX idx_hash (info_hash) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;INSERT IGNORE配合info_hash的唯一索引天然去重。同一个种子重复转换不会产生重复记录。info_hash用CHAR(40)存储因为 SHA1 十六进制字符串固定 40 位。5.3 性能优化缓存与并发如果转换量很大每次都要重新解析种子文件可以考虑加一层缓存。用 Redis 缓存 info hash 到磁力链接的映射$redis new Redis(); $redis-connect(127.0.0.1, 6379); $cacheKey magnet: . md5_file($file); $cached $redis-get($cacheKey); if ($cached) { $magnet $cached; } else { $parser new BtParser($file); $magnet $parser-toMagnet(); $redis-setex($cacheKey, 86400, $magnet); // 缓存 24 小时 }md5_file($file)作为缓存键文件内容不变就不会重复解析。setex设置 24 小时过期避免缓存无限增长。5.4 一个容易忽略的细节dn参数的长度限制磁力链接的dn参数在某些客户端里有长度限制过长的文件名会被截断。我一般会在拼接前判断一下超过 200 字节就截断$name $decoded[info][name] ?? ; if (strlen($name) 200) { $name substr($name, 0, 200); } $magnet . dn . urlencode($name);这个截断只影响显示名称不影响 info hash 和资源定位所以是安全的。5.5 验证转换结果是否正确改完代码怎么确认算出来的磁力链接是对的我一般用两个方法交叉验证一是拿同一个种子文件用主流 BT 客户端打开对比客户端显示的 info hash二是用transmission-show或aria2c --show-files这类命令行工具输出种子信息对比 hash 值。如果两边一致说明解析逻辑没问题。不一致的话优先检查 info 字段的字节范围是否截取正确这是最容易出错的地方。从那以后我每次改解析逻辑都会拿一个包含中文文件名、一个包含多文件、一个单文件小种子三个样本跑一遍对比确认无误再提交。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑