Use Cases

Решение CAPTCHA на японских и корейских сайтах

Картинка-капча с иероглифами или хангылем решается тем же методом Image/OCR, что и обычная латинская, — меняется один параметр: language=2. Он переключает распознавание на набор символов CJK, после чего японская и корейская капча возвращают нормальный текст в UTF-8, а не строку из вопросительных знаков.

Проблема почти никогда не в самой капче. Она в двух местах: в локальном OCR, обученном на латинице, и в кодировке ответа, который клиент по привычке декодирует как Latin-1. Ниже — что именно ломается на японских и корейских сайтах, как это чинится и какой код достаточно скопировать, чтобы получить рабочий результат.

Почему латинский OCR не читает японский и корейский текст

Латиница — это 26 букв с устойчивой формой. Японская капча может смешивать в одной картинке хирагану, катакану и кандзи. Корейская использует хангыль — 24 базовые буквы, которые собираются в слоговые блоки, и таких блоков около 11 000.

Отсюда три следствия:

  • Размер алфавита. Модель должна различать тысячи классов вместо десятков; локальный Tesseract без CJK-моделей здесь бесполезен.
  • Похожие глифы. Хирагана и катакана содержат визуально близкие пары знаков — при искажении они путаются чаще, чем 0 и O.
  • Байты, а не символы. Один символ CJK — это несколько байтов в UTF-8, и любая однобайтовая обработка ответа ломает текст.

Типы CAPTCHA по регионам

Регион Что встречается на практике Наборы символов Что решает CaptchaAI
Япония Картинка с хираганой/катаканой, reCAPTCHA v2/v3 Хирагана, катакана, кандзи, латиница Image/OCR, reCAPTCHA
Корея Картинка с хангылем, reCAPTCHA v2/v3, собственные слайдеры Хангыль, латиница Image/OCR, reCAPTCHA
Оба региона reCAPTCHA v2/v3 с локализованным интерфейсом Не применимо (токен) reCAPTCHA

Границы стоит оговорить сразу: hCaptcha и FunCaptcha на таких сайтах тоже попадаются, но CaptchaAI их не решает. GeeTest v4 значится как «скоро» и пока недоступен; GeeTest v3 поддерживается — см. решение GeeTest v3 через API. Рядом встречаются и бета-типы: CaptchaFox (beta), Friendly Captcha (beta), Lemin (beta).

Один параметр, который решает задачу: language=2

При отправке задачи в in.php методом base64 добавьте language=2. Это переключатель на CJK; отдельно указывать «японский» или «корейский» не нужно — система письма определяется автоматически. Дальше работает обычный поток: отправили задачу, получили ID задачи, опрашиваете res.php до готовности.

Второй обязательный шаг — кодировка. В requests явно задайте response.encoding = 'utf-8', а при записи в файл или БД проверьте, что вся цепочка (соединение, колонка, лог) тоже UTF-8. Большая часть жалоб «хангыль приходит битым» закрывается именно здесь.

Python: японская и корейская капча внутри живой сессии

import requests
import base64
import time

API_KEY = "YOUR_API_KEY"
SUBMIT_URL = "https://ocr.captchaai.com/in.php"
RESULT_URL = "https://ocr.captchaai.com/res.php"


def solve_japanese_captcha(image_path: str) -> str:
    """Solve a Japanese character image CAPTCHA."""
    with open(image_path, "rb") as f:
        image_b64 = base64.b64encode(f.read()).decode()

    resp = requests.post(SUBMIT_URL, data={
        "key": API_KEY,
        "method": "base64",
        "body": image_b64,
        "language": 2,          # CJK character support
        "json": 1,
    }, timeout=30).json()

    if resp.get("status") != 1:
        raise RuntimeError(f"Submit: {resp.get('request')}")

    task_id = resp["request"]
    for _ in range(24):
        time.sleep(5)
        poll = requests.get(RESULT_URL, params={
            "key": API_KEY, "action": "get", "id": task_id, "json": 1,
        }, timeout=15).json()

        if poll.get("request") == "CAPCHA_NOT_READY":
            continue
        if poll.get("status") == 1:
            return poll["request"]
        raise RuntimeError(f"Solve: {poll.get('request')}")

    raise RuntimeError("Timeout")


def solve_korean_captcha(image_path: str) -> str:
    """Solve a Korean hangul image CAPTCHA."""
    with open(image_path, "rb") as f:
        image_b64 = base64.b64encode(f.read()).decode()

    resp = requests.post(SUBMIT_URL, data={
        "key": API_KEY,
        "method": "base64",
        "body": image_b64,
        "language": 2,
        "json": 1,
    }, timeout=30).json()

    if resp.get("status") != 1:
        raise RuntimeError(f"Submit: {resp.get('request')}")

    task_id = resp["request"]
    for _ in range(24):
        time.sleep(5)
        poll = requests.get(RESULT_URL, params={
            "key": API_KEY, "action": "get", "id": task_id, "json": 1,
        }, timeout=15).json()

        if poll.get("request") == "CAPCHA_NOT_READY":
            continue
        if poll.get("status") == 1:
            return poll["request"]
        raise RuntimeError(f"Solve: {poll.get('request')}")

    raise RuntimeError("Timeout")


