本文记录了一个前端 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 的定义:
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 标准 |
/ 1e3 | 1719820800 | 除以 1000,转成秒 |
/ 60 | 28663680 | 除以 60,转成分钟 |
/ 24 | 477728 | 除以 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):
/d/g.json 长达数天甚至数月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 返回结构是 [番剧列表, 未知数字, 版本号]。
每条番剧是一个 17 维数组(长度固定,用索引确定字段):
// 从 raw 数组到业务对象的映射
const [id, cn, en, title, city, color, cover, fade,
cat, lat, lng, zoom, pointMeta, abbr, tags, priority, icon] = item;
关键字段:
id:番剧唯一 IDcn / en / title:中文名 / 英文名 / 原始标题lat / lng / zoom:地图定位(纬度、经度、缩放级别)pointMeta:该番剧所有取景地的坐标索引。格式是扁平数组——每 4 个元素为一个点位的信息:[id, lat, lng, priority, id, lat, lng, priority, ...]按番剧 ID 分桶到 6 个 JSON 文件中。每条记录也是一个数组:
const [bangumiId, theme, points[], modified] = entry;
bangumiId:对应 g.json 中番剧的 idtheme:番剧主题图(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 缓存策略、进制转换、分片加载、数组编码压缩,以及前后端数据组装。
如果你也在做一个数据抓取或第三方客户端开发,希望这份笔记对你有帮助。