ترميز وفك ترميز URL: دليل ترميز النسبة المئوية للمطورين
تفتح سجلات الخادم وترى هذا في سلسلة الاستعلام: %E4%BD%A0%E5%A5%BD. بيانات تالفة؟ خطأ برمجي؟ لا — هذه الأحرف الصينية 你好، حُوِّل كل حرف منها إلى ثلاث بايتات UTF-8 ثم رُمِّز بالنسبة المئوية. كل مطور ويب يمر بهذا: شيء يبدو معطلاً، لكن العنوان يعمل تماماً كما صُمِّم.
ترميز URL — أو ترميز النسبة المئوية — يجعل الأحرف الخاصة آمنة داخل العناوين. هذا الدليل يغطي آلية العمل على مستوى البايت، والفرق بين encodeURI وencodeURIComponent، والترميز الصحيح في أربع لغات برمجة، والأخطاء التي تُربك حتى المطورين المخضرمين.
الصق أي عنوان في أداة ترميز وفك ترميز URL وتابع النتائج مباشرة.
ما هو ترميز URL (ترميز النسبة المئوية)؟
العنوان يقبل فقط مجموعة صغيرة من أحرف ASCII. الحروف والأرقام وبعض الرموز تمر دون مشاكل. أي شيء آخر — مسافات، علامة العطف، نصوص صينية، رموز تعبيرية — يجب تحويله لصيغة يستطيع العنوان حملها.
ترميز النسبة المئوية يستبدل كل بايت غير آمن بعلامة % يليها رقمان ست عشريان. المسافة تصبح %20، وعلامة العطف %26. التسمية من بادئة %.
القواعد محددة في RFC 3986 (نُشر 2005، لا يزال المعيار المعتمد). حلّ محل RFC 2396 وضبط تعريفات الأحرف الآمنة والمحجوزة والتعامل مع النصوص غير ASCII.
أمثلة سريعة:
| المدخل | المرمَّز | السبب |
|---|---|---|
hello world | hello%20world | المسافات غير مسموحة في العناوين |
price=10&tax=2 | price%3D10%26tax%3D2 | = و & لهما معنى هيكلي |
中 | %E4%B8%AD | حرف غير ASCII ← بايتات UTF-8 ← ترميز نسبة مئوية |
🚀 | %F0%9F%9A%80 | رمز تعبيري ← 4 بايتات UTF-8 ← ترميز نسبة مئوية |
أي الأحرف تحتاج إلى ترميز؟
يقسم RFC 3986 الأحرف إلى ثلاث مجموعات. معرفة هذه المجموعات يوفر ساعات من تصحيح الأخطاء.
الأحرف غير المحجوزة (لا تُرمَّز أبداً)
هذه الأحرف الـ 66 تمر كما هي في أي جزء من العنوان:
A-Z a-z 0-9 - . _ ~
فقط: أحرف أبجدية، أرقام، شرطة، نقطة، شرطة سفلية، مَدّة. أي حرف آخر يحتاج ترميزاً.
الأحرف المحجوزة (تعتمد على السياق)
هذه الأحرف تعمل كفواصل هيكلية في العناوين:
| الحرف | دوره في بنية العنوان |
|---|---|
: | يفصل المخطط عن السلطة (https:) |
/ | يفصل أجزاء المسار |
? | يبدأ سلسلة الاستعلام |
# | يبدأ الجزء المرجعي |
& | يفصل معلمات الاستعلام |
= | يفصل مفتاح المعلمة عن قيمتها |
@ | يفصل معلومات المستخدم عن المضيف |
+ ! $ ' ( ) * , ; [ ] | أدوار محجوزة متنوعة |
القاعدة: عندما يؤدي الحرف المحجوز وظيفته الهيكلية، اتركه كما هو. عندما يظهر كبيانات (داخل قيمة معلمة مثلاً)، رمِّزه.
كل شيء آخر (يُرمَّز دائماً)
المسافات، الأقواس الزاوية، الأقواس المعقوفة، الأنابيب، الشرطات المائلة العكسية، وكل حرف غير ASCII (صينية، عربية، رموز تعبيرية) — يجب ترميزها قبل وضعها في العنوان.
المسافة حالة خاصة: RFC 3986 يرمِّزها %20، لكن نماذج HTML تستخدم +. تفاصيل هذا التعارض لاحقاً.
كيف يعمل ترميز URL فعلياً: مسار UTF-8
لأحرف ASCII، الأمر بسيط: خذ قيمة البايت بالست عشري وضع % قبلها. المسافة (بايت 32، ست عشري 20) تصبح %20.
للنصوص غير ASCII، الترميز يمر بثلاث خطوات:
الخطوة 1 — الحرف إلى نقطة ترميز Unicode.
الحرف é يُعيَّن لنقطة الترميز U+00E9. الرمز التعبيري 🚀 يُعيَّن لـ U+1F680.
الخطوة 2 — نقطة الترميز إلى بايتات UTF-8.
يستخدم UTF-8 من 1 إلى 4 بايتات حسب نطاق نقطة الترميز. é (U+00E9) تصبح بايتين: 0xC3 0xA9. رمز الصاروخ (U+1F680) يصبح أربع بايتات: 0xF0 0x9F 0x9A 0x80.
الخطوة 3 — كل بايت إلى %XX.
كل بايت من الخطوة 2 يحصل على ثلاثية ترميز النسبة المئوية الخاصة به.
إليك المسار الكامل لعدة أنواع من الأحرف:
| الحرف | نقطة الترميز | بايتات UTF-8 | المرمَّز | مضاعف الحجم |
|---|---|---|---|---|
A | U+0041 | 41 | A (لا يُرمَّز) | 1× |
| مسافة | U+0020 | 20 | %20 | 3× |
é | U+00E9 | C3 A9 | %C3%A9 | 6× |
中 | U+4E2D | E4 B8 AD | %E4%B8%AD | 9× |
🚀 | U+1F680 | F0 9F 9A 80 | %F0%9F%9A%80 | 12× |
تحقق بنفسك في JavaScript:
const char = '中';
const encoded = encodeURIComponent(char);
console.log(encoded); // '%E4%B8%AD'
// Trace the bytes
const bytes = new TextEncoder().encode(char);
console.log([...bytes].map(b => '%' + b.toString(16).toUpperCase()).join(''));
// '%E4%B8%AD' — matches
هذا التمدد يؤثر مباشرة على حدود طول العنوان. مثلاً: 20 حرفاً صينياً تضيف 180 حرفاً مرمَّزاً.
encodeURI مقابل encodeURIComponent — اختيار الدالة الصحيحة
الخلط بينهما هو أكثر خطأ شائع في ترميز العناوين بـ JavaScript. الاسمان متقاربان، لكن كل دالة ترمِّز مجموعة مختلفة تماماً.
encodeURI() | encodeURIComponent() | |
|---|---|---|
| الغرض | ترميز عنوان كامل | ترميز مكوّن واحد (مفتاح أو قيمة معلمة) |
| يحافظ على | : / ? # & = @ + $ , | لا شيء من هذه |
| يرمِّز | المسافات، غير ASCII، بعض علامات الترقيم | كل شيء عدا A-Z a-z 0-9 - _ . ~ ! ' ( ) * |
| استخدمها عندما | لديك عنوان كامل فيه مسافات أو Unicode في المسار | تبني معلمات استعلام من مدخلات المستخدم |
خطأ يصل إلى الإنتاج أكثر مما تتوقع:
// ❌ BUG: encodeURI does NOT encode &
const search = 'Tom & Jerry';
const bad = `https://api.example.com/search?q=${encodeURI(search)}`;
// Result: https://api.example.com/search?q=Tom%20&%20Jerry
// The & splits the query string — server sees q=Tom%20 and a separate param %20Jerry
// ✅ FIX: encodeURIComponent encodes & as %26
const good = `https://api.example.com/search?q=${encodeURIComponent(search)}`;
// Result: https://api.example.com/search?q=Tom%20%26%20Jerry
القاعدة: عند الشك، استخدم encodeURIComponent(). هي الخيار الصحيح في 95% من الحالات الفعلية.
جرّب كلا الوضعين جنباً إلى جنب في أداة ترميز URL ←
ترميز URL في كل لغة برمجة
JavaScript (المتصفح و Node.js)
// Encode a parameter value
const value = encodeURIComponent('price >= 100 & currency = €');
// 'price%20%3E%3D%20100%20%26%20currency%20%3D%20%E2%82%AC'
// Decode
const original = decodeURIComponent(value);
// 'price >= 100 & currency = €'
// Modern approach: URLSearchParams handles encoding automatically
const params = new URLSearchParams({ q: 'hello world', lang: '中文' });
console.log(params.toString());
// 'q=hello+world&lang=%E4%B8%AD%E6%96%87'
// Note: URLSearchParams uses + for spaces (form encoding)
Python
from urllib.parse import quote, unquote, urlencode
# Encode a path segment
quote('hello world/file name.txt', safe='/')
# 'hello%20world/file%20name.txt'
# Encode query parameters
urlencode({'q': '你好', 'page': '1'})
# 'q=%E4%BD%A0%E5%A5%BD&page=1'
# quote_plus uses + for spaces (form encoding)
from urllib.parse import quote_plus
quote_plus('hello world') # 'hello+world'
quote('hello world') # 'hello%20world'
Go
import "net/url"
// Encode a query value (uses + for spaces)
url.QueryEscape("hello world & more")
// "hello+world+%26+more"
// Encode a path segment (uses %20 for spaces)
url.PathEscape("hello world & more")
// "hello%20world%20&%20more"
// Build a URL safely with url.Values
params := url.Values{}
params.Set("q", "你好世界")
params.Set("page", "1")
fmt.Println(params.Encode())
// "page=1&q=%E4%BD%A0%E5%A5%BD%E4%B8%96%E7%95%8C"
Java
import java.net.URLEncoder;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
// Encode (uses + for spaces — Java follows form encoding)
String encoded = URLEncoder.encode("hello world & more", StandardCharsets.UTF_8);
// "hello+world+%26+more"
// For RFC 3986 compliance, replace + with %20
String rfc3986 = encoded.replace("+", "%20");
// "hello%20world%20%26%20more"
// Decode
String decoded = URLDecoder.decode(encoded, StandardCharsets.UTF_8);
// "hello world & more"
Go وJava يستخدمان ترميز النماذج افتراضياً (المسافات كـ +). لمخرجات RFC 3986، استبدل + بـ %20 بعد الترميز.
خمسة أخطاء في ترميز URL تُعطِّل الإنتاج
1. الترميز المزدوج (%2520 بدلاً من %20)
ترمِّز سلسلة، يمررها إطار العمل عبر ترميز ثانٍ، فتتحول % في %20 إلى %25. النتيجة: الخادم يرى نص %20 الحرفي بدلاً من مسافة.
العَرَض: العناوين تحتوي على %2520 أو %253D أو أنماط %25xx أخرى.
التشخيص: أي %25 في عنوان مشبوه — يعني أن حرف % تم ترميزه، مما يشير عادة إلى ترميز مزدوج.
الحل: فُكّ الترميز أولاً، ثم رمِّز مرة واحدة. لا ترمِّز سلسلة دون التأكد أنها غير مرمَّزة سابقاً.
// Detect double encoding
function isDoubleEncoded(str) {
return /%25[0-9A-Fa-f]{2}/.test(str);
}
// Safe encode: decode first, then encode
function safeEncode(str) {
try { str = decodeURIComponent(str); } catch (e) { /* not encoded, that's fine */ }
return encodeURIComponent(str);
}
2. + في أجزاء المسار
مطور يرمِّز اسم ملف بمكتبة تُخرِج + للمسافات. الملف my report.pdf يصبح my+report.pdf. الخادم يعامل + كعلامة جمع حرفية ويعيد 404.
القاعدة: + تعني مسافة فقط في سلاسل الاستعلام (بعد ?). في أجزاء المسار، + هي مجرد +. استخدم دائماً %20 للمسافات في المسارات.
3. عناوين إعادة التوجيه المعطلة في OAuth
عنوان التخويل يبدو هكذا:
https://auth.provider.com/authorize?redirect_uri=https://myapp.com/callback?code=abc&state=xyz
خادم OAuth يقرأ redirect_uri=https://myapp.com/callback?code=abc ويعامل state=xyz كمعلمة مستقلة. المصادقة تفشل.
الحل: رمِّز قيمة عنوان إعادة التوجيه بالكامل:
const redirectUri = 'https://myapp.com/callback?code=abc&state=xyz';
const authUrl = `https://auth.provider.com/authorize?redirect_uri=${encodeURIComponent(redirectUri)}`;
// redirect_uri=https%3A%2F%2Fmyapp.com%2Fcallback%3Fcode%3Dabc%26state%3Dxyz
4. نصوص غير مقروءة في السجلات
سجلات الخادم تعرض %E4%BD%A0%E5%A5%BD بدلاً من أحرف صينية. ليس خطأ — الترميز صحيح. المشكلة أن عارض السجلات لا يفك تسلسلات النسبة المئوية.
الحل: مرِّر السجلات عبر فاكّ ترميز، أو الصق العنوان في أداة فك ترميز URL لقراءة النص الأصلي.
5. فشل توقيع API
OAuth 1.0 وAWS Signature V4 يتطلبان ترميز RFC 3986 صارماً. لكن encodeURIComponent() في JavaScript لا ترمِّز ! و' و( و) و*. إن ظهرت في مدخلات التوقيع، لن يتطابق.
الحل: عالج المخرجات لاحقاً:
function rfc3986Encode(str) {
return encodeURIComponent(str).replace(/[!'()*]/g, c =>
'%' + c.charCodeAt(0).toString(16).toUpperCase()
);
}
%20 مقابل + — معضلة ترميز المسافة
معياران، حرف واحد، ارتباك لا ينتهي.
| المعيار | المسافة تصبح | أين ينطبق |
|---|---|---|
| RFC 3986 (صياغة URI) | %20 | في كل مكان في العنوان |
application/x-www-form-urlencoded | + | سلاسل الاستعلام من إرسال نماذج HTML |
اتفاقية + موروثة من بدايات الويب. عند إرسال نموذج <form> بـ method="GET"، المتصفح يرمِّز المسافات كـ + في سلسلة الاستعلام. سلوك مُدمج في مواصفات HTML ولن يختفي.
لكن + تعني “مسافة” في سلاسل الاستعلام فقط. في المسار، + حرفية. لذا https://example.com/my+file.pdf يقدم ملفاً اسمه my+file.pdf وليس my file.pdf.
إرشادات عملية:
- استخدم
%20عند بناء العناوين يدوياً أو ترميز أجزاء المسار. يعمل في كل مكان. - اقبل
+عند تحليل سلاسل الاستعلام من إرسال النماذج — إطار العمل الخاص بك يتعامل مع هذا على الأرجح. - لا تخلط بينهما. اختر اتفاقية واحدة لكل مكوّن والتزم بها.
ترميز URL والأمان
ترميز URL ليس تشفيراً
ترميز النسبة المئوية تحويل حتمي وقابل للعكس تماماً. لا مفتاح ولا سر. أي شخص يفك ترميز %48%65%6C%6C%6F إلى Hello في ثوانٍ.
لا تستخدم ترميز URL لإخفاء بيانات حساسة. استخدم HTTPS لتشفير الطلب كاملاً. العناوين تظهر في سجلات الخادم وسجل المتصفح وترويسات Referer — ضع البيانات الحساسة في أجسام الطلبات فقط.
هجمات إعادة التوجيه المفتوح
المهاجمون يصنعون عناوين مرمَّزة تتجاوز التحقق السطحي. معلمة إعادة توجيه تحتوي %2F%2Fevil.com تُفَك إلى //evil.com — يفسره المتصفح كعنوان نسبي للبروتوكول يشير لنطاق المهاجم.
الدفاع: تحقق دائماً من العنوان بعد فك الترميز، لا من الشكل المرمَّز. استخدم قوائم السماح لنطاقات إعادة التوجيه.
استغلال الترميز المزدوج
جدار حماية تطبيقات الويب (WAF) يفحص العناوين بحثاً عن وسوم <script>. المهاجم يرسل %253Cscript%253E — WAF يرى نصاً مرمَّزاً فيمرره. التطبيق يفك الترميز مرة فيصبح %3Cscript%3E، ثم مرة ثانية فينتج <script> — تجاوز ناجح للفلتر.
الدفاع: وحِّد المدخلات (فُكّ الترميز بالكامل) قبل أي فحص أمني. فك ترميز واحد لا يكفي.
للتعمق أكثر، راجع دليل أساسيات أمان الويب.
حدود طول العنوان ومتى يصبح الترميز مكلفاً
مواصفات HTTP لا تحدد حداً أقصى لطول العنوان، لكن كل طبقة في المكدس تفرض حدوداً عملية.
| الطبقة | الحد |
|---|---|
| التوصية العامة | 2,000 حرف |
| Chrome، Firefox | حوالي 2 ميغابايت (لكن الخوادم ترفض قبل ذلك بكثير) |
| Apache (افتراضي) | 8,190 بايت |
| Nginx (افتراضي) | 8,192 بايت |
| IIS | 16,384 بايت (سلسلة الاستعلام) |
| شبكات CDN، البروكسيات | متغير — غالباً 4,096-8,192 بايت |
الترميز يزيد طول العنوان بشكل ملحوظ. حرف صيني واحد يتمدد من حرف إلى 9 أحرف (%E4%B8%AD). رمز تعبيري يتمدد إلى 12. بـ 200 حرف صيني في سلسلة الاستعلام، تحصل على 1,800 حرف مرمَّز قبل حساب العنوان الأساسي.
عند بلوغ الحد: انقل البيانات من معلمات الاستعلام إلى جسم طلب POST. لواجهات البحث، استخدم نقطة نهاية POST تقبل جسم JSON بمعايير البحث.
الأسئلة الشائعة
ما هو ترميز URL ولماذا يحتاجه المطورون؟
ترميز URL (ترميز النسبة المئوية) يحوّل الأحرف غير المسموحة في العناوين إلى تسلسلات %XX ست عشرية. العناوين تدعم 66 حرفاً غير محجوز فقط من ASCII، فالمسافات وعلامات العطف ونصوص Unicode ومعظم علامات الترقيم تحتاج ترميزاً كي لا تكسر بنية العنوان.
ما الفرق بين encodeURI و encodeURIComponent؟
encodeURI() ترمِّز عنواناً كاملاً وتحافظ على الأحرف الهيكلية (://، /، ?، &). encodeURIComponent() ترمِّز كل شيء عدا A-Z a-z 0-9 - _ . ~ ! ' ( ) *. استخدم الثانية لقيم المعلمات، والأولى فقط عندما تريد إصلاح مسافات أو أحرف غير ASCII في عنوان كامل دون كسر بنيته.
لماذا يظهر %20 أحياناً كـ + في العناوين؟
كلاهما يمثل مسافة لكن من معيارين مختلفين. %20 من RFC 3986 ويعمل في كل أجزاء العنوان. + من مواصفات نماذج HTML ويعمل فقط في سلاسل الاستعلام — في المسار، + علامة جمع حرفية. %20 آمن دائماً؛ + اتفاقية قديمة باقية بسبب سلوك النماذج.
كيف أرمِّز نصاً في Python و JavaScript و Go و Java؟
JavaScript: encodeURIComponent('hello world') ← hello%20world. Python: urllib.parse.quote('hello world') ← hello%20world. Go: url.QueryEscape("hello world") ← hello+world. Java: URLEncoder.encode("hello world", UTF_8) ← hello+world. Go و Java يستخدمان افتراضياً ترميز النماذج (المسافة كـ +) — استبدل + بـ %20 لمخرجات RFC 3986.
هل يمكن استخدام ترميز URL للأمان أو التشفير؟
لا. الترميز قابل للعكس فوراً دون مفتاح — لا سرية فيه. احمِ البيانات الحساسة بـ HTTPS (تشفير TLS). العناوين مكشوفة في سجلات الخادم وسجل المتصفح وترويسات Referer، فضع البيانات الحساسة في أجسام الطلبات.
ما هو الترميز المزدوج وكيف أصلحه؟
يحدث عندما تُرمَّز سلسلة مرمَّزة سابقاً مرة أخرى. % في %20 تصبح %25، فتنتج %2520. الخادم يرى %20 كنص حرفي بدلاً من مسافة. الحل: فُكّ الترميز أولاً ثم رمِّز مرة واحدة. وجود %25 متبوعاً برقمين ست عشريين هو العلامة الكاشفة.
ما الحد الأقصى لطول العنوان؟
لا حد رسمي في مواصفات HTTP، لكن 2,000 حرف هو الحد الآمن عملياً. Apache يسمح بـ 8,190 بايت افتراضياً، وNginx بـ 8,192 بايت. الأحرف غير ASCII تتمدد 3-12 ضعفاً بالترميز، فالعناوين الدولية تبلغ الحد أسرع. للبيانات الكبيرة، انتقل إلى POST مع جسم طلب.