Gửi ảnh CAPTCHA tiếng Nhật hoặc tiếng Hàn tới OCR mà nhận về chuỗi vô nghĩa, nguyên nhân gần như luôn nằm ở một tham số: language. Đặt language=2 trong request tới in.php, CaptchaAI dùng mô hình cho bộ ký tự CJK thay vì bảng chữ Latin, và kết quả từ res.php là đúng chuỗi hiragana, katakana, kanji hay hangul trên ảnh.
Vì sao OCR Latin hỏng trên chữ Nhật và chữ Hàn
Mô hình OCR mặc định chỉ học vài chục glyph Latin. Chữ CJK phá vỡ giả định đó: số ký tự lớn gấp hàng trăm lần và một ảnh tiếng Nhật có thể trộn ba hệ chữ.
| Hệ chữ viết | Quy mô ký tự | Điều cần lưu ý |
|---|---|---|
| Hiragana (ひらがな) | 46 ký tự | Từ thuần Nhật; nét tròn |
| Katakana (カタカナ) | 46 ký tự | Từ vay mượn; nét góc cạnh |
| Kanji (漢字) | Hàng nghìn | CAPTCHA lấy tập phổ thông |
| Hangul (한글) | 24 chữ cái, ~11.000 khối | Cắt mép khối là sai cả cụm |
| Trộn hệ (Nhật) | Nhiều hệ trong một ảnh | Trường hợp khó nhất |
CAPTCHA thường gặp trên site Nhật và site Hàn
| Thị trường | Loại phổ biến | Bộ ký tự | Solver CaptchaAI |
|---|---|---|---|
| Nhật Bản | Ảnh hiragana/katakana, reCAPTCHA v2/v3 | Hiragana, katakana, kanji | Image/OCR |
| Hàn Quốc | Ảnh hangul, reCAPTCHA, thanh trượt tự xây | Hangul, Latin | Image/OCR |
| Cả hai | reCAPTCHA v2/v3 giao diện bản địa hóa | Không áp dụng (dạng token) | reCAPTCHA |
Lưu ý phạm vi hỗ trợ: CaptchaAI chưa hỗ trợ hCaptcha và FunCaptcha, GeeTest v4 mới ở mức sắp ra mắt. Với reCAPTCHA, giao diện tiếng Nhật hay tiếng Hàn không đổi cách xử lý; bộ ký tự chỉ là vấn đề của CAPTCHA hình ảnh.
Giải CAPTCHA hình ảnh tiếng Nhật bằng Python
Code dưới đây mã hóa ảnh sang base64, gửi task với language=2, rồi polling res.php mỗi 5 giây. Hàm cuối tải ảnh ngay trong Session đang mở để không mất cookie phiên lúc chờ.
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}")
Kết quả là chuỗi ký tự đã nhận dạng, điền thẳng vào ô nhập của form. CAPCHA_NOT_READY là trạng thái bình thường, cứ polling tiếp.
Giải CAPTCHA Nhật và Hàn bằng Node.js
Bản Node.js theo đúng luồng đó; hàm thứ hai dùng khi ảnh chỉ tải được kèm cookie phiên và header Accept-Language gốc.
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}`);
Tình huống thực tế: theo dõi giá hàng Nhật, Hàn từ TP.HCM
Mô hình quen thuộc ở Việt Nam: một agency đối chiếu giá hàng nhập từ Nhật, Hàn với giá niêm yết trên Shopee, Lazada, Tiki. Nguồn trong nước không chặn, còn cổng đại lý Nhật và Hàn dựng CAPTCHA hình ảnh ngay ở trang đăng nhập.
Tách riêng một bước lo giải CAPTCHA, đặt language=2 cho mọi ảnh CJK và lưu id task kèm chuỗi trả về để đối soát khi số liệu lệch. CaptchaAI tính theo thread chứ không theo lượt giải, nên thứ cần ước lượng là số CAPTCHA giải đồng thời: job chạy đêm nằm gọn trong BASIC ($15/tháng, 5 thread), thêm nguồn thì lên STANDARD ($30/tháng, 15 thread). Chỉ lấy dữ liệu công khai và giữ log tối thiểu theo tinh thần Nghị định 13/2023/NĐ-CP.
Lỗi hay gặp và cách xử lý
| Vấn đề | Nguyên nhân | Cách xử lý |
|---|---|---|
| Hiragana bị đọc thành katakana | Nhiều cặp glyph gần giống | Đặt language=2 |
| Chuỗi hangul bị vỡ ký tự | Phản hồi giải mã theo Latin-1 | Ép UTF-8: response.encoding = 'utf-8' |
| Ảnh trộn hệ chữ cho kết quả sai | Nhiều bộ ký tự trong một ảnh | Giữ ảnh gốc, không nén lại |
| Độ chính xác thấp trên chữ cách điệu | Ký tự bị bóp méo, nhiễu | Lấy độ phân giải cao nhất |
| Phiên hết hạn sau khi giải | Thời gian giải dài | Mở phiên sát lúc lấy ảnh |
Câu hỏi thường gặp
CaptchaAI có giải được hCaptcha trên site Nhật và Hàn không?
Không. hCaptcha và FunCaptcha không nằm trong các loại được hỗ trợ. Phần lớn form tại hai thị trường này vẫn là CAPTCHA hình ảnh hoặc reCAPTCHA.
Giải CAPTCHA ký tự CJK có tốn thêm phí không?
Không. Giá tính theo số thread giải đồng thời, không phụ thuộc loại CAPTCHA, và mỗi thread không giới hạn số lượt giải trong tháng. Chọn gói theo mức song song: BASIC ($15/tháng, 5 thread) cho job nhỏ, ADVANCE ($90/tháng, 50 thread) cho pipeline nhiều nguồn.
Chuỗi hangul in ra terminal bị hỏng font thì làm gì?
Đó là lỗi mã hóa phía bạn, không phải kết quả sai. Ép response.encoding = 'utf-8' trước khi đọc .text và kiểm tra terminal, file log có dùng UTF-8 không.
Có nên tiền xử lý ảnh trước khi gửi không?
Thường là không. Hãy gửi nguyên ảnh ở độ phân giải gốc; resize hay nén lại dễ làm mất chi tiết nét — thứ phân biệt các ký tự CJK gần giống nhau.
Bài viết liên quan
Bước tiếp theo
Pipeline đã chạy ổn với CAPTCHA Latin thì phần còn thiếu chỉ là language=2 và mã hóa UTF-8. Lấy API key CaptchaAI và chạy thử đoạn Python ở trên với một ảnh thật.
Đọc thêm: