☰
磁力链接结构解析与infohash生成校验实践
2026/10/31 18:42:41 网站建设 项目流程

1. 磁力链接并不是"链接",先把概念摆正

我第一次认真拆磁力链接,是在给一个内部镜像站做分发方案的时候。当时的需求很朴素:一批几十 GB 的构建产物和公开数据集,要发给分布在不同办公地点的团队,用中心化下载服务器带宽扛不住,就想到了 P2P 分发。运维同事甩给我一串东西,让我"按这个格式批量生成一批":

magnet:?xt=urn:btih:c12fe1c06bba254a9dc9f519b335aa7c1367a88a&dn=example-bundle&tr=udp%3A%2F%2Ftracker.example.org%3A6969

那会儿我以为磁力链接就是个"特殊网址",点开就能下。真正动手写生成脚本才发现完全不是这么回事——它更像一张身份证号 + 通讯录的组合卡片,里面没有半个字节的文件内容,只有一串 40 位的十六进制摘要,以及若干条可选的辅助信息。把这串字符的结构吃透之后,批量生成、校验、去重、失效排查这些活儿才真正跑顺。

这篇内容就是把我踩过的坑和整理出来的结论摊开讲。它适合三类人:一是做资源分发、镜像同步、内网传输的运维和开发;二是写下载器、做 P2P 客户端、需要处理 URI 解析的程序员;三是对这类编码格式好奇、想搞明白"那一长串乱码到底在说什么"的技术爱好者。看完你应该能做到:手写一条合法的磁力链接、用代码把任意一条磁力链接拆成结构化字段、在哈希对不上时快速定位问题出在哪一层。

先明确一个基本认知:磁力链接属于 BitTorrent 协议体系里的URI scheme 表达,它描述的是"去哪里找元数据",而不是"文件在哪里"。传统种子文件(.torrent)是先把元数据下载到本地再看内容,磁力链接则是先用摘要去网络里换元数据,换到了再按元数据下载。这个先后顺序的差别,直接决定了它的字段设计和容错策略。

提示:磁力链接只描述资源标识与获取路径,不承载任何内容。实际使用时请确认所分发资源的分发许可范围,内部构建产物、开源软件、公开数据集这类场景是最稳妥的。

2. 一条磁力链接的完整字段结构拆解

2.1 URI scheme 与主体骨架

先看骨架。一条标准磁力链接的整体形式是:

magnet:?<参数1>&<参数2>&<参数3>...

magnet:是 scheme,后面的?开始是查询串(query string),参数之间用&分隔,每个参数是键=值的形式。这个结构和普通网页 URL 的 query 部分完全同构,所以几乎所有语言自带的 URL 解析库都能直接拿来用——这是设计上的一个很聪明的偷懒,省得为磁力链接单独发明一套语法。

但有个细节必须注意:magnet:后面是?而不是//。它没有 authority 部分(没有主机名、没有端口),因为磁力链接不指向任何具体服务器。如果你按scheme://host/path的思维去解析,urlparse之类的库会把整个参数串当成 path 或 query 处理得很别扭,这点后面代码部分会专门处理。

参数的键都是小写字母,长度很短,基本都是两字母缩写,这套命名沿用了 URI 查询参数的极简风格。值里的特殊字符必须做百分号编码(percent-encoding),比如冒号:要写成%3A,斜杠/要写成%2F。为什么?因为:在 URI 里有 scheme 分隔的含义,&和=是参数分隔符,不转义的话解析器会直接切错位置。我见过最典型的事故就是手工拼 tracker 地址时忘了转义,结果一条链接被解析成了三条残缺参数。

2.2 xt 参数:唯一的必填项,也是身份核心

xt是 exact topic 的缩写,意思是"精确主题",它是整条磁力链接里唯一必须存在的参数。没有xt,这条链接在协议层面就没有意义,因为客户端根本不知道该去找什么。

xt的值是一个 URN(统一资源名),格式为:

urn:btih:<40位十六进制摘要>

btih是 BitTorrent InfoHash 的缩写,指的是种子元数据中info字典经过 bencode 编码后再做 SHA-1 计算得到的 20 字节摘要。这个值就是资源的"指纹"——内容变一个字节,指纹就全变。

btih后面的摘要支持两种编码形式,这是新手最容易懵的地方:

