Hướng dẫn unicode trong python

Hướng dẫn unicode trong python

Đã đăng vào thg 7 7, 2018 6:40 SA 4 phút đọc

1. Đặt vấn đề

Làm việc với Python 2 các bạn đã bao giờ gặp những trường hợp như thế này chưa?

UnicodeEncodeError: 'ascii' codec can't encode character u'\xa0' in position 20: ordinal not in range(128)

hoặc print, bắn log ra toàn ra ký tự này chưa

WARNING:root:��{�ꕶ�������e�X�g/

và khi đi tìm solution thì bị tẩu hỏa nhập ma với các khái niệm decode, encode và khi nào dùng chúng, các loại bảng mã Unicode, UTF-8, SHIFT-JIS... loằng xì ngoằng. Mình sẽ giúp các bạn làm sáng tỏ một số vấn đề trên

2. Các khái niệm

a, Bảng mã Unicode, UTF-8, SHIFT-JIS... là gì?

  • Bảng mã Unicode là bảng mã chứa gần như toàn bộ các kí tự của hầu hết các ngôn ngữ trên toàn cầu. Có nghĩa là gần như bất cứ ký tự nào đều mã hóa được qua bảng mã Unicode. Và mỗi một ký tự sẽ tương ứng với một một byte mã hóa. Trong python, ký tự mã hóa Unicode được bắt đầu bằng chữ \u. Ví dụ chữ Tiếng việtđược mã hóa là 'Ti\u1ebfng vi\u1ec7t'
  • Bảng mã UTF-8 là bảng mã hóa miêu tả bảng mã Unicode cho máy tính hiểu
  • SHIFT-JIS là bảng mã được sử dụng ở gần như toàn bộ các máy tính tại Nhật, được JIS đưa ra. Nó cũng có tác dụng tương tự như bảng mã UTF-8 là miêu tả ký tự cho máy tính hiểu. Vậy Unicode là một Character Set(còn nhiều loại Character Set khác), một bộ tập hợp các ký tự cho con người sử dụng, còn UTF-8, SHIFT-JIS, ASCII... là những Character Encoding, chúng mã hóa các ký tự thành các con số để giúp máy tính hiểu được.

b, String, decode, encode trong python 2

  • String trong python cũng là một object và được chia ra làm 2 loại(type): strunicode. Một chuỗi là kiểu unicode thì sẽ có chữ u đứng trước khi print. VD: u'Tiếng việt'. Mặc định trong python 2 thì mọi chuỗi sẽ đều được đưa về kiểu str
  • Decode và Encode là cách chuyển đổi qua lại giữa strunicode
    Hướng dẫn unicode trong python

3. Các vấn đề xảy ra khi làm việc với Unicode

Thông thường khi code hoặc tương tác với các dữ liệu có nội dung là tiếng anh, chỉ chứa các ký tự latinh thông thường thì gần như chẳng gặp vấn đề gì, chỉ khi tương tác với các ký tự đặc biệt, các ký tự có dấu thì chúng ta mới thấy Unicode trong python 2 rất khó chịu.

1, Sử dụng Unicode trong code

Các bạn thử viết đoạn mã sau vào file và chạy chúng:

print "Xin chào Việt Nam"

các bạn sẽ gặp ngay lỗi sau

Hướng dẫn unicode trong python
để sửa lỗi trên, ta sẽ thêm dòng # encoding: utf-8 vào dòng đầu tiên của file để mã hóa toàn bộ string trong file đó bằng utf-8

2, Độ dài của chuỗi

Xem ví dụ sau:

Hướng dẫn unicode trong python

Tại sao khi thêm chữ u vào phía trước thì nó còn 10 ký tự. Vì trong python 2 mặc định chuỗi sẽ được để là kiểu str và dùng bảng mã UTF-8 để mã hóa. Hãy xem thực sự chữ "Tiếng Việt"u"Tiếng Việt" được biểu diễn như thế nào:

Hướng dẫn unicode trong python

Hàm len sẽ đếm số byte để biểu diễn chuỗi đó, "Tiếng Việt" sử dụng 14 byte để biểu diễn còn u"Tiếng Việt" chỉ sử dụng 10 byte. Do đó, để làm việc với chuỗi trong python thì nên convert chúng về kiểu Unicode để tránh trường hợp như validate user_name 10 ký tự, người ta để là "Tiếng Việt" đủ 10 ký tự mà cứ báo lỗi dài quá

Hướng dẫn unicode trong python

P/S: Mình tạm dừng ở đây và sẽ quay trở lại ở phần tiếp theo

All rights reserved

Ví dụ sau minh họa 3 codecs bạn hay gặp nhất trong Python:

>>> s = "sky down no enemy"
>>> print type(s), len(s)
 17
>>> s = "thiên hạ vô địch"
>>> s
'thi\xc3\xaan h\xe1\xba\xa1 v\xc3\xb4 \xc4\x91\xe1\xbb\x8bch'
>>> print type(s), len(s)
 23
>>> s = unicode("thiên hạ vô địch", 'utf8')
>>> s
u'thi\xean h\u1ea1 v\xf4 \u0111\u1ecbch'
>>> print type(s), len(s)
 16

Nghiên cứu ví dụ trên. len(s) trong hai ví dụ đầu thực sự là số byte cần để lưu trữ s. len(s) trong ví dụ 3 là độ dài ký tự, dung lượng nhớ thực sự để lưu trữ s là 16 * 2 = 32 bytes.

Tại bất cứ một thời điểm nào trong chương trình Python bạn cũng phải xác định được bạn đang thao tác với chuỗi kiểu gì: ascii, unicode, hay utf8... Dùng lệnh sau để biết một chuỗi là unicode hay không:

>>> s
u'thi\xean h\u1ea1 v\xf4 \u0111\u1ecbch'
>>> type(s) == type(u"")
True
>>> ss = "sky down no enemy"
>>> type(ss) == type(u"")
False
>>> s = "thiên hạ vô địch"
>>> type(ss) == type(u"")
False

Cộng một chuỗi không phải unicode với một chuỗi unicode thì chuỗi không phải unicode sẽ được tự động convert sang unicode trước khi cộng:

>>> ss = "sky down no enemy"
>>> ss + u""
u'sky down no enemy'
>>> ss = "thiên hạ vô địch"
>>> ss + u""
Traceback (most recent call last):
  File "", line 1, in 
UnicodeDecodeError: 'ascii' codec can't decode byte 0xc3 in position 3: ordinal not in range(128)
>>> ss = unicode("thiên hạ vô địch", 'utf8')
>>> ss + u""
u'thi\xean h\u1ea1 v\xf4 \u0111\u1ecbch'

Trong tình huống thứ 2 bạn cố gắng cộng một chuỗi utf8 vào chuỗi unicode. Python tự động chuyển chuỗi utf8 thành chuỗi unicode bằng cách decode với codec='ascii', vì vậy gặp lỗi. Tình huống này tương đương với:

ss + u"" = ss.decode('ascii') + u""
>>> ss = "thiên hạ vô địch"
>>> ss.decode('ascii') + u""
Traceback (most recent call last):
  File "", line 1, in 
UnicodeDecodeError: 'ascii' codec can't decode byte 0xc3 in position 3: ordinal not in range(128)

Hai cách để cộng đúng là:

hoặc

unicode(ss, 'utf8') + u""

Các thao tác với chuỗi khác như join, split, find... cũng tương tự. Nghĩa là nếu có một tham số unicode thì các tham số còn lại được tự động chuyển sang unicode trước khi thực hiện thao tác. Ví dụ:

>>> ss = "sky down no enemy"
>>> ss.split(u" ")
[u'sky', u'down', u'no', u'enemy']
>>> l = ["sky", "down", "no", "enemy"]
>>> unichr(32).join(l)
u'sky down no enemy'

Tương tự thao tác sau sẽ gây lỗi:

>>> s = "thiên hạ vô địch"
>>> s.split(u" ")
Traceback (most recent call last):
  File "", line 1, in 
UnicodeDecodeError: 'ascii' codec can't decode byte 0xc3 in position 3: ordinal not in range(128)

Để chuyển một chuỗi tiếng Việt utf sang dạng viết hoa, bạn luôn luôn phải chuyển nó về unicode trước. Nghiên cứu ví dụ sau:

>>> s = "thiên hạ vô địch"
>>> s.upper()
'THI\xc3\xaaN H\xe1\xba\xa1 V\xc3\xb4 \xc4\x91\xe1\xbb\x8bCH'
>>> s
'thi\xc3\xaan h\xe1\xba\xa1 v\xc3\xb4 \xc4\x91\xe1\xbb\x8bch'
<>

So sánh với s ta thấy s.upper() không upper case được các ký tự ê, ạ, ô, đ, ị...

Để upper một chuỗi utf bạn phải chuyển nó về unicode trước, upper xong thì chuyển ngược lại. Hàm Upper sau nhận đầu vào là chuỗi utf, upper nó và trả về utf khác đã được upper, hàm này thích hợp cho mục đích upper tiếng Việt:

# -*- coding: utf-8 -*-

def Upper(s):
    if type(s) == type(u""):
        return s.upper()
    return unicode(s, "utf8").upper().encode("utf8")

if __name__ == "__main__":
    s = "thiên hạ vô địch"
    us = s.decode('utf8')

    print s.upper()
    print Upper(s)
    print Upper(us)

Kết quả:

THIêN Hạ Vô địCH
THIÊN HẠ VÔ ĐỊCH
THIÊN HẠ VÔ ĐỊCH

Đầu vào thích hợp của Upper là ascii, utf8, unicode, các dạng khác có thể sai: latin1, tcvn... Tương tự như vậy cho hàm lower, và capwords. Xem xét ví sau với capwords:

# -*- coding: utf-8 -*-

import string

s = "thiên hạ vô địch"

print string.capwords(s)
print string.capwords(s.decode('utf8'))

Kết quả:

Thiên Hạ Vô địch
Thiên Hạ Vô Địch

Các tên file hoặc tên thư mục là tiếng Việt có dấu cũng đòi hỏi cách xử lý đặc biệt. Giả sử bạn có thư mục abc với một file duy nhất tên là: tiếng việt.txt. Thư mục abc đặt trong thư mục cá nhân của bạn. Nghiên cứu đoạn chương trình sau:

# -*- coding: utf-8 -*-

import os

path = os.path.join(os.path.expanduser("~"), "abc")
files = [os.path.join(path, basename) for basename in os.listdir(path)]

print map(os.path.exists, files)

Chú ý:

  • Trên Windows path = "C:\Documents and Settings\YourAccountName\abc"
  • Trên Linux path = "/home/YourAccountName/abc"

Vì chỉ có duy nhất một file tiếng việt.txt trong thư mục abc nên chúng ta mong đợi kết quả in ra là: [True]. Tuy nhiên chỉ Linux cho câu trả lời này. Windows thì không. Không tin bạn thử xem. Chương trình trên cho dù đã được viết với việc tận dụng thư viện sẵn có os nhằm nâng cao tính khả chuyển nhưng vẫn không khả chuyển, khi tên file là tiếng Việt có dấu.

Giải quyết vấn đề này rất đơn giản bạn chỉ cần thay "abc" thành u"abc" để tất cả các đường dẫn tên file bị ép chuyển sang unicode là OK. Bạn làm như sau:

path = os.path.join(os.path.expanduser("~"), u"abc")

hoặc

path = os.path.join(os.path.expanduser(u"~"), "abc")

Các vấn đề tương tự cũng áp dụng cho hàm glob hoặc walk, Các hàm này cần các đường dẫn là chuỗi unicode để có thể lấy chính xác các file có tên tiếng Việt.

Loại bỏ dấu tiếng Việt. Trong nhiều trường hợp bạn cần loại bỏ dấu của một chuỗi tiếng Việt có dấu. Chẳng hạn chuyển tiếng việt thành tieng viet. Đây là một cách thức đơn giản giải quyết vấn đề này:

# -*- coding: utf-8 -*-

import string
import re

INTAB = "ạảãàáâậầấẩẫăắằặẳẵóòọõỏôộổỗồốơờớợởỡéèẻẹẽêếềệểễúùụủũưựữửừứíìịỉĩýỳỷỵỹđ"
INTAB = [ch.encode('utf8') for ch in unicode(INTAB, 'utf8')]

OUTTAB = "a"*17 + "o"*17 + "e"*11 + "u"*11 + "i"*5 + "y"*5 + "d"

r = re.compile("|".join(INTAB))
replaces_dict = dict(zip(INTAB, OUTTAB))

def khongdau(utf8_str):
    return r.sub(lambda m: replaces_dict[m.group(0)], utf8_str)

print khongdau("thiên hạ vô địch")
print khongdau("sky down no enemy")
print khongdau("THIÊN HẠ VÔ ĐỊCH")

Kết quả:

thien ha vo dich
sky down no enemy
THIÊN HẠ VÔ ĐỊCH

Test thứ ba cho thấy nó chưa làm việc với chữ hoa. Một chút cải tiến nhỏ để nó làm việc với chữ hoa các bạn có thể thêm vào dễ dàng. Chương trình này chưa được test kỹ về sự chính xác và về performance. Các bạn tự test và công bố kết quả nhé.

Slice với chuỗi tiếng việt dạng utf8 có thể gặp các vấn đề. Thao tác tương tự khi bạn đọc các khối dữ liệu utf8 với kích thước quy định trước từ file utf8. Xem xét các ví dụ sau:

>>> s = 'thiên hạ vô địch'
>>> s
'thi\xc3\xaan h\xe1\xba\xa1 v\xc3\xb4 \xc4\x91\xe1\xbb\x8bch'
>>> s[7:17]
'h\xe1\xba\xa1 v\xc3\xb4 \xc4'

chuỗi s[7:17] là chuỗi què. byte cuối cùng của chuỗi này \xc4 mới là một nửa của chữ cái đ ('\xc4\x91'), vì vậy mọi thao tác của bạn trên chuỗi này có tiềm năng gặp lỗi. chẳng hạn:

>>> unicode(s[7:17], 'utf8')
Traceback (most recent call last):
  File "", line 1, in 
  File "/usr/lib/python2.6/encodings/utf_8.py", line 16, in decode
    return codecs.utf_8_decode(input, errors, True)
UnicodeDecodeError: 'utf8' codec can't decode byte 0xc4 in position 9: unexpected end of data
>>> unicode(s[7:16], 'utf8')
u'h\u1ea1 v\xf4 '

Unicode áp dụng cho s[7:16] thì vô tư vì nó không bị què.

Việc đọc các file text utf8 cũng gặp tình huống tương tự. Chẳng hạn thao tác sau tiềm năng gặp lỗi:

f = open("file name", "r")
s = f.read(1000)
...
s.close()

Trong tình huống này bạn cố gắng đọc 1000 byte đầu tiên của file, s có thể là chuỗi tiếng Việt bị què như tình huống ở trên.

Đọc ghi file dữ liệu tiếng Việt. File chứa dữ liệu dạng văn bản tiếng Việt thường được ghi dưới dạng unicode hoặc utf8. Đoạn chương trình sau đọc nội dung utf8:

ff = open("anyfile", 'r')
content = ff.read()
ff.close()

Dữ liệu tiếng Việt lưu dưới dạng utf8 thường có BOM_UTF8 (= "\xef\xbb\xbf") ở đầu file. Bạn phải loại bỏ cái này trước khi có thể làm cái gì đó. Đoạn chương trình sau làm việc này:

import codecs

content = open("anyfile", 'r').read()
if content.startswith(codecs.BOM_UTF8):
    content = content[3:]

Chú ý rằng đoạn chương trình trên không thích hợp cho việc xử lý các file lớn (hơn 200MB). Với các file lớn bạn cần chia nhỏ thành các file nhỏ hơn.

codecs là thư viện chứa rất nhiều BOM.

>>> dir(codecs)
['BOM', 'BOM32_BE', 'BOM32_LE', 'BOM64_BE', 'BOM64_LE', 'BOM_BE', 'BOM_LE', 'BOM_UTF16', 'BOM_UTF16_BE', 'BOM_UTF16_LE', 'BOM_UTF32',
'BOM_UTF32_BE', 'BOM_UTF32_LE', 'BOM_UTF8', ...]

Dùng thư viện codecs bạn có thể nhanh chóng đọc nội dung file mà không mất nhiều công biến đổi encoding. Ví dụ:

# đọc toàn bộ nội dung của file vào content,
# nội dung của file được biết trước như là utf8,
# content sẽ là nội dung unicode.
content = codecs.open('your file name', 'r', 'utf8').read()
# mặc định đọc toàn bộ nội dung của file vào content
# (dạng mặc định là utf8)
content = codecs.open('your file name', 'r').read()

Ghi dữ liệu tiếng Việt ra file:

ff = open("filename", 'w').write(content)

Ở đây content là chuỗi utf8, nếu bạn đưa vào content là dạng unicode, nó sẽ được tự động chuyển về dạng utf8 trước khi được ghi ra file.