def solve_captcha_from_session(session: requests.Session,
                                captcha_url: str,
                                language: int = 2) -> str:
    """Download and solve a CAPTCHA within a session context."""
    resp = session.get(captcha_url, timeout=15)
    image_b64 = base64.b64encode(resp.content).decode()

    submit = requests.post(SUBMIT_URL, data={
        "key": API_KEY,
        "method": "base64",
        "body": image_b64,
        "language": str(language),
        "json": 1,
    }, timeout=30).json()

    if submit.get("status") != 1:
        raise RuntimeError(f"Submit: {submit.get('request')}")

    task_id = submit["request"]
    for _ in range(24):
        time.sleep(5)
        poll = requests.get(RESULT_URL, params={
            "key": API_KEY, "action": "get", "id": task_id, "json": 1,
        }, timeout=15).json()

        if poll.get("request") == "CAPCHA_NOT_READY":
            continue
        if poll.get("status") == 1:
            return poll["request"]
        raise RuntimeError(f"Solve: {poll.get('request')}")

    raise RuntimeError("Timeout")


# --- Usage ---

# Japanese CAPTCHA
jp_text = solve_japanese_captcha("japanese_captcha.png")
print(f"Japanese CAPTCHA: {jp_text}")

# Korean CAPTCHA from a live session
session = requests.Session()
session.headers["Accept-Language"] = "ko-KR,ko;q=0.9"
session.get("https://example.kr/login")  # establish session
kr_text = solve_captcha_from_session(session, "https://example.kr/captcha/image")
print(f"Korean CAPTCHA: {kr_text}")

Ключевая деталь — solve_captcha_from_session: картинка забирается тем же объектом Session, что и страница входа. Многие японские и корейские сайты привязывают изображение к cookie сессии, и картинка, скачанная отдельным «чистым» запросом, форме не соответствует.

JavaScript: тот же поток на Node.js

const API_KEY = "YOUR_API_KEY";
const SUBMIT_URL = "https://ocr.captchaai.com/in.php";
const RESULT_URL = "https://ocr.captchaai.com/res.php";
const fs = require("fs");

async function solveAsianCaptcha(imagePath) {
  const imageB64 = fs.readFileSync(imagePath, "base64");

  const body = new URLSearchParams({
    key: API_KEY,
    method: "base64",
    body: imageB64,
    language: "2",
    json: "1",
  });

  const resp = await (await fetch(SUBMIT_URL, { method: "POST", body })).json();
  if (resp.status !== 1) throw new Error(`Submit: ${resp.request}`);

  const taskId = resp.request;
  for (let i = 0; i < 24; i++) {
    await new Promise((r) => setTimeout(r, 5000));
    const url = `${RESULT_URL}?key=${API_KEY}&action=get&id=${taskId}&json=1`;
    const poll = await (await fetch(url)).json();
    if (poll.request === "CAPCHA_NOT_READY") continue;
    if (poll.status === 1) return poll.request;
    throw new Error(`Solve: ${poll.request}`);
  }
  throw new Error("Timeout");
}

async function solveFromUrl(captchaUrl, cookies = "") {
  const resp = await fetch(captchaUrl, {
    headers: { Cookie: cookies, "Accept-Language": "ja-JP,ja;q=0.9" },
  });
  const buffer = await resp.arrayBuffer();
  const imageB64 = Buffer.from(buffer).toString("base64");

  const body = new URLSearchParams({
    key: API_KEY, method: "base64", body: imageB64,
    language: "2", json: "1",
  });

  const submitResp = await (await fetch(SUBMIT_URL, { method: "POST", body })).json();
  if (submitResp.status !== 1) throw new Error(`Submit: ${submitResp.request}`);

  const taskId = submitResp.request;
  for (let i = 0; i < 24; i++) {
    await new Promise((r) => setTimeout(r, 5000));
    const url = `${RESULT_URL}?key=${API_KEY}&action=get&id=${taskId}&json=1`;
    const poll = await (await fetch(url)).json();
    if (poll.request === "CAPCHA_NOT_READY") continue;
    if (poll.status === 1) return poll.request;
    throw new Error(`Solve: ${poll.request}`);
  }
  throw new Error("Timeout");
}

// Usage
const jpText = await solveAsianCaptcha("japanese_captcha.png");
console.log(`Japanese: ${jpText}`);

Рекомендации по наборам символов