编码形式长度字符集示例特征
Base16(十六进制)40 字符0-9、a-f(大小写不敏感)c12fe1c06bba254a9dc9f519b335aa7c1367a88a
Base3232 字符A-Z、2-7(不区分大小写)YEX6DQDLXISUVHOJ6UM3GNNKPQJWPKEA

两者表达的是同一份 20 字节数据,只是书写方式不同。Base32 版本的存在意义主要是省字符、方便人工抄写和口头传递(去掉了容易混淆的数字 0/1 和字母 O/I/L 的部分歧义)。我做批量生成时统一用 Base16,因为下游系统对接时十六进制最好校验;但做纸质登记或者客服口述场景时,Base32 明显更友好。

判断方法很简单:数长度。40 位且只含0-9a-f就是 Base16,32 位且只含A-Z2-7就是 Base32。长度对不上、或者出现了字符集之外的字(比如 Base32 里混进了0或1),直接判定为非法,不用往下查。

2.3 dn、tr、xl、ws:四个高频可选参数

xt之外全是可选的,但有几个出现频率极高,值得逐个说清楚。

dn是 display name,显示名称。它就是个给人看的字符串,客户端在列表里展示用,不参与任何哈希计算,也不影响资源定位。所以它可以随便改——这一点在实际操作里非常有用:同一份资源,给不同团队分发时可以带上不同的dn前缀做标识,比如dn=%5Bteam-a%5Dbuild-20240612,而xt保持完全一致,P2P 网络里仍然认作同一份资源,能互相做种。

tr是 tracker,追踪服务器地址。一条链接可以带多个tr参数,客户端会依次尝试。值必须是完整 URL,且必须百分号编码。多 tracker 的意义是容错:某个 tracker 挂了,还有备用的。这里有个经验:tr的顺序会影响连接成功率,把响应最快的放前面能明显减少握手等待时间。我一般会把 UDP tracker 放前面、HTTP tracker 放后面,因为 UDP 握手开销小。

xl是 exact length,资源总字节数。它的作用是在元数据还没拿到之前,先告诉客户端"这个资源大概多大",方便做磁盘空间预检查和进度估算。这个值是十进制整数,不给单位。它不是必需的,但强烈建议加上——我们做过一次对比,带xl的链接在客户端里的"等待元数据"阶段用户体验好很多,因为进度条能有个分母。

ws是 web seed,网页种子地址,指向一个可以直接 HTTP 下载的镜像。它的价值在于兜底:P2P 网络里一个做种者都没有的时候,客户端还能从这个地址把文件拉下来。内网分发场景里这个参数特别好用,指向内网的静态文件服务即可。

参数全称是否必填作用是否参与哈希
xtexact topic是资源身份标识是(其值本身就是哈希)
dndisplay name否展示名称否
trtracker否(可多个)追踪服务器否
xlexact length否总字节数否
wsweb seed否HTTP 兜底源否
asacceptable source否可接受的来源类型否
xsexact source否元数据直链否

2.4 参数顺序、重复与容错规则

参数顺序在协议层面没有强制要求,xt放第一个纯粹是惯例。但解析器实现上有差异:有些简陋的实现默认第一个参数就是xt,一旦顺序变了就报错。所以我在生成端统一固定顺序为xt → dn → tr → xl → ws,牺牲一点自由度换兼容性,很划算。

重复参数的处理规则要分开看:tr允许多次出现,解析时应该收集成列表;而xt、dn、xl重复出现属于异常,稳妥做法是取第一个并记一条告警。我遇到过上游系统拼接时把xt写了两遍的情况,值还不同,这时候如果取最后一个就会指向错误的资源,取第一个反而是对的——因为第一个通常是原始生成的那条。

还有一类容错:值的百分号编码不规范。比如%3a(小写)和%3A(大写)应该等价,但有的解析器只认大写;再比如值里出现了未编码的空格。我的做法是解析前先做一次规范化:把所有百分号编码统一成大写十六进制,把裸露的空格替换成%20。这一步很便宜,但能挡掉大量脏数据。

3. 信息哈希从哪来:bencode 与 SHA-1 的完整链路

3.1 bencode 的四条编码规则

要理解btih的来历,绕不开 bencode。它是 BitTorrent 专用的序列化格式,规则极简,一共四条:

  • 字符串:<长度>:<内容>,长度是字节数。比如spam编码成4:spam,空字符串是0:。
  • 整数:i<数字>e。比如42是i42e,0是i0e,负数-3是i-3e。注意不能有前导零,i042e是非法的。
  • 列表:l<元素...>e。比如["a", 1]是l1:ai1ee。
  • 字典:d<键值对...>e,键必须是字符串,且必须按字节序升序排列。这一条是后面所有踩坑的源头。

