当前位置: 首页 > 工具软件 > go-torrent > 使用案例 >

Python3 解析 torrent 文件

岑光熙
2023-12-01

(可直接转到文末下载)

起因:

找资源的时候,在某些网站下载到torrent文件。虽说挺常见的,但是这玩意只能用第三方软件打开,总觉得不爽。另外也是单纯出于好奇心,想看看这玩意长什么样子,一探究竟。

在网上扒拉了挺多,感觉这个文件都被他们解析烂了,挺多看起来不错的例子。但还是没有找到感觉比较合适的,而且看起来貌似也不太难,所以还是决定自己动手。

关于torrent文件:

文件结构:

这个感觉不重要,我最后解析完得到的是一个 dict,大致长这样子:

{
    "announce": ...,
    "announce-list": [...],
    "encoding": "UTF-8",
    "info": {
        "length": ...,
        "name": ...,
        "piece length": ...,
        "pieces": ...
    }
}

发现跟网上许多文章的描述还是挺符合的。

数据类型:

这个最重要了,这个是一切分析的基础。

Torrent 使用的编码方式名为 BEncoding,它定义了 4 种数据类型:

  • 字符串:格式为:长度:内容。如:
    'test'      ->    4:test
    '1a2a.i'    ->    6:1a2a.i
  • 整数:格式为:i数字e。如:
    123    ->    i123e
    -30    ->    i-30e
  • 列表:格式为:l内容e。内容可以是任意这 4 种数据类型。如:
    ['str:1', -123]    ->    l5:str:1i-123e
  • 字典:格式为:d内容e。内容同样任意。如:
    {'length': 20}    ->    d6:lengthl20ee

这些数据类型初看很怪,之后觉得这样也还行,数据非常紧凑。

# 由基本数据类型得到以下一些标识符:

sign_num = b'0123456789' # 表示数字
sign_str = b':' # 字符串
sign_int = b'i' # 整数
sign_list= b'l' # 列表
sign_dict= b'd' # 字典
sign_end = b'e' # 整数和列表和字典的结尾

Python 解析

考虑到 列表 和 字典 里面塞了很多奇怪的东西,而且最主要是出于 字符串 本身的特殊性(什么都能往里面塞,但是本身格式又非常固定),所以首先要解决的问题是把字符串给提取出来。

处理数据类型:

开始,先读取文件为 bytes:

file = 'a.torrent'    # 文件名

f = open(file, 'rb')
s = f.read()    # 待操作的 bytes
f.close()

之后就是提取字符串:

# 这个函数用来获取字符串的长度

def len_str(s,i, start_safe=0):
    '''### 获取字符串/字节串的长度
    #### input:
    - s: 整体的字符串
    - i: 标识符 ":" 的位置
    - start_safe: 从此位置开始往后计算,此前的屏蔽掉

    #### return: 字符串的长度
    '''
    num=b''
    j=0
    while s[i-j-1:i-j] in sign_num and i-j>start_safe:
        num = s[i-j-1:i-j]+num
        j+=1

    if len(num)>0:
        return int(num)
    else:
        return 0

# 将里面的字符串全部提取
# 将所有的字符串全部转化为指定的字符:s
i=0
s_del_str = s

length_del=0
end_str=0
len_str_lis=[]
str_lis = []    # 提取出来的字符串

while i<len(s):
    if s[i:i+1]==sign_str:
        length=len_str(s,i, start_safe=end_str)
        len_str_lis.append(length)

        if length>0:
            # print(s[i+1:i+length+1])
            str_lis.append(s[i+1:i+length+1])
            s_del_str = s_del_str[:i-len(str(length))-length_del]+ b's' +s_del_str[i+1+length-length_del:]
            length_del += len(str(length))+length
            end_str = i+length+1
        
        i+=length
    i+=1

(变量名混乱不堪,见谅~)

这里代码不需要深究(屎山),毕竟修改了很多次,结果反正能用就行。你可以在变量 str_lis 里面看到提取的结果。

至此,我们将所有的字符串数据都统一转化成了一个字符:s

