哔哩哔哩AV号、BV号转换

哔哩哔哩把以前 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 号 <227< 2^{27} 时正确,同时应该在 <230< 2^{30} 时也是正确的。此代码以 WTFPL 开源。

UPD:之前的代码中,所有数位都被用到是乱凑的,实际上并不需要,目前只要低 6 位就足够了。(更大的 av 号需要 64 位整数存储,但是 b 站现在使用的应该还是 32 位整数,所以应该还要很久)

发现的方法:

首先从各种渠道的信息来看,应该是 base58 编码的。设 x 是一个钦定的 av 号,查询 58k+x,582k+x,583k+x,584k+x(kZ)58k+x,58^{2}k+x,58^{3}k+x,58^{4}k+x(k \in Z) 这些 av 号对应的 bv 号,发现 bv 号的第 12、11、4、9、5 位分别会变化。所以猜测这些是 58 进制下的相应位。

但是直接 base58 是不行的,所以猜测异或了一个大数,并且 base58 的字符表可能打乱了。经过实验,bv 号最低位相同的数,av 号的奇偶性相同,这一定程度上印证了之前的猜想。

接下来找了一些 av 号 xx,满足 xxx+1x+1 对应 bv 号的第 11 位不同。设异或的数为 XX,那么 [Xx58][X(x+1)58]\left [\frac{X\oplus x}{58} \right]\neq \left [\frac{X\oplus (x+1)}{58} \right]\oplus 表示异或)。

由于 av 号(除了最新的少量视频)最多只有 27 bits,所以可以设 X=227a+b(0b227)X=2^{27}a+b(0\leq b\leq 2^{27}) 。然后可以发现 XX 只和 227a mod 582^{27}a\ mod\ 58bb 有关,那么可以枚举这两个值(一共 22758=77846282242^{27} \cdot 58=7784628224 种情况)然后使用上面的式子检查,就能得到若干可能的 XX 只和 227a mod 582^{27}a\ mod\ 58bb

这里我得到的可能值如下:(左边是 227a mod 582^{27}a\ mod\ 58,右边是 bb

1
2
3
4
22 90983642
22 90983643
50 43234084
50 43234085

有奇有偶是因为异或 11 之后也能找到轮换表。而 90983642+43234085=227190983642+43234085=2^{27}-1 则使得模 5858 的余数刚好变成 22712^{27}-1 减它。

我取了 b=43234084,然后处理最低位,可以得到一个字符表,即 fZodR9XQDSUm21yCkr6zBqiveYah8bt4xsWpHnJE7jL5VG3guMTKNPAwcF

对于更高位,实际上还需要知道 227a mod 582,227a mod 583,...2^{27}a\ mod\ 58^{2},2^{27}a\ mod\ 58^{3},...,这些值也可以 枚举 58 次得到,最后我得到的值是 227a mod 584=17499682^{27}a\ mod\ 58^{4}=1749968

这时我发现,每一位的字符表是相同的(实际上只对 b=43234084 是这样的),然后再微调一下参数(上面代码中的两个 magic number 就相当于这里的 a,ba,b),最后处理了一下 227\geq 2^{27} 的情况就得到了这份代码。

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>