举个完整例子,一个极简的info字典:

{ "name": "demo", "length": 1024, "piece length": 262144, "pieces": <20字节SHA1拼接的二进制串> }

编码后按字典序排列键:length<name<piece length<pieces。这里要注意piece length中间有空格,空格字节是 0x20,小于所有小写字母,所以piece length排在pieces前面(因为第 6 个字节是空格 0x20 vs 字母 s 0x73)。这个细节如果排错了,哈希就全错。

注意:字典序比较的是原始字节,不是字符串的"自然语言顺序"。在 Python 里直接sorted(d.keys())得到的是 Unicode 码点顺序,对纯 ASCII 键来说结果一致,但如果键里出现非 ASCII 字符(虽然规范上不该有),就必须显式按 UTF-8 字节比较。

3.2 只对 info 字典做哈希,别把整个种子算进去

这是最关键的一条:btih的输入只有info字典这一块,不是整个种子文件。种子文件顶层还有announce、announce-list、creation date、comment等键,这些统统不参与哈希。

为什么要这么设计?因为announce这类信息随时可能变——你给同一个资源加个新 tracker,如果参与哈希,指纹就变了,P2P 网络里就被当成另一个文件,做种者白做了。只对info做哈希,保证了"内容不变则指纹不变",tracker 可以自由增删。这个想法的干净程度,值得在任何做内容寻址的系统里借鉴。

计算流程是这样的:

  1. 从完整种子中取出info字典。
  2. 对这个字典做 bencode 编码,得到字节串info_bytes。
  3. sha1(info_bytes)得到 20 字节摘要。
  4. 摘要转 Base16 得到 40 字符,或转 Base32 得到 32 字符。

代码写起来很短,但每一步都有坑:

import hashlib def btih_from_info(info_dict): # 假设 bencode 是自己实现或引入的库函数 info_bytes = bencode(info_dict) digest = hashlib.sha1(info_bytes).digest() return digest.hex() # Base16 # 或 base64.b32encode(digest).decode().rstrip('=')

3.3 三个让哈希对不上的隐形杀手

实际操作中哈希算不对,九成以上是下面三个原因,按出现频率排:

第一个是编码不一致。中文文件名是最典型的。同一个"测试.zip",如果一端按 UTF-8 编码,另一端按 GBK 编码,bencode 出来的字节长度和内容都不同,哈希自然不同。规范上name字段推荐 UTF-8,Windows 平台上的老工具经常不守规矩。我的处理方式是在生成和校验端都强制encode('utf-8'),一旦发现来源是别的编码,先转码再计算,并记录一条日志。

第二个是字典键排序。前面说过必须按字节序。有些手写实现图省事用了插入顺序,只要插入顺序恰好不是字典序,哈希就错。这种情况特别隐蔽,因为大部分简单例子里键刚好是按序插入的,测试能过,上线遇到复杂结构才炸。

第三个是二进制字段被"顺手美化"。pieces字段是一堆 20 字节 SHA-1 的原始拼接,里面大量不可打印字节。有的人为了调试方便,用latin-1解码成字符串再存,或者用repr()打印出来又粘回去,字节全变了。这个字段必须端到端当字节串处理,任何解码都是事故。

4. 动手实操:解析、生成与校验全流程

4.1 手工解析一条链接,先把眼睛练出来

在写代码之前,我建议先练一遍手工拆解,对字段位置有直觉之后,看日志会快很多。拿这条举例:

magnet:?xt=urn:btih:YEX6DQDLXISUVHOJ6UM3GNNKPQJWPKEA&dn=dataset-v2&tr=udp%3A%2F%2Ftracker.example.org%3A6969&xl=1073741824

拆解步骤:

  1. 去掉magnet:?,剩下查询串。
  2. 按&切成四段:xt=...、dn=...、tr=...、xl=...。
  3. xt的值是urn:btih:YEX6DQDLXISUVHOJ6UM3GNNKPQJWPKEA,去掉前缀得 32 字符、字符集在A-Z2-7内,判定为 Base32。
  4. tr的值做百分号解码,%3A还原成:、%2F还原成/,得到udp://tracker.example.org:6969。
  5. xl是1073741824,十进制,换算成 1 GiB。

