Unicode HOWTO

Phát hành:

1.12

Zz000zz này thảo luận về sự hỗ trợ của Python đối với đặc tả Unicode để biểu diễn dữ liệu văn bản và giải thích nhiều vấn đề khác nhau mà mọi người thường gặp phải khi cố gắng làm việc với Unicode.

Giới thiệu về Unicode

định nghĩa

Các chương trình ngày nay cần có khả năng xử lý nhiều loại ký tự khác nhau. Các ứng dụng thường được quốc tế hóa để hiển thị thông báo và đầu ra bằng nhiều ngôn ngữ mà người dùng có thể lựa chọn; cùng một chương trình có thể cần xuất ra thông báo lỗi bằng tiếng Anh, tiếng Pháp, tiếng Nhật, tiếng Do Thái hoặc tiếng Nga. Nội dung web có thể được viết bằng bất kỳ ngôn ngữ nào trong số này và cũng có thể bao gồm nhiều biểu tượng cảm xúc khác nhau. Kiểu chuỗi của Python sử dụng Tiêu chuẩn Unicode để biểu diễn các ký tự, cho phép các chương trình Python hoạt động với tất cả các ký tự có thể khác nhau này.

Unicode (https://www.unicode.org/) là một thông số kỹ thuật nhằm liệt kê mọi ký tự được sử dụng trong ngôn ngữ của con người và cung cấp cho mỗi ký tự một mã riêng. Các thông số kỹ thuật Unicode liên tục được sửa đổi và cập nhật để bổ sung thêm các ngôn ngữ và ký hiệu mới.

Zz000zz là thành phần nhỏ nhất có thể có của văn bản. 'A', 'B', 'C', v.v., đều là những ký tự khác nhau. 'È' và 'Í' cũng vậy. Các ký tự khác nhau tùy thuộc vào ngôn ngữ hoặc ngữ cảnh bạn đang nói đến. Ví dụ: có một ký tự cho "Số Một La Mã", 'Ⅰ', ký tự này tách biệt với chữ in hoa 'I'. Chúng thường trông giống nhau, nhưng đây là hai ký tự khác nhau có ý nghĩa khác nhau.

Tiêu chuẩn Unicode mô tả cách các ký tự được biểu thị bằng code points. Giá trị điểm mã là một số nguyên trong phạm vi từ 0 đến 0x10FFFF (khoảng 1,1 triệu giá trị, actual number assigned nhỏ hơn thế). Trong tiêu chuẩn và trong tài liệu này, một điểm mã được viết bằng ký hiệu U+265E để biểu thị ký tự có giá trị 0x265e (9,822 ở dạng thập phân).

Tiêu chuẩn Unicode chứa rất nhiều bảng liệt kê các ký tự và các điểm mã tương ứng của chúng:

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
...

Nghiêm túc mà nói, những định nghĩa này ngụ ý rằng việc nói 'đây là nhân vật U+265E' là vô nghĩa. U+265E là một điểm mã, đại diện cho một số ký tự cụ thể; trong trường hợp này, nó đại diện cho ký tự 'BLACK CHESS KNIGHT', '♞'. Trong bối cảnh không chính thức, sự phân biệt giữa điểm mã và ký tự này đôi khi sẽ bị lãng quên.

Một ký tự được thể hiện trên màn hình hoặc trên giấy bằng một tập hợp các phần tử đồ họa được gọi là glyph. Ví dụ: glyph cho chữ hoa A là hai nét chéo và một nét ngang, mặc dù chi tiết chính xác sẽ phụ thuộc vào phông chữ được sử dụng. Hầu hết mã Python không cần phải lo lắng về glyphs; việc tìm ra glyph chính xác để hiển thị nói chung là công việc của bộ công cụ GUI hoặc trình kết xuất phông chữ của thiết bị đầu cuối.

Mã hóa

Tóm tắt phần trước: chuỗi Unicode là một chuỗi các điểm mã, là các số từ 0 đến 0x10FFFF (1.114.111 thập phân). Chuỗi điểm mã này cần được biểu diễn trong bộ nhớ dưới dạng tập hợp code units và sau đó code units được ánh xạ tới byte 8 bit. Các quy tắc dịch chuỗi Unicode thành một chuỗi byte được gọi là character encoding hoặc chỉ là encoding.

Mã hóa đầu tiên bạn có thể nghĩ đến là sử dụng số nguyên 32 bit làm đơn vị mã, sau đó sử dụng biểu diễn số nguyên 32 bit của CPU. Trong cách biểu diễn này, chuỗi "Python" có thể trông như thế này:

P y th 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

Cách biểu diễn này đơn giản nhưng việc sử dụng nó lại bộc lộ một số vấn đề.

  1. Nó không di động được; các bộ xử lý khác nhau sắp xếp các byte khác nhau.

  2. Nó rất lãng phí không gian. Trong hầu hết các văn bản, phần lớn các điểm mã nhỏ hơn 127 hoặc nhỏ hơn 255, do đó, rất nhiều không gian bị chiếm bởi các byte 0x00. Chuỗi trên chiếm 24 byte so với 6 byte cần thiết cho biểu diễn ASCII. Việc tăng mức sử dụng RAM không quá quan trọng (máy tính để bàn có hàng gigabyte RAM và các chuỗi thường không lớn đến thế), nhưng việc mở rộng mức sử dụng băng thông ổ đĩa và mạng của chúng ta lên gấp 4 lần là không thể chấp nhận được.

  3. Nó không tương thích với các hàm C hiện có như strlen(), vì vậy cần phải sử dụng một họ hàm chuỗi rộng mới.

Do đó, mã hóa này không được sử dụng nhiều và thay vào đó mọi người chọn các mã hóa khác hiệu quả và tiện lợi hơn, chẳng hạn như UTF-8.

UTF-8 là một trong những mã hóa được sử dụng phổ biến nhất và Python thường mặc định sử dụng nó. UTF là viết tắt của "Định dạng chuyển đổi Unicode" và '8' có nghĩa là các giá trị 8 bit được sử dụng trong mã hóa. (Ngoài ra còn có các mã hóa UTF-16 và UTF-32, nhưng chúng ít được sử dụng hơn UTF-8.) UTF-8 sử dụng các quy tắc sau:

  1. Nếu điểm mã < 128 thì nó được biểu thị bằng giá trị byte tương ứng.

  2. Nếu điểm mã >= 128 thì nó sẽ chuyển thành một chuỗi gồm hai, ba hoặc bốn byte, trong đó mỗi byte của chuỗi nằm trong khoảng từ 128 đến 255.

UTF-8 có một số thuộc tính tiện lợi:

  1. Nó có thể xử lý bất kỳ điểm mã Unicode nào.

  2. Chuỗi Unicode được chuyển thành một chuỗi byte chỉ chứa các byte 0 được nhúng trong đó chúng đại diện cho ký tự null (U+0000). Điều này có nghĩa là các chuỗi UTF-8 có thể được xử lý bằng các hàm C như strcpy() và được gửi qua các giao thức không thể xử lý byte 0 cho bất kỳ thứ gì ngoài điểm đánh dấu cuối chuỗi.

  3. Một chuỗi văn bản ASCII cũng là văn bản UTF-8 hợp lệ.

  4. UTF-8 khá nhỏ gọn; phần lớn các ký tự thường được sử dụng có thể được biểu diễn bằng một hoặc hai byte.

  5. Nếu byte bị hỏng hoặc bị mất, có thể xác định điểm bắt đầu của điểm mã hóa UTF-8 tiếp theo và đồng bộ hóa lại. Dữ liệu 8 bit ngẫu nhiên cũng khó có thể trông giống như UTF-8 hợp lệ.

  6. UTF-8 là mã hóa theo định hướng byte. Mã hóa chỉ định rằng mỗi ký tự được biểu thị bằng một chuỗi cụ thể gồm một hoặc nhiều byte. Điều này tránh các vấn đề về thứ tự byte có thể xảy ra với mã hóa theo hướng số nguyên và từ, như UTF-16 và UTF-32, trong đó chuỗi byte thay đổi tùy thuộc vào phần cứng mà chuỗi được mã hóa.

Tài liệu tham khảo

Zz000zz có biểu đồ ký tự, bảng chú giải thuật ngữ và phiên bản PDF của đặc tả Unicode. Hãy chuẩn bị cho một số bài đọc khó. A chronology về nguồn gốc và sự phát triển của Unicode cũng có sẵn trên trang web.

Trên kênh Youtube Computerphile, Tom Scott giới thiệu ngắn gọn discusses the history of Unicode and UTF-8 (9 phút 36 giây).

Để giúp hiểu tiêu chuẩn, Jukka Korpela đã viết an introductory guide để đọc các bảng ký tự Unicode.

Một good introductory article khác được viết bởi Joel Spolsky. Nếu phần giới thiệu này không làm bạn hiểu rõ, bạn nên thử đọc bài viết thay thế này trước khi tiếp tục.

Các mục Wikipedia thường hữu ích; ví dụ: xem các mục dành cho "character encoding" và UTF-8.

Hỗ trợ Unicode của Python

Bây giờ bạn đã học được kiến thức cơ bản về Unicode, chúng ta có thể xem xét các tính năng Unicode của Python.

Kiểu chuỗi

Kể từ Python 3.0, loại str của ngôn ngữ này chứa các ký tự Unicode, nghĩa là bất kỳ chuỗi nào được tạo bằng "unicode rocks!", 'unicode rocks!' hoặc cú pháp chuỗi ba dấu ngoặc kép đều được lưu trữ dưới dạng Unicode.

Mã hóa mặc định cho mã nguồn Python là UTF-8, vì vậy bạn chỉ cần đưa ký tự Unicode vào một chuỗi ký tự:

thử:
    với open('/tmp/input.txt', 'r')  f:
        ...
ngoại trừ OSError:
    # Thông báo lỗi 'Không tìm thấy tệp'.
    print("Fichier non trouvé")

Lưu ý phụ: Python 3 cũng hỗ trợ sử dụng các ký tự Unicode trong mã định danh

répertoire = "/tmp/records.log"
với open(répertoire, "w")  f:
    f.write("kiểm tra\n")

Nếu bạn không thể nhập một ký tự cụ thể trong trình soạn thảo của mình hoặc muốn giữ mã nguồn chỉ ASCII vì lý do nào đó, bạn cũng có thể sử dụng chuỗi thoát trong chuỗi ký tự. (Tùy thuộc vào hệ thống của bạn, bạn có thể thấy ký tự chữ hoa thực tế thay vì lối thoát u.)

>>> "\N{GREEK CAPITAL LETTER DELTA}" # Using tên nhân vật
'\u0394'
>>> "\u0394" # Using giá trị hex 16 bit
'\u0394'
>>> "\U00000394" # Using giá trị hex 32-bit
'\u0394'

Ngoài ra, người ta có thể tạo một chuỗi bằng phương thức decode() của bytes. Phương thức này nhận đối số encoding, chẳng hạn như UTF-8 và tùy chọn một đối số errors.

Đối số errors chỉ định phản hồi khi chuỗi đầu vào không thể chuyển đổi theo quy tắc mã hóa. Các giá trị pháp lý cho đối số này là 'strict' (tăng ngoại lệ UnicodeDecodeError), 'replace' (sử dụng U+FFFD, REPLACEMENT CHARACTER), 'ignore' (chỉ loại bỏ ký tự ra khỏi kết quả Unicode) hoặc 'backslashreplace' (chèn chuỗi thoát \xNN). Các ví dụ sau đây cho thấy sự khác biệt:

>>> 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'

Mã hóa được chỉ định dưới dạng chuỗi chứa tên mã hóa. Python có khoảng 100 cách mã hóa khác nhau; xem Tham khảo Thư viện Python tại Mã hóa tiêu chuẩn để biết danh sách. Một số bảng mã có nhiều tên; ví dụ: 'latin-1', 'iso_8859_1''8859' đều là từ đồng nghĩa với cùng một mã hóa.

Chuỗi Unicode một ký tự cũng có thể được tạo bằng hàm tích hợp chr(), hàm này nhận số nguyên và trả về chuỗi Unicode có độ dài 1 chứa điểm mã tương ứng. Hoạt động ngược lại là hàm ord() tích hợp lấy chuỗi Unicode một ký tự và trả về giá trị điểm mã

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

Chuyển đổi sang byte

Phương thức ngược lại của bytes.decode()str.encode(), trả về biểu diễn bytes của chuỗi Unicode, được mã hóa trong encoding được yêu cầu.

Tham số errors giống với tham số của phương thức decode() nhưng hỗ trợ thêm một số trình xử lý khả thi. Ngoài 'strict', 'ignore''replace' (trong trường hợp này chèn dấu chấm hỏi thay vì ký tự không thể mã hóa), còn có 'xmlcharrefreplace' (chèn tham chiếu ký tự XML), backslashreplace (chèn chuỗi thoát \uNNNN) và namereplace (chèn chuỗi thoát \N{...}).

Ví dụ sau đây cho thấy các kết quả khác nhau:

>>> 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'&#40960;abcd&#1972;'
>>> u.encode('ascii', 'backslashreplace')
b'\\ua000abcd\\u07b4'
>>> u.encode('ascii', 'namereplace')
b'\\N{YI SYLLABLE IT}abcd\\u07b4'

Các quy trình cấp thấp để đăng ký và truy cập các mã hóa có sẵn được tìm thấy trong mô-đun codecs. Việc triển khai các mã hóa mới cũng đòi hỏi phải hiểu mô-đun codecs. Tuy nhiên, các chức năng mã hóa và giải mã được mô-đun này trả về thường ở mức độ thấp hơn mức bạn thấy thoải mái và việc viết các mã hóa mới là một nhiệm vụ chuyên biệt nên mô-đun sẽ không được đề cập trong HOWTO này.

Chữ Unicode trong mã nguồn Python

Trong mã nguồn Python, các điểm mã Unicode cụ thể có thể được viết bằng chuỗi thoát \u, theo sau là bốn chữ số hex cho điểm mã. Trình tự thoát \U tương tự, nhưng cần có tám chữ số hex chứ không phải bốn:

>>> s = "a\xac\u1234\u20ac\U00008000"
... # ^^^ thoát hex hai chữ số
... # ^^^^ Thoát Unicode bốn chữ số
... # ^^ ^^ ^^ ^^ ^^ Thoát Unicode tám chữ số
>>> [thứ tự(c) cho c trong s]
[97, 172, 4660, 8364, 32768]

Sử dụng chuỗi thoát cho các điểm mã lớn hơn 127 là phù hợp với liều lượng nhỏ nhưng sẽ gây khó chịu nếu bạn sử dụng nhiều ký tự có dấu, như khi bạn làm trong một chương trình có thông báo bằng tiếng Pháp hoặc một số ngôn ngữ sử dụng giọng khác. Bạn cũng có thể tập hợp các chuỗi bằng chức năng tích hợp sẵn chr(), nhưng điều này thậm chí còn tẻ nhạt hơn.

Lý tưởng nhất là bạn muốn có thể viết chữ bằng mã hóa tự nhiên của ngôn ngữ của mình. Sau đó, bạn có thể chỉnh sửa mã nguồn Python bằng trình soạn thảo yêu thích của mình. Trình soạn thảo này sẽ hiển thị các ký tự có dấu một cách tự nhiên và có các ký tự phù hợp được sử dụng trong thời gian chạy.

Python hỗ trợ viết mã nguồn bằng UTF-8 theo mặc định, nhưng bạn có thể sử dụng hầu hết mọi mã hóa nếu bạn khai báo mã hóa đang được sử dụng. Điều này được thực hiện bằng cách đưa vào một nhận xét đặc biệt ở dòng đầu tiên hoặc dòng thứ hai của tệp nguồn:

#!/usr/bin/env trăn
# -zz000zz-

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

Cú pháp được lấy cảm hứng từ ký hiệu của Emacs để chỉ định các biến cục bộ cho một tệp. Emacs hỗ trợ nhiều biến khác nhau nhưng Python chỉ hỗ trợ “coding”. Các ký hiệu -*- cho Emacs biết rằng nhận xét này rất đặc biệt; chúng không có ý nghĩa gì đối với Python nhưng chỉ là một quy ước. Python tìm kiếm coding: name hoặc coding=name trong bình luận.

Nếu bạn không bao gồm nhận xét như vậy, mã hóa mặc định được sử dụng sẽ là UTF-8 như đã đề cập. Xem thêm PEP 263 để biết thêm thông tin.

Thuộc tính Unicode

Đặc tả Unicode bao gồm cơ sở dữ liệu thông tin về các điểm mã. Đối với mỗi điểm mã được xác định, thông tin bao gồm tên ký tự, danh mục của nó, giá trị số nếu có (đối với các ký tự biểu thị khái niệm số như chữ số La Mã, phân số như một phần ba và bốn phần năm, v.v.). Ngoài ra còn có các thuộc tính liên quan đến hiển thị, chẳng hạn như cách sử dụng điểm mã trong văn bản hai chiều.

Chương trình sau hiển thị một số thông tin về một số ký tự và in giá trị số của một ký tự cụ thể:

nhập dữ liệu unicode

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

cho i, c trong liệt (u):
    print(i, '%04x' % ord(c), unicodedata.category(c), end=" ")
    in(unicodedata.name(c))

giá trị số # Get của ký tự thứ hai
print(unicodedata.numeric(u[1]))

Khi chạy, bản in này:

0 00e9 Ll LATIN SMALL LETTER E WITH ACUTE
1 0bf2 Không TAMIL NUMBER ONE THOUSAND
2 0f84 Mn TIBETAN MARK HALANTA
3 1770 Lô TAGBANWA LETTER SA
4 33af Vậy SQUARE RAD OVER S SQUARED
1000,0

Mã danh mục là chữ viết tắt mô tả bản chất của ký tự. Chúng được nhóm thành các danh mục như "Chữ cái", "Số", "Dấu câu" hoặc "Ký hiệu", lần lượt được chia thành các danh mục phụ. Để lấy mã từ kết quả đầu ra ở trên, 'Ll' có nghĩa là 'Chữ cái, chữ thường', 'No' có nghĩa là "Số, khác", 'Mn' là "Đánh dấu, không khoảng cách" và 'So' là "Ký hiệu, khác". Xem the General Category Values section of the Unicode Character Database documentation để biết danh sách mã danh mục.

So sánh chuỗi

Unicode gây thêm một số phức tạp khi so sánh các chuỗi, vì cùng một bộ ký tự có thể được biểu diễn bằng các chuỗi điểm mã khác nhau. Ví dụ: một chữ cái như 'ê' có thể được biểu thị dưới dạng một điểm mã U+00EA hoặc dưới dạng U+0065 U+0302, là điểm mã cho 'e', ​​theo sau là một điểm mã cho 'COMBINING CIRCUMFLEX ACCENT'. Chúng sẽ tạo ra cùng một đầu ra khi được in, nhưng một chuỗi là chuỗi có độ dài 1 và chuỗi kia có độ dài 2.

Một công cụ để so sánh không phân biệt chữ hoa chữ thường là phương thức chuỗi casefold() chuyển đổi một chuỗi thành dạng không phân biệt chữ hoa chữ thường theo thuật toán được mô tả bởi Tiêu chuẩn Unicode. Thuật toán này có cách xử lý đặc biệt đối với các ký tự như chữ cái tiếng Đức 'ß' (điểm mã U+00DF), trở thành cặp chữ cái viết thường 'ss'.

>>> đường phố = 'Gürzenichstraße'
>>> street.casefold()
'gürzenichstrasse'

Công cụ thứ hai là chức năng normalize() của mô-đun unicodedata để chuyển đổi chuỗi thành một trong một số dạng thông thường, trong đó các chữ cái theo sau là ký tự kết hợp được thay thế bằng các ký tự đơn. normalize() có thể được sử dụng để thực hiện so sánh chuỗi mà không báo cáo sai sự bất bình đẳng nếu hai chuỗi sử dụng các ký tự kết hợp khác nhau:

nhập dữ liệu unicode

def so sánh_str(s1, s2):
    chắc chắn NFD(s):
        trả về unicodedata.n normalize('NFD', s)

    trả về NFD(s1) == NFD(s2)

single_char = 'ê'
multiple_chars = '\N{LATIN SMALL LETTER E}\N{COMBINING CIRCUMFLEX ACCENT}'
print('độ dài của chuỗi đầu tiên=', len(single_char))
print('độ dài của chuỗi thứ hai=', len(multiple_chars))
print(so sánh_strs(single_char, multiple_chars))

Khi chạy, kết quả này xuất hiện:

$ python so sánh-str.py
độ dài của chuỗi đầu tiên = 1
độ dài của chuỗi thứ hai = 2
đúng

Đối số đầu tiên của hàm normalize() là một chuỗi đưa ra dạng chuẩn hóa mong muốn, có thể là một trong các 'NFC', 'NFKC', 'NFD' và 'NFKD'.

Tiêu chuẩn Unicode cũng chỉ định cách thực hiện so sánh không phân biệt chữ hoa chữ thường

nhập dữ liệu unicode

def so sánh_caseless(s1, s2):
    def NFD(s):
        trả về unicodedata.n normalize('NFD', s)

    trả về NFD(NFD(s1).casefold()) == NFD(NFD(s2).casefold())

cách sử dụng # Example
single_char = 'ê'
multiple_chars = '\N{LATIN CAPITAL LETTER E}\N{COMBINING CIRCUMFLEX ACCENT}'

print(compare_caseless(single_char, multiple_chars))

Điều này sẽ in True. (Tại sao NFD() được gọi hai lần? Bởi vì có một vài ký tự khiến casefold() trả về một chuỗi không chuẩn hóa, do đó kết quả cần được chuẩn hóa lại. Xem phần 3.13 của Tiêu chuẩn Unicode để biết phần thảo luận và ví dụ.)

Biểu thức chính quy Unicode

Các biểu thức chính quy được mô-đun re hỗ trợ có thể được cung cấp dưới dạng byte hoặc chuỗi. Một số chuỗi ký tự đặc biệt như \d\w có ý nghĩa khác nhau tùy thuộc vào việc mẫu được cung cấp dưới dạng byte hay chuỗi. Ví dụ: \d sẽ khớp với các ký tự [0-9] theo byte nhưng trong chuỗi sẽ khớp với bất kỳ ký tự nào trong danh mục 'Nd'.

Chuỗi trong ví dụ này có số 57 được viết bằng cả chữ số Thái và chữ số Ả Rập:

nhập lại
p = re.compile(r'\d+')

s = "Hơn \u0e55\u0e57 57 hương vị"
m = p.search(s)
print(repr(m.group()))

Khi được thực thi, \d+ sẽ khớp với các chữ số Thái Lan và in chúng ra. Nếu bạn cung cấp cờ re.ASCII cho compile() thì thay vào đó, \d+ sẽ khớp với chuỗi con "57".

Tương tự, \w khớp với nhiều loại ký tự Unicode nhưng chỉ [a-zA-Z0-9_] tính bằng byte hoặc nếu re.ASCII được cung cấp và \s sẽ khớp với các ký tự khoảng trắng Unicode hoặc [ \t\n\r\f\v].

Tài liệu tham khảo

Một số cuộc thảo luận thay thế hay về hỗ trợ Unicode của Python là:

Loại str được mô tả trong tài liệu tham khảo thư viện Python tại Loại chuỗi văn bản --- str.

Tài liệu dành cho mô-đun unicodedata.

Tài liệu dành cho mô-đun codecs.

Marc-André Lemburg đã trình bày a presentation titled "Python and Unicode" (PDF slides) tại EuroPython 2002. Các slide này là một cái nhìn tổng quan tuyệt vời về thiết kế các tính năng Unicode của Python 2 (trong đó loại chuỗi Unicode được gọi là unicode và các chữ bắt đầu bằng u).

Đọc và ghi dữ liệu Unicode

Khi bạn đã viết xong một số mã hoạt động với dữ liệu Unicode, vấn đề tiếp theo là đầu vào/đầu ra. Làm cách nào để đưa chuỗi Unicode vào chương trình của bạn và làm cách nào để chuyển đổi Unicode thành dạng phù hợp để lưu trữ hoặc truyền tải?

Có thể bạn không cần phải làm gì tùy thuộc vào nguồn đầu vào và đích đầu ra của mình; bạn nên kiểm tra xem các thư viện được sử dụng trong ứng dụng của bạn có hỗ trợ Unicode nguyên bản hay không. Ví dụ: trình phân tích cú pháp XML thường trả về dữ liệu Unicode. Nhiều cơ sở dữ liệu quan hệ cũng hỗ trợ các cột có giá trị Unicode và có thể trả về giá trị Unicode từ truy vấn SQL.

Dữ liệu Unicode thường được chuyển đổi sang một mã hóa cụ thể trước khi nó được ghi vào đĩa hoặc gửi qua ổ cắm. Bạn có thể tự mình thực hiện tất cả công việc: mở một tệp, đọc đối tượng byte 8 bit từ tệp đó và chuyển đổi các byte bằng bytes.decode(encoding). Tuy nhiên, cách tiếp cận thủ công không được khuyến khích.

Một vấn đề là bản chất nhiều byte của mã hóa; một ký tự Unicode có thể được biểu diễn bằng nhiều byte. Nếu bạn muốn đọc tệp theo từng đoạn có kích thước tùy ý (ví dụ: 1024 hoặc 4096 byte), bạn cần viết mã xử lý lỗi để phát hiện trường hợp chỉ một phần byte mã hóa một ký tự Unicode duy nhất được đọc ở cuối đoạn. Một giải pháp là đọc toàn bộ tệp vào bộ nhớ rồi thực hiện giải mã, nhưng điều đó khiến bạn không thể làm việc với các tệp cực lớn; nếu bạn cần đọc tệp 2 GiB, bạn cần 2 GiB RAM. (Thực sự hơn thế, vì trong ít nhất một lúc, bạn cần có cả chuỗi được mã hóa và phiên bản Unicode của nó trong bộ nhớ.)

Giải pháp là sử dụng giao diện giải mã mức thấp để xử lý trường hợp chuỗi mã hóa một phần. Công việc triển khai điều này đã được thực hiện cho bạn: hàm open() tích hợp có thể trả về một đối tượng giống như tệp giả sử nội dung của tệp ở dạng mã hóa được chỉ định và chấp nhận các tham số Unicode cho các phương thức như read()write(). Điều này hoạt động thông qua các tham số encodingerrors của open() được diễn giải giống như các tham số trong str.encode()bytes.decode().

Do đó, việc đọc Unicode từ một tệp rất đơn giản

với open('unicode.txt',coding='utf-8')  f:
    cho dòng trong f:
        in(repr(dòng))

Cũng có thể mở tệp ở chế độ cập nhật, cho phép cả đọc và ghi

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

Ký tự Unicode U+FEFF được sử dụng làm dấu thứ tự byte (BOM) và thường được viết dưới dạng ký tự đầu tiên của tệp để hỗ trợ tự động phát hiện thứ tự byte của tệp. Một số mã hóa, chẳng hạn như UTF-16, yêu cầu BOM xuất hiện ở đầu tệp; khi sử dụng mã hóa như vậy, BOM sẽ tự động được ghi dưới dạng ký tự đầu tiên và sẽ bị xóa âm thầm khi tệp được đọc. Có các biến thể của các mã hóa này, chẳng hạn như 'utf-16-le' và 'utf-16-be' dành cho các mã hóa endian nhỏ và endian lớn, chỉ định một thứ tự byte cụ thể và không bỏ qua BOM.

Ở một số khu vực, người ta cũng có quy ước sử dụng "BOM" khi bắt đầu các tệp được mã hóa UTF-8; tên này gây hiểu nhầm vì UTF-8 không phụ thuộc vào thứ tự byte. Dấu này chỉ thông báo rằng tệp được mã hóa bằng UTF-8. Để đọc những tệp như vậy, hãy sử dụng codec 'utf-8-sig' để tự động bỏ qua dấu nếu có.

Tên tệp Unicode

Hầu hết các hệ điều hành được sử dụng phổ biến hiện nay đều hỗ trợ tên tệp chứa các ký tự Unicode tùy ý. Thông thường, điều này được thực hiện bằng cách chuyển đổi chuỗi Unicode thành một số mã hóa khác nhau tùy thuộc vào hệ thống. Ngày nay Python đang chuyển sang sử dụng UTF-8: Python trên MacOS đã sử dụng UTF-8 cho một số phiên bản và Python 3.6 cũng đã chuyển sang sử dụng UTF-8 trên Windows. Trên hệ thống Unix sẽ chỉ có filesystem encoding. nếu bạn đã đặt các biến môi trường LANG hoặc LC_CTYPE; nếu chưa, mã hóa mặc định lại là UTF-8.

Hàm sys.getfilesystemencoding() trả về mã hóa để sử dụng trên hệ thống hiện tại của bạn, trong trường hợp bạn muốn thực hiện mã hóa theo cách thủ công nhưng không có nhiều lý do để bận tâm. Khi mở tệp để đọc hoặc ghi, thông thường bạn chỉ cần cung cấp chuỗi Unicode làm tên tệp và nó sẽ tự động được chuyển đổi sang mã hóa phù hợp cho bạn:

tên tệp = 'tên tệp\u4500abc'
với open(filename, 'w')  f:
    f.write('blah\n')

Các chức năng trong mô-đun os như os.stat() cũng sẽ chấp nhận tên tệp Unicode.

Hàm os.listdir() trả về tên tệp, điều này đặt ra một vấn đề: hàm này có nên trả về phiên bản Unicode của tên tệp hay trả về byte chứa phiên bản được mã hóa? os.listdir() có thể thực hiện cả hai, tùy thuộc vào việc bạn cung cấp đường dẫn thư mục dưới dạng byte hay chuỗi Unicode. Nếu bạn truyền một chuỗi Unicode làm đường dẫn, tên tệp sẽ được giải mã bằng cách sử dụng mã hóa của hệ thống tệp và một danh sách các chuỗi Unicode sẽ được trả về, trong khi truyền một đường dẫn byte sẽ trả về tên tệp dưới dạng byte. Ví dụ: giả sử filesystem encoding mặc định là UTF-8, chạy chương trình sau:

fn = 'tên tệp\u4500abc'
f = mở(fn, 'w')
f.close()

hệ điều hành nhập khẩu
print(os.listdir(b'.'))
print(os.listdir('.'))

sẽ tạo ra đầu ra sau:

$ python listdir-test.py
[b'tên tệp\xe4\x94\x80abc', ...]
['tên tệp\u4500abc', ...]

Danh sách đầu tiên chứa tên tệp được mã hóa UTF-8 và danh sách thứ hai chứa các phiên bản Unicode.

Lưu ý rằng trong hầu hết các trường hợp, bạn chỉ nên tiếp tục sử dụng Unicode với các API này. API byte chỉ nên được sử dụng trên các hệ thống có thể có tên tệp không thể giải mã được; bây giờ hầu như chỉ có các hệ thống Unix.

Mẹo viết chương trình nhận biết Unicode

Phần này đưa ra một số gợi ý về cách viết phần mềm xử lý Unicode.

Mẹo quan trọng nhất là:

Phần mềm chỉ nên hoạt động với các chuỗi Unicode bên trong, giải mã dữ liệu đầu vào càng sớm càng tốt và chỉ mã hóa đầu ra ở cuối.

Nếu bạn cố gắng viết các hàm xử lý chấp nhận cả chuỗi Unicode và chuỗi byte, bạn sẽ thấy chương trình của mình dễ bị lỗi khi bạn kết hợp hai loại chuỗi khác nhau. Không có mã hóa hoặc giải mã tự động: nếu bạn thực hiện, ví dụ: str + bytes, TypeError sẽ được nâng lên.

Khi sử dụng dữ liệu đến từ trình duyệt web hoặc một số nguồn không đáng tin cậy khác, một kỹ thuật phổ biến là kiểm tra các ký tự không hợp lệ trong chuỗi trước khi sử dụng chuỗi đó trong dòng lệnh được tạo hoặc lưu trữ chuỗi đó vào cơ sở dữ liệu. Nếu bạn đang thực hiện việc này, hãy cẩn thận kiểm tra chuỗi đã giải mã chứ không phải dữ liệu byte được mã hóa; một số mã hóa có thể có các thuộc tính thú vị, chẳng hạn như không có tính phỏng đoán hoặc không tương thích hoàn toàn với ASCII. Điều này đặc biệt đúng nếu dữ liệu đầu vào cũng chỉ định mã hóa, vì khi đó kẻ tấn công có thể chọn một cách thông minh để ẩn văn bản độc hại trong dòng byte được mã hóa.

Chuyển đổi giữa các mã hóa tệp

Lớp StreamRecoder có thể chuyển đổi một cách trong suốt giữa các mã hóa, lấy luồng trả về dữ liệu ở dạng mã hóa #1 và hoạt động giống như một luồng trả về dữ liệu ở dạng mã hóa #2.

Ví dụ: nếu bạn có tệp đầu vào f bằng tiếng Latin-1, bạn có thể gói nó bằng StreamRecoder để trả về các byte được mã hóa trong UTF-8:

new_f = codecs.StreamRecoding(f,
    # en/decode: được sử dụng bởi read() để mã hóa kết quả của nó và
    # by write() để giải mã đầu vào của nó.
    codecs.getencode('utf-8'), codecs.getdecode('utf-8'),

    # reader/writer: dùng để đọc và ghi vào luồng.
    codecs.getreader('latin-1'), codecs.getwriter('latin-1') )

Các tệp có Mã hóa không xác định

Bạn có thể làm gì nếu cần thay đổi một tệp nhưng không biết mã hóa của tệp? Nếu bạn biết mã hóa tương thích với ASCII và chỉ muốn kiểm tra hoặc sửa đổi các phần ASCII, bạn có thể mở tệp bằng trình xử lý lỗi surrogateescape:

với open(fname, 'r',coding="ascii",error="surrogateescape")  f:
    dữ liệu = f.read()

# make thay đổi chuỗi 'data'

với open(fname + '.new', 'w',
           hóa="ascii", lỗi = "surrogateescape") dưới dạng f:
    f.write(dữ liệu)

Trình xử lý lỗi surrogateescape sẽ giải mã mọi byte không phải ASCII dưới dạng điểm mã trong một phạm vi đặc biệt chạy từ U+DC80 đến U+DCFF. Sau đó, các điểm mã này sẽ chuyển trở lại thành các byte giống nhau khi trình xử lý lỗi surrogateescape được sử dụng để mã hóa dữ liệu và ghi lại dữ liệu.

Tài liệu tham khảo

Một phần của Mastering Python 3 Input/Output, bài nói chuyện PyCon 2010 của David Beazley, thảo luận về xử lý văn bản và xử lý dữ liệu nhị phân.

Zz000zz thảo luận các câu hỏi về mã hóa ký tự cũng như cách quốc tế hóa và bản địa hóa một ứng dụng. Những trang trình bày này chỉ đề cập đến Python 2.x.

The Guts of Unicode in Python là bài nói chuyện PyCon 2013 của Benjamin Peterson thảo luận về cách biểu diễn Unicode nội bộ trong Python 3.3.

Lời cảm ơn

Bản thảo đầu tiên của tài liệu này được viết bởi Andrew Kuchling. Kể từ đó, nó đã được sửa đổi thêm bởi Alexander Belopolsky, Georg Brandl, Andrew Kuchling và Ezio Melotti.

Xin cảm ơn những người sau đã phát hiện lỗi hoặc đưa ra đề xuất cho bài viết này: É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.