多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

B站视频ID从AV到BV的进化史:我是怎么在API开发里被坑哭又救回来的

B站视频ID从AV到BV的进化史:我是怎么在API开发里被坑哭又救回来的 说实话,刚接触B站API开发的时候,我整个人是懵圈的。那时候我还年轻,觉得写代码嘛,不就是调个接口,拿个数据,再渲染个页面,完事?太简单了。直到有一天,我想写个脚本去爬取某个UP主的历史视频列表,结果发现以前那种直接拼凑 av123456 这种URL的方式,突然就不灵了。页面跳转过去,要么报错,要么直接404。我去查文档,发现B站搞了个大动作,把原来那种纯数字的AV号(AID),换成了一串看起来像乱码的BV号(BVID)。比如以前是 av170001,现在变成了 BV1GJ411x7h1。这不仅仅是换个名字那么简单。这背后涉及到底层数据库索引的变化、安全性的提升,还有开发者社区的一片哀嚎。今天,我就想跟大家掏心窝子聊聊,我是怎么在这个“双轨制”的坑里摔了一跤,又是怎么通过重构代码,把这个问题彻底解决的。如果你也在做B站相关的开发,或者对这种ID转换机制感兴趣,这篇文章你应该能看进去。先说说为什么B站要搞这么一出。早期的AV号,其实就是数据库里的自增ID。比如第一个视频是av1,第二个是av2。这种ID有个巨大的安全隐患:任何人只要遍历数字,就能把B站所有的视频都爬一遍。这就好比你的家门钥匙是1号、2号、3号这样排下去的,小偷只要拿着钥匙试一遍,你家就没了。所以,B站引入了BV号。BV号基于Base58编码,还加上了校验位和混淆算法。你看那个 BV1GJ411x7h1,它看起来随机,但实际上它里面藏着原始的AV号。这种设计既保证了安全性,防止了恶意爬取,又保留了数据的可追溯性。但是,对于开发者来说,这就成了噩梦。因为B站的历史接口很多还是用AV号作为OID(Object ID)的,比如评论接口、弹幕接口。而新的视频详情接口,可能优先要求BV号。这就导致了一个尴尬的局面:你手里拿着BV号,想调评论接口,得先把它转成AV号;你手里拿着AV号,想调新的视频信息接口,又得把它转成BV号。如果每次调用都去请求B站的服务器帮你转换,那网络延迟就受不了了。而且,频繁请求转换接口,容易被B站的风控判定为异常行为,直接封IP。所以,最聪明的做法,就是在本地实现这个转换算法。这里就要提到一个非常优秀的开源项目:bilibili-api。这个项目由MoyuScript维护,它在GitHub和Gitee上都有镜像。它最核心的贡献之一,就是提供了一个本地化的、高性能的AV/BV互转方案。我研究了一下它的源码,发现这个转换算法其实挺巧妙的。它不是简单的查表,而是通过一系列位运算和字符置换来实现的。核心逻辑在 bilibili_api/utils/aid_bvid_transformer.py 这个文件里。咱们来看看它是怎么干的。首先,它定义了一些常量。比如 XOR_CODE 是一个异或掩码,MASK_CODE 是位掩码,BASE 是58,因为Base58编码嘛。还有一个 BV_LEN 是12,因为BV号固定是12位。转换的核心函数有两个:bvid2aid 和 aid2bvid。咱们先看 bvid2aid,也就是把BV号转回AV号。`python def bvid2aid(bvid: str) -> int:"""BV号转AV号"""# 首先,BV号的字符顺序是经过置换的,不能直接解码# 需要把特定的位置交换回来bvid = list(bvid)# 这里交换了第3和第9位,第4和第7位# 注意Python列表索引从0开始,所以这里是索引3,9和4,7bvid[3], bvid[9] = bvid[9], bvid[3]bvid[4], bvid[7] = bvid[7], bvid[4] # 去掉前缀 "BV1",因为前缀是固定的,不参与数值计算bvid = bvid[3:] tmp = 0# Base58解码for i in bvid:idx = data.index(i.encode()) # data是Base58的字符集tmp = tmp * BASE + idx # 最后通过异或和掩码还原出原始的AV号return (tmp & MASK_CODE) ^ XOR_CODE `这段代码看起来有点硬核,但逻辑很清晰。它先做“逆向置换”,把打乱的字符顺序还原,然后当作一个Base58的大整数进行解码,最后通过异或运算还原出原始的整数ID。反过来,aid2bvid 则是逆过程。`python def aid2bvid(aid: int) -> str:"""AV号转BV号"""# 初始化一个字节数组,填充默认值bytes = [b"B", b"V", b"1", b"0", b"0", b"0", b"0", b"0", b"0", b"0", b"0", b"0"]bv_idx = BV_LEN - 1 # 先进行异或和掩码处理,得到编码前的数值tmp = (MAX_AID | aid) ^ XOR_CODE # Base58编码while int(tmp) != 0:bytes[bv_idx] = data[int(tmp % BASE)]tmp //= BASEbv_idx -= 1 # 还原字符置换bytes[3], bytes[9] = bytes[9], bytes[3]bytes[4], bytes[7] = bytes[7], bytes[4] # 拼接成字符串return "".join([i.decode() for i in bytes]) `你会发现,这两个函数是对称的。这种设计非常优雅,而且速度极快。因为在本地进行位运算,比发起HTTP请求快了几个数量级。但是,光有转换函数还不够。在实际开发中,我们更需要的是一个“智能”的对象,它能自动处理这些细节,让我们开发者不用操心。这就是 bilibili-api 中 Video 类的魅力所在。在 bilibili_api/video.py 中,Video 类的构造函数非常贴心。你可以只传BV号,也可以只传AV号,甚至都不传(如果后续再设置的话)。`python class Video:def __init__(self, bvid: Union[None, str] = None, aid: Union[None, int] = None, credential: Union[None, Credential] = None,):# ID检查:bvid和aid必须提供其中之一if bvid is not None:self.set_bvid(bvid)elif aid is not None:self.set_aid(aid)else:raise ArgsException("请至少提供bvid和aid中的其中一个参数。") def set_bvid(self, bvid: str) -> None:"""设置bvid并自动计算对应的aid"""if not re.search("^BV[a-zA-Z0-9]{10}$", bvid):raise ArgsException("bvid提供错误,必须是以BV开头的纯字母和数字组成的12位字符串。")self.__bvid = bvid# 关键一步:自动转换self.__aid = bvid2aid(bvid) def set_aid(self, aid: int) -> None:"""设置aid并自动计算对应的bvid"""if aid raise ArgsException("aid不能小于或等于0。")self.__aid = aid# 关键一步:自动转换self.__bvid = aid2bvid(aid) `这个设计简直太棒了。这意味着,无论你的数据源给的是AV还是BV,你都可以统一用 Video 对象来包裹它。当你需要调用评论接口时,直接调用 video_obj.get_aid() 就能拿到AV号;当你需要调用视频详情接口时,调用 video_obj.get_bvid() 就能拿到BV号。这种“隐式转换”的方案,极大地降低了开发者的认知负担。你不需要在代码里到处写 if bvid.startswith("BV"): ... else: ... 这种判断逻辑,也不用担心忘记转换导致接口报错。当然,凡事都有两面性。隐式转换虽然方便,但在调试的时候,如果你发现数据不对,可能一时半会儿反应不过来是转换出了问题,还是API返回的问题。所以,我建议大家在开发阶段,可以适当打印一下中间变量,确认转换结果是否符合预期。比如,你可以写一个简单的验证函数:`python def validate_video_identifier(identifier: str) -> Tuple[bool, str, Optional[int]]:"""验证视频标识符并返回类型和转换结果"""if identifier.startswith("BV") and len(identifier) == 12:# 验证BV号格式if re.match(r"^BV[a-zA-Z0-9]{10}$", identifier):try:aid = bvid2aid(identifier)return True, "bvid", aidexcept:return False, "invalid", Noneelif identifier.startswith("av"):# 处理AV号try:aid = int(identifier[2:])if aid > 0:return True, "aid", aidexcept:passreturn False, "unknown", None `这个函数可以帮你快速排查问题。比如,当你拿到一个奇怪的字符串,不确定它是AV还是BV,或者格式有误,这个函数能给你明确的反馈。在实际项目中,我还发现了一个性能优化的点。如果你的应用需要处理大量的视频ID转换,比如批量解析一个UP主的所有视频,那么频繁的函数调用可能会成为瓶颈。虽然本地计算很快,但也不是零成本。这时候,缓存就派上用场了。Python的 functools.lru_cache 装饰器是神器。`python from functools import lru_cache@lru_cache(maxsize=1024) def cached_bvid2aid(bvid: str) -> int:"""带缓存的BV到AV转换"""return bvid2aid(bvid)@lru_cache(maxsize=1024) def cached_aid2bvid(aid: int) -> str:"""带缓存的AV到BV转换"""return aid2bvid(aid) `加上缓存后,重复的转换请求直接从内存中读取结果,速度几乎是瞬间完成的。对于大多数场景,1024的缓存大小已经足够了。毕竟,一个UP主的视频数量通常不会超过这个数,而且热点视频会被反复访问,缓存命中率很高。说到这里,我想再聊聊B站API的整体架构设计。bilibili-api 项目采用了一种分层架构。应用层提供统一的 Video 类,转换层负责底层的算法实现,API适配层则负责根据目标接口的要求,自动选择正确的标识符格式。这种设计思想,其实值得所有API封装库借鉴。很多第三方库,要么只支持BV,要么只支持AV,要么要求开发者手动转换,体验都很差。而 bilibili-api 通过这种“透明化”的处理,让开发者感觉不到底层的变化,这才是好库该有的样子。当然,技术总是在发展的。B站的标识符系统未来会不会再变?很有可能。随着数据量的爆炸式增长,Base58编码可能也会遇到瓶颈。未来可能会出现更复杂的分布式ID生成方案,比如雪花算法的变种,或者基于区块链的不可篡改ID。但无论如何,核心思想不会变:既要保证唯一性和安全性,又要兼顾兼容性和性能。对于咱们开发者来说,面对这种变化,最好的策略就是“拥抱变化,但保持抽象”。不要在你的业务逻辑里硬编码任何ID格式。就像我上面说的,封装一个 Video 对象,或者类似的对象,把所有的ID操作都封装在里面。这样,即使B站明天把BV号改成CV号,你只需要修改封装层的一个函数,业务代码一行都不用改。最后,我想分享一点个人感悟。做API开发,尤其是这种大厂平台的API,往往充满了坑。B站的反爬策略、接口的频繁变更、标识符的诡异转换,每一个都可能让你debug到深夜。但正是这些挑战,逼着我们写出更健壮、更优雅、更抽象的代码。当你看着自己的代码,能够优雅地处理各种边缘情况,能够无缝兼容新旧系统,那种成就感,是写“Hello World”无法比拟的。所以,如果你正在做B站相关的开发,不妨试试 bilibili-api 这个库。它不仅能帮你解决AV/BV转换的问题,还能帮你处理登录状态、弹幕解析、评论获取等一系列复杂操作。它的文档虽然不算特别详细,但源码非常清晰,值得细细研读。毕竟,站在巨人的肩膀上,我们才能看得更远。希望这篇文章能帮到正在坑里挣扎的你。如果有什么疑问,或者有更好的优化建议,欢迎在评论区留言
返回列表