手工走一遍你会发现,除了xt需要做编码判断,其他参数都是"解码即用",没什么玄机。真正的复杂性全在xt背后的元数据那一侧。

4.2 用 Python 写一个健壮的解析器

下面这段是我实际在用的解析逻辑,比大多数示例多了编码判断和容错处理:

import base64 import binascii from urllib.parse import parse_qs, unquote def parse_magnet(uri: str) -> dict: if not uri.startswith('magnet:?'): raise ValueError('not a magnet uri') query = uri[len('magnet:?'):] # keep_blank_values=True 保留 `tr=` 这种空值,便于报错定位 raw = parse_qs(query, keep_blank_values=True) result = {'trackers': [], 'web_seeds': [], 'raw': raw} xt_list = raw.get('xt', []) if not xt_list: raise ValueError('missing xt parameter') xt = unquote(xt_list[0]) prefix = 'urn:btih:' if not xt.lower().startswith(prefix): raise ValueError(f'unsupported xt prefix: {xt[:20]}') token = xt[len(prefix):] if len(token) == 40: # Base16:只允许 0-9a-f lower = token.lower() if any(c not in '0123456789abcdef' for c in lower): raise ValueError('bad base16 infohash') result['infohash_hex'] = lower result['encoding'] = 'base16' elif len(token) == 32: # Base32:补 padding 后解码,长度必须还原成 20 字节 token_u = token.upper() if any(c not in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567' for c in token_u): raise ValueError('bad base32 infohash') pad = (-len(token_u)) % 8 try: digest = base64.b32decode(token_u + '=' * pad) except binascii.Error as e: raise ValueError(f'base32 decode failed: {e}') if len(digest) != 20: raise ValueError('infohash must be 20 bytes') result['infohash_hex'] = digest.hex() result['encoding'] = 'base32' else: raise ValueError(f'infohash length invalid: {len(token)}') if 'dn' in raw: result['display_name'] = unquote(raw['dn'][0]) if 'xl' in raw: try: result['exact_length'] = int(raw['xl'][0]) except ValueError: result['exact_length'] = None for t in raw.get('tr', []): t = unquote(t) if t: result['trackers'].append(t) for w in raw.get('ws', []): w = unquote(w) if w: result['web_seeds'].append(w) return result

几处值得说明的设计取舍:

parse_qs本身就做了百分号解码,所以外面不必再unquote一次——但我上面还是加了,是因为有些客户端生成链接时会做双重编码(把%3A又编成%253A),多解一次能救回来。这个属于防御性写法,正常链接不受影响。

Base32 必须补 padding。Python 的b32decode要求输入长度是 8 的倍数,32 位刚好是 4 个 8 字符块,理论上是齐的,但有些实现会去掉尾部的=,导致长度变成 30 或 31,这时候补齐就必要了。解码后一定要校验长度等于 20 字节,这是最后一道关。

统一输出为十六进制。不管输入是哪种编码,内部一律转成小写十六进制作为唯一标识。我吃过一次亏:去重逻辑里一个用 Base16 一个用 Base32 存,明明是同一份资源,被当成两条记录,白白多存了一份镜像。

4.3 从零生成一条链接:把参数算准

生成端的关键是"边算边记"。假设手里有一批文件要打包成一个资源:

import hashlib import os import base64 from urllib.parse import quote def bencode(obj) -> bytes: if isinstance(obj, bool): raise TypeError('bencode has no boolean') if isinstance(obj, bytes): return str(len(obj)).encode() + b':' + obj if isinstance(obj, str): b = obj.encode('utf-8') return str(len(b)).encode() + b':' + b if isinstance(obj, int): return b'i' + str(obj).encode() + b'e' if isinstance(obj, (list, tuple)): return b'l' + b''.join(bencode(x) for x in obj) + b'e' if isinstance(obj, dict): pairs = sorted(obj.items(), key=lambda kv: kv[0].encode('utf-8')) body = b''.join(bencode(k) + bencode(v) for k, v in pairs) return b'd' + body + b'e' raise TypeError(f'unsupported type: {type(obj)}') def build_info(files, piece_length): """files: [(相对路径, 绝对路径), ...]""" total = sum(os.path.getsize(p) for _, p in files) pieces = bytearray() buf = bytearray() for _, path in files: with open(path, 'rb') as f: while True: chunk = f.read(1 << 20) if not chunk: break buf.extend(chunk) while len(buf) >= piece_length: pieces.extend(hashlib.sha1(buf[:piece_length]).digest()) del buf[:piece_length] # 最后一块不足 piece_length 也要单独算一个 hash if buf: pieces.extend(hashlib.sha1(bytes(buf)).digest()) info = { 'name': os.path.basename(files[0][1]) if len(files) == 1 else 'bundle', 'piece length': piece_length, 'pieces': bytes(pieces), } if len(files) == 1: info['length'] = total else: info['files'] = [ {'length': os.path.getsize(p), 'path': rel.split('/')} for rel, p in files ] return info def make_magnet(info, name, trackers=(), web_seeds=(), length=None, use_base32=False): info_bytes = bencode(info) digest = hashlib.sha1(info_bytes).digest() if use_base32: token = base64.b32encode(digest).decode().rstrip('=') else: token = digest.hex() parts = [f'xt=urn:btih:{token}', f'dn={quote(name, safe="")}'] for t in trackers: parts.append(f'tr={quote(t, safe="")}') if length is not None: parts.append(f'xl={length}') for w in web_seeds: parts.append(f'ws={quote(w, safe="")}') return 'magnet:?' + '&'.join(parts)

几个参数选择的实际考量,这部分是文档里不会写的:

piece length怎么定。太小会让pieces字段变得极大——每个分片只占 20 字节摘要,1 万个分片就是 200 KB 的无用负担;太大则影响并行下载的粒度,单分片失败重传的代价也高。经验值是让总分片数落在 1000 到 2000 之间,具体算法是piece_length ≈ 总大小 / 1500,再向上取整到 2 的幂(16 KiB、32 KiB、64 KiB……)。比如 8 GiB 的资源,8*1024^3/1500 ≈ 5.7 MiB,取 8 MiB 比较合适。

quote(name, safe="")里的safe=""不能省。默认safe='/',会把斜杠保留原样,导致dn里的斜杠被当成路径分隔符。全转义最安全。

pieces必须是bytes。我在bencode里对bytes和str分开处理,就是为了避免这个字段被误当成文本。生成完之后建议打印一下len(pieces) == 分片数 * 20做个自检。

4.4 校验环节:双向比对才算稳

生成完别急着发出去,至少做两步校验。

第一步,把生成的链接用自己的解析器跑一遍,把infohash_hex解出来,和digest.hex()对比。这一步能抓住编码写错、Base32 少了 padding 这类问题。

第二步,用解析出的哈希反查元数据(如果本地有种子文件或 P2P 缓存),把拿到的info字典重新 bencode 一遍,再算 SHA-1,和解析值对比。这一步能抓住"生成的链接和实际资源对不上"的情况,比如批量生成时文件列表错位。

parsed = parse_magnet(magnet_uri) assert parsed['infohash_hex'] == digest.hex() assert parsed['exact_length'] == total_size assert len(parsed['trackers']) == len(trackers)

我在批量生成脚本里把这三条断言写死了,跑一万条大概能拦住三到五个问题,都是那种肉眼绝对看不出来的。

5. 常见问题排查实录

5.1 客户端的"元数据获取中"卡死不动

这是最高频的现象,表现是链接能识别、姓名能显示、就是进度永远停在 0%。

排查顺序应该是这样:先看xt是不是被写坏了。用 4.2 的解析器跑一遍,如果长度或字符集不对,链接本身就是废的。再确认这个哈希在网络上是否真的有人做种——连不上和解析错误是两件事,别在解析器上白费功夫。

如果链接合法但没人做种,ws参数是最后一根救命稻草。内网场景下放一个内网的静态文件地址,客户端能直接走 HTTP 拉完整文件,虽然 P2P 的意义没了,但至少能交付。

另一个隐蔽原因是tr全失效。我遇到过一条链接的四个 tracker 里有三个已经下线,第四个是 UDP 的但内网防火墙封了 UDP,结果客户端一直重试。解决办法是加一个内网可访问的 tracker,或者干脆只依赖 DHT——DHT 不依赖任何 tracker,只要网络允许节点发现,慢慢也能连上。

5.2 哈希对不上,逐层拆开验证

这类问题的排查我总结成一张表,从外到内逐层排除,比盲猜高效得多:

