前言
不知道各位看官是否有过类似的经历。好不容易找到一个电影的种子文件,想用百度云的离线下载功能去下载文件,却被百度云无情提示“离线文件因含有违规内容被系统屏蔽无法下载”!假设有这么一个场景,比如最近有一部电影《速度与激情7》很火,百度为了保护版权方的利益,对于凡是种子文件中包含了关键字“速度与激情7”的,一律提示包含有违规内容,禁止下载。
于是乎,后来就有了通过“BEncode Editor”这款工具来修改其中一些敏感性文字的方法来修改种子文件,以达到下载的目的。比如在上一个例子中,我们把种子文件中凡是包含有“速度与激情7”的文字全部修改为“电影7”,然后保存种子文件再下载的话,就不会被百度屏蔽了。修改的方法如下图所示。
本来故事到此应该就已经结束了。但是后来有两点促使了我编写一个自动化的种子文件修改工具。第一点是因为有的种子文件包含的文件实在太多了,每一个文件的文件名都改过去很麻烦,第二点是因为能用电脑解决的重复性劳动我就懒得用手解决。所以才有了下面的故事。
种子文件编码格式
BT种子文件使用了一种叫bencoding的编码方法来保存数据[1]。
编码规则如下:strings(字符串)编码为:<字符串长度>:<字符串>例如: 4:test 表示为字符串test4:例子 表示为字符串“例子”字符串长度单位为字节没开始或结束标记integers(整数)编码为:i<整数>e开始标记i,结束标记为e例如: i1234e 表示为整数1234i-1234e 表示为整数-1234整数没有大小限制i0e 表示为整数0i-0e 为非法以0开头的为非法如: i01234e 为非法lists(列表)编码为:l
节点的定义
为了对BT文件有一个直观的印象,我们还是以速度与激情7这个BT文件为例,从图中为各位看官做一下介绍。仔细观察下图,我们发现在图中的节点无非是三种类型,第一种是根节点,第二种是键值对节点(字典也是一个特殊的键值对节点,其键为名字,而值为其所有子节点),第三种列表节点。
简单的BT文件解析器
可以看到bencoding编码中的四种类型都有一个标识头,比如整数类型以'i'开始,string类型以数字开始。利用这一特性,对于每一个类型,我们先尝试读一个字符,并根据读入的字符判断读入的是什么类型,如‘i’为整形,'d'为字典,'l'为列表而剩下的数字则为字符串。
那么接下来的思路就非常清晰了,我们需要四个方法来分别解析数字,字符串,字典和列表。其中数字和字符串类型只用于表示值,而不能作为容器;列表和字典类型都可以作为容器,故还有一个parent参数,用于向父节点添加子节点。
1 private byte[] AnalysisInteger(); // 解析整形,由于会超出Int32的表示范围,用byte[]代替
2
3 private byte[] AnalysisString(); // 解析字符串,考虑到编码问题,这里用byte[]表示
4
5 private void AnalysisList(IBNode parent); // 解析列表
6
7 private void AnalysisDictionary(IBNode parent); // 解析字典
由于到BT文件是树状结构的,这里我们使用递归来实现对BT文件的解析。可以确定的,BT文件一定是以一个字典类型开始的,所以我们先调用AnalysisDictionary方法,并把参数根节点传给它。之后在该方法中通过读入下一个字符来判断是什么类型,并调用相应的方法来解析该类型,而相应的方法又通过相同的方法继续调用另外的方法,如此循环,直到解析完毕,这也正是递归的思想。下边就是我实现的一个简单的BT文件解析器,返回的是一个IBNode类型的根节点。
1 ///
2 /// 一个最简单的BT文件分析器
3 ///
4 class CommonAnalyser:IAnalyser
5 {
6 private byte[] torrentStream = null;
7 private int index = 0;
8 private List
9 private BNodeFactory _bNodeFactory = null;
10
11 public CommonAnalyser()
12 {
13 torrentStream = null;
14 _bNodeList = new List
15 _bNodeFactory = new BNodeFactory(_bNodeList);
16 index = 0;
17 }
18
19 public IBNode Analysis(byte[] torrentStream)
20 {
21 // 清空上一次处理的信息
22 _bNodeList = new List
23 _bNodeFactory = new BNodeFactory(_bNodeList);
24 index = 0;
25
26 this.torrentStream = torrentStream;
27 // bt文件一定是一个字典开始的
28
29 DictNode rootNode = (DictNode)_bNodeFactory.GetBNode('d');
30 AnalysisDictionary(rootNode);
31 return rootNode;
32 }
33
34 ///
35 /// 取出当前字符,并指针后移
36 ///
37 ///
38 private char GetCurrentCharMove()
39 {
40 return (char)torrentStream[index++];
41 }
42
43 ///
44 /// 取出当前字符,并指针不后移
45 ///
46 ///
47 private char GetCurrentChar()
48 {
49 return (char)torrentStream[index];
50 }
51
52 private void AnalysisDictionary(IBNode parent)
53 {
54 // 字典一定是d开始的
55 if (GetCurrentCharMove() != 'd')
56 return;
57
58 // 循环分析键值对
59 do
60 {
61 KeyValueNode keyValueNode = (KeyValueNode)_bNodeFactory.GetBNode('k');
62 // 键值对,键一定是string
63 keyValueNode.SetKey(AnalysisString());
64 // 值
65 switch (GetCurrentChar())
66 {
67 case 'i': // 数字
68 keyValueNode.SetValue(AnalysisInteger());
69 keyValueNode.ValueType = 'i';
70 break;
71 case 'd': // 字典
72 AnalysisDictionary(keyValueNode);
73 keyValueNode.ValueType = 'd';
74 break;
75 case 'l': // 列表
76 AnalysisList(keyValueNode);
77 keyValueNode.ValueType = 'l';
78 break;
79 default: // 字符串
80 keyValueNode.SetValue(AnalysisString());
81 keyValueNode.ValueType = 's';
82 break;
83 }
84 parent.Child.Add(keyValueNode);
85 } while (GetCurrentChar() != 'e');
86 GetCurrentCharMove();
87 }
88
89 private void AnalysisList(IBNode parent)
90 {
91 // 列表一定是l开始的
92 if (GetCurrentCharMove() != 'l')
93 return;
94
95 int count = 0;
96 // 循环读入列表项
97 do
98 {
99 ListItemNode listItemNode = (ListItemNode)_bNodeFactory.GetBNode('l');
100 switch (GetCurrentChar())
101 {
102 case 'i': // 数字
103 listItemNode.SetValue(AnalysisInteger());
104 listItemNode.ValueType = 'i';
105 break;
106 case 'd': // 字典
107 AnalysisDictionary(listItemNode);
108 listItemNode.ValueType = 'd';
109 break;
110 case 'l': // 列表
111 AnalysisList(listItemNode);
112 listItemNode.ValueType = 'l';
113 break;
114 default:
115 listItemNode.SetValue(AnalysisString());
116 listItemNode.ValueType = 's';
117 break;
118 }
119 listItemNode.ListIndex = count++;
120 parent.Child.Add(listItemNode);
121 } while (GetCurrentChar() != 'e');
122 GetCurrentCharMove();
123 }
124
125 // 由于有些数字太大,用string来代替int
126 private byte[] AnalysisInteger()
127 {
128 // 数字一定是i开始e结尾的
129 if (GetCurrentCharMove() != 'i')
130 return null;
131
132 //StringBuilder builder = new StringBuilder();
133 List
134 char currentChar = ' ';
135 while ((currentChar = GetCurrentCharMove()) != 'e')
136 {
137 //builder.Append(currentChar);
138 integerByte.Add((byte)currentChar);
139 }
140
141 return integerByte.ToArray();
142 }
143
144 private byte[] AnalysisString()
145 {
146 char currentChar = GetCurrentCharMove();
147 // 字符串一定是数字开始开始
148 if (currentChar < '0' || currentChar > '9')
149 return null;
150
151 StringBuilder builder = new StringBuilder();
152
153 do
154 {
155 builder.Append(currentChar);
156 currentChar = GetCurrentCharMove();
157 } while (currentChar >= '0' && currentChar <= '9');
158
159 // 中间必须为:
160 if (currentChar != ':')
161 return null;
162
163 int length = Int32.Parse(builder.ToString());
164 byte[] buffer = new byte[length];
165 for (int i = 0; i < length; ++i)
166 {
167 buffer[i] = torrentStream[index++];
168 //builder.Append(GetCurrentCharMove());
169 }
170
171 return buffer;
172 }
173
174 public List
175 {
176 get
177 {
178 return _bNodeList;
179 }
180 set
181 {
182 _bNodeList = value;
183 }
184 }
185 }
View Code
显示BT文件树状图
好不容易解析完了,当然要先把它显示出来看是否正确。这里我们仿照“BEncode Editor”这款工具的界面来显示。简单分析一下,其实就是使用了一个TreeView的控件来显示。由于我们解析出来的节点和TreeView控件的节点正好是一一对应的,所以这里也用一个递归就能实现了。
1 private void ConstructTree(TreeNode tParent, IBNode bParent)
2 {
3 tParent.Text = bParent.ToString(); // 这里用了一点格式化的方法,详见完整代码
4 foreach (IBNode bNode in bParent.Child)
5 {
6 TreeNode tNode = new TreeNode();
7 tNode.Text = bNode.ToString();
8 tNode.Tag = bNode;
9 tParent.Nodes.Add(tNode);
10 ConstructTree(tNode, bNode);
11 }
12 }
显示效果就像下面这个样子。已经和上面BT文件修改工具很像了。
修改BT文件
至今为止我们都在做重复的工作,模仿已有的工具,那么接下来就是新的内容了。经过我的仔细观察后发现,百度云离线下载检测的关键词主要为
{ "name", "name.utf-8", "path", "path.utf-8", "comment", "comment.utf-8", "publisher", "publisher-url", "publisher-url.utf-8", "publisher.utf-8"}
这些键后面的值。只要我们把这些后面对应的值改为一些不敏感的词,那么就能躲过百度的审查。
为了把刚学的设计模式用上去,我在之前定义IBNode接口的时候预留了一个方法。
1 ///
2 /// 接受修改
3 ///
4 ///
5 void Accept(IVisitor visitor);
这里主要用到了设计模式当中访问者模式。现在要修改这些键对用的值,我们只需要遍历一遍所有的节点,并在每一个节点上调用一次Accept方法,让访问者去做修改的工作就可以了。访问者的代码如下,大致思想就是判断键值如果在我们上文提到的那些键值中的其中一个,那么就把其对应的值改为“somename”,相应百度应该不会把somename认为是敏感的词。
1 ///
2 /// 用于修改特定键值对节点的值
3 ///
4 class KeyValueVisitor:IVisitor
5 {
6 private string[] tabooString = { "name", "name.utf-8", "path", "path.utf-8", "comment", "comment.utf-8",
7 "publisher", "publisher-url", "publisher-url.utf-8", "publisher.utf-8"};
8 public void Visit(KeyValueNode keyValueNode)
9 {
10 string key = keyValueNode.Key;
11 foreach (string name in tabooString)
12 {
13 if (key.Equals(name))
14 {
15 // 普通键值对
16 if (keyValueNode.Child.Count == 0)
17 keyValueNode.SetValue(Encoding.UTF8.GetBytes("somename"));
18 else // 列表项,通常是文件名
19 {
20 // 保留文件名,其余替换为somename
21 string value = ((ListItemNode)keyValueNode.Child[0]).Value;
22 int startIndex = value.LastIndexOf(".");
23 value = String.Format("{0}.{1}", "somename", value.Substring(startIndex+1));
24 (keyValueNode.Child[0] as ListItemNode).SetValue(Encoding.UTF8.GetBytes(value));
25 }
26 break;
27 }
28 }
29 }
30 }
View Code
修改完了之后把BT文件按读取的顺序写回文件就可以了。同样也是一个递归的方法,这里就不再赘述了,至此,BT文件的修改就大功告成了。
小结
除了文中提到的一些功能外,整个小工具还加上了日志记录,批量转换功能,也算是平时闲着的无聊之作吧。
下面回到正题,为什么篇名还要加上福利二字呢?我开始动手写这个修改工具的时候也是没有想到,原来通过还能修改一些动作片的种子文件,从而逃过百度的审查,顺利使用百度云离线。整个工程源码下载见链接部分。至于效果怎么样,谁用谁知道吧^_^
链接
种子文件编码格式:http://www.cnblogs.com/hnrainll/archive/2011/07/26/2117423.html
可执行文件(博客园):https://files.cnblogs.com/files/fantacity/BTTool%287.8%29.rar
Github 项目地址: http://github.com/yosef-gao/BTTool