这样做的合理性是:除了字符串以外的其他 3 种数据,都非常干净,只包含那些特定字符,如数字和 l, d, e。于是剩下的数据就非常简洁,我将字符串替换掉之后的数据为:

b'dsssllselselselselselselselselselselselselselselselselselselselselselselselselselselselselselselselselselselselselselselselselselselselselselselselselselseesssi1560759851esssdsi334632621esssi524288essee'

之后同理去处理数字:

def len_int(s,i):
    j=1
    while s[i+j:i+j+1] in sign_num and i+j<len(s):
        j+=1

    return j-1

# 去除数字,修改为字符:i

i=0
s_del_int=s_del_str
length_del_int=0
int_lis=[]

while i<len(s_del_str):
    if s_del_str[i:i+1] == sign_int:
        length=len_int(s_del_str,i)
        print(s_del_str[i+1:i+1+length])
        int_lis.append(int(s_del_str[i+1:i+1+length]))
        s_del_int = s_del_int[:i-length_del_int]+ b'i' +s_del_int[i+2+length-length_del_int:]

        i+=length
        length_del_int+=length+1
    i+=1

至此将数字替换为了字符:i

只剩下列表(l开头e结尾)和字典(d开头e结尾),其中的字符串统一改为's',数字统一改为'i'

剩下的数据为:

b'dsssllselselselselselselselselselselselselselselselselselselselselselselselselselselselselselselselselselselselselselselselselselselselselselselselselselseesssisssdsisssissee'

之后是列表和字典:

s_list_dict=s_del_int.replace(b'l',b'[').replace(b'd',b'{')
s_del_list_dict=s_list_dict

lis_sign_near=[]
i=0
while i<len(s_list_dict):
    if s_list_dict[i:i+1]==b'[':
        lis_sign_near.append(b']')
    elif s_list_dict[i:i+1]==b'{':
        lis_sign_near.append(b'}')
    
    elif s_list_dict[i:i+1]==b'e':
        s_del_list_dict=s_del_list_dict[:i]+lis_sign_near[-1]+s_del_list_dict[i+1:]
        lis_sign_near=lis_sign_near[:-1]
    
    i+=1

s_del_list_dict = s_del_list_dict.replace(b'[s]',b's')

print(s_del_list_dict)

得到以下结果:

b'{sss[ssssssssssssssssssssssssssssssssssssssssssssssssss]sssisss{sisssiss}}'

现在就已经是彻底干净了,已经能看出来是个 dict 了,之后就是把数据重组起来。

数据重组

构建 dict 样式:

i=0
j=0
sign_lis=False
k_lis=[]

data=s_del_list_dict
lis_sign_near=[]
while i<len(s_del_list_dict):
    if s_del_list_dict[i:i+1] in [b's',b'i'] and s_del_list_dict[i+1:i+2] not in [b']',b'}']:
        j+=1
        if not sign_lis:
            k_lis[-1]+=1
            if k_lis[-1]%2==1:
                data=data[:i+j]+b':'+data[i+j:]
            else:
                data=data[:i+j]+b','+data[i+j:]
        else:
            data=data[:i+j]+b','+data[i+j:]
    
    elif s_del_list_dict[i:i+1] in [b'[',b'{']:
        lis_sign_near.append(s_del_list_dict[i:i+1])
        if s_del_list_dict[i:i+1]==b'{':
            k_lis.append(0)
            
    elif s_del_list_dict[i:i+1] in [b']',b'}']:
        lis_sign_near=lis_sign_near[:-1]
        j+=1
        data=data[:i+j]+b','+data[i+j:]

        if s_del_list_dict[i:i+1]==b'}' and len(k_lis)>0:
            k_lis=k_lis[:-1]
        
        if len(lis_sign_near)>0:
            if lis_sign_near[-1]==b'[':
                sign_lis=True
            else:
                sign_lis=False
                k_lis[-1]+=1

    if len(lis_sign_near)>0:
        if lis_sign_near[-1]==b'[':
            sign_lis=True
        else:
            sign_lis=False
    
    i+=1
    

