tagsuggestion标签联想 API 接口文档

在线地址:https://tagsuggest.zeabur.app/api/tags/suggest
无需 API Key、无需注册,浏览器里可以直接 fetch(已开放 CORS)。

这个接口本来是给 WeBooru 自己的搜索框做输入联想的:你打「海贼」它告诉你对应的 tag 是 One Piece,你打 miku 它告诉你中文叫「初音未来」。因为数据本身就是一份维护了很久的中英日对照标签库,所以单独拎出来也能当一个小工具用——比如给你自己的图站加联想、批量把 tag 翻成中文、写脚本查某个 tag 大概有多少作品。


一、请求

1
GET /api/tags/suggest?q=<关键词>&source=<图源>
参数 类型 必填 说明
q string 是 要查的关键词,最少 1 个字符。可以是中文、日文、英文
source string 否 传 pixiv 查 Pixiv 标签库;不传或传其它值查 Booru 标签库

完整示例:

1
https://tagsuggest.zeabur.app/api/tags/suggest?q=one_piece&source=pixiv

[!IMPORTANT]
这是公益站自用的接口,不承诺 SLA:可能限流、可能随站点改版调整参数。个人小工具、自用脚本随便调,请不要做高频轮询或大规模抓取。


二、返回结构

固定返回一个对象,结果放在 results 数组里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
"results": [
{
"name": "One Piece",
"cn_name": "海贼王",
"count": 263718,
"category": 0
},
{
"name": "Uta (One Piece)",
"cn_name": "乌塔(海贼王)",
"count": 24353,
"category": 0
},
{
"name": "One Piece 100 Users Bookmark",
"cn_name": "海贼王_100users收藏",
"count": 19583,
"category": 0
}
]
}
字段 类型 说明
name string 该图源上真正使用的标签本体。Booru 用下划线,Pixiv 用空格,直接拿去搜索即可
cn_name string 中文译名。可能是空的字符串
count number 热度:Pixiv 库是收藏数,Booru 库是作品数
category number 标签类别,仅 Booru 库有意义,见下表

category 取值(Pixiv 库恒为 0):

值 含义 界面上的颜色
0 通用 / 描述性标签 蓝
1 画师 红
3 作品 / 版权 紫
4 角色 绿
5 元数据 / 风格 橙

上面那份 one_piece 的 Booru 结果就是很好的例子:one_piece 是作品(category: 3),而 one-piece_swimsuit(连体泳衣)只是恰好同名的通用标签(category: 0)。


三、匹配规则(本篇最该看的一段)

这些规则来自实现本身,踩过才知道:

  1. 只匹配一个词。 查询串里只要出现空格,直接返回空数组,不会去猜。所以前端联想都是取「最后一个空格后的那一段」再发请求。

  2. 中文走中文列,其它走原名列。 关键词里含中文(CJK 字符)时匹配 cn_name 字段;否则匹配 name 字段,且不区分大小写。

  3. 纯数字(≥4 位)会返回两条「快捷指令」,而不是搜索标签——这是给站内搜索用的暗号:

    1
    2
    3
    4
    5
    6
    {
    "results": [
    { "name": "pixiv_user:1234567", "cn_name": "uid搜索作者:{q}在pixiv图源搜哦", "count": 0 },
    { "name": "pixiv_pool:1234567", "cn_name": "pid搜索作品:{q}", "count": 0 }
    ]
    }

    {q} 是字面量,没有被替换——当作已知的小瑕疵即可。把返回的 name 原样粘进 WeBooru 的搜索框,就能跳到「该 UID 画师的作品」或「该 PID 的作品」。

  4. 最多 30 条,按热度(count)从高到低排。命中的标签超过 30 个时只给最热的前 30 条。

  5. 开头的 - 会被忽略(-miku 与 miku 等价)。

  6. 出错不报错:数据库查询异常时也返回 HTTP 200 + {"results": []},方便前端少写分支。唯一的例外是完全没传 q,此时是 422:

    1
    { "detail": [ { "type": "missing", "loc": ["query", "q"], "msg": "Field required", "input": null } ] }

四、动手试

4.1 在浏览器地址栏直接看