排查层级检查什么典型症状
URI 层xt长度、字符集、前缀长度 41 或 39,含非法字符
编码层Base16/Base32 判断是否正确把 Base16 当 Base32 解,报解码失败
字节层解码后是否恰好 20 字节Base32 补位错误导致 18 或 22 字节
序列化层bencode 键排序、整数无前导零结构对但哈希总差一位
字段层pieces是否为原始字节解码成文本后哈希全变
内容层info字典是否与资源一致哈希合法但下载内容校验失败

有个特别坑的情况要单独提:种子文件被"重新打包"过。有些工具在保存种子时会顺手加上creation date、comment、encoding这些键。如果这些键加在info内部,哈希就变了;加在顶层则不影响。判断方法是把两个版本的info字典都 bencode 出来做字节级 diff,通常会看到多出来的那几个键。

5.3 中文名乱码与百分号编码陷阱

中文dn显示成乱码,原因基本只有两个:生成时没做 UTF-8 编码,或者解析时用错了编码解。

百分号编码的标准做法是对 UTF-8 字节逐个转义,测试.zip应该变成%E6%B5%8B%E8%AF%95.zip。如果看到的是%B2%E2%CA%D4.zip这种,那就是按 GBK 编的,说明生成端用了老式工具。

还有一种情况更阴险:dn里带了+号。在某些实现里+会被解码成空格(这是表单编码的遗留习惯),导致文件名里凭空多出空格。稳妥做法是生成时把+也转义成%2B,或者解析时用unquote而不是unquote_plus。

提示:dn只是展示用,改它不影响资源一致性。所以遇到乱码其实不用纠结,客户端里手动改个名字照样能用,真正要保证正确性的只有xt。

5.4xl与实际大小不一致怎么办

xl是可选参数,客户端一般只把它当"预估",实际大小以元数据里的length或files汇总为准。所以不一致通常不会导致下载失败,但会让人对进度条产生误判——比如xl写小了,进度条跑到 100% 之后还在增加,看起来像卡住。

我的做法是:xl一定要从实际文件列表累加得出,别手写、别四舍五入。多文件资源尤其要注意,是所有文件大小之和,不是最大那个。生成脚本里用sum(os.path.getsize(p) for p in paths)一行解决,别偷懒。

5.5 一份速查清单

出问题时按这个顺序过一遍,绝大部分能当场定位:

  • 链接是否以magnet:?开头,注意是?不是//
  • xt是否存在,是否只有一条
  • 去掉urn:btih:后是 40 位(Base16)还是 32 位(Base32)
  • 字符集是否符合对应编码的约束
  • tr是否做过百分号编码,解码后是不是合法 URL
  • 本机是否能访问 tracker 的协议端口(UDP/HTTP)
  • dn的中文是否按 UTF-8 编码
  • xl是否等于所有文件大小之和
  • 生成端和校验端的 bencode 实现是否一致(键排序、整数格式)

6. 我在实际使用中攒下的几条经验

关于xt的两种编码,我现在一律在系统内部存 Base16,对外展示时按场景选。原因是十六进制不区分大小写这件事太省心了——用户从任何地方复制过来,toupper或tolower一下就能比对。Base32 虽然短,但大小写敏感性带来的歧义反而增加了沟通成本,只有在需要口述传递的场合才值得用。

批量生成的性能上有个小技巧:piece length的计算和分片哈希是纯 CPU 密集的 IO 操作,文件多的时候很容易被磁盘拖住。我用的是"分片哈希单线程、文件读取多进程"的结构,每个进程负责一批文件,主进程只做info组装和最终哈希。实测下来比单线程快三倍多,瓶颈从 CPU 变成了磁盘顺序读。如果文件本身就在 SSD 上,那基本就是 IO 打满了,再优化收益有限。

还有一个我踩过的坑值得提醒:不要用字符串拼接过长的链接。一条带十几个 tracker 的磁力链接可能超过一千字符,用+拼接在 Python 里是 O(n²) 的,批量生成几万条时能明显感觉到慢。改成列表append再join就行,一行改动。

最后说一点关于 DHT 的观察。磁力链接之所以能"无 tracker 也能用",靠的是 DHT 网络——它把"哈希 → 拥有该资源的节点"这个映射分散存在各个节点上。这意味着一条合法的xt即使不带任何tr,理论上也有可能被找到,只是速度慢、依赖网络中节点的配合程度。所以在内部隔离网络里,别指望 DHT,老老实实配一个内网 tracker 加一个ws兜底,这是最稳的组合,我们后来所有分发方案都按这个结构来做,一次没出过交付事故。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询