All
PC硬件
经验记录
生活杂谈
日常踩坑
前端技术
测试
2026-06-02
经验记录
typescript
0
anitabi.cn 接口参数逆向分析

anitabi.cn 接口参数逆向分析

本文记录了一个前端 App 开发中的常见场景:在做 Anime Map 客户端时,需要从 anitabi.cn 获取地图数据。对方没有公开 API 文档,所以只能从网页源码里找接口。


背景

anitabi.cn 是一个动漫巡礼地图站点,上面标注了各番剧的取景地坐标、图片等信息。我想做一个移动端 App 来展示这些数据,第一步就是搞清楚它的数据从哪儿来。

打开 DevTools 的 Network 面板,刷新页面,很快就能看到一些 JSON 请求:

/d/g.json?d=fwl3
/d/g0.json?d=fwl3
/d/g1.json?d=fwl3
...

这些就是我们要的数据接口。但注意 URL 末尾那个 ?d=fwl3——每次刷新可能都不一样,而且直接去掉 ?d= 去请求,大概率会 403 或拿到过期的缓存数据。

所以核心问题变成了:这个 d 参数是怎么生成的?


寻找 d 参数的生成逻辑

第一步:搜索关键词

在 DevTools 的 Sources 面板里全局搜索 d/g.json?d=,很快就能找到这样一段代码:

const P = T();
const h = `/d/g.json?d=${P}`;
const m = await I(h, r => { e(r * 0.3) });

这里 T() 就是生成 d 值的函数。

第二步:定位函数 T

继续往上翻,找 T 的定义:

import { h as z, i as T } from "./5361fdd2.js";

说明 T 是从 5361fdd2.js 导入的,并且在该模块中名叫 i

第三步:找到最终实现

打开 5361fdd2.js,拉到文件末尾看导出映射:

// 伪代码示意
export { ..., Ee as i, ... }

i 就是 Ee。搜索 Ee 函数,找到它的实现:

const Ee = () => (
  Math.floor(Date.now() / 1e3 / 60 / 24) + 6
).toString(36);

就是一行代码。


拆解公式

Math.floor(Date.now() / 1e3 / 60 / 24) + 6).toString(36)

一步步来:

步骤运算含义
Date.now()1719820800000当前时间戳(毫秒),JavaScript 标准
/ 1e31719820800除以 1000,转成秒
/ 6028663680除以 60,转成分钟
/ 24477728除以 24,转成小时……不对,我们其实想做的是:秒 → 分钟 → 小时 → 天

更直观的写法其实是:

Math.floor(Date.now() / (1000 * 60 * 60 * 24))
// = 从 1970.1.1 到现在的天数

1e3 = 1000/ 1e3 / 60 / 24 连除就是先除 1000 得秒,再除 60 得分钟,再除 24 得天。结果是 UTC 天数

然后:

  • + 6:偏移量。不知道 anitabi 为什么选 6,可能是为了和某个起始日期对齐,也可能只是为了和"裸天数"错开,让直接猜 URL 的人多一步。
  • .toString(36):把数字转成 36 进制。36 进制的字符集是 0-9a-z,比 10 进制更紧凑。比如十进制 20613 在 36 进制下是 fwl3,短了一半。

补充知识:Number.prototype.toString(radix) 支持 2-36 进制。36 进制是 JavaScript 内置的最大进制,常用于生成短哈希、短邀请码等。

用当前日期验证一下:

const d = () => (Math.floor(Date.now() / 1000 / 60 / 24) + 6).toString(36);
d(); // => "fwl3"(取决于你看到这篇文章的日期)

如果明天的 URL 是 ?d=fwm4,后天的 URL 是 ?d=fwm5——增长规律是连续的,说明公式成立。

为什么要加这个参数?

d 参数是一个按天轮换的缓存破坏器(Cache Buster)

  • 如果不加,CDN 可能缓存 /d/g.json 长达数天甚至数月
  • 如果加随机数,每次刷新 URL 都不同,CDN 每次都回源,浪费带宽
  • d 按天变化,每天换一次,CDN 每天只回源一次,其余请求都命中缓存

兼顾了数据新鲜度缓存效率


数据接口一览

确认 d 参数后,所有接口就通了。anitabi.cn 一共提供了 8 个 JSON 端点:

接口用途返回结构
/d/g.json?d=xxx番剧列表+总版本号[RawGBangumi[], number, number]
/d/g0.json?d=xxx第 1 批番剧详情RawGDetail[]
/d/g1.json?d=xxx第 2 批番剧详情RawGDetail[]
/d/g2.json?d=xxx第 3 批番剧详情RawGDetail[]
/d/g3.json?d=xxx第 4 批番剧详情RawGDetail[]
/d/g4.json?d=xxx第 5 批番剧详情RawGDetail[]
/d/g5.json?d=xxx第 6 批番剧详情RawGDetail[]
/api/bangumi/icons.svg?d=xxx番剧图标 SVG见下文

g.json:番剧清单

g.json 返回结构是 [番剧列表, 未知数字, 版本号]

每条番剧是一个 17 维数组(长度固定,用索引确定字段):

// 从 raw 数组到业务对象的映射
const [id, cn, en, title, city, color, cover, fade,
       cat, lat, lng, zoom, pointMeta, abbr, tags, priority, icon] = item;

关键字段:

  • id:番剧唯一 ID
  • cn / en / title:中文名 / 英文名 / 原始标题
  • lat / lng / zoom:地图定位(纬度、经度、缩放级别)
  • pointMeta:该番剧所有取景地的坐标索引。格式是扁平数组——每 4 个元素为一个点位的信息:[id, lat, lng, priority, id, lat, lng, priority, ...]

g0-g5.json:番剧详情(分片)

按番剧 ID 分桶到 6 个 JSON 文件中。每条记录也是一个数组:

const [bangumiId, theme, points[], modified] = entry;
  • bangumiId:对应 g.json 中番剧的 id
  • theme:番剧主题图(SVG 图标相关信息)[src, ids[], modified, w, h]
  • points[]:取景地详情列表
  • modified:最后修改时间戳

缓存策略

这套接口的调用流程是:

1. 请求 g.json,拿到 remoteModified
2. 对比本地缓存的 modified
   ├─ 一致 → 直接使用缓存,跳过后续请求
   └─ 不一致 → 并行请求 g0-g5.json,合并数据写入缓存

g.json 的第三个字段就是版本号,用来判断数据是否需要更新。这是一种增量拉取策略:每次先请求一个轻量的摘要文件(g.json),确认有变化再拉详情。


数据组装

拿到所有原始数据后,需要把 g.json 的番剧信息和 g0-g5 的详情合并,这是典型的分片数据整合

合并逻辑示意:

for each 番剧 in g.json:
    detail = detailMap.get(番剧.id)       // 从 g0-g5 中找到对应详情
    
    // 将 pointMeta 建成查找表
    pointLookup = {}
    for (i = 0; i < pointMeta.length; i += 4):
        pointLookup[pointMeta[i]] = { geo, priority }
    
    // 将详情中的裸 point 与坐标索引合并
    points = detail.points.map(rawPoint => {
        meta = pointLookup[rawPoint.id]
        return { ...rawPoint, geo: meta.geo, priority: meta.priority }
    })

为什么 g.json 要把坐标和详情分开?这是一个关注点分离的设计:g.json 用于地图渲染(只需要位置信息),g0-g5 用于详情页展示(需要图片、描述等),而且 g0-g5 的数据量远大于 g.json,可以按需加载。

对开发者来说,这意味着两轮请求和一次归并操作,不算复杂。


一些值得注意的点

1. 数组即 Schema

anitabi 没有用 JSON 对象和字段名,而是用定长数组 + 位置索引来编码数据。这是一种压缩策略——数组的 JSON 序列化比对象短得多(不需要重复写字段名),对于按 KB 计费的大数据量接口,能省下 30-50% 的传输体积。

代价是代码可读性差,需要维护位置到字段的映射表。

2. 接口参数也是逆向产物

d 参数需要逆向 JS 才能拿到算法。这意味着即使别人发现了这些接口,如果没注意到 d 参数会过期,直接硬编码 URL 的话,第二天就失效了——这是一种轻量的反爬手段。

3. CDN 缓存和实时性的平衡

Anitabi 的数据不是实时更新的(按天级别),所以用 d 参数做天级缓存破坏是合理的。对于需要分钟级更新数据的场景,这种方法就不适用了。


总结

整个逆向过程可以概括为 4 步:

Network 面板抓接口 → 发现 d 参数 → Sources 面板搜 JS → 定位 Ee 函数

最终还原出的公式只有一行代码,但它背后涉及:CDN 缓存策略、进制转换、分片加载、数组编码压缩,以及前后端数据组装。

如果你也在做一个数据抓取或第三方客户端开发,希望这份笔记对你有帮助。

Back
© 2022 BBF Powered byNext.js&Prisma&Tailwind.css