Система письма Объём Что учитывать
Хирагана 46 базовых знаков Фонетическое письмо для исконно японских слов
Катакана 46 базовых знаков Фонетическое письмо для заимствований
Кандзи Тысячи знаков Общие с китайским; в капчах используется частотное подмножество
Хангыль 24 буквы, ~11 000 слоговых блоков Корейский фонетический алфавит
Смешанное письмо (Япония) Хирагана + катакана + кандзи + латиница Самый сложный случай: несколько систем в одной картинке

Практический вывод: исходное разрешение важнее любой постобработки. Если в форме есть кнопка «обновить капчу», а картинка пришла сильно сжатой, дешевле запросить новую.

Сценарий: сбор цен с японского B2B-каталога

Типичная задача для команды из Москвы, Минска или Алматы — снимать цены и наличие с каталога японского или корейского поставщика для собственного прайс-листа. Форма входа отдаёт картинку с катаканой, сессия живёт около пятнадцати минут, а полный обход каталога занимает дольше.

Что здесь работает:

  1. Вход выполняется один раз, cookie сохраняются, капча решается только на логине — не на каждой странице.
  2. Заголовок Accept-Language выставляется в ja-JP или ko-KR: часть сайтов иначе отдаёт другую разметку и другой блок капчи.
  3. При истечении сессии парсер повторяет вход, а не падает; тайм-аут на распознавание берётся с запасом, особенно на мобильных и нестабильных каналах.
  4. Собирается только то, что вы вправе обрабатывать: если в выгрузку попадают контактные данные сотрудников поставщика, применимы 152-ФЗ «О персональных данных» и GDPR-подобная дисциплина для трансграничных сценариев. Это вопрос вашей юридической проверки.

По стоимости: тарификация у CaptchaAI идёт по потокам, а не по числу решений. Для одного парсера с последовательным обходом хватает BASIC ($15/мес, 5 потоков). Если каталогов несколько и они обходятся параллельно, разумная ступень — STANDARD ($30/мес, 15 потоков) или ADVANCE ($90/мес, 50 потоков). Решения внутри тарифа отдельно не тарифицируются, поэтому расход предсказуем даже при неровной нагрузке — удобно, когда вы выставляете счёт в локальной валюте, а платите в долларах.

Поиск неисправностей

Симптом Причина Что сделать
Хирагана распознаётся как катакана Похожие по начертанию знаки при сильном искажении Проверьте, что передан language=2, и запросите картинку в большем разрешении
Хангыль приходит «кракозябрами» Ответ декодируется как Latin-1 Задайте response.encoding = 'utf-8' и проверьте кодировку на всём пути до хранилища
Смешанное письмо распознаётся частично В одной картинке несколько систем письма Тот же language=2; при регулярных сбоях сохраняйте проблемные изображения для разбора
Низкая точность на «декоративном» шрифте Сильная стилизация и шум Берите изображение в исходном разрешении, без ресайза и повторного JPEG-сжатия
Сессия истекает после решения Долгое распознавание сложных знаков Открывайте сессию заранее и отправляйте форму сразу после получения ответа
ERROR_WRONG_USER_KEY или пустой ответ Ключ не подставлен или обрезан Убедитесь, что YOUR_API_KEY заменён, а ключ читается из переменной окружения

Часто задаваемые вопросы

Нужны ли разные параметры для японской и корейской капчи?

Нет. В обоих случаях достаточно language=2 — система письма определяется автоматически, включая китайские иероглифы. Различается только контекст запроса к сайту: заголовок Accept-Language и cookie сессии.

Почему хангыль приходит в виде нечитаемых символов?

Это ошибка кодировки на вашей стороне, а не результат распознавания. Ответ приходит в UTF-8, и проблема возникает, если клиент, консоль, файл или колонка в БД работают в другой кодировке. Проверяйте всю цепочку целиком, а не только код парсера.

Отличается ли reCAPTCHA на японских и корейских сайтах?

Нет. reCAPTCHA основана на токене и не зависит от языка интерфейса: нужен sitekey со страницы, дальше поток стандартный — см. решение reCAPTCHA v2 через API. Локализованная надпись на виджете на процесс не влияет.

Сколько потоков нужно для парсера японского каталога?

Считайте по числу одновременных запросов на распознавание, а не по числу страниц. Последовательный парсер занимает один поток, поэтому BASIC ($15/мес, 5 потоков) закрывает и его, и запас на повторы. Параллельный обход нескольких доменов — повод перейти на STANDARD ($30/мес, 15 потоков).

Что делать, если вместо картинки стоит Cloudflare?

Тогда это другой тип задачи: Cloudflare Turnstile и Cloudflare Challenge решаются отдельными методами, без OCR. Порядок действий описан в руководстве по решению Cloudflare Turnstile через API.

С чего начать

Если вы ещё не подключали API, начните с быстрого старта CaptchaAI: получите ключ и первый решённый токен, затем вернитесь к коду выше и добавьте language=2.

Комментарии для этой статьи отключены.