哔哩哔哩把以前 av
+ 数字的稿件地址格式更换为 BV
+ 字母数字的格式,类似 Youtube 的稿件编码方式已经有一段时间了。最近闲来无聊,搜了下,发现大佬们已经破解了转换的方法,并给出了 Python 测试代码。正好最近在写小工具合集,写着玩,就用 js 写了一个,留作备用。
前言
哔哩哔哩 AV、BV 号转换
网上应该已经有了类似工具,不过还是喜欢用自己写的,有问题也好改。有需要的可以用一下。
原文(来自知乎mcfx 的答案
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 table='fZodR9XQDSUm21yCkr6zBqiveYah8bt4xsWpHnJE7jL5VG3guMTKNPAwcF' tr={} for i in range (58 ): tr[table[i]]=i s=[11 ,10 ,3 ,8 ,4 ,6 ] xor=177451812 add=8728348608 def dec (x ): r=0 for i in range (6 ): r+=tr[x[s[i]]]*58 **i return (r-add)^xor def enc (x ): x=(x^xor)+add r=list ('BV1 4 1 7 ' ) for i in range (6 ): r[s[i]]=table[x//58 **i%58 ] return '' .join(r) print (dec('BV17x411w7KC' ))print (dec('BV1Q541167Qg' ))print (dec('BV1mK4y1C7Bz' ))print (enc(170001 ))print (enc(455017605 ))print (enc(882584971 ))
互相转换脚本,如果算法没猜错,可以保证在 av 号 < 2 27 < 2^{27} < 2 2 7 时正确,同时应该在 < 2 30 < 2^{30} < 2 3 0 时也是正确的。此代码以 WTFPL 开源。
UPD:之前的代码中,所有数位都被用到是乱凑的,实际上并不需要,目前只要低 6 位就足够了。(更大的 av 号需要 64 位整数存储,但是 b 站现在使用的应该还是 32 位整数,所以应该还要很久)
发现的方法:
首先从各种渠道的信息来看,应该是 base58 编码的。设 x 是一个钦定的 av 号,查询 58 k + x , 5 8 2 k + x , 5 8 3 k + x , 5 8 4 k + x ( k ∈ Z ) 58k+x,58^{2}k+x,58^{3}k+x,58^{4}k+x(k \in Z) 5 8 k + x , 5 8 2 k + x , 5 8 3 k + x , 5 8 4 k + x ( k ∈ Z ) 这些 av 号对应的 bv 号,发现 bv 号的第 12、11、4、9、5 位分别会变化。所以猜测这些是 58 进制下的相应位。
但是直接 base58 是不行的,所以猜测异或了一个大数,并且 base58 的字符表可能打乱了。经过实验,bv 号最低位相同的数,av 号的奇偶性相同,这一定程度上印证了之前的猜想。
接下来找了一些 av 号 x x x ,满足 x x x 和 x + 1 x+1 x + 1 对应 bv 号的第 11 位不同。设异或的数为 X X X ,那么 [ X ⊕ x 58 ] ≠ [ X ⊕ ( x + 1 ) 58 ] \left [\frac{X\oplus x}{58} \right]\neq \left [\frac{X\oplus (x+1)}{58} \right] [ 5 8 X ⊕ x ] = [ 5 8 X ⊕ ( x + 1 ) ] ( ⊕ \oplus ⊕ 表示异或)。
由于 av 号(除了最新的少量视频)最多只有 27 bits,所以可以设 X = 2 27 a + b ( 0 ≤ b ≤ 2 27 ) X=2^{27}a+b(0\leq b\leq 2^{27}) X = 2 2 7 a + b ( 0 ≤ b ≤ 2 2 7 ) 。然后可以发现 X X X 只和 2 27 a m o d 58 2^{27}a\ mod\ 58 2 2 7 a m o d 5 8 和 b b b 有关,那么可以枚举这两个值(一共 2 27 ⋅ 58 = 7784628224 2^{27} \cdot 58=7784628224 2 2 7 ⋅ 5 8 = 7 7 8 4 6 2 8 2 2 4 种情况)然后使用上面的式子检查,就能得到若干可能的 X X X 只和 2 27 a m o d 58 2^{27}a\ mod\ 58 2 2 7 a m o d 5 8 和 b b b 。
这里我得到的可能值如下:(左边是 2 27 a m o d 58 2^{27}a\ mod\ 58 2 2 7 a m o d 5 8 ,右边是 b b b )
1 2 3 4 22 90983642 22 90983643 50 43234084 50 43234085
有奇有偶是因为异或 1 1 1 之后也能找到轮换表。而 90983642 + 43234085 = 2 27 − 1 90983642+43234085=2^{27}-1 9 0 9 8 3 6 4 2 + 4 3 2 3 4 0 8 5 = 2 2 7 − 1 则使得模 58 58 5 8 的余数刚好变成 2 27 − 1 2^{27}-1 2 2 7 − 1 减它。
我取了 b=43234084
,然后处理最低位,可以得到一个字符表,即 fZodR9XQDSUm21yCkr6zBqiveYah8bt4xsWpHnJE7jL5VG3guMTKNPAwcF
。
对于更高位,实际上还需要知道 2 27 a m o d 5 8 2 , 2 27 a m o d 5 8 3 , . . . 2^{27}a\ mod\ 58^{2},2^{27}a\ mod\ 58^{3},... 2 2 7 a m o d 5 8 2 , 2 2 7 a m o d 5 8 3 , . . . ,这些值也可以 枚举 58 次得到,最后我得到的值是 2 27 a m o d 5 8 4 = 1749968 2^{27}a\ mod\ 58^{4}=1749968 2 2 7 a m o d 5 8 4 = 1 7 4 9 9 6 8 。
这时我发现,每一位的字符表是相同的(实际上只对 b=43234084
是这样的),然后再微调一下参数(上面代码中的两个 magic number 就相当于这里的 a , b a,b a , b ),最后处理了一下 ≥ 2 27 \geq 2^{27} ≥ 2 2 7 的情况就得到了这份代码。
Vue + JS 实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 <template > <div > </div > </template > <script > export default { name : "bilibiliBv2av" , data : () => ({ aid : "19390801" , bvid : "" , table : "fZodR9XQDSUm21yCkr6zBqiveYah8bt4xsWpHnJE7jL5VG3guMTKNPAwcF" , tr : {}, s : [11 , 10 , 3 , 8 , 4 , 6 ], xor : 177451812 , add : 8728348608 , }), mounted ( ) { this .init (); }, methods : { init ( ) { for (let i = 0 ; i < 58 ; i++) { this .tr [this .table [i]] = i; } this .enc (this .aid ); }, dec (x ) { try { let r = 0 ; for (let i = 0 ; i < 6 ; i++) { r += this .tr [x[this .s [i]]] * 58 ** i; } this .aid = (r - this .add ) ^ this .xor ; } catch (e) { this .aid = "" ; } }, enc (x ) { try { x = parseInt (x); if (!isNaN (x)) { x = (x ^ this .xor ) + this .add ; const r = [ "B" , "V" , "1" , " " , " " , "4" , " " , "1" , " " , "7" , " " , " " , ]; for (let i = 0 ; i < 6 ; i++) { r[this .s [i]] = this .table [Math .floor (x / 58 ** i) % 58 ]; } this .bvid = r.join ("" ); } else { this .bvid = "" ; } } catch (e) { this .bvid = "" ; } }, }, }; </script >