把下面这条贴进地址栏回车,就是上面那份 JSON(Firefox / Chrome 会自动格式化):

1
https://tagsuggest.zeabur.app/api/tags/suggest?q=one_piece&source=pixiv

4.2 curl

1
2
3
4
5
6
7
8
9
10
11
12
13
# Pixiv 库:英文/日文关键词
curl "https://tagsuggest.zeabur.app/api/tags/suggest?q=one_piece&source=pixiv"

# Pixiv 库:中文关键词(走 cn_name 匹配)
# 中文要先编码再放进 URL,否则会被判成非法请求(400)
curl -G "https://tagsuggest.zeabur.app/api/tags/suggest" \
--data-urlencode "q=海贼" --data-urlencode "source=pixiv"

# 也可以直接写百分号编码:q=%E6%B5%B7%E8%B4%BC
curl "https://tagsuggest.zeabur.app/api/tags/suggest?q=%E6%B5%B7%E8%B4%BC&source=pixiv"

# Booru 库(不带 source)
curl "https://tagsuggest.zeabur.app/api/tags/suggest?q=one_piece"

[!TIP]
直接把中文写进 URL 丢给 curl,服务器会返回 400——这是 curl 不做百分号编码导致的,
浏览器、requests、fetch 都会自动处理,所以只有手写 curl 时会踩到。用 --data-urlencode
或预先编码即可。

4.3 Python

1
2
3
4
5
6
7
8
9
10
11
12
import requests

API = "https://tagsuggest.zeabur.app/api/tags/suggest"

def suggest(word: str, source: str = ""):
"""查标签联想;source="pixiv" 走 Pixiv 库,否则走 Booru 库。"""
r = requests.get(API, params={"q": word, "source": source}, timeout=10)
r.raise_for_status()
return r.json().get("results", [])

for item in suggest("初音", "pixiv")[:5]:
print(f'{item["name"]:<28} {item["cn_name"]:<24} {item["count"]:,}')

输出大致是:

1
2
3
hatsune miku                 初音未来(VOCALOID)        145,600
yuki miku 雪初音(初音未来) 5,200
asahina mafuyu 朝比奈真冬(世界计划_多彩舞台!_feat._初音未来) 4,100

4.4 JavaScript(浏览器 / Node 18+)

1
2
3
4
5
6
7
8
9
const API = "https://tagsuggest.zeabur.app/api/tags/suggest";

async function suggest(word, source = "") {
const url = `${API}?q=${encodeURIComponent(word)}&source=${encodeURIComponent(source)}`;
const resp = await fetch(url);
return (await resp.json()).results;
}

console.log(await suggest("miku"));

4.5 单文件 Demo(复制即用)

