در دنیای کامپیوتر، تمام اطلاعات به صورت باینری (0 و 1) ذخیره و پردازش میشوند، زیرا ساختار سختافزاری کامپیوترها بر اساس سیستم باینری عمل میکند. اما ما انسانها با کاراکترها مانند حروف (A - B - C)، نمادها (! - #) و غیره سروکار داریم. پس باید راهی برای تبدیل این کاراکترها به اعداد باینری و برعکس وجود داشته باشد. این کار از طریق سیستمهای کدگذاری کاراکتر انجام میشود که قوانین و استانداردهایی را برای این تبدیل تعریف میکنند.
بیایید سفری به گذشته داشته باشیم. اولین بار که کامپیوترها تولید شدند، افراد باید راهی پیدا میکردند تا کاراکترهای متنی را به شکل باینری تبدیل کنند. یکی از اولین سیستمهای کدگذاری که برای این منظور ایجاد شد، سیستم ASCII بود. این سیستم از 7 بیت برای هر کاراکتر استفاده میکرد، یعنی میتوانست 128 کاراکتر مختلف را کدگذاری کند. این 128 کاراکتر شامل حروف بزرگ و کوچک انگلیسی، اعداد، علایم نگارشی (مثل !، #، ، @ و…) و برخی کاراکترهای کنترلی (مثل \n برای خط جدید) بودند. در سیستم ASCII هر کاراکتر یک کد عددی مشخص دارد که به آن، کد ASCII میگویند. به عبارت دیگر، هر کاراکتر در ASCII دارای یک مقدار عددی (از 0 تا 127) است که نماینده آن کاراکتر است. مثلا :
حرف A در ASCII کد عددی 65 دارد.
حرف B در ASCII کد عددی 66 دارد.
کاراکتر خط جدید کد عددی 10 دارد.
این کد ها به Binary تبدیل میشوند و به این شکل در حافظه کامپیوتر ذخیره میشوند :
برای مثال عدد 65 بصورت باینری 01000001 ذخیره میشود که معادل حرف A هست.
ولی مشکلی وجود داشت !؟
ASCII فقط 128 کاراکتر را پوشش میداد، که برای زبانهای مختلف جهانی کافی نبود. به عنوان مثال :
زبانهای غیر انگلیسی که کاراکترهای بیشتری داشتند، نمیتوانستند تحت این سیستم پوشش داده شوند.
حتی ایموجیها و نمادهای اخیراً متداول نیز نمیتوانستند تحت این سیستم ذخیره شوند.
حالا راه حل چی بود ؟ Unicode
با گسترش استفاده از کامپیوتر در سراسر جهان و نیاز به پوشش دادن زبانها و کاراکترهای مختلف، نیاز به یک سیستم کدگذاری جامعتر به وجود آمد. اینجا بود که Unicode وارد میدان شد. هدف از ایجاد Unicode این بود که تمام کاراکترها، زبانها و نمادها در سراسر جهان پوشش داده شوند.
در Unicode هم، هر کاراکتر دارای یک کد عددی خاصه که به آن Code Point میگویند. این کدها بصورت Hexadecimal (مبنای 16) نشان داده میشوند و اغلب با پیشوند +U معرفی میشوند. به عنوان مثال :
کد یونیکد حرف A مقدار U+0041 است.
کد یونیکد حرف B مقدار U+0042 است.
کد یونیکد ایموجی 😊 مقدار U+1F60A است.
درنهایت هدف از ایجاد Unicode این بود که تمام کاراکترها، زبانها و نمادها در سراسر جهان پوشش داده شوند.
ما فهمیدیم که Unicode، هر کاراکتر را با یک کد عددی به نام کد پوینت (Code Point) معرفی میکند. اما تنها داشتن کد پوینت کافی نیست ! باید روشی برای ذخیره و انتقال این کدها هم داشته باشیم. سه روش اصلی برای ذخیرهسازی و انتقال کد پوینتهای Unicode وجود دارد که به آنها UTF-8 و UTF-16 و UTF-32 میگویند. بریم با ساده ترین توضیحات این موارد رو بررسی کنیم...
آرتا میخواد پیام "سلام 😊" رو برای سارا ارسال کنه. این پیام ترکیبی از کاراکترهای فارسی و یک ایموجی هست. علی میدونه که برای اینکه این پیام به درستی به سارا برسه، باید از UTFها استفاده کنه.
UTF-8 :
در UTF-8 هر کاراکتر میتونه بین 1 تا 4 بایت باشه. این یعنی حروف ساده (مثل حروف انگلیسی) فقط 1 بایت و کاراکترهای پیچیدهتر (مثل ایموجیها) بایت های بیشتر نیاز دارند. مثال پیام "سلام 😊" :
س : 3 بایت
ل : 2 بایت
ا : 2 بایت
م : 3 بایت
😊 : 4 بایت
بنابراین، این پیام به این شکل ذخیره میشه : (یک دنبالهای از بایتها)
وقتی آرتا این پیام رو ارسال میکنه، رشته بایتی ناشی از اون به سمت سارا ارسال میشه. وقتی پیام به سارا میرسه، نرمافزارش میدونه که چطور این بایتها رو به کاراکترها برگردونه، چون از UTF-8 استفاده شده.
UTF-16 :
در UTF-16 هر کاراکتر میتونه 2 یا 4 بایت باشه. بیشتر کاراکترهای فارسی در 2 بایت ذخیره میشن، اما ایموجی به 4 بایت نیاز داره. مثال پیام "سلام 😊" :
س : 2 بایت
ل : 2 بایت
ا : 2 بایت
م : 2 بایت
😊 : 4 بایت
پیام به این شکل ذخیره میشه : (دنبالهای از بایتها، اما این بار بیشتر کاراکترها 2 بایت هستند)
مثل UTF-8، وقتی که این پیام ارسال میشه، بایتها به سمت سارا میرن، و سارا نرمافزارش میدونه که پیام از UTF-16 استفاده میکنه.
UTF-32 :
در UTF-32 هر کاراکتر دقیقا 4 بایت هست، بدون توجه به پیچیدگی کاراکتر. مثال پیام "سلام 😊" :
س : 4 بایت
ل : 4 بایت
ا : 4 بایت
م : 4 بایت
😊 : 4 بایت
پیام به این شکل ذخیره میشه : (همه کاراکترها 4 بایتی هستند)
وقتی که آرتا این پیام رو ارسال میکنه، بایتها به شکل 4 بایتی به سمت سارا ارسال میشن، و سارا به راحتی اونها رو به کاراکترها تبدیل میکنه.
چگونه نوع UTF مشخص میشود؟
هنگام ارسال دادهها (مثلاً وقتی آرتا پیامش رو ارسال میکنه) هدر اطلاعاتی وجود داره که نوع کدگذاری رو مشخص میکنه. مثلاً :
Content-Type: text/plain; charset=UTF-8
به این شکل نرمافزار سارا میدونه که باید از UTF-8 برای decode کردن استفاده کنه.
دلیل وجود و کاربردهای هر UTF :
UTF-8 :
انعطافپذیری : کاراکترهای ساده مانند حروف انگلیسی فقط 1 بایت نیاز دارند، و کاراکترهای پیچیدهتر (مثل کاراکترهای فارسی یا ایموجیها) بایت بیشتری نیاز دارند. (یعنی مثل UTF-32 نیست که هر کاراکتر فقط باید 4 بایت باشه)
فشردگی : به دلیل مصرف کم فضا برای کاراکترهای رایج، برای فایلهای متنی کوچک بسیار مناسب است.
سازگاری با ASCII : تمامی کاراکترهای ASCII (128 کاراکتر اول) معادل خود را در UTF-8 دارند و دقیقاً به همان شکل ذخیره میشوند.
کجا ها استفاده میشه ؟
صفحات وب (به دلیل فشردگی و سازگاری با سیستمهای موجود)
فایلهای متنی ساده (مثل ایمیلها و اسناد)
UTF-16 :
کاراکترهای دو بایتی : بسیاری از کاراکترهای زبانهای پیچیدهتر مانند عربی، چینی، ژاپنی و هندی به طور پیشفرض در 2 بایت ذخیره میشوند. این ویژگی سبب میشود که فضای ذخیرهسازی برای این زبانها بهینهتر و کارآمدتر باشد. به عنوان مثال، بسیاری از کاراکترهای رایج در این زبانها به طور مستقیم در همان 2 بایت جا میگیرند و نیاز به تخصیص فضای بیشتر ندارند.
تعادل بین فضا و کارایی : UTF-16 یک توازن مناسب بین مصرف حافظه و عملکرد فراهم میکند. بسیاری از زبانهای جهان شامل کاراکترهای زیادی هستند که در UTF-16 به صورت کاراکترهای دو بایتی ذخیره میشوند. این تعادل به معنای کاهش فضای مصرفی نسبت به UTF-32 و افزایش کارایی در پردازش نسبت به UTF-8 است.
کجا ها استفاده میشه ؟
سیستمعاملها و APIها : ویندوز از UTF-16 برای نمایندگی کاراکترها در APIهای داخلی خود استفاده میکند.
زبانهای برنامهنویسی : جاوا و C# از UTF-16 برای نمایندگی داخلی کاراکترهای رشتهها استفاده میکنند.
پایگاههای داده : برخی پایگاههای داده مانند SQL Server از UTF-16 برای ذخیرهسازی و مدیریت دادههای متنی پشتیبانی میکنند.
UTF-32 :
ساده و ثابت : هر کاراکتر دقیقاً در 4 بایت ذخیره میشود. این سادگی برای پردازشهای سریع مزیت دارد.
بینیاز از تغییر سایز : برای زبانها و نمادهایی که باید به سرعت و بدون تغییر سایز بایتها پردازش شوند، مناسب است.
کجا ها استفاده میشه ؟
سیستمهای داخلی که نیاز به پردازش سریع دادهها دارند (مثلاً در برخی سیستمهای پایگاه داده یا برنامههای تحلیل متنی پیشرفته)
مواردی که حجم دادهها مهم نیست و نیاز به پردازش کاراکترها بدون تعیین اندازه آنها مهم است.
درنهایت :
از UTF-8 استفاده کنید اگر به دنبال فشردگی و سازگاری با ASCII هستید، به خصوص برای متنهای ساده و صفحات وب.
از UTF-16 استفاده کنید اگر با زبانها و اسکریپتهای پیچیدهتر کار میکنید و یک تعادل بین فضای مصرفی و کارایی نیاز دارید.
از UTF-32 استفاده کنید اگر سادگی و سرعت پردازش برای شما اهمیت دارد و حجم ذخیرهسازی برای شما مسئلهای نیست.
تمرین با پایتون
پایتون یک متد مفید به نام encode داره که به ما کمک میکنه متنها را به بایتها تبدیل کنیم.
در پایتون، رشتهها به صورت پیشفرض با استفاده از Unicode ذخیره میشوند. اما برای ذخیره آنها در فایلها یا ارسال از طریق شبکه، نیاز به تبدیل این رشتهها به بایتها داریم. متد encode برای همین منظور استفاده میشه. این متد یک آرگومان (ورودی) میگیره که مشخص میکنه از چه نوع کدگذاری استفاده بشه (مثلا UTF-8 یا UTF-16 یا UTF-32 که البته اگه کدگذاری مشخص نشه، به طور پیشفرض از UTF-8 استفاده میکنه) بیایید این را با یک مثال ببینیم :
text = 'سلام 😊' # utf-8 ذخیره بصورت utf8_data = text.encode('utf-8') print(utf8_data) # Result : b'\xd8\xb3\xd9\x84\xd8\xa7\xd9\x85 \xf0\x9f\x98\x8a' # utf-16 ذخیره بصورت utf16_data = text.encode('utf-16') print(utf16_data) # Result : b'\xff\xfe3\x06(\x06-\x06(\x06M\x06 \x00=\xd8\n\xde' # utf-32 ذخیره بصورت utf32_data = text.encode('utf-32') print(utf32_data) # Result : b'\xff\xfe\x00\x002\x06\x00\x00(\x06\x00\x00-\x06\x00\x00(\x06\x00\x00M\x06\x00\x00 \x00\x00\x00=\xd8\n\xde'
طول رشتههای کدگذاری شده توسط UTF-8 و UTF-16 تقریبا هم اندازست. چرا ؟
برای کاراکترهای فارسی، حجم خروجی UTF-8 و UTF-16 تقریباً مشابه است (هر کاراکتر 2 بایت) اما UTF-32 به دلیل استفاده از 4 بایت برای هر کاراکتر حجم بیشتری دارد.
b در ابتدای رشته ها به چه معناست ؟
وقتی شما در پایتون از encode استفاده میکنید، یک رشته از نوع بایت (byte string) تولید میشود. بیایید ببینیم این به چه معنی است :
رشتههای عادی (strings) :
در پایتون، وقتی رشتهای مثل 'hello' دارید، این رشته حاوی کاراکترهای یونیکد است. یونیکد مجموعهای از کاراکترهاست که شامل حروف، اعداد، علامتها و حتی ایموجیها میشود. این نوع رشتهها میتوانند انواع مختلف کاراکترها را پشتیبانی کنند.
رشتههای بایت (byte strings) :
رشتههای بایت، رشتههایی هستند که به جای کاراکترهای پیچیده، از بایتهای خام تشکیل شدهاند. بایتها دنبالهایی از 0 و 1 هستند که هر بایت 8 بیت است. برای نشان دادن این نوع رشتهها، پایتون از حرف b در ابتدای آن استفاده میکند. این b به شما میگوید که این رشته از نوع باینری (بایت استرینگ) است، و هر کاراکتر در آن در واقع یک بایت (8 بیت) است.
دلیل استفاده از بایت استرینگها این است که بایتها فرمت سادهتری هستند که برای انتقال و ذخیرهسازی اطلاعات دیجیتال مناسبترند.
چرا خروجیها به شکل \xff\xfe\x00\x00h\x00\x00\x00… نمایش داده میشوند ؟
اگه یک بایت را به صورت 8 بیت (0 و 1) نمایش دهیم، خواندن و درک آن برای انسان مشکل است. بنابراین، به جای نمایش 0 و 1، بایتها (که هر کدام 8 بیت هستند) به صورت دو رقمیهای هگزادسیمال (پایه 16) نمایش داده میشوند، که خواندن آنها آسانتر است. به عنوان مثال :
هر xff\ در باینری برابر است با 11111111.
ابتدا x\ قرار میگیره و بعدش از 00 تا ff که برای Hexadecimal هست قرار میگیره تا بیت های اون رو بگه. همینطور که بالاتر گفته شد، ff که از Hexadecimal هست، داخل باینری میشه 11111111.
متد decode
متد decode برعکس encode عمل میکنه. یعنی یک سری بایتها را به یک رشته Unicode تبدیل میکند. این متد برای تبدیل دادههای بایتی به رشته استفاده میشود. (توجه داشته باشید که برای decode کردن، ابتدا باید دادههایی که encode شدهاند را داشته باشیم) مثال :
a = 'سلام 😊'.encode('utf-8') print(a) # Result : b'\xd8\xb3\xd9\x84\xd8\xa7\xd9\x85 \xf0\x9f\x98\x8a' b = a.decode('utf-8') print(b) # Result : سلام 😊
حالا که داغیم، یک موضوع دیگه رو بررسی کنیم...
a = 'Hello' print(a.encode('utf-8')) # Result : b'Hello' print(a.encode('utf-16')) # Result : b'\xff\xfeH\x00e\x00l\x00l\x00o\x00' print(a.encode('utf-32')) # Result : b'\xff\xfe\x00\x00H\x00\x00\x00e\x00\x00\x00l\x00\x00\x00l\x00\x00\x00o\x00\x00\x00'
من اومدم Hello رو با UTF-8 رمزگذاری کردم، اما چرا خروجی که برگشت 'b'Hello هست اما خروجی داخل UTF-16 و UTF-32 به یه شکل دیگست؟
a.encode('utf-8') : وقتی ما از utf-8 استفاده میکنیم، هر حرف انگلیسی مثل ‘H’، ‘e’، ‘l’، ‘o’ با یک عدد (که معادل یک بایت یا 8 بیت هست) نمایش داده میشه. برای همین وقتی a.encode('utf-8') رو اجرا میکنیم، خروجی b'Hello' میشه. این یعنی رشته ما به یک سری بایت تبدیل شده. ‘b’ اولش نشون میده که ما با بایتها سر و کار داریم. در واقع هر کاراکتر Hello اینجا با یک عدد یا یک بایت نمایش داده شده و دقیقا مثل قبل به نظر میرسه.
این بایتها داخل کامپیوتر به صورت دودویی (0 و 1) ذخیره میشن. اما وقتی پایتون میخواد این بایتها رو به ما نشون بده، اونها رو به شکل کاراکتری (در صورتی که معادل کاراکتری داشته باشند) نشون میده و اگه معادل کاراکتری نداشته باشن، بصورت Hexa نمایش میده که فضای زیادی گرفته نشه.
a.encode('utf-16') : اما وقتی با utf-16 رمزگذاری میکنیم، قضیه فرق میکنه. در utf-16 هر کاراکتر معمولا با دو بایت نمایش داده میشه. به خاطر همین، هر حرف Hello در خروجی دو بایت اشغال میکنه، مثلاً H به شکل H\x00 نمایش داده میشه و نمیشه همون H رو قرار داد چون مثل UTF-8 یک بایت نیست که همون یک بایت نماینده این کاراکتر باشه، بلکه برای H دو بایت وجود داره و میاد این دو بایت رو در قالب Hexa نمایش میده.
a.encode('utf-32') : در utf-32 هم باز قضیه همینه، فقط هر کاراکتر با 4 بایت نمایش داده میشه، به همین خاطر H به شکل H\x00\x00\x00 و … نمایش داده میشه.
قسمت آخر مقاله به کد زیر دقت کن :
print('salam'.encode('ascii')) # b'salam' # اینجا خطایی وجود نداره چرا که تمام کاراکتر های داخل رشته داخل اسکی هم وجود دارن # print('سلام'.encode('ascii')) # اینجا خطایی به وجود میاد که میگه این کاراکتر ها معادلش داخل اسکی وجود نداره print('سلام'.encode('utf-8')) # هیچ خطایی نمیده چرا که کاراکتر های داخل رشته درون این سیستم کدگذاری وجود داره و میتونه معادلش رو بدست بیاره print(ord('س')) # 1587 # کد عددی کاراکتری که بهش دادم رو از سیستم 'یونیکد' برمیگردونه print(chr(1587)) # کد عددی یک کاراکتر رو بهش میدیم و اون کاراکتر رو برامون نمایش میده # Result : س