跳至正文
LNN的博客!

奇怪的AV号-BV号转换程序增加了!REMASTERED

是的,时隔六年,我决定重制一下本博客的第一篇文章。

2020年3月,B站推出了 bvid(BV号)来代替之前的 aid(AV号)作为每个视频的唯一编号。bvid 长 12 个字符,开头固定为“BV1”,之后 9 位为 58 进制编码,可通过算法与 aid 相互转换。

时过境迁啊,当初最早被披露的算法已不适用于后来 aid 值较大的视频。这次我用我们的老朋友文言和新朋友 WhatLangUiua 三种语言了实现番号转换,三种语言各有特点,一起来看看吧!

文言 wenyan

注曰。『庚子年春。嗶站易視頻之標識。廢舊之亞號。立新號曰彼號。
凡十二字。首三字恆為「BV1」。後九字以五十八進制編碼。可依算法與舊號互易轉化。』

吾嘗觀位經之書方悟異或」「補零右移之義
吾嘗觀列經之書方悟索一之義

注曰。『數之極大。「位經」之法不可算其全。乃一分爲二。各算異或。銜之而得全。』
吾有四二九四九六七二九六名之曰大數界」。
吾有名之曰大異或」。欲行是術必先得」。乃行是術曰
  異或」。補零右移名之曰」。
  大數界」。大數界」。以施異或」。名之曰」。
  大數界」。乃得矣
是謂大異或之術也

吾有『FcwAPNKTMug3GV5Lj7EJnHpWsx4tb8haYeviqBz6rkCy12mUSDQX9RdoZf』名之曰字母表」。
吾有名之曰亂序表」。亂序表
吾有二二七五二四二六四一四七六八二七名之曰異或數」。

今有名之曰亞號至彼號」。欲行是術必先得亞號」。乃行是術曰
  大異或亞號異或數」。名之曰」。
  吾有名之曰數位」。
  為是
    五十八所餘幾何名之曰」。
    數位」。
    」。五十八昔之是矣
  云云
  吾有『BV1』名之曰彼號」。
  亂序表中之
    數位」。字母表彼號」。昔之彼號是矣
  云云
  乃得彼號」。
是謂亞號至彼號之術也

今有名之曰彼號至亞號」。欲行是術必先得彼號」。乃行是術曰
  吾有名之曰數位」。
  吾有名之曰」。
  亂序表中之
    彼號」。名之曰」。
    索一字母表」。昔之數位是矣
    昔之是矣
  云云
  吾有名之曰」。
  為是
    數位」。名之曰」。
    五十八」。昔之是矣
    昔之是矣
  云云
  大異或異或數」。乃得矣
是謂彼號至亞號之術也

亞號至彼號114771575769869」。書之
彼號至亞號『BV1MugkzCEBn』『AV』書之

文言的默认编译目标 JavaScript 并不支持超过 32 位整数的按位运算,不过好在 B 站视频的 aid 仍然在 64 位浮点数能够正确处理的整数范围内,为此我引入了一个大異或函数来实现:

function bigXor(a, b) {
  const low = (a ^ b) >>> 0 // 直接计算异或(只保留低 32 位)并转为无符号
  const high = (a / 4294967296) ^ (b / 4294967296) // 相当于 a、b 都右移 32 位后计算异或
  return high * 4294967296 + low // 搞半天还要自己拼
}

WhatLang啥语言

(2>|:&&:&\/flr@:&*-]<)divmod=_
(2>:< bxor@ \(4294967296/)#\_<bxor@ 4294967296*+)bigxor=_
(
  2275242641476827bigxor@
  58divmod@\ 58divmod@\ 58divmod@\ 58divmod@\ 58divmod@\ 58divmod@\ 58divmod@\ 58divmod@\ 9>
  2,\8,2\;\8\; 4,\7,4\;\7\; reverse@\_
  ("FcwAPNKTMug3GV5Lj7EJnHpWsx4tb8haYeviqBz6rkCy12mUSDQX9RdoZf"\,)#\_ ""join@\_ "BV1"\+
)av2bv=_
(
  [(^BV1?)]""repl@ ("FcwAPNKTMug3GV5Lj7EJnHpWsx4tb8haYeviqBz6rkCy12mUSDQX9RdoZf"\in@)#\_
  reverse@\_ 2,\8,2\;\8\; 4,\7,4\;\7\;
  |58*+ 58*+ 58*+ 58*+ 58*+ 58*+ 58*+ 58*+]01-,\_
  2275242641476827bigxor@
)bv2av=_