data=data.replace(b',]',b']').replace(b',}',b'}')[:-1]
print(data)

结果十分明了:

b'{s:s,s:[s,s,s,s,s,s,s,s,s,s,s,s,s,s,s,s,s,s,s,s,s,s,s,s,s,s,s,s,s,s,s,s,s,s,s,s,s,s,s,s,s,s,s,s,s,s,s,s,s,s],s:s,s:i,s:s,s:{s:i,s:s,s:i,s:s}}'

数据填充:

由于前面把字符串和整数给销毁掉了,所有现在要把 's', 'i' 这些数据给填充回去:

# 用特殊字符作过渡
data = data.decode('utf8').replace('s','"ABBCCCDDDD"').replace('i','"EFFGG"')

# 填充
for i in str_lis:
    if len(i)<100:
        try:
            data=data.replace("ABBCCCDDDD",str(i.decode('utf8')), 1)
        except:
            data=data.replace("ABBCCCDDDD","can not decode", 1)
    else:
        data=data.replace("ABBCCCDDDD","too long", 1)

for i in int_lis:
    data=data.replace("EFFGG",str(i), 1)

# 意外情况:
while '""' in data:
    data=data.replace('""','"')
while '\t' in data:
    data=data.replace('\t','')
while '\n' in data:
    data=data.replace('\n','-')

现在就已经完全是一个 dict 了,可以输出变量 data 查看

最后保存为 json 文件

import json
result_json = json.loads(data)

f=open(file.replace('.torrent','.json'),'w', encoding='utf8')
f.write(json.dumps(result_json, indent=4))
f.close()

至此大功告成!

文末

参考链接:

第二篇:BEncoding · BT协议规范(简体中文版)

暂时不知道怎么上传文件,先贴出整局的代码吧。输入torrent文件路径就能使用了~

import json

# 二话不说,先读取
file=input('请输入torrent文件:\n')

if file[0] in ["'",'"']:
    file=file[1:-1]
f=open(file,'rb')
s=f.read()
f.close()

# %%
# 由基本数据类型得到以下一些标识符:
sign_num = b'0123456789' # 表示数字
sign_str = b':' # 字符串
sign_int = b'i' # 整数
sign_list= b'l' # 列表
sign_dict= b'd' # 字典
sign_end = b'e' # 整数和列表和字典的结尾


# %%
# 提取字符串

# 这个函数用来获取字符串的长度
def len_str(s,i, start_safe=0):
    '''### 获取字符串/字节串的长度
    #### input:
    - s: 整体的字符串
    - i: 标识符 ":" 的位置
    - start_safe: 从此位置开始往后计算,此前的屏蔽掉

    #### return: 字符串的长度
    '''
    num=b''
    j=0
    while s[i-j-1:i-j] in sign_num and i-j>start_safe:
        num = s[i-j-1:i-j]+num
        j+=1

    if len(num)>0:
        return int(num)
    else:
        return 0

# %%
# 将里面的字符串全部提取
# 将所有的字符串全部转化为指定的字符:s
i=0
s_del_str = s

length_del=0
end_str=0
len_str_lis=[]
str_lis = []

while i<len(s):
    if s[i:i+1]==sign_str:
        length=len_str(s,i, start_safe=end_str)
        len_str_lis.append(length)

        if length>0:
            # print(s[i+1:i+length+1])
            str_lis.append(s[i+1:i+length+1])
            s_del_str = s_del_str[:i-len(str(length))-length_del]+ b's' +s_del_str[i+1+length-length_del:]
            length_del += len(str(length))+length
            end_str = i+length+1
        
        i+=length
    i+=1


# %%
# 提取数字
def len_int(s,i):
    j=1
    while s[i+j:i+j+1] in sign_num and i+j<len(s):
        j+=1

    return j-1

i=0
s_del_int=s_del_str
length_del_int=0
int_lis=[]

while i<len(s_del_str):
    if s_del_str[i:i+1] == sign_int:
        length=len_int(s_del_str,i)
        # print(s_del_str[i+1:i+1+length])
        int_lis.append(int(s_del_str[i+1:i+1+length]))
        s_del_int = s_del_int[:i-length_del_int]+ b'i' +s_del_int[i+2+length-length_del_int:]

        i+=length
        length_del_int+=length+1
    i+=1


