راهنمای عملی یونیکد

انتشار:

1.12

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

مقدمه‌ای بر یونیکد

تعاریف

برنامه‌های امروزی باید بتوانند تنوع گسترده‌ای از نویسه‌ها را مدیریت کنند. برنامه‌های کاربردی اغلب بین‌المللی‌سازی می‌شوند تا پیام‌ها و خروجی‌ها را به زبان‌های مختلف قابل انتخاب توسط کاربر نمایش دهند؛ ممکن است همان برنامه نیاز داشته باشد پیام خطایی را به انگلیسی، فرانسوی، ژاپنی، عبری یا روسی نمایش دهد. محتوای وب می‌تواند به هر یک از این زبان‌ها نوشته شود و همچنین می‌تواند شامل انواع نمادهای ایموجی باشد. نوع رشته پایتون از استاندارد یونیکد برای نمایش نویسه‌ها استفاده می‌کند، که به برنامه‌های پایتون امکان می‌دهد با تمام این نویسه‌های ممکن متفاوت کار کنند.

یونیکد (https://www.unicode.org/) مشخصاتی است که هدف آن فهرست کردن تمام نویسه‌های مورد استفاده در زبان‌های انسانی و اختصاص یک کد یکتا به هر نویسه است. مشخصات یونیکد به‌طور مداوم برای افزودن زبان‌ها و نمادهای جدید بازنگری و به‌روزرسانی می‌شود.

نویسه کوچک‌ترین کامپوننت ممکن یک متن است. 'A'، 'B'، 'C' و غیره، همگی نویسه‌های متفاوتی هستند. 'È' و 'Í' نیز همین‌طور هستند. نویسه‌ها بسته به زبان یا زمینه‌ای که در مورد آن صحبت می‌کنید، متفاوت هستند. برای مثال، نویسه‌ای برای «عدد رومی یک»، 'Ⅰ'، وجود دارد که از حرف بزرگ 'I' جدا است. این دو معمولاً یکسان به نظر می‌رسند، اما این‌ها دو نویسه متفاوت هستند که معانی متفاوتی دارند.

استاندارد یونیکد توصیف می‌کند که نویسه‌ها چگونه با نقاط کد (code points) نشان داده می‌شوند. مقدار یک نقطه کد، یک عدد صحیح در بازه‌ی ۰ تا 0x10FFFF است (حدود ۱٫۱ میلیون مقدار، تعداد واقعی اختصاص‌داده‌شده کمتر از آن است). در استاندارد و در این سند، یک نقطه کد با استفاده از نماد U+265E نوشته می‌شود که به معنای نویسه‌ای با مقدار 0x265e (۹٬۸۲۲ در مبنای ده) است.

استاندارد یونیکد شامل جدول‌های بسیاری است که نویسه‌ها و نقاط کد متناظر آن‌ها را فهرست می‌کنند:

0061    'a'; LATIN SMALL LETTER A
0062    'b'; LATIN SMALL LETTER B
0063    'c'; LATIN SMALL LETTER C
...
007B    '{'; LEFT CURLY BRACKET
...
2167    'Ⅷ'; ROMAN NUMERAL EIGHT
2168    'Ⅸ'; ROMAN NUMERAL NINE
...
265E    '♞'; BLACK CHESS KNIGHT
265F    '♟'; BLACK CHESS PAWN
...
1F600   '😀'; GRINNING FACE
1F609   '😉'; WINKING FACE
...

به‌طور دقیق، این تعاریف ایجاب می‌کنند که گفتن «این نویسه U+265E است» بی‌معنا باشد. U+265E یک نقطه کد (code point) است که نمایانگر نویسه‌ای خاص است؛ در این مورد، نمایانگر نویسه‌ی «BLACK CHESS KNIGHT»، «♞» است. در بافت‌های غیررسمی، گاهی این تمایز میان نقاط کد و نویسه‌ها فراموش می‌شود.

یک نویسه روی نمایشگر یا روی کاغذ با مجموعه‌ای از عناصر گرافیکی نمایش داده می‌شود که به آن گلیف (glyph) می‌گویند. برای مثال، گلیف یک نویسه‌ی A بزرگ از دو خط مورب و یک خط افقی تشکیل شده است، اگرچه جزئیات دقیق به قلم استفاده‌شده بستگی دارد. در بیشتر کدهای Python، نیازی به نگرانی درباره گلیف‌ها نیست؛ تعیین گلیف صحیح برای نمایش معمولاً وظیفه‌ی یک جعبه‌ابزار GUI یا رندرکننده‌ی قلم پایانه است.

کدگذاری‌ها

برای جمع‌بندی بخش پیشین: یک رشته‌ی یونیکد، دنباله‌ای از نقاط کد است که اعدادی از ۰ تا 0x10FFFF (۱٬۱۱۴٬۱۱۱ در مبنای ده) هستند. این دنباله از نقاط کد باید در حافظه به‌صورت مجموعه‌ای از واحدهای کد بازنمایی شود، و سپس واحدهای کد به بایت‌های ۸ بیتی نگاشت می‌شوند. قواعد تبدیل یک رشته‌ی یونیکد به دنباله‌ای از بایت‌ها، کدگذاری نویسه یا صرفاً یک کدگذاری نامیده می‌شوند.

اولین کدگذاری که ممکن است به آن فکر کنید، استفاده از اعداد صحیح ۳۲ بیتی به‌عنوان واحد کد (code unit) و سپس استفاده از بازنمایی CPU از اعداد صحیح ۳۲ بیتی است. در این بازنمایی، رشته‌ی "Python" ممکن است به این شکل باشد:

   P           y           t           h           o           n
0x50 00 00 00 79 00 00 00 74 00 00 00 68 00 00 00 6f 00 00 00 6e 00 00 00
   0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23

این بازنمایی ساده است، اما استفاده از آن تعدادی مشکل ایجاد می‌کند.

  1. قابل‌حمل نیست؛ پردازنده‌های مختلف، بایت‌ها را به ترتیب متفاوتی مرتب می‌کنند.

  2. این روش بسیار اتلاف‌کننده‌ی فضا است. در بیشتر متن‌ها، بیشتر نقاط کد (code points) کمتر از ۱۲۷ یا کمتر از ۲۵۵ هستند، بنابراین فضای زیادی توسط بایت‌های 0x00 اشغال می‌شود. رشته‌ی بالا در مقایسه با ۶ بایت مورد نیاز برای نمایش ASCII، ۲۴ بایت فضا اشغال می‌کند. افزایش مصرف RAM اهمیت چندانی ندارد (رایانه‌های رومیزی گیگابایت‌ها RAM دارند و رشته‌ها معمولاً آن‌قدر بزرگ نیستند)، اما افزایش استفاده‌ی ما از پهنای باند دیسک و شبکه با ضریب ۴ تحمل‌ناپذیر است.

  3. این با توابع موجود C مانند strlen() سازگار نیست، بنابراین باید از خانواده‌ی جدیدی از توابع رشته‌ی پهن (wide string) استفاده شود.

بنابراین این کدگذاری چندان استفاده نمی‌شود و در عوض، افراد کدگذاری‌های دیگری را انتخاب می‌کنند که کارآمدتر و مناسب‌تر هستند، مانند UTF-8.

UTF-8 یکی از پرکاربردترین کدگذاری‌ها است و پایتون اغلب به‌طور پیش‌فرض از آن استفاده می‌کند. UTF مخفف «Unicode Transformation Format» است و «8» به این معناست که در این کدگذاری از مقادیر ۸ بیتی استفاده می‌شود. (کدگذاری‌های UTF-16 و UTF-32 نیز وجود دارند، اما کمتر از UTF-8 استفاده می‌شوند.) UTF-8 از قواعد زیر پیروی می‌کند:

  1. اگر نقطه‌کد کمتر از ۱۲۸ باشد، با مقدار بایت متناظر بازنمایی می‌شود.

  2. اگر نقطه‌کد بزرگ‌تر یا مساوی ۱۲۸ باشد، به دنباله‌ای از دو، سه یا چهار بایت تبدیل می‌شود، که در آن هر بایتِ دنباله بین ۱۲۸ و ۲۵۵ است.

UTF-8 دارای چندین ویژگی مناسب است:

  1. می‌تواند هر نقطه‌کد یونیکد را پردازش کند.

  2. یک رشته یونیکدی به دنباله‌ای از بایت‌ها تبدیل می‌شود که فقط در جاهایی حاوی بایت‌های صفر تعبیه‌شده است که این بایت‌ها نمایانگر نویسه تهی (U+0000) باشند. این بدان معناست که رشته‌های UTF-8 می‌توانند توسط توابع C مانند strcpy() پردازش شوند و از طریق پروتکل‌هایی که نمی‌توانند بایت‌های صفر را برای هیچ منظوری به‌جز نشانگرهای پایان رشته مدیریت کنند، ارسال شوند.

  3. یک رشته از متن ASCII، یک متن UTF-8 معتبر نیز هست.

  4. UTF-8 نسبتاً فشرده است؛ بیشتر نویسه‌های پرکاربرد را می‌توان با یک یا دو بایت نمایش داد.

  5. اگر بایت‌ها خراب یا از دست رفته باشند، می‌توان آغاز نقطه کد بعدیِ کدگذاری‌شده با UTF-8 را تعیین کرد و همگام‌سازی مجدد انجام داد. همچنین بعید است که داده‌های تصادفی ۸ بیتی شبیه UTF-8 معتبر به نظر برسند.

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

ارجاع‌ها

وب‌سایت کنسرسیوم یونیکد دارای جدول‌های نویسه‌ها، واژه‌نامه و نسخه‌های PDF از مشخصات یونیکد است. برای مطالعه‌ای دشوار آماده باشید. گاه‌شماری از پیدایش و گسترش یونیکد نیز در این وب‌سایت در دسترس است.

در کانال یوتیوب Computerphile، تام اسکات تاریخچه‌ی یونیکد و UTF-8 را به‌اختصار بررسی می‌کند (۹ دقیقه و ۳۶ ثانیه).

برای کمک به درک این استاندارد، Jukka Korpela یک راهنمای مقدماتی برای خواندن جدول‌های نویسه‌های یونیکد نوشته است.

یک مقاله‌ی مقدماتی خوب دیگر توسط Joel Spolsky نوشته شده است. اگر این مقدمه مطالب را برای شما روشن نکرد، باید سعی کنید پیش از ادامه، این مقاله‌ی جایگزین را بخوانید.

مقاله‌های ویکی‌پدیا اغلب مفید هستند؛ برای مثال، مقاله‌های «کدگذاری نویسه» و UTF-8 را ببینید.

پشتیبانی پایتون از یونیکد

اکنون که مبانی یونیکد را آموخته‌اید، می‌توانیم ویژگی‌های یونیکد پایتون را بررسی کنیم.

نوع رشته

از پایتون 3.0، نوع str این زبان شامل نویسه‌های یونیکد است، به این معنا که هر رشته‌ای که با استفاده از "unicode rocks!"، 'unicode rocks!' یا سینتکس رشته‌های سه‌نقل‌قولی ایجاد شود، به‌صورت یونیکد ذخیره می‌شود.

کدگذاری پیش‌فرض برای کد منبع پایتون UTF-8 است، بنابراین می‌توانید به‌سادگی یک نویسه‌ی یونیکد را در یک رشته‌ی لفظی (string literal) قرار دهید:

try:
    with open('/tmp/input.txt', 'r') as f:
        ...
except OSError:
    # 'File not found' error message.
    print("Fichier non trouvé")

نکته جانبی: پایتون 3 همچنین از نویسه‌های یونیکد در شناسه‌ها پشتیبانی می‌کند:

répertoire = "/tmp/records.log"
with open(répertoire, "w") as f:
    f.write("test\n")

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

>>> "\N{GREEK CAPITAL LETTER DELTA}"  # Using the character name
'\u0394'
>>> "\u0394"                          # Using a 16-bit hex value
'\u0394'
>>> "\U00000394"                      # Using a 32-bit hex value
'\u0394'

علاوه بر این، می‌توانید با استفاده از متد decode() کلاس bytes یک رشته ایجاد کنید. این متد یک آرگومان encoding، مانند UTF-8، و به‌اختیار یک آرگومان errors می‌گیرد.

آرگومان errors پاسخ را زمانی که رشته ورودی نمی‌تواند بر اساس قواعد کدگذاری تبدیل شود، مشخص می‌کند. مقادیر مجاز برای این آرگومان عبارت‌اند از 'strict' (پرتاب استثنای UnicodeDecodeError'replace' (استفاده از U+FFFD، REPLACEMENT CHARACTER'ignore' (فقط نویسه را از نتیجه‌ی یونیکد حذف می‌کند)، یا 'backslashreplace' (یک دنباله‌ی خنثی‌سازی \xNN درج می‌کند). مثال‌های زیر تفاوت‌ها را نشان می‌دهند:

>>> b'\x80abc'.decode("utf-8", "strict")
Traceback (most recent call last):
    ...
UnicodeDecodeError: 'utf-8' codec can't decode byte 0x80 in position 0:
  invalid start byte
>>> b'\x80abc'.decode("utf-8", "replace")
'\ufffdabc'
>>> b'\x80abc'.decode("utf-8", "backslashreplace")
'\\x80abc'
>>> b'\x80abc'.decode("utf-8", "ignore")
'abc'

کدگذاری‌ها به‌صورت رشته‌هایی شامل نام کدگذاری مشخص می‌شوند. پایتون دارای حدود ۱۰۰ کدگذاری مختلف است؛ برای دیدن فهرست آن‌ها به مرجع کتابخانه پایتون در کدگذاری‌های استاندارد مراجعه کنید. برخی کدگذاری‌ها چندین نام دارند؛ برای مثال، 'latin-1'، 'iso_8859_1' و '8859' همه نام‌های مترادف برای یک کدگذاری هستند.

رشته‌های یونیکد تک‌نویسه‌ای را همچنین می‌توان با تابع توکار chr() ایجاد کرد، که اعداد صحیح را می‌گیرد و رشته‌ای یونیکد به طول ۱ برمی‌گرداند که حاوی نقطه کد متناظر است. عملیات معکوس، تابع توکار ord() است که یک رشته یونیکد تک‌نویسه‌ای را می‌گیرد و مقدار نقطه کد را برمی‌گرداند:

>>> chr(57344)
'\ue000'
>>> ord('\ue000')
57344

تبدیل به بایت

متد معکوس bytes.decode()، str.encode() است که یک بازنمایی bytes از رشته یونیکد را برمی‌گرداند؛ این بازنمایی با کدگذاری درخواستی کدگذاری شده است.

پارامتر errors همان پارامتر متد decode() است، اما از چند هندلر ممکن دیگر نیز پشتیبانی می‌کند. علاوه بر 'strict'، 'ignore' و 'replace' (که در این حالت به‌جای نویسه غیرقابل کدگذاری، یک علامت سؤال درج می‌کند)، 'xmlcharrefreplace' (که یک ارجاع نویسه XML درج می‌کند)، backslashreplace (که یک دنباله خنثی‌سازی \uNNNN درج می‌کند) و namereplace (که یک دنباله خنثی‌سازی \N{...} درج می‌کند) نیز وجود دارند.

مثال زیر نتایج متفاوت را نشان می‌دهد:

>>> u = chr(40960) + 'abcd' + chr(1972)
>>> u.encode('utf-8')
b'\xea\x80\x80abcd\xde\xb4'
>>> u.encode('ascii')
Traceback (most recent call last):
    ...
UnicodeEncodeError: 'ascii' codec can't encode character '\ua000' in
  position 0: ordinal not in range(128)
>>> u.encode('ascii', 'ignore')
b'abcd'
>>> u.encode('ascii', 'replace')
b'?abcd?'
>>> u.encode('ascii', 'xmlcharrefreplace')
b'ꀀabcd޴'
>>> u.encode('ascii', 'backslashreplace')
b'\\ua000abcd\\u07b4'
>>> u.encode('ascii', 'namereplace')
b'\\N{YI SYLLABLE IT}abcd\\u07b4'

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

مقادیر لفظی یونیکد در کد منبع پایتون

در کد منبع پایتون، نقاط کد مشخص یونیکد را می‌توان با استفاده از دنباله‌ی خنثی‌سازی \u نوشت که پس از آن ۴ رقم مبنای شانزده می‌آید و نقطه‌کد را مشخص می‌کند. دنباله‌ی خنثی‌سازی \U مشابه است، اما به ۸ رقم مبنای شانزده نیاز دارد، نه ۴:

>>> s = "a\xac\u1234\u20ac\U00008000"
... #     ^^^^ two-digit hex escape
... #         ^^^^^^ four-digit Unicode escape
... #                     ^^^^^^^^^^ eight-digit Unicode escape
>>> [ord(c) for c in s]
[97, 172, 4660, 8364, 32768]

استفاده از دنباله‌های خنثی‌سازی برای نقاط کد بزرگ‌تر از ۱۲۷ در مقادیر کم مشکلی ندارد، اما اگر از نویسه‌های لهجه‌دار زیادی استفاده کنید، آزاردهنده می‌شود؛ همان‌طور که در برنامه‌ای با پیام‌هایی به فرانسوی یا زبان دیگری که از علامت‌های تلفظی استفاده می‌کند، چنین خواهد بود. همچنین می‌توانید رشته‌ها را با استفاده از تابع توکار chr() بسازید، اما این کار حتی خسته‌کننده‌تر است.

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

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

#!/usr/bin/env python
# -*- coding: latin-1 -*-

u = 'abcdé'
print(ord(u[-1]))

این سینتکس از نمادگذاری Emacs برای مشخص کردن متغیرهای محلی یک پرونده الهام گرفته است. Emacs از متغیرهای مختلفی پشتیبانی می‌کند، اما Python فقط از 'coding' پشتیبانی می‌کند. نمادهای -*- به Emacs نشان می‌دهند که آن کامنت ویژه است؛ این نمادها برای Python هیچ معنایی ندارند، اما یک قرارداد هستند. Python در کامنت به دنبال coding: name یا coding=name می‌گردد.

اگر چنین کامنتی را وارد نکنید، همان‌طور که پیش‌تر ذکر شد، کدگذاری پیش‌فرض به‌کاررفته UTF-8 خواهد بود. برای اطلاعات بیشتر، PEP 263 را نیز ببینید.

ویژگی‌های یونیکد

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

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

import unicodedata

u = chr(233) + chr(0x0bf2) + chr(3972) + chr(6000) + chr(13231)

for i, c in enumerate(u):
    print(i, '%04x' % ord(c), unicodedata.category(c), end=" ")
    print(unicodedata.name(c))

# Get numeric value of second character
print(unicodedata.numeric(u[1]))

هنگام اجرا، این چاپ می‌شود:

0 00e9 Ll LATIN SMALL LETTER E WITH ACUTE
1 0bf2 No TAMIL NUMBER ONE THOUSAND
2 0f84 Mn TIBETAN MARK HALANTA
3 1770 Lo TAGBANWA LETTER SA
4 33af So SQUARE RAD OVER S SQUARED
1000.0

کدهای دسته، مخفف‌هایی هستند که ماهیت نویسه را توصیف می‌کنند. این کدها در دسته‌هایی مانند «حرف (Letter)»، «عدد (Number)»، «نمادگذاری (Punctuation)» یا «نماد (Symbol)» گروه‌بندی می‌شوند و این دسته‌ها خود به زیردسته‌هایی تقسیم می‌شوند. با در نظر گرفتن کدهای خروجی بالا، 'Ll' به معنای «حرف، کوچک (Letter, lowercase)»، 'No' به معنای «عدد، دیگر (Number, other)»، 'Mn' به معنای «علامت، بدون فاصله (Mark, nonspacing)» و 'So' به معنای «نماد، دیگر (Symbol, other)» است. برای مشاهده‌ی فهرستی از کدهای دسته، بخش General Category Values در مستندات Unicode Character Database را ببینید.

مقایسه رشته‌ها

یونیکد مقایسه‌ی رشته‌ها را تا حدی پیچیده می‌کند، زیرا مجموعه‌ی یکسانی از نویسه‌ها می‌تواند با دنباله‌های متفاوتی از نقطه‌کدها نمایش داده شود. برای مثال، نویسه‌ای مانند 'ê' می‌تواند به‌صورت یک نقطه‌کد U+00EA، یا به‌صورت U+0065 U+0302 نمایش داده شود، یعنی نقطه‌کدِ 'e' که پس از آن نقطه‌کدی برای 'COMBINING CIRCUMFLEX ACCENT' می‌آید. این دو هنگام چاپ خروجی یکسانی تولید می‌کنند، اما یکی رشته‌ای به طول ۱ و دیگری رشته‌ای به طول ۲ است.

یکی از ابزارها برای مقایسه‌ی بدون حساسیت به بزرگی و کوچکی حروف، متد رشته‌ای casefold() است که رشته را با پیروی از الگوریتمی توصیف‌شده در استاندارد یونیکد به شکلی بدون حساسیت به بزرگی و کوچکی حروف تبدیل می‌کند. این الگوریتم برای نویسه‌هایی مانند حرف آلمانی 'ß' (نقطه کد U+00DF) که به جفت حرف کوچک 'ss' تبدیل می‌شود، برخورد ویژه‌ای دارد.

>>> street = 'Gürzenichstraße'
>>> street.casefold()
'gürzenichstrasse'

ابزار دوم، تابع normalize() در ماژول unicodedata است که رشته‌ها را به یکی از چند صورت نرمال تبدیل می‌کند؛ در این صورت‌های نرمال، حروفی که پس از آن‌ها یک نویسه ترکیبی آمده است با نویسه‌های تکی جایگزین می‌شوند. می‌توان از normalize() برای انجام مقایسه‌ی رشته‌ها استفاده کرد تا اگر دو رشته به‌شکل متفاوتی از نویسه‌های ترکیبی استفاده می‌کنند، نابرابری به‌اشتباه گزارش نشود:

import unicodedata

def compare_strs(s1, s2):
    def NFD(s):
        return unicodedata.normalize('NFD', s)

    return NFD(s1) == NFD(s2)

single_char = 'ê'
multiple_chars = '\N{LATIN SMALL LETTER E}\N{COMBINING CIRCUMFLEX ACCENT}'
print('length of first string=', len(single_char))
print('length of second string=', len(multiple_chars))
print(compare_strs(single_char, multiple_chars))

هنگام اجرا، این خروجی را می‌دهد:

$ python compare-strs.py
length of first string= 1
length of second string= 2
True

اولین آرگومان تابع normalize() رشته‌ای است که فرم نرمال‌سازی مورد نظر را مشخص می‌کند و می‌تواند یکی از 'NFC'، 'NFKC'، 'NFD' و 'NFKD' باشد.

استاندارد یونیکد همچنین چگونگی انجام مقایسه‌های بدون حساسیت به بزرگی و کوچکی حروف را مشخص می‌کند:

import unicodedata

def compare_caseless(s1, s2):
    def NFD(s):
        return unicodedata.normalize('NFD', s)

    return NFD(NFD(s1).casefold()) == NFD(NFD(s2).casefold())

# Example usage
single_char = 'ê'
multiple_chars = '\N{LATIN CAPITAL LETTER E}\N{COMBINING CIRCUMFLEX ACCENT}'

print(compare_caseless(single_char, multiple_chars))

این True را چاپ می‌کند. (چرا NFD() دو بار فراخوانی می‌شود؟ زیرا چند نویسه باعث می‌شوند casefold() یک رشته‌ی غیرنرمال را بازگرداند، بنابراین نتیجه باید دوباره نرمال‌سازی شود. برای توضیح و یک نمونه، بخش 3.13 استاندارد یونیکد را ببینید.)

عبارت‌های باقاعده یونیکد

عبارت‌های باقاعده پشتیبانی‌شده توسط ماژول re را می‌توان به‌صورت بایت یا رشته ارائه کرد. برخی از دنباله‌های نویسه‌ای خاص مانند \d و \w معانی متفاوتی دارند، بسته به اینکه الگو به‌صورت بایت یا رشته ارائه شود. برای مثال، \d در بایت‌ها با نویسه‌های [0-9] مطابقت می‌کند، اما در رشته‌ها با هر نویسه‌ای که در دسته 'Nd' باشد مطابقت می‌کند.

در این مثال، عدد ۵۷ در رشته هم با ارقام تایلندی و هم با ارقام عربی نوشته شده است:

import re
p = re.compile(r'\d+')

s = "Over \u0e55\u0e57 57 flavours"
m = p.search(s)
print(repr(m.group()))

هنگام اجرا، \d+ با ارقام تایلندی تطابق پیدا می‌کند و آن‌ها را چاپ می‌کند. اگر پرچم re.ASCII را به compile() بدهید، \d+ در عوض با زیررشته‌ی «57» تطابق پیدا می‌کند.

به‌طور مشابه، \w با طیف وسیعی از نویسه‌های یونیکد مطابقت می‌کند، اما در بایت‌ها یا اگر re.ASCII ارائه شده باشد، فقط با [a-zA-Z0-9_] مطابقت می‌کند، و \s با نویسه‌های فضای سفید یونیکد یا [ \t\n\r\f\v] مطابقت خواهد کرد.

ارجاع‌ها

برخی بحث‌های جایگزین خوب درباره‌ی پشتیبانی پایتون از یونیکد عبارتند از:

نوع str در مرجع کتابخانه‌ی پایتون، در Text Sequence Type --- str شرح داده شده است.

مستندات ماژول unicodedata.

مستندات ماژول codecs.

مارک آندره لمبورگ در EuroPython 2002 ارائه‌ای با عنوان «Python and Unicode» (اسلایدهای PDF) داشت. این اسلایدها مروری عالی بر طراحی قابلیت‌های یونیکد Python 2 است (که در آن نوع رشته یونیکد unicode نامیده می‌شود و لفظ‌ها با u شروع می‌شوند).

خواندن و نوشتن داده‌های یونیکد

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

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

داده‌های یونیکد معمولاً پیش از آنکه روی دیسک نوشته شوند یا از طریق سوکت ارسال شوند، به یک کدگذاری مشخص تبدیل می‌شوند. می‌توانید تمام این کارها را خودتان انجام دهید: پرونده‌ای را باز کنید، یک شیء بایتی ۸‌بیتی از آن بخوانید و بایت‌ها را با bytes.decode(encoding) تبدیل کنید. با این حال، روش دستی توصیه نمی‌شود.

یکی از مشکلات، ماهیت چندبایتی کدگذاری‌هاست؛ یک نویسه‌ی یونیکد می‌تواند با چند بایت بازنمایی شود. اگر بخواهید پرونده را به‌صورت تکه‌هایی با اندازه‌ی دلخواه (مثلاً ۱۰۲۴ یا ۴۰۹۶ بایت) بخوانید، باید کد مدیریت خطایی برای پوشش حالتی بنویسید که در آن تنها بخشی از بایت‌هایی که یک نویسه‌ی یونیکد را کدگذاری می‌کنند، در انتهای یک تکه خوانده می‌شود. یک راه‌حل می‌تواند این باشد که کل پرونده را در حافظه بخوانید و سپس کدگشایی را انجام دهید، اما این کار شما را از کار با پرونده‌های بسیار بزرگ بازمی‌دارد؛ اگر نیاز داشته باشید پرونده‌ی ۲ GiB را بخوانید، به ۲ GiB حافظه‌ی RAM نیاز دارید. (در واقع بیشتر، زیرا حداقل برای یک لحظه باید هم رشته‌ی کدگذاری‌شده و هم نسخه‌ی یونیکد آن را در حافظه داشته باشید.)

راه‌حل این است که از رابط کدگشایی سطح پایین برای تشخیص حالت دنباله‌های کدگذاری ناقص استفاده شود. کار پیاده‌سازی این مورد قبلاً برای شما انجام شده است: تابع توکار open() می‌تواند یک شیء شبه‌پرونده برگرداند که فرض می‌کند محتوای پرونده در یک کدگذاری مشخص قرار دارد و پارامترهای یونیکد را برای متدهایی مانند read() و write() می‌پذیرد. این کار از طریق پارامترهای encoding و errors در open() انجام می‌شود که دقیقاً مانند پارامترهای موجود در str.encode() و bytes.decode() تفسیر می‌شوند.

بنابراین، خواندن یونیکد از یک پرونده ساده است:

with open('unicode.txt', encoding='utf-8') as f:
    for line in f:
        print(repr(line))

همچنین می‌توانید پرونده‌ها را در حالت به‌روزرسانی باز کنید، که امکان خواندن و نوشتن را فراهم می‌کند:

with open('test', encoding='utf-8', mode='w+') as f:
    f.write('\u4500 blah blah blah\n')
    f.seek(0)
    print(repr(f.readline()[:1]))

نویسه‌ی یونیکد U+FEFF به‌عنوان نشانگر ترتیب بایت (BOM) استفاده می‌شود و اغلب به‌عنوان نخستین نویسه‌ی یک پرونده نوشته می‌شود تا به تشخیص خودکار ترتیب بایت‌های پرونده کمک کند. برخی کدگذاری‌ها، مانند UTF-16، انتظار دارند که یک BOM در ابتدای پرونده وجود داشته باشد؛ هنگامی که از چنین کدگذاری‌ای استفاده شود، BOM به‌صورت خودکار به‌عنوان نخستین نویسه نوشته می‌شود و هنگام خواندن پرونده، به‌طور خاموش حذف می‌شود. گونه‌هایی از این کدگذاری‌ها وجود دارند، مانند 'utf-16-le' و 'utf-16-be' برای کدگذاری‌هایی با بایت کوچک‌اندیان (little-endian) و بزرگ‌اندیان (big-endian)، که یک ترتیب بایت مشخص را تعیین می‌کنند و BOM را نادیده نمی‌گیرند.

در برخی حوزه‌ها، همچنین مرسوم است که از «BOM» در ابتدای پرونده‌های کدگذاری‌شده با UTF-8 استفاده شود؛ این نام گمراه‌کننده است، زیرا UTF-8 به ترتیب بایت‌ها وابسته نیست. این نشانه صرفاً اعلام می‌کند که پرونده با UTF-8 کدگذاری شده است. برای خواندن چنین پرونده‌هایی، از کدک 'utf-8-sig' استفاده کنید تا در صورت وجود نشانه، به‌طور خودکار آن را نادیده بگیرد.

نام‌پرونده‌های یونیکد

بیشتر سیستم‌عامل‌های رایج امروزی از نام پرونده‌هایی که حاوی نویسه‌های یونیکد دلخواه هستند پشتیبانی می‌کنند. معمولاً این کار با تبدیل رشته یونیکدی به یک کدگذاری خاص پیاده‌سازی می‌شود که بسته به سیستم متفاوت است. امروزه پایتون در حال همگرایی به سمت استفاده از UTF-8 است: پایتون در MacOS برای چندین نسخه از UTF-8 استفاده کرده است و پایتون 3.6 نیز در ویندوز به استفاده از UTF-8 روی آورد. در سیستم‌های یونیکسی، تنها در صورتی یک کدگذاری سامانه فایل‌بندی وجود خواهد داشت که متغیرهای محیطی LANG یا LC_CTYPE را تنظیم کرده باشید؛ در غیر این صورت، کدگذاری پیش‌فرض باز هم UTF-8 است.

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

filename = 'filename\u4500abc'
with open(filename, 'w') as f:
    f.write('blah\n')

توابع ماژول os مانند os.stat() همچنین نام پرونده‌های یونیکد را می‌پذیرند.

تابع os.listdir() نام پرونده‌ها را برمی‌گرداند، که این موضوع مسئله‌ای را مطرح می‌کند: آیا باید نسخه‌ی یونیکد نام پرونده‌ها را برگرداند، یا باید بایت‌هایی شامل نسخه‌های کدگذاری‌شده را برگرداند؟ os.listdir() می‌تواند هر دو کار را انجام دهد، بسته به این‌که مسیر پوشه را به‌صورت بایت یا یک رشته‌ی یونیکد ارائه کرده باشید. اگر یک رشته‌ی یونیکد را به‌عنوان مسیر بدهید، نام پرونده‌ها با استفاده از کدگذاری سامانه فایل‌بندی کدگشایی می‌شوند و فهرستی از رشته‌های یونیکد برگردانده می‌شود، در حالی که دادن یک مسیر بایتی، نام پرونده‌ها را به‌صورت بایت برمی‌گرداند. برای مثال، با فرض این‌که کدگذاری سامانه فایل‌بندی پیش‌فرض UTF-8 باشد، اجرای برنامه‌ی زیر:

fn = 'filename\u4500abc'
f = open(fn, 'w')
f.close()

import os
print(os.listdir(b'.'))
print(os.listdir('.'))

خروجی زیر را تولید خواهد کرد:

$ python listdir-test.py
[b'filename\xe4\x94\x80abc', ...]
['filename\u4500abc', ...]

نخستین فهرست شامل نام پرونده‌های کدگذاری‌شده با UTF-8 است، و دومین فهرست شامل نسخه‌های یونیکد است.

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

نکاتی برای نوشتن برنامه‌های آگاه از یونیکد

این بخش چند پیشنهاد درباره‌ی نوشتن نرم‌افزاری که با یونیکد سروکار دارد ارائه می‌دهد.

مهم‌ترین نکته این است:

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

اگر تلاش کنید توابع پردازشی بنویسید که هم رشته‌های یونیکد و هم رشته‌های بایتی را می‌پذیرند، متوجه خواهید شد که برنامه‌ی شما هر جا که این دو نوع مختلف رشته را با هم ترکیب کنید، در برابر اشکال‌ها آسیب‌پذیر است. هیچ کدگذاری یا کدگشایی خودکاری وجود ندارد: اگر برای مثال str + bytes را انجام دهید، یک TypeError پرتاب خواهد شد.

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

تبدیل بین کدگذاری‌های پرونده

کلاس StreamRecoder می‌تواند به‌صورت شفاف بین کدگذاری‌ها تبدیل انجام دهد؛ این کلاس جریانی را می‌گیرد که داده‌ها را در کدگذاری #۱ برمی‌گرداند و مانند جریانی رفتار می‌کند که داده‌ها را در کدگذاری #۲ برمی‌گرداند.

برای مثال، اگر یک پرونده ورودی f دارید که با Latin-1 کدگذاری شده است، می‌توانید آن را با یک StreamRecoder بپیچید تا بایت‌های کدگذاری‌شده با UTF-8 را برگرداند:

new_f = codecs.StreamRecoder(f,
    # en/decoder: used by read() to encode its results and
    # by write() to decode its input.
    codecs.getencoder('utf-8'), codecs.getdecoder('utf-8'),

    # reader/writer: used to read and write to the stream.
    codecs.getreader('latin-1'), codecs.getwriter('latin-1') )

پرونده‌هایی با کدگذاری ناشناخته

اگر نیاز داشته باشید تغییری در یک پرونده ایجاد کنید، اما کدگذاری پرونده را ندانید، چه کاری می‌توانید انجام دهید؟ اگر می‌دانید کدگذاری آن با ASCII سازگار است و فقط می‌خواهید بخش‌های ASCII را بررسی یا تغییر دهید، می‌توانید پرونده را با هندلر خطای surrogateescape باز کنید:

with open(fname, 'r', encoding="ascii", errors="surrogateescape") as f:
    data = f.read()

# make changes to the string 'data'

with open(fname + '.new', 'w',
          encoding="ascii", errors="surrogateescape") as f:
    f.write(data)

هندلر خطای surrogateescape هر بایت غیر ASCII را به‌صورت نقاط کد در بازه‌ای خاص از U+DC80 تا U+DCFF کدگشایی می‌کند. سپس وقتی از هندلر خطای surrogateescape برای کدگذاری داده و نوشتن مجدد آن استفاده شود، این نقاط کد به همان بایت‌ها تبدیل می‌شوند.

ارجاع‌ها

بخشی از Mastering Python 3 Input/Output، یک سخنرانی PyCon 2010 از David Beazley، به پردازش متن و مدیریت داده‌های دودویی می‌پردازد.

اسلایدهای PDF مربوط به ارائه مارک-آندره لمبورگ با عنوان "Writing Unicode-aware Applications in Python" به بررسی مسائل کدگذاری نویسه‌ها و همچنین چگونگی بین‌المللی‌سازی و بومی‌سازی یک برنامه می‌پردازد. این اسلایدها تنها پایتون 2.x را پوشش می‌دهند.

The Guts of Unicode in Python یک سخنرانی در PyCon 2013 از بنجامین پترسون است که به بررسی بازنمایی داخلی یونیکد در پایتون 3.3 می‌پردازد.

قدردانی‌ها

پیش‌نویس اولیه این سند توسط Andrew Kuchling نوشته شد. از آن زمان تاکنون، این سند توسط Alexander Belopolsky، Georg Brandl، Andrew Kuchling و Ezio Melotti بیشتر بازبینی شده است.

با تشکر از افراد زیر که به خطاهای این مقاله اشاره کرده یا پیشنهادهایی درباره آن ارائه کرده‌اند: Éric Araujo، Nicholas Bastin، Nick Coghlan، Marius Gedminas، Kent Johnson، Ken Krugler، Marc-André Lemburg، Martin von Löwis، Terry J. Reedy، Serhiy Storchaka، Eryk Sun، Chad Whitacre و Graham Wideman.