الفارق بين حلّ صورة CAPTCHA لاتينية وأخرى مكتوبة بالكانجي أو الهانغول لا يتجاوز معاملاً واحداً داخل الطلب: language=2. أرسل الصورة بترميز base64 إلى in.php مع هذا المعامل، استطلع النتيجة من res.php، وستعود إليك السلسلة النصية بأحرفها الأصلية دون أن تثبّت نموذج تعرّف ضوئي إضافياً على خادمك ودون مكتبة خاصة بكل لغة.
المشكلة العملية إذاً ليست في نموذج التعرّف، بل في ما يحيط به: جلسة تنتهي صلاحيتها قبل أن تصل النتيجة، ترميز يحوّل الهانغول إلى رموز مشوّهة في السجلات، وصفحة يابانية تخلط ثلاثة أنظمة كتابة داخل صورة واحدة بعرض 120 بكسل. ونعالجها هنا بالترتيب الذي تصادفها فيه أثناء البناء.
لماذا يتعثّر التعرّف الضوئي اللاتيني أمام الكانجي والهانغول
محرك OCR مضبوط على الأبجدية اللاتينية يفترض 26 حرفاً وأشكالها. الصورة اليابانية أو الكورية تنسف هذه الفرضية: مجموعة الأحرف أكبر بمئات المرات، والتعرّف في الكورية يجري على المقطع المركّب لا على الحرف المفرد.
| نظام الكتابة | حجم مجموعة الأحرف | ما الذي يصعّب التعرّف |
|---|---|---|
| هيراغانا ひらがな | 46 حرفاً أساسياً | حروف منحنية متقاربة الشكل مثل き وさ |
| كاتاكانا カタカナ | 46 حرفاً أساسياً | خطوط مستقيمة تشبه نظيراتها في الهيراغانا |
| كانجي 漢字 | آلاف الأحرف، وتستخدم اختبارات CAPTCHA مجموعة فرعية شائعة منها | كثافة الشرطات داخل مساحة صغيرة |
| هانغول 한글 | 24 حرفاً أساسياً تتجمّع في نحو 11000 مقطع | الوحدة المقروءة هي المقطع الكامل وليست الحرف |
| مزيج ياباني | هيراغانا + كاتاكانا + كانجي + لاتينية | أصعب الحالات: أكثر من نظام كتابة في صورة واحدة |
القيمة language=2 في CaptchaAI تغطي عائلة CJK كاملة: اليابانية والكورية والصينية. لا معامل منفصل لكل لغة، والصورة المختلطة تُعالَج بالقيمة نفسها.
خريطة أنواع CAPTCHA في السوقين اليابانية والكورية
قبل كتابة أي سطر، حدّد نوع التحدي فعلياً؛ فالصورة النصية ليست الحالة الوحيدة، وكثير من البوابات انتقل إلى أنواع قائمة على الرمز.
| السوق | ما تصادفه غالباً | المسار المناسب |
|---|---|---|
| اليابان | صور نصية بالكانا والكانجي، إلى جانب reCAPTCHA v2/v3 وCloudflare Turnstile | Image/OCR للصور، ومسار الرمز للنوعين الآخرين |
| كوريا | صور بالهانغول، reCAPTCHA، ومنزلقات تمرير في بعض البوابات | Image/OCR للصور، وGeeTest v3 حيث يُستخدم |
| المشترك | reCAPTCHA بواجهة مترجمة بالكامل | لا فرق: الحل قائم على الرمز ولا تؤثر فيه لغة الواجهة |
الأنواع القائمة على الرمز لها مساراتها المستقلة: راجع دليل حل reCAPTCHA v2 عبر الـ API والتعامل مع Cloudflare Turnstile وحل GeeTest v3. أما GeeTest v4 فلا يزال قيد الإعداد ولم يصبح متاحاً بعد، فلا تبنِ عليه خطة تشغيل.
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}")
حلقة الاستطلاع هنا 24 محاولة بفاصل 5 ثوانٍ، أي مهلة قصوى تقارب دقيقتين. الهامش مريح للصور المعقّدة لكنه أطول من عمر بعض الجلسات، لذلك أنشئ الجلسة قبل الإرسال مباشرة. وترويسة Accept-Language المضبوطة على ko-KR هي التي تضمن أنك تقرأ النسخة الكورية من الصفحة نفسها.
JavaScript: المسار نفسه بأسلوب غير متزامن
إذا كان جامع البيانات مبنياً على Node.js مع Puppeteer أو Playwright، فالمنطق لا يتغيّر: ترميز base64، إرسال، استطلاع، ثم استخدام النص؛ الانتظار وحده يجري عبر await بدل time.sleep.
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}`);
مع صفحة يابانية بدّل Accept-Language إلى ja-JP، وحافظ على ملفات تعريف الارتباط نفسها بين طلب الصورة وإرسال النص؛ أي انقطاع يعني تحدياً جديداً وحلاً لن تستخدمه.
سيناريو تشغيلي: فريق بيانات خليجي يراقب موردين في طوكيو وسيول
خذ حالة شائعة في السوق العربية: فريق تجارة إلكترونية في الرياض يحتاج تحديثاً يومياً لقوائم أسعار موردين على بوابات يابانية وكورية يملك حسابات نظامية عليها. التحديات ثلاثة، ولا علاقة لأيّها بنموذج التعرّف:
- فارق التوقيت. طوكيو وسيول تسبقان الرياض بست ساعات. تشغيل التحديث عند منتصف الليل بتوقيت الرياض يقع في ساعات الصباح هناك، وهي نافذة يكون فيها الحمل على البوابة أخفّ عادة.
- حجم التوازي. ثمانية موردين ولكل منهم بضع صفحات محمية. باقة STANDARD بسعر 30 دولاراً شهرياً تمنح 15 thread متزامناً بعدد حلول غير محدود لكل thread، وهي كافية هنا؛ ومع عشرات المصادر تنتقل إلى ADVANCE بسعر 90 دولاراً و50 thread. الحساب على التزامن لا على عدد الحلول، فالتكلفة الشهرية معروفة سلفاً.
- الترميز في نهاية السلسلة. النص الكوري يمرّ عادة عبر قاعدة بيانات ثم لوحة تقارير؛ تأكد أن الاتصال يستخدم
utf8mb4أو ما يعادله، وإلا اكتشفت التشويه بعد أسبوع لا عند الحل.
القاعدة العامة: اقصر الحل على الخطوات التي تفتح بيانات فعلية — تسجيل الدخول وصفحة القائمة — لا على كل صفحة وسيطة.
أخطاء شائعة وطريقة معالجتها
| العرض | السبب المرجّح | المعالجة |
|---|---|---|
| الهانغول يظهر رموزاً مشوّهة في السجلات | فك الترميز بـ Latin-1 بدلاً من UTF-8 | اضبط response.encoding = 'utf-8' وتحقق من ترميز الطرفية وقاعدة البيانات |
| خلط بين حرف هيراغانا ونظيره في الكاتاكانا | صورة منخفضة الدقة أو أعيد تحجيمها قبل الإرسال | مرّر الصورة بدقتها الأصلية دون ضغط إضافي، واحتفظ بـ language=2 |
| انتهاء الجلسة قبل إرسال النص المحلول | زمن الحل أطول من مهلة الجلسة على الخادم | أنشئ الجلسة قبل الحل مباشرة وأرسل النص فور وصوله |
| ظهور تحديات كثيرة خلال فترة قصيرة | إيقاع الطلبات أو مستوى التوازي أعلى مما يحتمله المصدر | خفّض التكرار، ثبّت الجلسات، وراجع جودة الخادم الوسيط |
| بيانات ناقصة رغم نجاح الحل | أداة الاستخراج تقرأ عرضاً قديماً للصفحة | استخرج البيانات بعد تطبيق النتيجة داخل الجلسة نفسها |
| ارتفاع التكلفة أسرع من المتوقع | إعادة محاولات زائدة على صفحات غير ضرورية | تتبّع عدد المحاولات لكل مصدر وحدّد سقفاً لها |
الأسئلة الشائعة
هل أحتاج إلى إعدادات مختلفة بين الموقع الياباني والكوري؟
لا. القيمة language=2 واحدة لعائلة CJK كلها، والخدمة تتعرّف على نظام الكتابة من الصورة نفسها. ما يتغيّر بين الحالتين هو سياق الموقع فقط: ترويسة Accept-Language وملفات تعريف الارتباط ومسار الصفحة.
ما تأثير ترويسة Accept-Language على النتيجة؟
لا تؤثر على دقة التعرّف، لكنها تحدّد أي نسخة من الصفحة تصلك. إن طلبت النسخة الإنجليزية ثم أرسلت النتيجة إلى مسار النسخة اليابانية فقد يرفض الخادم الطلب. اضبط الترويسة مرة واحدة على مستوى الجلسة.
كم thread أحتاج لتشغيل هذا المسار؟
احسب ما تعالجه في اللحظة نفسها لا في اليوم. مشروع فردي بصفحتين أو ثلاث يكفيه BASIC بسعر 15 دولاراً شهرياً مع 5 threads، وجدولة يومية على عدة مواقع تحتاج STANDARD بسعر 30 دولاراً و15 thread. كل thread يعالج حلولاً غير محدودة خلال الشهر، فالسقف هو التزامن وحده.
هل تُحلّ منزلقات التمرير في البوابات الكورية بالطريقة نفسها؟
لا. المنزلق ليس صورة نصية ولا يمرّ عبر Image/OCR. إن كان من نوع GeeTest v3 فله مسار مستقل، أما المنزلقات الخاصة ببوابة بعينها فتأكد من نوعها قبل أن تبني عليها خطة.
هل يمكن تشغيل السكربت من خادم في الشرق الأوسط؟
نعم. الاتصال بالـ API عبر HTTPS عادي، وزمن الرحلة من الرياض أو القاهرة لا يُذكر أمام زمن الحل. الانتباه يستحقه مصدر الطلب: بعض البوابات المحلية تعرض تحديات أكثر لزيارات من خارج المنطقة، وهنا يفيد خادم وسيط قريب جغرافياً.
الخطوات التالية
ابدأ بمفتاح API وصورة اختبار واحدة، ثم أضف الجلسة والاستطلاع بعد التأكد من صحة النص العائد.