# Tags de personaje y contenido R-18 en Pixiv — informe técnico

Fecha: 28-10-2026. Leyenda: ✅ = verificado con fuente o ejecutado hoy · ⚠️ = inferencia o práctica de comunidad sin documentación oficial.

## 0. Resumen

- **Leer tags/popularidad: sí.** `pixivpy3` sigue funcionando y hay un endpoint web público (`/rpc/cps.php`) que resuelve nombre → tag japonés con su traducción inglesa. Es la pieza que faltaba en el pipeline.
- **Publicar por API: no.** No hay API oficial de escritura y no he encontrado ninguna librería mantenida que suba ilustraciones. La subida es manual (o navegador automatizado, con riesgo).
- **El R-18 no es un parámetro.** Ni `filter` ni `search_target` lo controlan: depende del ajuste de la cuenta y, en la práctica, del truco de añadir `R-18` al término de búsqueda.
- **El etiquetado IA no es un tag, es una casilla obligatoria** del formulario. Y **FANBOX prohíbe el contenido generado por IA**, así que no es un destino válido para este proyecto.

## 1. `pixivpy3`

### Estado

| Dato | Valor |
|---|---|
| PyPI última versión | **3.7.5**, publicada 2024-03-03 ✅ |
| Versión en `master` | **3.13.0** (`pyproject.toml`), sin publicar ✅ |
| Último push al repo | 2025-07-31 ✅ |
| Estrellas / issues abiertos | 2058 / 57 ✅ |
| Licencia | Unlicense |

El paquete **no está muerto pero sí ralentizado**: casi dos años sin release en PyPI mientras `master` va seis versiones por delante. Instala desde git si quieres los modelos pydantic y `webview_novel()`:

```bash
pip install -U "git+https://github.com/upbit/pixivpy.git"
```

⚠️ Consecuencia práctica: en 3.7.5 las respuestas son `JsonDict` (acceso por atributo y por clave); en 3.13 son modelos pydantic. El código de abajo funciona con ambas.

### Autenticación (2025-2026)

✅ El login por usuario/contraseña **ya no existe** (README, issue #158): Pixiv puso reCAPTCHA. El único camino es el refresh token.

✅ Forma actual de obtenerlo: [`gppt`](https://github.com/eggplants/get-pixivpy-token) v5, que ya soporta **OAuth2 PKCE** (pegas el código, sin contraseña) además del flujo e2e con Selenium. Repo con push el 2026-10-02.

```bash
pip install gppt
gppt configure   # usuario+password, o método "oauth"
gppt login       # escribe ~/.config/gppt/<perfil>.token.json
```

```python
import gppt
from pixivpy3 import AppPixivAPI

tok = gppt.get_token()
api = AppPixivAPI()
api.auth(refresh_token=tok.refresh_token)   # rota el refresh token: guarda el nuevo
```

⚠️ El refresh token **caduca/rota**; hay issues de cuentas cuyo token deja de refrescar (#374). Persiste el token que devuelve `auth()`.

### `AppPixivAPI` vs `PixivAPI` vs `ByPassSniApi`

| Clase | Qué es | Estado |
|---|---|---|
| `AppPixivAPI` | API de la app iOS (`app-api.pixiv.net`) | ✅ la única que se usa |
| `PixivAPI` | API pública antigua | ✅ eliminada del paquete (2022, commit por #201) |
| `ByPassSniApi` | `AppPixivAPI` + DoH + `HostHeaderSSLAdapter` para saltarse el bloqueo DNS/SNI de `app-api.secure.pixiv.net` | ✅ existe, pero **solo sirve para China**; no arregla nada más |

✅ **Probado hoy (28-10-2026): sin token, todo `app-api` devuelve `400 OAuth process ... invalid_request`** — incluidos `illust_detail` y `illust_ranking`, que históricamente funcionaban sin auth. En 2026 el token es obligatorio para todo.

### Endpoints útiles (firmas copiadas de `pixivpy3/aapi.py`)

| Método | Firma real (resumida) |
|---|---|
| `search_illust` | `(word, search_target="partial_match_for_tags", sort="date_desc", duration=None, start_date=None, end_date=None, filter="for_ios", search_ai_type=None, offset=None, req_auth=True)` |
| `illust_detail` | `(illust_id, req_auth=True)` |
| `illust_related` | `(illust_id, filter="for_ios", seed_illust_ids=None, offset=None, viewed=None)` |
| `user_illusts` | `(user_id, type="illust", filter="for_ios", offset=None)` |
| `user_bookmarks_illust` | `(user_id, restrict="public", filter="for_ios", max_bookmark_id=None, tag=None)` |
| `illust_ranking` | `(mode="day", filter="for_ios", date=None, offset=None)` |
| `trending_tags_illust` | `(filter="for_ios")` |
| `search_novel` | `(word, search_target=..., sort="date_desc", merge_plain_keyword_results="true", ...)` |
| `parse_qs` | `(next_url)` — classmethod, para paginar |

✅ **No existen** en la librería `illust_search` (es `search_illust`) ni `search_autocomplete`. Lo que hace de autocompletado es el endpoint web de la sección 3.

### R-18: no hay parámetro

✅ `search_illust` no tiene `mode` ni ningún flag de contenido. El owner del repo lo dice en [#365](https://github.com/upbit/pixivpy/issues/365): *«除了 ranking 这类榜单接口，search 是没有"只筛选 R-18"选项的»*, y la solución que él mismo propone es **añadir `R-18` al término de búsqueda**.

✅ `filter` **no filtra contenido**: es un flag de formato de respuesta de la app iOS (`for_ios`), presente también en `illust_detail`. Issue abierta [#370](https://github.com/upbit/pixivpy/issues/370): pasarlo vacío (`filter=""`) provoca `"filter contains invalid value"` en `search_illust`. Omítelo.

✅ Las cuentas sin R-18 activado no ven R-18 por ninguna vía. El tipo `_MODE` incluye `day_r18`, `day_male_r18`, `day_female_r18`, `week_r18`, `week_r18g` para `illust_ranking`, pero el comentario del propio código los marca como *"(Past)"* ⚠️ — no he podido comprobar que sigan vivos sin token.

✅ `search_ai_type`: `0` = todo, `1` = solo humano, `2` = solo IA; y `illust_ai_type` en la respuesta: `1` humano, `2` IA ([#410](https://github.com/upbit/pixivpy/issues/410), corrección de docs aún sin mergear). Confirmado de forma independiente: una obra con tag `AIイラスト` devuelve `aiType: 2` en el ajax web.

### Rate limits y baneo

⚠️ Pixiv **no publica límites**. Lo que sí está escrito en las [políticas](https://policies.pixiv.net/): *«クローラーなどのプログラムを使って作品を収集する行為、サーバに極端な負荷をかける行為は禁止します»* — recolección masiva con crawlers y carga extrema están prohibidas, y se reservan el derecho a exigirte que pares. Práctica de comunidad: 1 req/s o menos, backoff exponencial, una sola cuenta, nada de ráfagas de cientos de páginas.

## 2. Alternativas y estado de las librerías

| Nombre | Plataforma | Última versión | Estado |
|---|---|---|---|
| `pixivpy3` | PyPI | 3.7.5 (2024-03) | ✅ viva, mantenimiento lento |
| `pixivpy` | PyPI | 3.7.0 (2022-02) | ✅ mismo repo, alias de Python 2 |
| `pixiv-api` | PyPI | 1.0.0 (2021-02), push 2024-08 | ⚠️ abandonada de facto |
| `pixiv-api-client` | **npm** (no PyPI: 404) | 0.27.0 (2024-04) | ⚠️ viva a medias, JS |
| `pixiv-app-api` | npm | 1.2.1 (2020) | ❌ muerta |
| `pixiv.ts` | npm | 0.8.9 (2026-03) | ✅ viva pero **repo archivado** |
| `PixivUtil2` | Python, CLI | activo | ✅ descarga masiva, no publica |
| `pixiv.moe` | web | — | ❌ SSL roto, inaccesible hoy |

### Sin autenticación: ajax web

✅ Endpoints vivos hoy, documentados en [pixiv-ajax-api-docs](https://github.com/daydreamer-json/pixiv-ajax-api-docs) (repo **desactualizado**, aviso del propio autor):

| Uso | URL |
|---|---|
| Buscar obras | `GET /ajax/search/artworks/{kw}?word={kw}&mode=all\|safe\|r18&s_mode=s_tag_full\|s_tag&order=date_d&p=N` |
| Info de tag | `GET /ajax/search/tags/{tag}` |
| Autocompletado | `GET /rpc/cps.php?keyword={kw}&lang=ja\|en` |
| Ranking | `GET /ranking.php?format=json&mode=daily_r18` |

✅ **Probado**: `mode=r18` **sin login devuelve 200 pero ignora el filtro** (todas las obras con `xRestrict: 0`). No da error: falla en silencio, que es peor. Con cookie `PHPSESSID` de una cuenta con R-18 activo sí funciona.

✅ Riesgo real de bloqueo: `dic.pixiv.net` responde **403 con challenge de Cloudflare** a peticiones con UA de navegador. `www.pixiv.net` admite hoy el ajax sin cookie, pero eso puede cambiar sin aviso.

### API oficial

✅ **No existe API pública de terceros.** El blog oficial de developers lo decía en 2016 (*«pixivにはパブリックなAPIが提供されていない»*) y no hay anuncio posterior. Lo único "oficial" es `pixiv ID連携` (login OAuth para identificar usuarios) y acuerdos de partnership, ninguno de los cuales da lectura ni escritura de obras. ⚠️ No he encontrado ningún programa de socios abierto con acceso a ilustraciones.

### Publicar: no

✅ No hay endpoint de subida en `app-api` (la lista completa de métodos de `aapi.py` no incluye ninguno de creación de obra; lo más cercano son `illust_bookmark_add` y `user_follow_add`).
✅ Búsqueda en GitHub de *pixiv uploader*: 4 repos, el más relevante con 8 estrellas. No hay herramienta mantenida.
⚠️ Automatizar la subida con Selenium/Playwright es técnicamente posible y **choca con la cláusula de carga del servidor** citada arriba. No lo recomiendo.

## 3. Tags e idioma

### El puente JP ↔ EN que sí funciona

✅ `GET https://www.pixiv.net/rpc/cps.php?keyword=...&lang=...` devuelve candidatos con `tag_name`, `access_count`, `type` (`prefix` | `tag_translation`) y, cuando buscas en inglés, `tag_translation`. Probado hoy:

| Consulta (`lang=en`) | `tag_name` | `tag_translation` | `access_count` |
|---|---|---|---|
| `hatsune` | 初音ミク | hatsune miku | 2.394.975.205 |
| `frieren` | 葬送のフリーレン | Frieren: Beyond Journey's End | 1.162.524.122 |
| `yor forger` | ヨル・フォージャー | Yor Forger | 592.214.710 |

Y al revés (`lang=ja`) devuelve `初音ミク`, `フリーレン`, `オリジナル`… con su `access_count`. **Esto resuelve nombre → tag japonés canónico sin token.**

Complementos ✅:
- `GET /ajax/search/tags/{tag}` → `pixpedia.abstract`, `yomigana`, `siblingsTags`, `childrenTags` y `tagTranslation.romaji` (sirve para desambiguar y detectar variantes).
- En `app-api` con `set_accept_language("en-us")`, cada tag trae `translated_name` en inglés (`example_tag_translations.py` del repo).

⚠️ El autocompletado **no es exhaustivo con términos explícitos**: `中出し` no apareció como tag exacto (solo `中出し固め`, `中出しごっくん`), mientras `断面図` sí. Verifica los tags R-18 contra la página del tag antes de darlos por buenos.

### Convenciones

| Tag | Quién lo pone | Nota |
|---|---|---|
| `R-18` / `R-18G` | el autor | ✅ **no basta**: hay que activar además la restricción de edad de la obra. «タグを付けるだけでは年齢制限が行われない» ([ayuda](https://www.pixiv.help/hc/ja/articles/235646007)) |
| `オリジナル` | el autor | para OC; si es fanart, va el tag de la serie |
| `AIイラスト` / `AI生成` | el autor (tag) | ✅ **no es el mecanismo oficial**: el oficial es la casilla «AI生成作品» |
| `10000users入り` | **otros usuarios** | ✅ no lo pone el autor ni Pixiv: lo añaden lectores cuando la obra pasa el umbral ([百科](https://dic.pixiv.net/a/users入り)). No lo pongas tú |

✅ **Límite de tags: 10 por obra, mínimo 1.** Otros usuarios pueden añadir y borrar tags; el autor puede bloquearlos («タグをロック», «他のユーザーによるタグ編集を許可しない») — [ayuda oficial](https://www.pixiv.help/hc/ja/articles/235585008).

✅ **X/Twitter**: límite duro son los 280 caracteres; la recomendación oficial y de la industria es **1-2 hashtags**, y a partir de 3 el clasificador de spam penaliza. Para el crosspost: 2 como máximo.

### Etiquetado de IA (obligatorio)

✅ Según el [anuncio oficial del 31-05-2023](https://www.pixiv.net/info.php?id=9640): *«all posts for which all or most of the creative process was carried out by AI must be labeled as "AI-generated"»*, y Pixiv **puede añadir la etiqueta por su cuenta** si detecta que la obra es IA. En la revisión de guías de **2026-03-18** se añadió como prohibición explícita **poner un ajuste que no coincida con el contenido** (falso negativo *y* falso positivo). Consecuencia práctica: si no marcas la casilla, la obra puede quedar etiquetada igualmente, y los usuarios que filtran IA no la verán.

✅ **pixivFANBOX prohíbe el contenido generado por IA** desde 2023-05-31 (mismo anuncio + [política de FANBOX](https://official.fanbox.cc/posts/5932126) + [ayuda](https://fanbox.pixiv.help/hc/ja/articles/20299395982489)). También `pixivリクエスト`. **Pixiv sí lo permite.**

### Mapeo catálogo → tags

✅ Ya tienes la mitad hecha: `Engine/character_keys.py` da una clave estable por personaje y `Archivos/Danbooru tags.csv` (columna `type`: 3=copyright, 4=personaje) valida existencia. Flujo realista:

1. **Nombre de la clave** (`Furina`, `Makima_v2`) → limpiar sufijos `_IL`, `_v2`.
2. **`cps.php` con `lang=ja`** → tag japonés + alternativas ordenadas por `access_count`.
3. **`cps.php` con el término inglés** (o `translated_name` vía app-api) → tag inglés.
4. **Serie**: ⚠️ no es derivable con fiabilidad de la API. Se saca del `pixpedia.abstract` (suele nombrar la obra) y se confirma a mano una vez por personaje; guárdalo en un JSON.

## 4. R-18 específico

✅ **Ajustes de cuenta necesarios**: (a) fecha de nacimiento ≥18 y «年齢制限作品» activado en *設定 → 閲覧と表示* para **ver**; (b) al **publicar**, marcar la casilla de restricción de edad de la obra, porque el tag `R-18` por sí solo no la aplica.

✅ **Tags de nicho** (existencia y `access_count` comprobados hoy vía autocompletado): `断面図` (3.315.989.503), `事後` (1.892.362.765), `パイズリ希望` (56.498.532), `中出し固め` (50.000.777), `オリジナル` (56.647.592.353). Los obvios (`中出し`, `フェラ`, `巨乳`, `拘束`, `着衣`) existen pero el autocompletado los devuelve parcialmente filtrados ⚠️.

| Plataforma | Anime/ilustrado | IA | R-18 explícito |
|---|---|---|---|
| **pixiv** | ✅ | ✅ con casilla obligatoria | ✅ con restricción de edad |
| **FANBOX** | ✅ | ❌ prohibido | ✅ (sin IA) |
| **Patreon** | ✅ | ✅ en categoría Adult/18+; solo con consentimiento si imita a personas reales ✅ | ✅ en Adult/18+ |
| **X** | ✅ | ✅ (sin obligación de etiqueta, pero conviene) | ⚠️ restringido; el contenido explícito se marca como sensible |

Esto condiciona el etiquetado: en Pixiv **siempre** `R-18` + casilla de edad + casilla IA; en Patreon el equivalente es la categoría Adult/18+, no un tag.

## 5. Recomendación práctica

**Automatiza**: resolver tag JP/EN, contar popularidad y co-ocurrencia, y generar el borrador de título/descripción/tags (10 máximo, ordenados de más específico a más general).
**A mano**: la subida. Es donde está el riesgo de cuenta y donde no hay API.

**¿Conviene PixivPy solo para leer?** Sí. Es la única pieza madura del ecosistema, el coste de mantenerla es cero y la alternativa (scraping del HTML) es más frágil y más claramente contraria a los términos (Cloudflare ya bloquea `dic.pixiv.net`).

### Script: personaje → tag JP, tag EN, serie y top tags R-18

```python
"""pixiv_tags.py — resuelve un personaje y mide con qué tags se publica en R-18.

Requisitos:  pip install "git+https://github.com/upbit/pixivpy.git" requests
Entorno:     PIXIV_REFRESH_TOKEN=...   (gppt login)
"""
import os
import time
import json
from collections import Counter

import requests
from pixivpy3 import AppPixivAPI, PixivError

UA = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
      "(KHTML, like Gecko) Chrome/124.0 Safari/537.36")
WEB = requests.Session()
WEB.headers.update({"User-Agent": UA, "Referer": "https://www.pixiv.net/",
                    "Accept": "application/json"})

GENERIC = {"オリジナル", "女の子", "イラスト", "漫画", "R-18", "R-18G",
           "AIイラスト", "AI生成", "1girl", "solo", "女", "着衣", "裸"}
SLEEP = 1.2  # ⚠️ no hay límite oficial; 1 req/s es la práctica segura


def retry(fn, *a, **kw):
    """Backoff exponencial para 429/5xx/red."""
    for i in range(4):
        try:
            return fn(*a, **kw)
        except (PixivError, requests.RequestException) as e:
            if i == 3:
                raise
            wait = 2 ** i * 3
            print(f"  ! {type(e).__name__}: reintento en {wait}s", flush=True)
            time.sleep(wait)


def cps(keyword, lang):
    r = WEB.get("https://www.pixiv.net/rpc/cps.php",
                params={"keyword": keyword, "lang": lang}, timeout=20)
    r.raise_for_status()
    return r.json().get("candidates", [])


def resolve(name):
    """Nombre (ES/EN/JP) -> tag japonés canónico + alternativas."""
    cands = cps(name, "ja") or cps(name, "en")
    if not cands:
        return None
    cands.sort(key=lambda c: -int(c.get("access_count", 0)))
    return {"jp": cands[0]["tag_name"],
            "alts": [c["tag_name"] for c in cands[1:6]]}


def field(obj, key, default=None):
    """Compatible con JsonDict (3.7.5) y pydantic (3.13)."""
    if isinstance(obj, dict):
        return obj.get(key, default)
    return getattr(obj, key, default)


def collect(api, word, pages=3):
    """search_illust paginando con next_url."""
    out = []
    qs = {"word": word, "search_target": "partial_match_for_tags",
          "sort": "date_desc", "filter": "for_ios"}
    for p in range(pages):
        res = retry(api.search_illust, **qs)
        out.extend(field(res, "illusts") or [])
        nxt = api.parse_qs(field(res, "next_url"))
        if not nxt:
            break
        qs = nxt
        time.sleep(SLEEP)
    return out


def is_generic(tag):
    return tag in GENERIC or tag.endswith("users入り") or tag.startswith("10000")


def analyse(api, jp_tag, pages=3):
    """Devuelve tag EN, serie (conjetura) y top de co-ocurrencia en R-18."""
    # 1) tag EN: lo da el propio API con Accept-Language: en-us
    api.set_accept_language("en-us")
    illusts = collect(api, f"{jp_tag} R-18", pages=pages)
    counter, en_name = Counter(), None
    for ill in illusts:
        for t in (field(ill, "tags") or []):
            name = field(t, "tag")
            en = field(t, "translated_name")
            if name == jp_tag and en:
                en_name = en
            if not name or name == jp_tag or is_generic(name):
                continue
            counter[name] += 1

    # 2) serie: ⚠️ heurística, confirmar a mano una vez por personaje
    series = None
    if illusts:
        top, hits = counter.most_common(1)[0]
        if hits / len(illusts) >= 0.6:
            series = top

    return {
        "jp": jp_tag,
        "en": en_name,
        "series_guess": series,
        "works_sampled": len(illusts),
        "top_tags": counter.most_common(20),
    }


def main(name):
    api = AppPixivAPI()
    api.auth(refresh_token=os.environ["PIXIV_REFRESH_TOKEN"])
    api.set_accept_language("en-us")

    hit = resolve(name)
    if not hit:
        raise SystemExit(f"sin resultados para {name!r}")
    data = analyse(api, hit["jp"])
    data["alternatives"] = hit["alts"]
    print(json.dumps(data, ensure_ascii=False, indent=2))


if __name__ == "__main__":
    import sys
    main(sys.argv[1] if len(sys.argv) > 1 else "Frieren")
```

## Fuentes

- PyPI `pixivpy3`: https://pypi.org/pypi/pixivpy3/json
- Código y firmas: https://github.com/upbit/pixivpy/blob/master/pixivpy3/aapi.py · `bapi.py` · `example_tag_translations.py`
- Issue R-18 en search: https://github.com/upbit/pixivpy/issues/365
- Issue `filter` vacío: https://github.com/upbit/pixivpy/issues/370
- Issue `search_ai_type` / `illust_ai_type`: https://github.com/upbit/pixivpy/issues/410
- Endpoints ajax: https://github.com/daydreamer-json/pixiv-ajax-api-docs
- Políticas (crawlers, apps no oficiales): https://policies.pixiv.net/
- Anuncio IA 2023 + FANBOX: https://www.pixiv.net/info.php?id=9640
- Política IA de FANBOX: https://official.fanbox.cc/posts/5932126
- Límite de 10 tags: https://www.pixiv.help/hc/ja/articles/235585008
- R-18 no basta con el tag: https://www.pixiv.help/hc/ja/articles/235646007
- Ver R-18: https://www.pixiv.help/hc/ja/articles/235585288
- `users入り`: https://dic.pixiv.net/a/users入り
- gppt (token): https://github.com/eggplants/get-pixivpy-token
- Patreon IA + Adult: https://support.patreon.com/hc/en-us/articles/34055590411789
- Revisión de guías IA 2026-03: https://aiteller.jp/blog/news-pixiv-ai-guideline-2026