下面这段存成 demo.html 双击打开,就是一个带键盘操作的联想输入框。它演示了三件正事:取最后一个词、丢弃过期响应(输入快时防止旧结果覆盖新结果)、点选后补一个空格。

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
71
72
73
74
75
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>WeBooru 标签联想 Demo</title>
<style>
body { font: 15px/1.6 system-ui, sans-serif; max-width: 620px; margin: 48px auto; }
.field { position: relative; }
#q { width: 100%; padding: 10px 12px; font-size: 15px; box-sizing: border-box; }
#list { position: absolute; left: 0; right: 0; max-height: 300px; overflow-y: auto;
display: none; border: 1px solid #ccc; background: #fff; z-index: 10; }
.row { display: flex; gap: 8px; padding: 7px 10px; cursor: pointer; }
.row:hover, .row.active { background: #eef4ff; }
.en { color: #0073ff; font-family: monospace; }
.cnt { margin-left: auto; color: #888; font-size: 12px; }
</style>
</head>
<body>
<input id="q" placeholder="试试:one_piece / 海贼 / 初音">
<select id="src">
<option value="pixiv">pixiv</option>
<option value="">booru</option>
</select>
<div id="list"></div>
<p id="status" style="color:#888;font-size:12px"></p>

<script>
const API = "https://tagsuggest.zeabur.app/api/tags/suggest";
const $q = document.getElementById("q"), $list = document.getElementById("list");
const $src = document.getElementById("src"), $status = document.getElementById("status");
let items = [], active = -1, timer = null, seq = 0;

const fmt = n => n >= 1e6 ? (n/1e6).toFixed(1)+"M" : n >= 1e3 ? (n/1e3).toFixed(1)+"k" : String(n);
const render = () => {
$list.innerHTML = items.map((it, i) =>
`<div class="row${i === active ? " active" : ""}" data-i="${i}">
<span class="en">${it.name.replace(/_/g, " ")}</span>
<span>${it.cn_name || ""}</span><span class="cnt">${fmt(it.count)}</span>
</div>`).join("");
$list.style.display = items.length ? "block" : "none";
};

async function query(word) {
const url = `${API}?q=${encodeURIComponent(word)}&source=${encodeURIComponent($src.value)}`;
const mine = ++seq; // 丢弃过期响应
const resp = await fetch(url);
const data = await resp.json();
if (mine !== seq) return;
items = data.results || []; active = items.length ? 0 : -1;
render(); $status.textContent = `${items.length} 条结果`;
}

$q.addEventListener("input", () => {
const word = $q.value.split(/\s+/).pop(); // 只查最后一个词
clearTimeout(timer);
if (!word) { items = []; render(); return; }
timer = setTimeout(() => query(word), 250);
});

$q.addEventListener("keydown", e => {
if (!items.length) return;
if (e.key === "ArrowDown") { active = (active + 1) % items.length; render(); e.preventDefault(); }
if (e.key === "ArrowUp") { active = (active - 1 + items.length) % items.length; render(); e.preventDefault(); }
if (e.key === "Enter" && active >= 0) { $q.value = items[active].name + " "; items = []; render(); }
if (e.key === "Escape") { items = []; render(); }
});

$list.addEventListener("click", e => {
const row = e.target.closest(".row"); if (!row) return;
$q.value = items[+row.dataset.i].name + " "; items = []; render(); $q.focus();
});
document.addEventListener("click", e => { if (!e.target.closest(".field, #list")) { items = []; render(); } });
</script>
</body>
</html>

Demo 的实际效果:

标签联想 Demo:输入 one_piece 后返回 30 条,含中文译名与热度

中文关键词同样能查(走中文列):

同一个 Demo 输入「海贼」的结果,第一条是 ONEPIECE


五、什么时候用哪个库

Booru 库(source 留空) Pixiv 库(source=pixiv)
标签写法 hatsune_miku(下划线) hatsune miku / 初音ミク(空格)
热度含义 图站作品数 Pixiv 作品数
有类别信息 有(画师/作品/角色…) 无,恒为 0
适合 Danbooru / Gelbooru / Yande 等图站场景 Pixiv 场景、中文找日文 tag

两边是两套独立维护的词表,同一个概念可能查到不同结果,这是正常的——one_piece 在 Booru 库排第一的是「连体泳衣」,在 Pixiv 库排第一的才是「海贼王」。


六、相关接口

接口 作用 说明
POST /api/tags/sync 从线上仓库重新拉取整套标签库 管理操作,需在站内后台点击,约 38 MB
POST /api/tag/correct 提交某条标签的译名纠错 站内点「纠错」时调用,见 纠错说明

标签库本身由两个长期维护的 GitHub 仓库提供,站内可一键同步:

  • ffdkj/Pixiv_Tag-Chinese-English-Translation-Table
  • ffdkj/Danbooru_Tag-Chinese-English-Translation-Table

七、常见问题

Q:能不能商用 / 大量调用?
数据来自公开翻译表,接口本身无鉴权。请自用、低频调用;要做正式产品建议自己拉取那两个仓库建库。

Q:为什么中文搜出来的 count 很大但 name 是空的?
不会为空。若某个词在库里没有译名,cn_name 会是空字符串(""),此时该条只会靠 name 命中。

Q:source 传了 danbooru 会怎样?
和留空一样,走 Booru 库。只有精确传 pixiv 才切到 Pixiv 库。

Q:返回 30 条,怎么拿更多?
不支持分页。把关键词写长一点、更精确即可。也可以把github的翻译库下到本地就可以换着花样查了。


本文对应的站点版本:webooru_server_version_sfw,接口行为以线上实测为准(示例响应均为真实返回)。