Note: see the link below for the English version of this article.

https://duongnt.com/codecs-register-error

Xử lý lỗi codecs với 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ểu UnicodeDecodeError. 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ặc bytes đ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 ASCIIour_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.

A software developer from Vietnam and is currently living in Japan.

Leave a Reply