URL Encoding: کدگذاری درصدی چطور کار می‌کند

URL Encoding هر بایتی را که نمی‌تواند بدون تغییر در آدرس وب بیاید، با یک علامت % و مقدار هگزادسیمال همان بایت جایگزین می‌کند.

همین حالا امتحان کنید: کدگذار و کدگشای URL رمزگشای URL

یک URL فقط می‌تواند مجموعه کوچکی از نویسه‌های ASCII را در خود داشته باشد و چند تا از این نویسه‌ها وظیفه مشخصی دارند: / بخش‌های مسیر را از هم جدا می‌کند، ? شروع query را نشان می‌دهد، & پارامترها را از هم جدا می‌کند و # ابتدای fragment را مشخص می‌کند. همین که یک داده شامل فاصله، حرف لهجه‌دار یا یکی از این نویسه‌ها باشد، باید کدگذاری شود. URL Encoding، که کدگذاری درصدی (percent-encoding) هم نامیده می‌شود، در RFC 3986 تعریف شده است. پیامی که این کدگذاری در واقع به سرور می‌دهد این است: «این نویسه بخشی از مقدار است، نه جداکننده».

اصل کار: هر بایت به سه نویسه تبدیل می‌شود

یک نویسه کدگذاری‌شده به شکل % و پس از آن دو رقم هگزادسیمال نوشته می‌شود که مقدار یک بایت را نشان می‌دهند (RFC 3986، بخش ۲٫۱). کد نویسه فاصله ۳۲ است که در مبنای شانزده می‌شود 20، پس فاصله به %20 تبدیل می‌شود. کدگذاری هر متنی سه مرحله دارد:

  1. متن را به بایت‌های UTF-8 تبدیل کنید.
  2. بایت‌هایی را که نماینده نویسه‌های رزرونشده هستند (حروف ASCII، رقم‌ها و - . _ ~) بدون تغییر باقی بگذارید.
  3. هر بایت دیگر را با % و مقدار هگزادسیمالش جایگزین کنید، ترجیحاً با حروف بزرگ.

یک نویسه خارج از ASCII در UTF-8 چند بایت فضا می‌گیرد، و به همین دلیل یک حرف ممکن است به چند گروه درصدی تبدیل شود:

نویسهبایت‌های UTF-8شکل کدگذاری‌شده
فاصله20%20 (یا + در فرم)
&26%26
=3D%3D
+2B%2B
éC3 A9%C3%A9
€E2 82 AC%E2%82%AC
😀F0 9F 98 80%F0%9F%98%80

به این ترتیب «café & thé» به caf%C3%A9%20%26%20th%C3%A9 تبدیل می‌شود. انتخاب UTF-8 دلبخواهی نیست: RFC 3986 آن را توصیه می‌کند (بخش ۲٫۵) و مرورگرها و زبان‌های برنامه‌نویسی امروزی از آن استفاده می‌کنند. بعضی سیستم‌های قدیمی متن را با Latin-1 کدگذاری می‌کردند که در آن é فقط یک بایت است و به %E9 تبدیل می‌شود. رمزگشای UTF-8 این دنباله را رد می‌کند: در JavaScript، decodeURIComponent('%E9') خطای URIError: URI malformed (URI بدشکل) می‌دهد.

نویسه‌های رزروشده و رزرونشده

RFC 3986 نویسه‌ها را در دو گروه قرار می‌دهد:

  • رزرونشده (unreserved، بخش ۲٫۳): A-Z a-z 0-9 - . _ ~. این نویسه‌ها هیچ‌وقت نیازی به کدگذاری ندارند و استاندارد از شما می‌خواهد آن‌ها را کدگذاری نکنید: %41 و A یک معنا دارند.
  • رزروشده (reserved، بخش ۲٫۲): جداکننده‌های کلی : / ? # [ ] @ و جداکننده‌های فرعی ! $ & ' ( ) * + , ; =. این نویسه‌ها ساختار URL را می‌سازند.

قاعده در یک جمله خلاصه می‌شود: نویسه رزروشده وقتی نقش جداکننده دارد بدون تغییر نوشته می‌شود، و وقتی بخشی از داده است کدگذاری می‌شود. مقایسه کنید:

https://example.com/search?q=bread&milk      -> q = "bread", plus an empty parameter "milk"
https://example.com/search?q=bread%26milk    -> q = "bread&milk"

در خط اول، مقدار q فقط «bread» است و «milk» یک پارامتر خالی جداگانه می‌شود؛ در خط دوم، مقدار q کل «bread&milk» است. هر نویسه دیگری، از جمله فاصله، گیومه، خود %، حروف لهجه‌دار و ایموجی‌ها، هر جا که بیاید باید کدگذاری شود. بنابراین علامت % به شکل %25 نوشته می‌شود: «100 %» همراه با فاصله‌اش به 100%20%25 تبدیل می‌شود.

فاصله: %20 یا +؟

طبق RFC 3986، فاصله به %20 کدگذاری می‌شود. اما فرم‌های HTML از قالب دیگری به نام application/x-www-form-urlencoded استفاده می‌کنند که در آن فاصله به + تبدیل می‌شود و یک علامت به‌علاوه واقعی باید به شکل %2B نوشته شود. مرورگرها این قالب را برای فرم‌هایی که با GET فرستاده می‌شوند به کار می‌برند و بیشتر سرورها query string را به همین روش رمزگشایی می‌کنند.

همین موضوع به یک اشتباه رایج منجر می‌شود. در مسیر، + فقط یک علامت به‌علاوه است. اما در query string معمولاً فاصله خوانده می‌شود: جست‌وجوی «C++» که بدون کدگذاری فرستاده شود، یعنی ?q=C++، به شکل «C» و پس از آن دو فاصله به سرور می‌رسد. شکل درست کدگذاری‌شده آن ?q=C%2B%2B است.

زبانفاصله به شکل %20 (RFC 3986)فاصله به شکل + (فرم‌ها)
JavaScriptencodeURIComponent()URLSearchParams
PHPrawurlencode()urlencode()، http_build_query()
Pythonurllib.parse.quote()quote_plus()، urlencode()

در query string هر دو شکل درست‌اند، به شرطی که طرفی که رمزگشایی می‌کند از همان قراردادی پیروی کند که طرف کدگذار استفاده کرده است.

کدگذاری در JavaScript: از کدام تابع استفاده کنیم

JavaScript دو تابع داخلی دارد که در نویسه‌هایی که دست‌نخورده باقی می‌گذارند با هم فرق دارند. برای یک مقدار تکی، چه پارامتر query باشد چه یک بخش از مسیر، از encodeURIComponent استفاده کنید: این تابع همه‌چیز را به‌جز A-Z a-z 0-9 - _ . ! ~ * ' ( ) کدگذاری می‌کند. encodeURI فرض می‌کند یک URL کامل با ساختار درست به آن داده شده، پس جداکننده‌هایی مثل &، / و ? را دست نمی‌زند، حتی وقتی بخشی از داده شما باشند. فهرست دقیق نویسه‌هایی که هر تابع کدگذاری نمی‌کند، و حالت‌هایی که encodeURI هنوز ابزار درستی است، در راهنمای encodeURI یا encodeURIComponent آمده است.

وقتی یک URL را از بخش‌هایش می‌سازید، امن‌ترین راه این است که بگذارید API مربوط به URL هر بخش را در جای مناسب خودش کدگذاری کند:

const url = new URL('https://example.com/search');
url.searchParams.set('q', 'café & thé');
url.searchParams.set('sort', 'price');
url.href;  // 'https://example.com/search?q=caf%C3%A9+%26+th%C3%A9&sort=price'

دقت کنید که searchParams از قرارداد فرم‌ها پیروی می‌کند، به همین دلیل فاصله‌ها به شکل + درمی‌آیند.

مثال در JavaScript، PHP و Python

در ادامه همان مقدار در هر سه زبان کدگذاری و رمزگشایی شده، با هر دو قرارداد برای فاصله:

// JavaScript
encodeURIComponent('café & thé');                 // 'caf%C3%A9%20%26%20th%C3%A9'
new URLSearchParams({ q: 'café & thé' }).toString(); // 'q=caf%C3%A9+%26+th%C3%A9'
decodeURIComponent('caf%C3%A9%20%26%20th%C3%A9'); // 'café & thé'

// PHP (file saved as UTF-8)
rawurlencode('café & thé');                // caf%C3%A9%20%26%20th%C3%A9
urlencode('café & thé');                   // caf%C3%A9+%26+th%C3%A9
http_build_query(['q' => 'café & thé']);   // q=caf%C3%A9+%26+th%C3%A9
urldecode('caf%C3%A9+%26+th%C3%A9');       // café & thé

# Python
from urllib.parse import quote, quote_plus, urlencode, unquote_plus
quote('café & thé')                    # 'caf%C3%A9%20%26%20th%C3%A9'
quote_plus('café & thé')               # 'caf%C3%A9+%26+th%C3%A9'
urlencode({'q': 'café & thé'})         # 'q=caf%C3%A9+%26+th%C3%A9'
unquote_plus('caf%C3%A9+%26+th%C3%A9') # 'café & thé'

دو جزئیات دانستنی است. در Python، quote() به‌طور پیش‌فرض اسلش را دست نمی‌زند: quote('a/b') مقدار a/b را برمی‌گرداند. برای کدگذاری یک مقدار مستقل، safe='' را بفرستید تا a%2Fb به دست بیاید. در PHP، urlencode() علامت مد (tilde) را هم به %7E کدگذاری می‌کند، در حالی که rawurlencode() آن را همان‌طور که هست باقی می‌گذارد، همان‌طور که RFC 3986 می‌خواهد.

کدگذاری دوباره

کدگذاری رشته‌ای که از قبل کدگذاری شده، هر % را به %25 تبدیل می‌کند. %20 به %2520 و caf%C3%A9 به caf%25C3%25A9 تبدیل می‌شود. در قرارداد فرم‌ها، این + است که مشکل را لو می‌دهد: در PHP، urlencode(urlencode('a b')) مقدار a%2Bb را برمی‌گرداند که سرور آن را به‌جای «a b» به «a+b» رمزگشایی می‌کند.

علت تقریباً همیشه یکی است: دو لایه از کد یک مقدار را کدگذاری می‌کنند؛ مثلاً یک کتابخانه HTTP پارامترهایی را کدگذاری می‌کند که خودتان قبلاً کدگذاری کرده‌اید. همین دام در سمت رمزگشایی هم وجود دارد: در PHP، $_GET از قبل مقدارهای رمزگشایی‌شده را در خود دارد و صدا زدن urldecode() روی آن‌ها یک بار دیگر رمزگشایی می‌کند، که از جمله علامت‌های + مشروع را به فاصله تبدیل می‌کند. قاعده این است: مقدار خام را یک بار، در لحظه‌ای که وارد URL می‌شود، کدگذاری کنید و هنگام خواندن هم یک بار رمزگشایی کنید.

فقط یک حالت چیزی را که شبیه کدگذاری دوباره است توجیه می‌کند: URLی که به‌عنوان پارامتر یک URL دیگر فرستاده می‌شود، مثلاً در redirect=. اگر URL داخلی از قبل %20 داشته باشد، شکل کدگذاری‌شده‌اش %2520 خواهد داشت و این درست است: گیرنده یک بار رمزگشایی می‌کند و URL اصلی را پس می‌گیرد.

کدگذاری و رمزگشایی آنلاین

برای بررسی یک مقدار بدون نوشتن کد، کدگذار URL هر دو حالت encodeURIComponent و encodeURI را دارد تا بتوانید نتیجه آن‌ها را روی متن خودتان مقایسه کنید. کدگشای URL همزمان با تایپ رمزگشایی می‌کند و مثل فرم‌ها + را فاصله می‌خواند؛ پس یک علامت به‌علاوه واقعی را باید در آن به شکل %2B وارد کنید. همه این کارها داخل مرورگر خودتان انجام می‌شود.

سوالات متداول

چرا فاصله در URL به %20 تبدیل می‌شود؟

کد نویسه فاصله در مبنای ده ۳۲ است که در مبنای شانزده 20 می‌شود. URL Encoding هر بایت را به شکل % و پس از آن دو رقم هگزادسیمال همان بایت می‌نویسد؛ نتیجه می‌شود %20.

آیا حروف لهجه‌دار در URL باید کدگذاری شوند؟

بله. حرفی مثل é به بایت‌های UTF-8 تبدیل می‌شود و بعد هر بایت کدگذاری می‌شود: é به %C3%A9 تبدیل می‌شود. مرورگرها اغلب در نوار آدرس شکل رمزگشایی‌شده را نشان می‌دهند، اما درخواستی که فرستاده می‌شود شکل کدگذاری‌شده را دارد.

تفاوت %20 و + چیست؟

%20 کدگذاری استاندارد فاصله طبق RFC 3986 است. علامت + فقط در داده‌های فرم به معنای فاصله است، که در عمل یعنی در query string؛ در مسیر، یک علامت به‌علاوه است.

کدام نویسه‌ها هیچ‌وقت نیازی به کدگذاری ندارند؟

نویسه‌های رزرونشده RFC 3986: حروف ASCII بدون علامت، رقم‌ها، خط تیره، نقطه، زیرخط و علامت مد (tilde). همه نویسه‌های دیگر وقتی بخشی از داده باشند باید کدگذاری شوند.
همین حالا امتحان کنید: کدگذار و کدگشای URL رمزگشای URL