Note: see the link below for the English version of this article.
https://duongnt.com/codecs-register-error
Một số người nghĩ rằng việc thực hiện encode/decode trong Python là đơn giản và không quan trọng. Nhưng đó là do họ chưa gặp phải những tình huống trúc trắc. Thật may là Python có hỗ trợ Unicode đầy đủ. Và nhờ vào hàm codecs.register_error
, chúng ta có thể tùy ý thay đổi cách xử lý lỗi.
Nhắc lại về encoding trong Python
Một số khái niệm cơ bản
Trong Python 3, khái niệm string và byte có nhiều thay đổi lớn. Chúng ta cần nắm được một số khái niệm dưới đây.
- Character: là khái niệm trừu tượng, chỉ một ký hiệu trong một hệ thống chữ viết.
- Code point: một con số tương ứng với một character nhất định trong một hệ quy chuẩn. Thông thường ta dùng chuẩn Unicode.
- Byte: là chuỗi byte tương ứng với một code point trong một hệ thống encode nhất định.
- Encode: quá trình chuyển từ code point sang byte.
- Decode: quá trình chuyển ngược từ byte về lại code point.
Dưới đây là một số ví dụ.
Character | Code point | Byte (ASCII) | Byte (UTF-8) |
---|---|---|---|
1 | U+0031 | \x31 | \x31 |
a | U+0061 | \x61 | \x61 |
μ | U+03BC | Không hỗ trợ | \xce\xbc |
ụ | U+1EE5 | Không hỗ trợ | \xe1\xbb\xa5 |
Trong các ví dụ trên, ta thấy rằng một số code point có thể được encode bằng UTF-8 nhưng lại không thể encode bằng ASCII
. Đó là do ASCII
chỉ dùng 7 bit để encode mỗi ký tự, trong khi UTF-8 có thể dùng đến 4 byte cho mỗi ký tự.
Chúng ta không thể encode μ
bằng ASCII
. Và chúng ta cũng không thể decode chuỗi byte UTF-8 của μ
bằng ASCII
.
'μ'.encode('ASCII')
# Throws: UnicodeEncodeError: 'ascii' codec can't encode character '\u03bc' in position 0: ordinal not in range(128)
data = 'μ'.encode('UTF-8')
data.decode('ASCII')
# Throws: UnicodeDecodeError: 'ascii' codec can't decode byte 0xce in position 0: ordinal not in range(128)
Handler xử lý lỗi built-in
Cả hàm str.encode và hàm bytes.decode đều hỗ trợ một tham số với tên gọi errors
. Nếu ta truyền tên của một handler vào tham số này, runtime sẽ sử dụng handler đó để xỷ lý các lỗi nó gặp phải. Dưới đây là các handler mặc định.
utf8_bytes = 'Hà Nội'.encode(encoding='utf-8')
utf8_bytes.decode(encoding='ascii', errors='strict')
# UnicodeDecodeError: 'ascii' codec can't decode byte 0xc3 in position 1: ordinal not in range(128)
utf8_bytes.decode(encoding='ascii', errors='ignore')
# 'H Ni'
utf8_bytes.decode(encoding='ascii', errors='replace')
# 'H�� N���i'
utf8_bytes.decode(encoding='ascii', errors='backslashreplace')
# 'H\\xc3\\xa0 N\\xe1\\xbb\\x99i'
utf8_bytes.decode(encoding='ascii', errors='surrogateescape')
# 'H\udcc3\udca0 N\udce1\udcbb\udc99i'
Tự tạo handler để xử lý lỗi
Trong phần lớn các trường hợp, các handler mặc định là đủ dùng. Nhưng nếu ta muốn sử dụng logic của riêng mình để xử lý lỗi thì ta có thể tự tạo handler.
Dùng hàm register_error để đăng ký handler mới
Đầu tiên, ta phải thêm handler của mình vào danh sách trong module codecs bằng hàm register_error
.
def our_handler(e):
# logic xử lý lỗi của ta
# codecs.register_error('<tên để gọi>', <tên handler>)
codecs.register_error('our_handler_name', our_handler)
Sau đó, ta có thể truyền cái tên our_handler_name
vào tham số errors
như khi dùng handler sẵn có.
utf8_bytes.decode(encoding='ascii', errors='our_handler_name')
Cách tạo handler mới
Một handler phải thỏa mãn được 2 điều kiện dưới đây.
- Nhận vào một tham số. Khi handler xử lý lỗi encode, tham số này sẽ có kiểu
UnicodeEncodeError
. Còn khi handler xử lý lỗi decode, tham số có kiểuUnicodeDecodeError
. Dưới đây là các trường trong error.encoding
: chuẩn encoding đang được sử dụng.object
: làstring
đang được encode, hoặcbytes
đang được decode.start
: index tại điểm đầu tiên ta gặp lỗi.end
: index tại điểm cuối cùng ta gặp lỗi.reason
: thông báo nguyên nhân lỗi.
- Trả về một tuple với 2 phần tử
- Phần tử đầu tiên là giá trị để thay thế cho ký tự ta không thể encode, hoặc byte ta không thể decode.
- Phần tử thứ 2 là index để tiếp tục encode hay decode.
Ví dụ minh họa
Chúng ta sẽ thử tạo handler để thay thế ký tự lỗi bằng ký tự *
. Ta cũng sẽ ghi lại thông tin chi tiết về lỗi.
def our_handler(e):
if e is UnicodeEncodeError:
print('Encounter an error while encoding a character')
else: # e is UnicodeDecodeError
print('Encounter an error while decoding a byte')
print(f'encoding: {e.encoding}')
print(f'object: {e.object}')
print(f'start: {e.start}')
print(f'reason: {e.reason}')
return "*", e.end # tiếp tục encode/decode sau vị trí gặp lỗi
Xử lý lỗi khi encode
Ta sẽ encode dòng chữ Hà Nội
bằng chuẩn ASCII
và xử lý lỗi với our_handler
.
print('Hà Nội'.encode('ascii', errors='our_handler_name'))
Kết quả là như sau.
Encounter an error while decoding a character
encoding: ascii
object: Hà Nội
start: 1
reason: ordinal not in range(128)
Encounter an error while decoding a character
encoding: ascii
object: Hà Nội
start: 4
reason: ordinal not in range(128)
b'H* N*i'
Có thể thấy là hàm our_handler
đã phát hiện ra 2 vị trí mà ASCII
không hỗ trợ. Đó là index 1 (à)
và index 4 (ộ)
. Ta đã thay thế cả hai bằng ký tự *
.
Xử lý lỗi khi decode
Tương tự trên, ta sẽ decode biến utf8_bytes
đã tạo ở phần trước bằng ASCII
và our_handler
.
utf8_bytes.decode(encoding='ascii', errors='our_handler_name')
Dưới đây là kết quả.
Encounter an error while decoding a byte
encoding: ascii
object: b'H\xc3\xa0 N\xe1\xbb\x99i'
start: 1
reason: ordinal not in range(128)
Encounter an error while decoding a byte
encoding: ascii
object: b'H\xc3\xa0 N\xe1\xbb\x99i'
start: 2
reason: ordinal not in range(128)
Encounter an error while decoding a byte
encoding: ascii
object: b'H\xc3\xa0 N\xe1\xbb\x99i'
start: 5
reason: ordinal not in range(128)
Encounter an error while decoding a byte
encoding: ascii
object: b'H\xc3\xa0 N\xe1\xbb\x99i'
start: 6
reason: ordinal not in range(128)
Encounter an error while decoding a byte
encoding: ascii
object: b'H\xc3\xa0 N\xe1\xbb\x99i'
start: 7
reason: ordinal not in range(128)
H** N***i
Một điều đáng chú ý ở đây là ký tự a
được chuyển thành **
, còn ộ
được chuyển thành ***
. Nguyên nhân vì utf-8
là chuẩn encode với độ dài không cố định. Ký tự a
được encode bằng 2 byte (\xc3\xa0
), còn ộ
được encode bằng 3 byte (\xe1\xbb\x99
). Mỗi khi ta gặp phải một trong năm byte trên, ASCII
sẽ bị lỗi, và runtime sẽ gọi hàm our_handler
.
Kết thúc
Nếu ta chỉ cần làm việc với file ASCII
thì vấn đề encode trong Python là đơn giản. Nhưng trong phần lớn các trường hợp, chúng ta sẽ phải sử dụng Unicode. Khi đó, hiểu biết về cách tự viết handler để xử lý lỗi sẽ có ích cho chúng ta.