كيفية تهريب سلاسل JSON: الأحرف وstringify والمزالق
تهريب سلسلة JSON يعني تحويل نص اعتباطي إلى سلسلة يمكنها الجلوس بأمان داخل مستند JSON كقيمة سلسلة حرفية. هناك حفنة من الأحرف، وهي علامة الاقتباس المزدوجة والشرطة المائلة العكسية وأحرف التحكم مثل السطر الجديد والمسافة الجدولية، تحمل معنى بنيوياً أو أنها ببساطة غير قانونية داخل سلسلة JSON، لذا يُستبدل كل منها بتسلسل تهريب آمن مثل \" أو \\ أو \n. وإن فعلت ذلك خطأً توقفت حمولتك عن التحليل.
تصطدم بهذا باستمرار: تداخل كائن JSON داخل آخر كحقل سلسلة، أو لصق مقتطف شيفرة متعدد الأسطر داخل قيمة إعداد، أو بناء جسم طلب REST يدوياً من أجل curl. يغطي هذا الدليل بالضبط أيّ الأحرف تحتاج إلى تهريب، ويوضح الالتباس بين التهريب وJSON.stringify، ويمرّ عبر تداخل JSON داخل JSON وتهريب Unicode، ويسرد المزالق التي تُفسد الحمولات بهدوء. إن كنت تريد فقط تهريب شيء ما الآن، فأداتنا تهريب JSON تفعل ذلك في المتصفح، لكن تابع القراءة لتفهم لماذا تعمل بالطريقة التي تعمل بها.
ما هو تهريب سلاسل JSON؟
تهريب سلاسل JSON هو عملية تحويل سلسلة خام إلى صيغة آمنة للتضمين داخل مستند JSON. يحجز JSON مجموعة صغيرة من الأحرف التي تحمل معنى بنيوياً: علامة الاقتباس المزدوجة " تحدّ السلسلة، والشرطة المائلة العكسية \ تبدأ تسلسل تهريب. كما أن أحرف التحكم أدنى من U+0020، أي الأسطر الجديدة والمسافات الجدولية وأحرف العودة إلى أول السطر، غير مسموح لها بالظهور حرفياً داخل سلسلة JSON إطلاقاً. يستبدل التهريب كلاً منها بتسلسل آمن بحيث تُحلَّل السلسلة الناتجة بنظافة في أي مكان.
متى تحتاجه فعلاً؟ تتكرر بضعة مواقف مراراً:
- JSON داخل JSON: غلاف webhook، أو رسالة Kafka، أو سجل تدقيق يخزّن جسم طلب كحقل سلسلة، لذا يجب تهريب JSON الداخلي قبل إسناده.
- إعداد مكتوب يدوياً: إسقاط سكربت shell متعدد الأسطر، أو استعلام SQL، أو مقتطف شيفرة داخل قيمة JSON مفردة يعني تحويل كل سطر جديد إلى
\n. - أجسام طلبات REST: بناء جسم JSON يدوياً من أجل
curlأو عميل HTTP، حيث يجب أن تنجو علامات الاقتباس والأسطر الجديدة من الـshell ومن النقل عبر الشبكة. - ترميز آمن للسجلات: كتابة محتوى مقدَّم من المستخدم داخل سطر سجل مهيكَل دون السماح لعلامة اقتباس أو سطر جديد محقون بإفساد التنسيق.
انتبه أيضاً لترتيب العمليات. إن كنت تنطلق من JSON فوضوي أو غير موثوق، فتحقق منه أولاً كي تهرّب شيئاً جيد التكوين؛ الصقه في منسق JSON لتنسيقه بشكل مقروء وفحصه، ثم هرّب النتيجة النظيفة. تهريب نفاية يعطيك فقط نفاية مهرَّبة.
أيّ الأحرف يجب تهريبها في JSON
تحدّد مواصفة JSON قائمة دقيقة وقصيرة. سبعة أحرف لها تهريب مخصص من حرفين، وكل ما عداها أدنى من U+0020 يعود إلى تهريب Unicode بصيغة \uXXXX. إليك المجموعة الكاملة لأحرف تهريب JSON:
| الحرف | يُهرَّب إلى | ملاحظات |
|---|---|---|
" (U+0022) | \" | محدّد السلسلة |
\ (U+005C) | \\ | بادئة التهريب (حالة الشرطة المائلة العكسية في json escape backslash) |
| سطر جديد (U+000A) | \n | |
| عودة إلى أول السطر (U+000D) | \r | |
| مسافة جدولية (U+0009) | \t | |
| مسافة للخلف (U+0008) | \b | |
| تغذية النموذج (U+000C) | \f | |
| أحرف تحكم أخرى < U+0020 | \uXXXX | مثال: U+0000 ← \u0000 |
ما لا يحتاج إلى تهريب لا يقلّ أهمية. الشرطة المائلة الأمامية / حرف عادي تماماً (تهريبها اختياري، ومفيد فقط في حالة ضيقة واحدة مغطّاة أدناه). علامات الاقتباس المفردة لا تحتاج إلى تهريب أبداً لأن JSON لا يستخدمها كمحدِّدات. وكل حرف قابل للطباعة عند U+0020 أو أعلى، بما في ذلك جميع أحرف UTF-8 متعددة البايتات مثل é أو 日 أو 😀، صالح كما هو.
إليك الفرق ملموساً. المدخل الخام على اليمين، وسلسلة JSON المهرَّبة الحرفية على اليسار:
Input:
She said "hello" then left.
Escaped:
"She said \"hello\"\tthen left."
صارت علامتا الاقتباس المزدوجتان \" وصارت المسافة الجدولية \t. الآن السلسلة آمنة للإسقاط في أي محلّل JSON، أو سطر سجل، أو جسم طلب.
تهريب JSON مقابل JSON Stringify: ما الفرق؟
تتخطّى معظم الدروس هذه النقطة، فتُربك كثيراً من الناس. التهريب وJSON.stringify ليسا عمليتين مختلفتين، بل منظوران لنفس العملية الواحدة.
JSON.stringify(value) يُسلسِل أي قيمة JavaScript إلى تمثيلها النصي بصيغة JSON. وحين تصادف تلك القيمة أن تكون سلسلة، فإن تسلسلها يعني لفّها بعلامتي اقتباس مزدوجتين وتهريب الأحرف الخاصة بداخلها. وهذا هو بالضبط تهريب JSON. فـJSON.stringify("a\tb") يعيد السلسلة المؤلفة من سبعة أحرف "a\tb"، بما فيها علامتا الاقتباس.
السؤال العملي هو ما إذا كنت تريد علامتي الاقتباس الخارجيتين تلك. وهذا يقابل مباشرةً خيار اللفّ بعلامتي اقتباس مزدوجتين (Wrap in double quotes) في أداة تهريب JSON:
| الوضع | المخرج للمدخل a"b | متى تستخدمه |
|---|---|---|
| اللفّ مفعَّل | "a\"b" | سلسلة JSON حرفية كاملة، مطابقة لـJSON.stringify. أسندها إلى متغير أو الصقها بعد النقطتين. |
| اللفّ معطَّل | a\"b | الجسم المهرَّب فقط، دون علامتي اقتباس محيطتين. استخدمه حين تكتب علامتي الاقتباس بنفسك داخل مستند JSON. |
لذا إن بحثت عن “json stringify” ووصلت إلى هنا، فالنموذج الذهني بسيط: stringify لسلسلة يساوي تهريباً باللفّ المفعَّل. والصيغة غير المقتبسة هي الشيء نفسه بعد نزع علامتي الاقتباس الخارجيتين.
كيفية تهريب سلسلة من أجل JSON في الشيفرة
القاعدة الذهبية: لا تصنع يدوياً سلسلة من استدعاءات replace() أبداً. كل لغة رئيسية تُشحَن مع مُسلسِل JSON يتعامل مع علامات الاقتباس والشرطات المائلة العكسية وأحرف التحكم وUnicode على نحو صحيح. استعِن به.
JavaScript
const text = 'She said "hi"\nthen left.';
const escaped = JSON.stringify(text);
console.log(escaped);
// "She said \"hi\"\nthen left."
JSON.stringify على سلسلة يعطيك القيمة الحرفية الكاملة المقتبسة. تريد الجسم فقط؟ اقتطع الحرف الأول والأخير: JSON.stringify(text).slice(1, -1).
Python
import json
text = 'She said "hi"\nthen left.'
print(json.dumps(text))
# "She said \"hi\"\nthen left."
print(json.dumps(text, ensure_ascii=False))
# "She said \"hi\"\nthen left." (non-ASCII kept as UTF-8)
json.dumps يفترض ensure_ascii=True، الذي يهرّب كل حرف غير ASCII إلى \uXXXX، وهو نفس سلوك وضع الأداة الآمن لـASCII. مرّر ensure_ascii=False للإبقاء على UTF-8 الخام.
PHP
<?php
$text = "café \"quoted\"\nline";
echo json_encode($text);
// "caf\u00e9 \"quoted\"\nline" (default escapes non-ASCII to \uXXXX)
echo json_encode($text, JSON_UNESCAPED_UNICODE);
// "café \"quoted\"\nline"
json_encode يهرّب افتراضياً كلاً من الأحرف غير ASCII والشرطات المائلة الأمامية. أضِف JSON_UNESCAPED_UNICODE للإبقاء على الحروف ذات العلامات مقروءة، وJSON_UNESCAPED_SLASHES لترك / وشأنها.
Go وJava
في Go، يعيد json.Marshal(text) البايتات المهرَّبة المقتبسة:
b, _ := json.Marshal(`a "quoted" line`)
// b == `"a \"quoted\" line"`
في Java، ينتج objectMapper.writeValueAsString(text) من Jackson أو JSONObject.quote(text) من org.json نفس القيمة الحرفية المقتبسة. أياً كانت اللغة، اتّكئ على المكتبة، فهي تعرف بالفعل كل حالة طرفية كنت ستنساها.
تضمين JSON داخل JSON (JSON-in-JSON)
هذا هو السبب الأكثر شيوعاً الذي يدفع الناس إلى تهريب JSON يدوياً. غالباً ما يخزّن غلاف webhook، أو سجل طابور رسائل، أو سجل تدقيق جسم طلب كامل كحقل سلسلة. ولفعل ذلك، يجب تهريب JSON الداخلي أولاً.
إليك كائناً صغيراً يسافر عبر طبقتين من الترميز:
1. Inner object: {"a":1}
2. Escaped as a string: "{\"a\":1}"
3. Placed in envelope: {"payload": "{\"a\":1}"}
صارت كل " في الكائن الداخلي \"، ولُفّ الكل بزوج خارجي واحد من علامتي الاقتباس. والنتيجة قيمة سلسلة صالحة واحدة يمكنك إسنادها إلى payload.
المشكلة مع التداخل الأعمق هي أن الشرطات المائلة العكسية تتكاثر. تهريب سلسلة مهرَّبة سلفاً يهرّب شرطاتها المائلة العكسية أيضاً، فكل طبقة تضاعفها تقريباً: علامة اقتباس داخلية كانت \" تصير \\\" طبقةً للخارج، و\\\\\" طبقةً أخرى للخارج. JSON داخل JSON بعمق ثلاث طبقات صعب القراءة فعلاً، ولهذا تنفع الأداة هنا. وللذهاب في الاتجاه الآخر وسحب الكائن الداخلي خارج السلسلة، مرّره عبر أداة فكّ تهريب JSON.
Unicode وتهريب \uXXXX
افتراضياً، يَسعد JSON بـUTF-8 الخام. تبقى é كما هي é، وتبقى 日 كما هي 日، ويكون المستند أكثر قابلية للقراءة بفضل ذلك. لست بحاجة إلى تهريب أي حرف Unicode قابل للطباعة.
فمتى قد تلجأ إذاً إلى مخرج \uXXXX الآمن لـASCII؟ فقط حين لا يمكن الوثوق بنظام تالٍ مع UTF-8: بوابات SOAP أو XML القديمة، أو خطوط تسجيل معيّنة، أو ترويسات البريد الإلكتروني، أو ملفات مصدرية يجب أن تبقى ASCII خالصاً. في الوضع الآمن لـASCII، يصير كل حرف فوق U+007F تهريب \uXXXX، فتتحوّل café إلى caf\u00e9. إنها أكثر ضجيجاً لكنها ASCII بايتاً ببايت، وتُفكّ عائدةً إلى الأصل في أي محلّل متوافق.
هناك دقيقة واحدة. \uXXXX يرمّز وحدة شيفرة UTF-16 مفردة بطول 16 بت، لكن الأحرف خارج المستوى متعدد اللغات الأساسي، مثل الرموز التعبيرية والأبجديات النادرة، تحتاج إلى 21 بت. يعالجها JSON بـزوج بدائل (surrogate pair): تهريبان \uXXXX متتاليان. الوجه المبتسم 😀 (U+1F600) يصير \ud83d\ude00. تفعل معظم المُسلسِلات هذا نيابةً عنك؛ والخطر هو مُهرِّب مكتوب يدوياً يُصدر بديلاً وحيداً غير مزدوج.
إن كانت أزواج البدائل ونقاط الشيفرة أرضاً جديدة عليك، فإن دليل UTF-8 vs UTF-16 vs Unicode يفكّك بالضبط كيف يتطابق حرف مفرد مع بايتات ووحدات شيفرة. إنه السياق المفقود وراء سبب احتياج رمز تعبيري واحد إلى تهريبين.
فكّ التهريب: قراءة JSON المهرَّب عكسياً
للتهريب معكوس. لإعادة "a\tb" إلى النص الحقيقي ذي السطرين أو المسافة الجدولية، تحلّله: JSON.parse(str) في JavaScript، وjson.loads(str) في Python. يمشي المحلّل عبر كل تسلسل تهريب ويعيد بناء الأحرف الأصلية، بما فيها أزواج البدائل.
حين يفشل فكّ التهريب، يكون الخطأ غالباً “تسلسل تهريب غير صالح”، وله بضعة أسباب معتادة:
- شرطة مائلة عكسية وحيدة قبل حرف لا يعترف به JSON كتهريب، مثل
\q. - تهريب مختلَق مثل
\x41، إذ لا يملك JSON تهريباً سداسياً عشرياً بصيغة\x؛ بل يستخدم\uفقط. - تهريب
\uمبتور بأقل من أربع خانات سداسية عشرية، مثل\u00. - علامة اقتباس مزدوجة شاردة أو غير متوازنة تكسر حدّ السلسلة.
تحقّق من أن كل شرطة مائلة عكسية تبدأ أحد التهريبات الصالحة (\n \r \t \b \f \" \\ \/ \uXXXX) وأن علامات الاقتباس متزاوجة. وللسلاسل المهرَّبة المنسوخة من وسط سطر سجل، حيث تُركت علامات الاقتباس الخارجية وراءها، تقبل أداتنا فكّ تهريب JSON الجسم بعلامات اقتباس محيطة أو بدونها وتفكّ ترميزه في كلتا الحالتين.
مزالق تهريب JSON الشائعة
معظم الحمولات المكسورة تعود إلى أحد هذه الأخطاء الستة.
1. التهريب المزدوج. تهريب نص مهرَّب سلفاً يحوّل \n إلى \\n و\" إلى \\\"، فيقرأ المستهلك شرطة مائلة عكسية وn حرفياً بدلاً من سطر جديد. يحدث هذا عادةً حين تكون خدمة في الأعلى قد هرّبت القيمة بـJSON سلفاً ثم تهرّبها أنت مرة أخرى. افكّ التهريب أولاً للتحقق من الحالة الراهنة، ثم هرّب مرة واحدة بالضبط.
2. نسيان علامات الاقتباس الخارجية. مع اللفّ معطَّلاً تحصل على الجسم المهرَّب فقط، لا سلسلة كاملة. لصق hello \"world\" مباشرةً حيث تُتوقَّع قيمة JSON غير صالح لأن علامات الاقتباس المحيطة مفقودة. إما أن تُبقي اللفّ مفعَّلاً أو تكتب علامات الاقتباس بنفسك.
3. الإفراط في تهريب غير ASCII. تشغيل الوضع الآمن لـASCII حين يتعامل المستهلك مع UTF-8 جيداً يضخّم المخرج بلا داعٍ. تصير café هي caf\u00e9 بلا سبب: أصعب قراءةً، وأكبر على الشبكة، وبلا أي فائدة. اتركه معطَّلاً ما لم يطلب نظام قديم محدد ASCII خالصاً.
4. تهريب الشرطة المائلة الأمامية بشكل انعكاسي. تهريب / يهمّ في موضع واحد بالضبط: JSON مُضمَّن داخل وسم HTML بصيغة <script>، حيث ستُغلق السلسلة الفرعية </script> الوسمَ مبكراً بغضّ النظر عن سياق JSON. تهريب / إلى \/ يبطل مفعولها. خارج تلك الحالة الواحدة، تهريب الشرطات المائلة فوضى محضة، فاتركه معطَّلاً لأجسام REST وملفات الإعداد وحمولات الرسائل.
5. سلاسل استبدال مصنوعة يدوياً. خط أنابيب replace('"', '\\"') يدوي ينسى شيئاً ما دائماً تقريباً، سواء كان حرف تحكم أو مسافة للخلف أو زوج بدائل. استخدم مُسلسِل اللغة، الذي يغطّي المواصفة كاملةً.
6. التهريب دون فكّ تهريب أبداً (أو فكّ التهريب مرتين). يجب أن تتوازن الرحلة ذهاباً وإياباً. هرّب مرة عند الدخول، وافكّ التهريب مرة عند الخروج. افكّ التهريب مرتين وستُشوّه الشرطات المائلة العكسية الحقيقية التي كانت جزءاً من البيانات.
تمييز إضافي يستحق التثبيت: تهريب JSON ليس ترميز URL أو الترميز بالنسبة المئوية. إنهما يحلّان مشاكل مختلفة لوسائل نقل مختلفة، وخلطهما (أي ترميز قيمة بالنسبة المئوية ثم تهريبها بـJSON، أو العكس) يُنتج فوضى لا يمكن لأي محلّل قراءتها بنظافة. يغطّي دليل ترميز وفك ترميز URL متى يكون الترميز بالنسبة المئوية هو الأداة الصحيحة وكيف يختلف عمّا يفعله JSON.
الأسئلة الشائعة
ماذا يعني تهريب سلسلة في JSON؟
يعني استبدال الأحرف التي تحمل معنى بنيوياً لدى JSON، أي علامة الاقتباس المزدوجة والشرطة المائلة العكسية وأحرف التحكم مثل السطر الجديد والمسافة الجدولية، بتسلسلات تهريب آمنة مثل \" و\\ و\n. يمكن تضمين النتيجة كسلسلة حرفية داخل مستند JSON دون كسر التحليل.
ما الأحرف التي يجب تهريبها في JSON؟
علامة الاقتباس المزدوجة، والشرطة المائلة العكسية، والسطر الجديد، والعودة إلى أول السطر، والمسافة الجدولية، والمسافة للخلف، وتغذية النموذج، يحصل كل منها على تهريب مخصص، وكل حرف تحكم آخر أدنى من U+0020 يصير \uXXXX. الأحرف القابلة للطباعة وUTF-8 متعددة البايتات لا تحتاج إلى تهريب؛ والشرطة المائلة الأمامية اختيارية وتهمّ فقط داخل وسوم HTML بصيغة <script>.
هل تهريب JSON هو نفسه JSON.stringify؟
في الغالب منظوران لعملية واحدة. JSON.stringify المطبَّق على سلسلة يلفّها بعلامتي اقتباس مزدوجتين ويهرّب الأحرف الخاصة بداخلها، وهذا هو تهريب JSON. اللفّ المفعَّل يساوي الصيغة المقتبسة (مطابقة لـJSON.stringify)؛ واللفّ المعطَّل يعطيك الجسم المهرَّب فقط دون علامات الاقتباس المحيطة.
كيف أهرّب سلسلة من أجل JSON في JavaScript أو Python؟
في JavaScript استخدم JSON.stringify(str)؛ وفي Python استخدم json.dumps(str). اعتمد دائماً على الدالة المضمَّنة بدلاً من سلسلة replace مكتوبة يدوياً، فالمضمَّنات تعالج بشكل صحيح Unicode وأحرف التحكم وكل حالة طرفية كنت ستفوّتها لولا ذلك.
لماذا يُكسَر JSON الخاص بي بشرطات مائلة عكسية زائدة؟
السبب المعتاد هو التهريب المزدوج: تهريب نص مهرَّب سلفاً، فتصير \n هي \\n ويقرأ المستهلك شرطة مائلة عكسية وn حرفياً بدلاً من سطر جديد. افكّ تهريب القيمة أولاً للتحقق من حالتها الحقيقية، ثم هرّبها مرة واحدة بالضبط.
هل أحتاج إلى تهريب الشرطات المائلة الأمامية أو Unicode في JSON؟
لا حاجة لأيّ منهما. الـ/ حرف عادي ويحتاج إلى تهريب فقط حين تُضمّن JSON داخل وسم HTML بصيغة <script>، لمنع التسلسل </script> من إغلاقه مبكراً. يبقى Unicode كـUTF-8 خام افتراضياً؛ استخدم \uXXXX فقط حين لا يستطيع نظام تالٍ التعامل مع UTF-8.