114771575769869 av2bv@. `\n`
"BV1MugkzCEBn" bv2av@. `\n`

WhatLang 实现的基本思路与文言大差不差,用到了来自 LNN 函数库的经典函数 divmod 来进行进制转换,也定义了一个相同的 bigxor 函数。秉持函数内尽量避免使用变量(因为变量都是全局的)的原则,在进制转换部分我直接把操作写 8 遍,而非使用循环;排列数位顺序使用了倒序再进行两次交换的方法,而非直接查表。正则表达式在 WhatLang 中十分常用,因此在 bvid 转 aid 时,我顺手利用正则替换让输入的字符串可以省略前缀 BV1,或只省略 BV 保留一个 1 也可以。(文言和 Uiua 实现都必须带上 BV1 前缀,其实纯粹是懒了)

Uiua

┌─╴Bvid
  Digits  "FcwAPNKTMug3GV5Lj7EJnHpWsx4tb8haYeviqBz6rkCy12mUSDQX9RdoZf"
  Mask    2275242641476827
  Order   2_4_6_5_7_3_8_1_0
  Call    "BV1" ˜Digits 0Order 58 ((0|0)Mask)
└─╴
Bvid 114771575769869
°Bvid "BV1MugkzCEBn"

作为阵列编程(array programming)语言,Uiua 这边的画风完全不一样了。整个 Bvid~Call 函数最复杂的部分居然是计算按位异或:常量 Mask 为异或常数的二进制展开,(0|0) 即不论正着还是反着运行,都对相对应的二进制位进行“不等于”运算,位数较短的补零。58 进制的转换也是用一个内置函数( base)就搞定了,剩下排列数位顺序和对应字母表两次查表,最后在开头补上 "BV1" 完事。

得益于 Uiua 的反函数机制,这个函数正着跑是 aid 转 bvid,反着跑(° un)就是 bvid 转 aid,除了按位异或的部分需要手动定义反函数以外全都可以自动完成,十分舒适。

最后还是附上完整的 JavaScript 实现

const TABLE = "FcwAPNKTMug3GV5Lj7EJnHpWsx4tb8haYeviqBz6rkCy12mUSDQX9RdoZf"
const MASK = 2275242641476827n

function av2bv(aid) {
  const digits = []
  let n = BigInt(aid) ^ MASK
  for (let i = 0; i < 9; i++) {
    digits.push(TABLE[Number(n % 58n)])
    n /= 58n
  }
  ;[digits[2], digits[8]] = [digits[8], digits[2]]
  ;[digits[4], digits[7]] = [digits[7], digits[4]]
  digits.reverse()
  return "BV1" + digits.join("")
}
function bv2av(bvid) {
  bvid = bvid.replace(/^BV1?/, "")
  const digits = bvid.split("", 9).reverse()
  ;[digits[2], digits[8]] = [digits[8], digits[2]]
  ;[digits[4], digits[7]] = [digits[7], digits[4]]
  let n = 0n
  for (let i = 0; i < 9; i++) {
    n += BigInt(TABLE.indexOf(digits[i]) * (58 ** i))
  }
  return Number(n ^ MASK)
}

不使用 BigInt:

const TABLE = "FcwAPNKTMug3GV5Lj7EJnHpWsx4tb8haYeviqBz6rkCy12mUSDQX9RdoZf"
const MASK = 2275242641476827
function bigXor(a, b) {
  const low = (a ^ b) >>> 0
  const high = (a / 4294967296) ^ (b / 4294967296)
  return high * 4294967296 + low
}

function av2bv(aid) {
  const digits = []
  let n = bigXor(aid, MASK)
  for (let i = 0; i < 9; i++) {
    digits.push(TABLE[n % 58])
    n = Math.floor(n / 58)
  }
  ;[digits[2], digits[8]] = [digits[8], digits[2]]
  ;[digits[4], digits[7]] = [digits[7], digits[4]]
  digits.reverse()
  return "BV1" + digits.join("")
}
function bv2av(bvid) {
  bvid = bvid.replace(/^BV1?/, "")
  const digits = bvid.split("", 9).reverse()
  ;[digits[2], digits[8]] = [digits[8], digits[2]]
  ;[digits[4], digits[7]] = [digits[7], digits[4]]
  let n = 0
  for (let i = 0; i < 9; i++) {
    n += TABLE.indexOf(digits[i]) * (58 ** i)
  }
  return bigXor(n, MASK)
}

评论区

加载基于 GitHub issues 的 utteranc.es 评论区组件……