# %%
# 处理列表和字典
s_list_dict=s_del_int.replace(b'l',b'[').replace(b'd',b'{')
s_del_list_dict=s_list_dict

lis_sign_near=[]
i=0
while i<len(s_list_dict):
    if s_list_dict[i:i+1]==b'[':
        lis_sign_near.append(b']')
    elif s_list_dict[i:i+1]==b'{':
        lis_sign_near.append(b'}')
    
    elif s_list_dict[i:i+1]==b'e':
        s_del_list_dict=s_del_list_dict[:i]+lis_sign_near[-1]+s_del_list_dict[i+1:]
        lis_sign_near=lis_sign_near[:-1]
    
    i+=1

s_del_list_dict = s_del_list_dict.replace(b'[s]',b's')

# %%
# 相当难搞
i=0
j=0
sign_lis=False
k_lis=[]

data=s_del_list_dict
lis_sign_near=[]
while i<len(s_del_list_dict):
    if s_del_list_dict[i:i+1] in [b's',b'i'] and s_del_list_dict[i+1:i+2] not in [b']',b'}']:
        j+=1
        if not sign_lis:
            k_lis[-1]+=1
            if k_lis[-1]%2==1:
                data=data[:i+j]+b':'+data[i+j:]
            else:
                data=data[:i+j]+b','+data[i+j:]
        else:
            data=data[:i+j]+b','+data[i+j:]
    
    elif s_del_list_dict[i:i+1] in [b'[',b'{']:
        lis_sign_near.append(s_del_list_dict[i:i+1])
        if s_del_list_dict[i:i+1]==b'{':
            k_lis.append(0)
            
    elif s_del_list_dict[i:i+1] in [b']',b'}']:
        lis_sign_near=lis_sign_near[:-1]
        j+=1
        data=data[:i+j]+b','+data[i+j:]

        if s_del_list_dict[i:i+1]==b'}' and len(k_lis)>0:
            k_lis=k_lis[:-1]
        
        if len(lis_sign_near)>0:
            if lis_sign_near[-1]==b'[':
                sign_lis=True
            else:
                sign_lis=False
                k_lis[-1]+=1

    if len(lis_sign_near)>0:
        if lis_sign_near[-1]==b'[':
            sign_lis=True
        else:
            sign_lis=False
    
    i+=1
    

# 最后填充数据

data=data.replace(b',]',b']').replace(b',}',b'}')[:-1]
data=data.decode('utf8').replace('s','"temp_str"').replace('i','"temp_int"')

# %%
# 填充数据
for i in str_lis:
    if len(i)<100:
        try:
            data=data.replace('temp_str',str(i.decode()), 1)
        except:
            data=data.replace('temp_str',"can't decode", 1)
    else:
        data=data.replace('temp_str','too long', 1)

for i in int_lis:
    data=data.replace('temp_int',str(i), 1)


# 一些意外情况
while '""' in data:
    data=data.replace('""','"')
while '\t' in data:
    data=data.replace('\t','-')
while '\n' in data:
    data=data.replace('\n','-')

# %%
# 输出为json
# 这里输出为一种肉眼可见的方便的格式(json)
# 更人性化的输出后续在此基础上自己动手修改

try:
    result_json = json.loads(data) # except,主要就是上面那些意外情况
    if 'info' in result_json:
        print('\ninfo:')
        print(json.dumps(result_json['info'], indent=4, ensure_ascii=False))

    f=open(file.replace('.torrent','.json'),'w', encoding='utf8')
    f.write(json.dumps(result_json, indent=4, ensure_ascii=False))
    f.close()

    input('\n  读取成功\n\n详细信息已保存至同名json文件中')

except:
    input('\n  读取失败:字符串转为字典的时候出错\n\n赶紧滚去修bug')

有相关问题欢迎在评论区提出,说不定啥时候就看到了~

 类似资料: