یونیکد و رمزگذاری کاراکتر در پایتون

November 2021

یونیکد و رمزگذاری کاراکتر در پایتون

گاهی مدیریت رمزگشایی کاراکتر در پایتون یا هر زبان برنامه‌نویسی دیگری سخت می‌شود. در جاهایی مانند Stack Overflow هزاران سوال انباشته شده که از سردرگمی در مورد استثناهایی مانند UnicodeDecodeError و UnicodeEncodeError نشات می‌گیرند. هدف از تهیه این متن آموزشی زدودن مه ابهام از مبحث استثناء‌ها یعنی Exception است و همچنین این موضوع را روشن می‌کند که تجربه کار با متن و داده‌های باینری در پایتون 3 می‌تواند بی‌دردسر و لذت‌بخش باشد. پایتون به طرز قوی و قدرتمندی از یونیکد پشتیبانی می‌کند، اما مسلط شدن به آن کمی زمان‌بر است.

 

این متن آموزشی با سایر متون آموزشی متفاوت است زیرا مستقل از دانستن زبان برنامه‌نویسی نوشته شده، بلکه از روی عمد پایتون محور است. البته در ابتدا کمی با مفاهیم مربوط به زبان برنامه‌نویسی آشنا خواهید شد، اما بعد از آن صرفا توضیحات و مسائل در مورد پایتون هستند و سعی شده تا حد امکان سنگینی متن و مفهوم در پاراگراف به حداقل برسد. خواهید دید که چطور از مفاهیم مربوط به رمزگذاری کاراکترها در کدهای زنده پایتون استفاده می‌کنیم.

 

پس خواندن و مسلط شدن به این متن آموزشی در پایان به موارد زیر دست پیدا خواهید کرد:

 

رمزگذاری کاراکتر و سیستم‌های عددی به قدری به هم مرتبط هستند که هر دوی آنها باید در یک مبحث آموزش پوشش داده شوند، در غیر این صورت رفتار شما نسبت به  هر یک از آنها کاملاً ناکافی خواهد بود.

 

نکته: این مقاله بر اساس پایتون 3 نوشته شده است. به ویژه اینکه تمام مثال‌هایی که از کدها آورده شده در پوسته CPython 3.7.2 نوشته شده، با این وجود تمام نسخه‌های پیش از پایتون 3 (بیشتر آنها) باید عملکرد متنی مشابهی داشته باشند. اگر هنوز از پایتون 2 استفاده می‌کنید و از تفاوت‌های عملکرد پایتون 2 و پایتون 3 در رابطه با متن و داده‌های باینری می‌ترسید، امیدواریم این آموزش به شما کمک کند تا تغییر لازم را انجام دهید و بدون ترس به نسخه جدید‌تر روی آورید.

 

مقاله پیشنهادی: معرفی لیست‌های پیوندی در پایتون

 

 #  رمزگذاری(Encoding) کاراکتر چیست؟

اگر به صد‌ها روش نرسد مطمئن باشید ده‌ها شیوه مختلف برای رمزگذاری کاراکتر وجود دارد. برای آشنا شدن با این روش‌ها و درک عملکرد آنها، بهترین راه این است که با پوشش دادن یکی از ساده‌ترین شیوه‌های رمزگذاری کاراکتر یعنی ASCII آغاز کنیم.

 

چه علوم کامپیوتر را به صورت خودآمور خوانده و چه به صورت رسمی در دانشگاه فراگرفته باشید، به احتمال زیاد تا به حال یک یا دو بار جدول ASCII را دیده‌اید. ASCII نقطه خوبی برای شروع یادگیری در مورد رمزگذاری کاراکتر است زیرا کوچک است و شامل رمزگذاری می‌شود. (همانطور که کاملا مشخص است، بیش از حد کوچک است.)

 

جدول ASCII شامل موارد زیر می‌شود:

 

 

خب تعریف رسمی‌تری از رمزگذاری کاراکتر چیست؟

در سطوح بسیار بالا، رمزگذاری روشی برای ترجمه کاراکترها (مانند حروف، علائم نگارشی، نمادها، فضای خالی و کاراکترهای کنترلی) به اعداد صحیح و در نهایت به بیت است. هر کاراکتر می‌تواند به شکل دنباله‌ای منحصر به فرد از بیت‌ها درآمده و رمزگذاری شود. اگر به مفهوم بیت مسلط نیستید، نگران نباشید، به زودی به آنها می‌رسیم.

 

دسته‌بندی‌های مختلف و متنوعی برای گروهی از کاراکترها مطرح شده‌اند. هر کاراکتر کد نقطه(Code Point) مربوط به خود را دارد که می‌توانید آن را فقط به عنوان یک عدد صحیح در نظر بگیرید. در جدول ASCII کاراکترها در بازه‌های مختلفی قرار می‌گیرند:

Code Point Range Class
0 through 31 Control/non-printable characters
32 through 64 Punctuation, symbols, numbers, and space
65 through 90 Uppercase English alphabet letters
91 through 96 Additional graphemes, such as [ and \
97 through 122 Lowercase English alphabet letters
123 through 126 Additional graphemes, such as { and |
127 Control/non-printable character (DEL)

 

کل جدول ASCII شامل 128 کاراکتر می‌شود. جدولی که در ادامه آمده مجموعه کاملی از کاراکترهای مجاز ASCII را نشان می دهد. اگر کاراکتری را در اینجا نمی‌بینید، واضیح است که نمی‌توانید آن را به عنوان متن چاپ شده در رمزگذاری ASCII بیان کنید.

Code Point Character (Name) Code Point Character (Name)
0 NUL (Null) 64 @
1 SOH (Start of Heading) 65 A
2 STX (Start of Text) 66 B
3 ETX (End of Text) 67 C
4 EOT (End of Transmission) 68 D
5 ENQ (Enquiry) 69 E
6 ACK (Acknowledgment) 70 F
7 BEL (Bell) 71 G
8 BS (Backspace) 72 H
9 HT (Horizontal Tab) 73 I
10 LF (Line Feed) 74 J
11 VT (Vertical Tab) 75 K
12 FF (Form Feed) 76 L
13 CR (Carriage Return) 77 M
14 SO (Shift Out) 78 N
15 SI (Shift In) 79 O
16 DLE (Data Link Escape) 80 P
17 DC1 (Device Control 1) 81 Q
18 DC2 (Device Control 2) 82 R
19 DC3 (Device Control 3) 83 S
20 DC4 (Device Control 4) 84 T
21 NAK (Negative Acknowledgment) 85 U
22 SYN (Synchronous Idle) 86 V
23 ETB (End of Transmission Block) 87 W
24 CAN (Cancel) 88 X
25 EM (End of Medium) 89 Y
26 SUB (Substitute) 90 Z
27 ESC (Escape) 91 [
28 FS (File Separator) 92 \
29 GS (Group Separator) 93 ]
30 RS (Record Separator) 94 ^
31 US (Unit Separator) 95 _
32 SP (Space) 96 `
33 ! 97 a
34 " 98 b
35 # 99 c
36 $ 100 d
37 % 101 e
38 & 102 f
39 ' 103 g
40 ( 104 h
41 ) 105 i
42 * 106 j
43 + 107 k
44 , 108 l
45 - 109 m
46 . 110 n
47 / 111 o
48 0 112 p
49 1 113 q
50 2 114 r
51 3 115 s
52 4 116 t
53 5 117 u
54 6 118 v
55 7 119 w
56 8 120 x
57 9 121 y
58 : 122 z
59 ; 123 {
60 < 124 |
61 = 125 }
62 > 126 ~
63 ? 127 DEL (delete)

 

دوره پیشنهادی: دوره آموزش الگوریتم‌نویسی در پایتون

 

 +  ماژول string پایتون

ماژول string پایتون برای ثابت‌هایی که از نوع string هستند و در در مجموعه کاراکترهای ASCII قرار می‌گیرند مانند فروشگاهی چند منظوره عمل می‌کند.

 

آنچه در اینجا آورده شده هسته اصلی این ماژول به همراه تمام عظمت و شکوه آن است:

# From lib/python3.7/string.py

whitespace = ' \t\n\r\v\f'
ascii_lowercase = 'abcdefghijklmnopqrstuvwxyz'
ascii_uppercase = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
ascii_letters = ascii_lowercase + ascii_uppercase
digits = '0123456789'
hexdigits = digits + 'abcdef' + 'ABCDEF'
octdigits = '01234567'
punctuation = r"""!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~"""
printable = digits + ascii_letters + punctuation + whitespace

 

بیشتر این ثابت‌ها باید خود را در نام شناسه مربوط به خودشان مستند کنند. در ادامه به طور خلاصه اعداد شش رقمی(hexdigits) و هشت رقمی(octdigits) را پوشش خواهیم داد.

 

می‌توانید این ثابت‌ها را  برای دستکاری رشته‌های عادی و روزمره به کار ببرید:

>>> import string

>>> s = "What's wrong with ASCII?!?!?"
>>> s.rstrip(string.punctuation)
'What's wrong with ASCII'

 

 نکته: string.printable شامل تمام string.whitespace‌ها می‌شود. این روش با شیوۀ دیگری که برای آزمایش قابل چاپ بودن کاراکتر وجود دارد، یعنی استفاده از دستور str.isprintable() کمی متفاوت است و تفاهم ندارند، روش دوم می‌گوید هیچ یک از کاراکترهای{'\v', '\n', '\r', '\f', '\t'} قابل چاپ در نظر گرفته نمی‌شوند.

 

وجود چنین تفاوت ظریفی میان این دو روش به دلیل تعریف str.isprintable()است: این روش چیزی را قابل چاپ در نظر می گیرد که اگر و تنها اگر «همه کاراکترهای آن در repr() قابل چاپ باشند.»

 

ویدیو پیشنهادی: ویدیو آموزش ماژول hashlib در پایتون

 

 +  مروری کوتاه

حالا وقت خوبی است که مرور کوتاهی در مورد بیت(bit)، یعنی اساسی‌ترین واحد اطلاعاتی که کامپیوتر می‌شناسد، داشته باشیم.

 

بیت سیگنالی است که تنها دو حالت برای آن امکان‌پذیر است. برای درک بهتر این موضوع می‌توانیم بیت را با نمادهای مختلف نشان دهیم اما از لحاظ معنایی همه آنها به یک معنا دلالت می‌کنند:

 

جدول ASCII که در بخش قبل در مورد آن صحبت کردیم از چیزی استفاده می‌کند که من و شما آنها را صرفا اعداد می‌نامیم و با این نام می‌شناسیم (اعداد0 تا 127)، اما اگر بخواهیم این اعداد را دقیق تر بررسی کنیم این اعداد در پایه 10 (اعشاری) هستند.

 

همچنین می‌توانید هر یک از این اعداد در مبنای 10 را با دنباله‌ای از بیت‌ها (یعنی همان عدد در مبنای 2) بیان کنید. در ادامه نسخه‌های باینری یا دودویی اعداد 0 تا 10 در مبنای 10 و 2 ارائه شده است:

Decimal Binary (Compact) Binary (Padded Form)
0 0 00000000
1 1 00000001
2 10 00000010
3 11 00000011
4 100 00000100
5 101 00000101
6 110 00000110
7 111 00000111
8 1000 00001000
9 1001 00001001
10 1010 00001010

 

توجه داشته باشید که هر چه عدد n در مبنای 10 بزرگتر باشد، برای نمایش کاراکتر آن عدد به بیت‌های مهم‌تری نیاز دارید.

 

راه مفیدی برای نمایش رشته‌های ASCII به عنوان دنباله‌ای از بیت‌ها در پایتون وجود دارد که در ادامه آن را نشان می‌دهیم. هر کاراکتر رشته ASCII به 8 بیت شبه کدگذاری می‌شود، با فاصله‌های بین دنباله‌های 8 بیتی که هر کدام نشان دهنده یک کاراکتر واحد هستند:

>>> def make_bitseq(s: str) -> str:
...     if not s.isascii():
...         raise ValueError("ASCII only allowed")
...     return " ".join(f"{ord(i):08b}" for i in s)

>>> make_bitseq("bits")
'01100010 01101001 01110100 01110011'

>>> make_bitseq("CAPS")
'01000011 01000001 01010000 01010011'

>>> make_bitseq("$25.43")
'00100100 00110010 00110101 00101110 00110100 00110011'

>>> make_bitseq("~5")
'01111110 00110101'

 

 نکته: isascii() در پایتون 3.7 معرفی شده است.

 

 

f-string f"{ord(i):08b} از شیوۀ تعیین مشخصات قالب در پایتون استفاده می‌کند، که راهی است برای تعیین قالب‌بندی فیلدهای جایگزین در قالب‌بندی رشته‌ها (strings) است:

 

 

البته این ترفندی بود که عمدتاً برای سرگرمی به کار می‌رود و اگر برای هر کاراکتری که در جدول ASCII وجود ندارد، از آن استفاده کنید به شدت شکست خواهد خورد. در ادامه این موضوع را مطرح می‌کنیم سایر شیوه‌های رمزگذاری چطور این مشکل را حل می‌کنند.

 

دوره پیشنهادی: دوره آموزش پایتون (python)

 

 +  به بیت‌های بیشتری نیاز داریم!

یک فرمول بسیار مهم وجود دارد که به تعریف بیت ارتباط دارد. اگر تعداد بیت را n در نظر بگیریم، مقادیر مجزا و متفاوتی که از این تعداد بیت به دست آمده و می‌تواند ارائه شود 2 به توان n است:

def n_possible_values(nbits: int) -> int:
    return 2 ** nbits

 

یعنی:

 

بنابراین از این فرمول نتیجه‌گیری می‌کنیم که: چگونه می‌توانیم با توجه به بازه‌ای از مقادیر متمایز ممکن، تعداد بیت‌هایی که برای نمایش کامل محدوده نیاز داریم را پیدا کنیم؟ در واقع داریم تلاش می‌کنیم تا مقدار n را در معادله 2n = x به دست بیاوریم(که x را خودمان از قبل می‌دانیم).

 

در واقع عملکرد آن اینطور است:

>>> from math import ceil, log

>>> def n_bits_required(nvalues: int) -> int:
...     return ceil(log(nvalues) / log(2))

>>> n_bits_required(256)
8

 

باید برای n_bits_required() سقفی در نظر بگیرید تا بتوانید مقادری که توان مشخصی از 2 نیستند را حساب کنید. فرض کنید باید مجموعه کاراکتری شامل 110 کاراکتر در مجموع را ذخیره کنید. اگر چنین محاسبه‌ای داشته باشیم، ساده‌لوحانه عمل کرده‌ایم که مقدار مورد نیاز ما log(110) / log(2) == 6.781 بیت باشد، اصلا چیزی به نام 0.781 بیت وجود ندارد. 110 کاراکتر به 7 بیت نیاز دارند، نه 6، زیرا نیازی به اسلات‌های پایانی نیست و آنها غیر ضروری هستند:

>>> n_bits_required(110)
7

 

همه مواردی که تا کنون ذکر کردیم برای اثبات یک مفهوم به کار می‌رود، همه اینها را گفتیم تا به شما ثابت شود و نشان دهیم که ASCII، به طور دقیق، کدی 7 بیتی است. جدول ASCII که در بالا مشاهده کردید شامل 128 کد نقطه (code point) و کاراکتر، از 0 تا 127 است. این مقدار برای نمایش به 7 بیت نیاز دارد:

>>> n_bits_required(128)  # 0 through 127
7
>>> n_possible_values(7)
128

 

مشکل این است که کامپیوترهای مدرن چیز زیادی را در اسلات‌های 7 بیتی ذخیره نمی‌کنند. آنها در واحدهای 8 بیتی که معمولاً به عنوان بایت شناخته می‌شوند، تردد می‌کنند.

 

 نکته: در طول این آموزش، فرض ما بر این است که هر بار از کلمه بایت استفاده می‌کنیم به 8 بیت اشاره دارد، همانطور که از دهه 1960 به بعد، به جای واحد ذخیره‌سازی دیگری از این واحد استفاده می‌شود. اگر خودتان تمایل دارید می‌توانید آن را اکتیت یا هشتایی بنامید.

 

فضای ذخیره‌سازی مورد استفاده ASCII نیمه خالی است. اگر دلیل نیمه خالی بودن این فضا برای شما واضح و روشن نیست، بهتر است یکبار دیگر به جدول تبدیل اعداد اعشاری به اعداد باینری که در بالا آمد فکر کرده و آن را مرور کنید. برای بیان  اعداد 0 و 1 هم می‌توانیم فقط از 1 بیت استفاده کنیم و هم می‌توانیم با استفاده از 8 بیت به ترتیب به شکل 00000000 و 00000001 آنها را بیان کنیم، به فضای خالی در این شیوۀ نوشتن دقت کنید.

 

به همین ترتیب اعداد 0 تا 3 را می‌توانید فقط با استفاده از2 بیت یا به صورت 00 تا 11 بیان کنید، یا می‌توانید به استفاده از شیوۀ 8 بیتی برای بیان آنها به ترتیب عبارت‌های 00000000، 00000001، 00000010 و 00000011 را بنویسید. بالاترین کد نقطه (code point) ASCII، یعنی 127، فقط به 7 بیت مهم نیاز دارد.

 

با علم به این موضوع، می‌توانید ببینید که عبارت make_bitseq() رشته‌های ASCII را به شکل str از نوع بایت نشان می‌دهد، طوری که هر کاراکتر فقط یک بایت مصرف می‌کند:

>>> make_bitseq("bits")
'01100010 01101001 01110100 01110011'

 

استفادۀ ناکافی ASCII از بایت‌های 8 بیتی که کامپیوترهای جدید ارائه می‌دادند باعث شد تا گروهی از رمزگذاری‌های متناقض و غیررسمی ایجاد شود که در رمزگذاری کاراکتر 8 بیتی از هر کاراکتر اضافی مشخصی همراه با 128 کدنقطه (code point) موجود باقی مانده استفاده می‌کردند.

 

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

 

با گذشت سال‌ها، طرح رمزگذاری mega-scheme آمد و همه آنها را تحت کنترل خود درآورد. هرچند، قبل از اینکه به آنجا برسیم، اجازه دهید یک دقیقه در مورد سیستم‌های اعداد صحبت کنیم، این سیستم‌ها زیربنای اصلی طرح‌های رمزگذاری کاراکتر هستند.

 

دوره پیشنهادی: دوره آموزش Matplotlib

 

 #  پوشش تمام مبناها: سیستم‌های عدد دیگر

در بحث ASCII که در بالا مطرح شد، دیدید که در محدوده 0 تا 127 هر کاراکتر به عدد صحیحی نگاشت می‌شود.

 

این محدوده اعداد به صورت اعشاری (مبنای 10) بیان می شود. این روشی است که شما، من و بقیه انسان ها به شمردن عادت کرده ایم، بدون دلیلی پیچیده تر از این که 10 انگشت داریم.

 

اما سیستم‌های عددی دیگری هم وجود دارند که به ویژه در کد منبع CPython رایج هستند. در حالی که «اعداد پایه و اساسی» تمام این سیستم‌ها یکسان هستند، تمام سیستم‌های عددی صرفا روش‌های متفاوتی برای بیان اعداد به شمار می‌روند.

 

اگر از شما بپرسم که رشته «11» چه عددی را نشان می‌دهد، حق دارید با تعجب من را نگاه کنید و بگویید مشخص است عدد 11 را نشان می‌دهد.

 

با این حال، این بازنمایی رشته‌ای در سیستم‌های عددی مختلف می‌تواند اعداد مختلفی را بیان کند. علاوه بر سیستم دهدهی(decimal)، گزینه‌های جایگزین آن شامل سیستم‌های عددی زیر می‌شوند که هر یک به نوبه خود رایج هستند:

 

اما این که می‌گوییم در سیستم عددی معینی، اعداد در پایه N نشان داده می‌شوند چه معنایی دارد؟

 

بهترین راهی که برای بیان معنای آن می‌دانم این است: یعنی تعداد انگشتانی که در آن سیستم عددی بر اساس آنها حساب می‌کنیم.

 

اگر هنوز هم می‌خواهید مقدمه‌ای کامل و با جزئیات بیشتری در مورد سیستم‌های عددی داشته باشید، کتاب کد (Code) چارلز پتزولد کتاب فوق‌العاده جالبی است که مبانی کدهای کامپیوتری را با جزئیات بیشتری بررسی می‌کند.

 

یکی از راه‌هایی که نشان می‌دهد سیستم‌های عددی مختلف چطور چیزی را تفسیر می‌کنند، استفاده از متد int() پایتون است. اگر یک رشته str را به متد int() بفرستید، پایتون فرض می‌کند که رشته فرستاده شده عددی را در پایه 10 بیان می‌کند، مگر اینکه خودتان بگویید محتوای رشته چیزی غیر از این است:

>>> int('11')
11
>>> int('11', base=10)  # 10 is already default
11
>>> int('11', base=2)  # Binary
3
>>> int('11', base=8)  # Octal
9
>>> int('11', base=16)  # Hex
17

 

روش‌های رایج‌تری برای اینکه به پایتون بگویید عدد صحیح شما در مبنای غیر از ده است، وجود دارد. پایتون شکل‌های حرفی (literal forms) از هر یک از جایگزین‌های سیستم عددی بالای را می‌پذیرد:

Type of Literal Prefix Example
n/a n/a 11
Binary literal 0b or 0B 0b11
Octal literal 0o or 0O 0o11
Hex literal 0x or 0X 0x11

 

تمام اینها زیر مجموعه‌هایی از اشکال حرفی عدد صحیح (integer literals) هستند. می‌توانید ببینید که این عبارت‌ها و مقداری غیر از مبنای پیشفرض به ترتیب با فراخوانی int()  نتایج یکسانی تولید می‌کنند. برای پایتون همه آنها فقط اعداد صحیح یا int هستند:  

>>> 11
11
>>> 0b11  # Binary literal
3
>>> 0o11  # Octal literal
9
>>> 0x11  # Hex literal
17

 

در ادامه شیوه تایپ معادل‌های باینری، اکتال و هگزادسیمال اعداد دهدهی 0 تا 20 ارائه شده است. هر یک از این عبارت‌ها در پوسته مفسر پایتون یا کد منبع آن کاملاً معتبر و همه آنها از نوع int هستند:

Decimal Binary Octal Hex
0 0b0 0o0 0x0
1 0b1 0o1 0x1
2 0b10 0o2 0x2
3 0b11 0o3 0x3
4 0b100 0o4 0x4
5 0b101 0o5 0x5
6 0b110 0o6 0x6
7 0b111 0o7 0x7
8 0b1000 0o10 0x8
9 0b1001 0o11 0x9
10 0b1010 0o12 0xa
11 0b1011 0o13 0xb
12 0b1100 0o14 0xc
13 0b1101 0o15 0xd
14 0b1110 0o16 0xe
15 0b1111 0o17 0xf
16 0b10000 0o20 0x10
17 0b10001 0o21 0x11
18 0b10010 0o22 0x12
19 0b10011 0o23 0x13
20 0b10100 0o24 0x14

 

 

 به چه دلیلی از این نحوهای حرفی (literal symtax) از نوع int یا عدد صحیح به عنوان جایگزین یکدیگر استفاده می‌کنیم؟ به طور خلاصه باید بگوییم زیرا 2، 8، و 16 همه توان‌های 2 هستند، در حالی که 10 توان 2 نیست. این سه سیستم عددی جایگزین گاها بیان مقادیر به روشی مناسبی و قابل فهم برای کامپیوتر راهی را ارائه می‌کنند. به عنوان مثال، عدد 65536 یا 216، 10000 در هگزادسیمال یا x10000 0 هگزادسیمال حرفی پایتون است.

 

ویدیو پیشنهادی: ویدیو آموزش متدهای isinstance و issubclass در پایتون

 

 #  ورود یونیکد(unicode)

همانطور که دیدید، مشکل ASCII این است که مجموعه کاراکترهای آن به اندازه کافی بزرگ نیست تا بتواند مجموعه زبان‌ها، گویش‌‌ها، نمادها و نگاره‌‌های جهان را در خود جای دهد. (حتی آنقدر بزرگ نیست که بتواند فقط زبان انگلیسی را پوشش دهد.)

 

اساساً یونیکد با همان هدفی که ASCII ایجاد شد، تشکیل و به خدمت گرفته شده، اما یونیکد شامل مجموعه بسیار، بسیار، بسیار بزرگتری از کد نقطه‌ها (code point)‌ است. تعداد انگشت‌شماری  از شیوه‌های رمزگذاری وجود دارد که در مدت زمان بین پیدایش ASCII و یونیکد به وجود آمدند، اما واقعا ارزش ذکر کردن ندارند زیرا یونیکد و یکی از طرح‌های رمزگذاری آن، یعنی UTF-8، به صورت گسترده ای مورد استفاده قرار گرفته است.

 

یونیکد را به عنوان یک نسخه عظیمی از جدول ASCII در نظر بگیرید که 1,114,112 نقطه کد (code point) ممکن دارد. این نقطه‌کدها از 0 تا 1،114،111، یا از 0 تا 17 * (216) - 1، یا 10ffff x 0 هگزادسیمال هستند. در واقع ASCII زیرمجموعه کاملی از یونیکد است. همانطور که به صورت منطقی انتظار دارید، 128 کاراکتر اول جدول یونیکد دقیقاً با کاراکترهای ASCII مطابقت دارد.

 

اگر بخواهیم از نظر فنی دقیق باشیم، خود یونیکد یک شیوۀ رمزگذاری نیست. بلکه، یونیکد با رمزگذاری کاراکترهای مختلف پیاده‌سازی می شود که به زودی در مورد آن بیشتر خواهید آموخت. بهتر است یونیکد را مانند mapping (چیزی شبیه dict) یا جدول پایگاه داده 2 ستونی در نظر بگیرید. این نقشه کاراکترهایی (مانند «a»، «¢»، یا حتی «ቈ») را به اعداد صحیح متمایز و مثبتی نگاشت می‌کند. برای رمزگذاری کاراکتر باید بیت‌های بیشتری ارائه شود تا کاراکترهای بیشتری نگاشت شوند. 

 

تقریباً هر کاراکتری که می توانید تصور کنید در یونیکد وجود دارد، از جمله موارد غیر قابل چاپ بیشتر. یکی از موارد مورد علاقه من علامت تغییر جهت متن از راست به چپ است با کد 8207 است و در متونی از آن استفاده می‌شود که هم اسکریپت‌های مربوط به زبان‌هایی را دارد که از چپ به راست نوشته می‌شوند و هم اسکریپت مربوط به زبان‌هایی که از راست به چپ نوشته می‌شوند، مانند مقاله ای که شامل پاراگراف های انگلیسی و عربی است.

 

نکته: دنیای رمزگذاری کاراکترها شامل جزئیات فنی دقیق و زیادی می‌شود، برخی از افراد تمایل دارند تا جزئیات بیشتری دراین مورد بدانند. یکی از این جزئیات این است که تنها ۱٬۱۱۱٬۹۹۸ نقطه کد یونیکد به دلایل نگاشت کاراکترهایی قدیمی و زبان‌های کهن قابل استفاده هستند.

 

 

 +  encoding در مقابل UTF-8

طولی نکشید که برنامه‌نویسان متوجه شدند که تمام کاراکترهای موجود در دنیا در یک بایت جمع نمی‌شوند. بدیهی است که شیوه‌های رمزگذاری جدید و جامع تر برای رمزگذاری برخی از کاراکترها باید از چندین بایت استفاده کنند.

 

همچنین در بالا متوجه شدید که یونیکد از نظر فنی کاملا شیوۀ رمزگذاری کاراکتر نیست. علت این موضوع چیست؟

 

چیزی وجود دارد که یونیکد به شما نمی‌گوید: یونیکد به شما نمی‌گوید چگونه بیت‌های واقعی را از متن دریافت کنید درواقع یونیکد فقط در مورد نقطه کد (code point) به شما می‌گوید. در مورد نحوه تبدیل متن به داده‌های باینری و برعکس آن به شما اطلاعات کافی نمی‌دهد.

 

یونیکد به خودی خود شیوۀ رمزگذاری نیست بلکه استاندارد رمزگذاری انتزاعی است. اینجاست که UTF-8 و دیگر طرح‌های رمزگذاری وارد بازی می‌شوند. استاندارد یونیکد (نگاشت کاراکترها به نقطه کد (code point)) چندین شیوۀ رمزگذاری مختلف را در مورد مجموعه کاراکترهای واحد خود تعریف می کند.

 

UTF-8 و اقوام درجه دوی آن، UTF-16 و UTF-32، که کمتر از آنها استفاده می‌شود، فرمت‌های رمزگذاری هستند که کاراکترهای یونیکد را به عنوان داده‌های باینری یک یا چند بایت به ازای هر کاراکتر نمایش می‌دهند و بازنمایی می‌کنند. در مورد UTF-16 و UTF-32 بیشتر صحبت خواهیم کرد، اما UTF-8 بیشترین سهم از این بحث را به خود اختصاص داده است.

 

این مباحث ما را به تعریفی می‌رساند که تا همین الان هم صحبت کردن در مورد آن به اندازه کافی دیر شده است. تعریف رسمی رمزگذاری و رمزگشایی چیست؟

 

مقاله پیشنهادی: آموزش کار با mysql در پایتون

 

 +  encoding و decoding در پایتون

نوع str در پایتون 3 به معنای نمایش متون قابل خواندن توسط انسان است و می‌تواند حاوی هر یک از کاراکترهای یونیکد باشد.

 

برخلاف آن نوع بایت، داده‌های باینری یا توالی‌هایی از بایت‌های خام را نشان می‌دهد که ذاتاً هیچ رمزگذاری به آنها متصل نیست.

 

رمزگذاری و رمزگشایی فرآیند‌های هستند که با انجام هر یک از آنها از یکی به دیگری رفته‌ایم:

یونیکد در پایتون

 

در عبارت‌های .encode() و .decode()،به طور پیش فرض پارامتر رمزگذاری «utf-8» است، اگرچه به طور کلی بهتر است آن را مشخص کنیم تا دستورات ما ایمن‌تر و بدون ابهام‌تر باشند:

>>> "résumé".encode("utf-8")
b'r\xc3\xa9sum\xc3\xa9'
>>> "El Niño".encode("utf-8")
b'El Ni\xc3\xb1o'

>>> b"r\xc3\xa9sum\xc3\xa9".decode("utf-8")
'résumé'
>>> b"El Ni\xc3\xb1o".decode("utf-8")
'El Niño'

 

نتیجه عبارت str.encode() یک شی از نوع بایت است. هر دوی آنها به شکل بایت حرفی (مانند b«\xc3\xa9sum\xc3\xa9») و بازنمایی بایت‌ها فقط توسط کاراکترهای ASCII مجاز است.

 

به همین دلیل است که وقتی El Niño”.encode("utf-8")"، فراخوانی می‌شود، از آنجایی که E1 با کد ASCII تطبیق دارد همانطور که هست نشان داده می‌شود، اما n که علامت تیلدا بر روی آن قرار دارد با عبارت «\xc3\xb1» بازنمایی می‌شود. این دنباله که ظاهر نامرتبی دارد دو بایت 0xc3 و 0xb1 را بر مبنای هگز نشان می‌دهد:

>>> " ".join(f"{i:08b}" for i in (0xc3, 0xb1))
'11000011 10110001'

 

یعنی برای نمایش باینری کاراکتر ñ در UTF-8 به دو بایت نیاز داریم.

 

نکته: اگر عبارت help(str.encode) را تایپ کنید، احتمالاً کد پیش‌فرض encoding='utf-8’ را خواهید دید. حواستان باشد تا از آن صرفنظر کنید و فقط از عبارت ()résumé".encode"  استفاده کنید، زیرا ممکن است عبارت‌های پیش فرض در ویندوزهای پیش از نسخه Python 3.6 متفاوت باشد.

 

دوره پیشنهادی: دوره اول آموزش پروژه محور پایتون

 

 +  پایتون 3: همه چیز در یک یونیکد

در پایتون 3 و به ویژه UTF-8 یونیکد همه کاره است. یعنی:

 

 

ویژگی دیگر هم وجود دارد که با جزئیات بیشتری همراه است، و آن این است که پیش‌فرض رمزگذاری (encoding) در دستور open() که تابعی داخلی است به پلتفرم و مقدار حاصل از locale.getpreferredencoding() وابسته است:

>>> # Mac OS X High Sierra
>>> import locale
>>> locale.getpreferredencoding()
'UTF-8'

>>> # Windows Server 2012; other Windows builds may use UTF-16
>>> import locale
>>> locale.getpreferredencoding()
'cp1252'

 

باز هم باید تاکید کنم که درسی که در اینجا می‌گیرید و باید به آن توجه کنید در مورد جهانی بودن UTF-8 است، به خاطر داشته باشید حتی اگر رمزگذاری غالب باشد، مراقب فرضیات و پیش‌فرض‌ها باشیم. هر چقدر کد شما واضح‌تر باشد بهتر و ضرری متوجه شما نخواهد شد.

 

ویدیو پیشنهادی: ویدیو آموزش متدهای eval و exec در پایتون

 

 +  یک بایت، دو بایت، سه بایت، چهار

یکی از ویژگی‌های مهم UTF-8 این است که نوعی رمزگذاری با طول متغیر است. شاید وسوسه شوید به معنای این عبارت دقت نکرده و از آن عبور کنید، اما اشتباه می‌کنید این جمله ارزش آن را دارد که در عمق آن فرو برویم.

 

بخش مربوط به ASCII که پیش از این مطرح کردیم را مرور کنید. در سرزمین توسعه یافته-ASCII هر چیزی حداکثر به یک بایت فضا نیاز دارد. با عبارت مولد زیر (generator expression) می‌توانید به راحتی و به سرعت این موضوع را ثابت کنید:

>>> all(len(chr(i).encode("ascii")) == 1 for i in range(128))
True

 

UTF-8 کاملاً متفاوت است. کاراکتر یونیکد مشخصی که در نظر دارید می‌تواند از یک تا چهار بایت را اشغال کند. در ادامه مثالی از یک کاراکتر یونیکد می‌آوریم که چهار بایت را اشغال می‌کند:

نحوه کار یونیکد در پایتون

 

موضوعی که مطرح شد یکی از ویژگی‌های ظریف اما مهم تابع len() است:

 

جدول زیر به صورت خلاصه بیان می‌کند که به طور کلی چه نوع کاراکترهایی در هر چه دسته و طول بایتی قرار می‌گیرند:

 

مقاله پیشنهادی: ساخت بازی سنگ، کاغذ، قیچی با پایتون

 

 +  اطلاعات بیشتر درمورد UTF-16 و UTF-32

بیایید به بررسی دو نوع شیوۀ رمزگذاری دیگر یعنی UTF-16 و UTF-32 برگردیم.

 

در عمل تفاوت‌های بین این دو نوع UTF-8 قابل توجه است. در اینجا مثالی از تفاوت عمده این دو نوع رمزگذار با تبدیل گرد کردن آن آورده شده است:

 

در این مورد، رمزگذاری چهار حرف یونانی بر اساس UTF-8 و سپس رمزگشایی و تبدیل آنها به متن بر مبنای UTF-16، متنی از نوع رشته (str) ایجاد می کند که به زبانی کاملاً متفاوت یعنی زبان کره ای تبدیل است.

 

نتایجی مانند این که به وضوح و آشکارا اشتباه هستند زمانی امکان پذیر است که در هر دو جهت رمزگذاری و رمزگشایی از شیوۀ یکسانی استفاده نشود. دو نوع مختلف از رمزگشایی شی (object) یکسانی از جنس بایت (Bytes) ممکن است نتایج مختلفی ایجاد کند که حتی به زبان یکسانی هم نباشند.

 

این جدول محدوده یا تعداد بایت‌های زیر UTF-8، UTF-16 و UTF-32 را خلاصه می‌کند:

Encoding Bytes Per Character (Inclusive) Variable Length
UTF-8 1 to 4 Yes
UTF-16 2 to 4 Yes
UTF-32 4 No

 

جالب است بدانید یکی دیگر از ویژگی‌های جالب خانواده UTF در مورد میزان فضایی است که UTF-8 اشغال می‌کند، همیشه اینطور نیست که این فضا نسبت به فضایی که UTF-16 اشغال می‌کند، کمتر باشد. ممکن است این موضوع از نظر ریاضی غیر منطقی به نظر برسد، اما کاملاً عملی و ممکن است:

>>> text = "記者 鄭啟源 羅智堅"
>>> len(text.encode("utf-8"))
26
>>> len(text.encode("utf-16"))
22

 

علت این موضوع که فضای اشغالی UTF-8 همیشه از فضای اشغالی UTF-16 کمتر نیست این است که نقطه کدها (code point) در محدوده U+0800 تا U+FFFF (2048 تا 65535 در دستگاه دهدهی) سه بایت در UTF-8 اشغال می‌کند در حالی که UTF-16  تنها دو بایت را اشغال می‌کنند.

 

بدون در نظر گرفتن این موضوع که کاراکترهای زبانی که با آن کار می‌کنید معمولاً در این محدوده هستند یا خیر، به هیچ وجه توصیه نمی‌کنم که سوار قطار UTF-16 شوید،. علاوه بر دلایل دیگری که وجود دارد، یکی از دلایل قوی برای استفاده از UTF-8 این است که در دنیای رمزگذاری، همرنگ جماعت شدن ایده خوبی است.

 

نیازی به تذکر ندارد که در سال 2019 هستیم و حافظه کامپیوتر به قدر کافی ارزان است، بنابراین استفاده از UTF-16 برای صرفه جویی در 4 بایت واقعا ارزشش را ندارد.

 

مقاله پیشنهادی: اجرای برنامه‌های فلسک با داکر

 

 #  توابع داخلی پایتون

خب تا اینجا قسمت‌های سخت را پشت سر گذاشته‌اید، اکنون نوبت آن است هرچه از پایتون دیده و خوانده‌اید را به کار ببرید.

 

پایتون گروهی از توابع داخلی دارد که تاحدی به سیستم‌های عددی و رمزگذاری کاراکتر مربوط می‌شوند، این توابع عبارتند از:

 

این  توابع می‌توانند بر اساس اهدافی که دارند به صورت منطقی با هم گروه‌بندی شوند:

 

1. توابع ascii()، bin()،().hex، و().oct برای به دست آوردن بازنمایی و ارائه متفاوت از یک ورودی هستند. خروجی هر یک از این توابع یک رشته (Str) است. اولین تابع یعنی ascii()، فقط شی‌ای از نوع ASCII نمایش می‌دهد و از از کاراکترهای غیر ASCII فراری است. خروجی‌های سه مورد باقیمانده عدد صحیحی به ترتیب از نوع باینری، هگزادسیمال و اکتالی (هشتایی) است. این توابع  صرفا اعداد صحیح را نمایش می‌دهند و تغییر اساسی بر روی ورودی انجام نمی‌دهند.

 

2. توابع bytes()، str()و int() برای نوع کاراکتری که می‌پذیرند یعنی به ترتیب، بایت bytes، رشته str و عددصحیح int کلاس می‌سازند. هر یک از آنها برای تبدیل خودکار نوع داده به نوع مورد نظر خود راه‌های متفاوتی دارند. به عنوان مثال، همانطور که پیش از این هم دیدید، در حالی که احتمالا تابع int(11.0) رایج‌تر است، ممکن است همین تابع را به این شکل هم ببینید: (‘11’, base=16).

 

3. توابع ord() و chr() معکوس یکدیگر هستند، طوری که تابع ord() در پایتون کاراکتر رشته‌ای (str) را به نقطه کد (code point) پایه 10 خود تبدیل می‌کند، در حالی که chr() برعکس این کار را انجام می‌دهد یعنی نقطه پوینت کاراکتر را به رشته تبدیل می‌کند.

 

در ادامه نگاهی به هر یک از این ۹ عملکرد دقیق‌تری خواهیم داشت:

Function Signature Accepts Return Type Purpose
ascii() ascii(obj) Varies str ASCII only representation of an object, with non-ASCII characters escaped
bin() bin(number) number: int str Binary representation of an integer, with the prefix "0b"
bytes() bytes(iterable_of_ints)

bytes(s, enc[, errors])

bytes(bytes_or_buffer)

bytes([i])
Varies bytes Coerce (convert) the input to bytes, raw binary data
chr() chr(i) i: int

i>=0

i<=1114111
str Convert an integer code point to a single Unicode character
hex() hex(number) number: int str Hexadecimal representation of an integer, with the prefix "0x"
int() int([x])

int(x, base=10)
Varies int Coerce (convert) the input to int
oct() oct(number) number: int str Octal representation of an integer, with the prefix "0o"
ord() ord(c) c: str

len(c) == 1
int Convert a single Unicode character to its integer code point
str() str(object=’‘)

str(b[, enc[, errors]])
Varies str Coerce (convert) the input to str, text

 

 

در ادامه برای تأیید موارد فوق برخی از شواهد و آورده شده است:

>>> (
...     "a" ==
...     "\x61" == 
...     "\N{LATIN SMALL LETTER A}" ==
...     "\u0061" ==
...     "\U00000061"
... )
True

 

دو اخطار و پیش‌بینی احتیاطی مهم در این مورد وجود دارد:

 

1. تمام این فرم‌ها و اشکال در مورد همه کاراکتر جواب نمی‌دهد. بازنمایی هگزای عدد صحیح 300 0x012c است، که به سادگی در اسکیپ کد یا رمز گریز 2 هگزا رقمی2 "\xhh" قرار نمی‌گیرد. بزرگترین نقطه کدی که می‌توانید به زور در این دنباله گریز قرار دهید "\xff" ("ÿ"). به همین ترتیب برای کاراکتر "\ooo"، فقط تا کاراکتر "\777" ("ǿ") جواب می‌دهد.

 

2. برای کاراکترهای \xhh، \uxxxx، و \Uxxxxxxxx، تعداد رقم‌های مورد نیاز دقیقاً به اندازه‌ای است که در این مثال‌ها نشان داده شده است. این موضوع می‌تواند شما را وارد حلقه‌ای تکراری کند زیرا جدول‌های حاوی کاراکترهای یونیکد معمولا کدهای کاراکترها را با U+ و تعداد متغیر کاراکترهای هگز نمایش می‌دهند. نکته اصلی آنها این است که جدول‌های یونیکد اغلب فضای خالی این کدها را صفر نمی‌کنند.

 

مقاله پیشنهادی: OrderedDict و dict در پایتون

 

 #  رمزگذاریهای دیگر در پایتون

تا اینجا چهار روش رمزگذاری کاراکتر را مشاهده کرده‌اید:

هزاران روش دیگر هم وجود دارند.

 

یکی از مثال‌هایی که در این مورد وجود دارد Latin-1 (که ISO-8859-1 هم نامیده می‌شود)، که از نظر فنی پیش فرض پروتکل انتقال هایپرتکست (HTTP) به ازای هر RFC 2616 است. نوع لاتین-1 در ویندوز مختص خود ویندوز است و cp1252 دارد.

 

 نکته: استفاده از ISO-8859-1 هنوز هم بسیار رایج است. از RFC 2616 به‌عنوان پیش‌فرض رمزگذاری محتوای پاسخ HTTP یا HTTPS استفاده می‌شود. اگر کلمه «متن» در هدر Content-Type باشد و هیچ شیوۀ رمزگذاری دیگری مشخص نشده باشد، درخواست‌ها از ISO-8859-1 استفاده می‌کنند.

 

فهرست کاملی از انواع روش‌های رمزگذاری‌های که پذیرفته شده‌اند در مستندات مربوط به ماژول کدک‌ها (codecs) که بخشی از کتابخانه استاندارد پایتون هستند، پنهان شده است.

 

یکی دیگر از روش‌های رمزگذاری شناخته شده مفید که بهتر است در مورد آن بدانید «Unicode-escape» است. اگر یک رشته (str) رمزگشایی شده داشته باشید و بخواهید به سرعت از یونیکد گریز آن به صورت حرفی بازنمایی یا نمایشی به دست آورید، می توانید این رشته رمزگذاری شده را در .encode() قرار دهید:

>>> alef = chr(1575)  # Or "\u0627"
>>> alef_hamza = chr(1571)  # Or "\u0623"
>>> alef, alef_hamza
('ا', 'أ')
>>> alef.encode("unicode-escape")
b'\\u0627'
>>> alef_hamza.encode("unicode-escape")
b'\\u0623'

 

 

 #  داده یونیکد unicodedata

بهتر دیدیم که در آخر در مورد داده‌های یونیکد (Unicodedata) از کتابخانه استاندارد پایتون هم صحبت کنیم، این داده‌ها به شما امکان می‌دهد با پایگاه داده کاراکترهای یونیکد (UCD) تعامل داشته و در این کتابخانه جستجو کنید:

>>> import unicodedata

>>> unicodedata.name("€")
'EURO SIGN'
>>> unicodedata.lookup("EURO SIGN")
'€'

 

 

 #  نتیجه گیری

در این مقاله، به شکل گسترده و تاثیرگذاری در مورد رمزگذاری کاراکتر در پایتون صحبت کردیم. مسائل مختلف و زیادی را در این مقاله پوشش دادیم، مواردی مانند:

مقالات مرتبط

انجام عملیات‌های ریاضی در پایتون

بازبینی فریمورک جنگو - مزایا و معایب

متد append پایتون

مدیریت خطای KeyError در پایتون