phương pháp nhanh hơn và không cần cài thêm package
code hoàn chỉnh:
import unicodedata
BANG_XOA_DAU = str.maketrans(
"ÁÀẢÃẠĂẮẰẲẴẶÂẤẦẨẪẬĐÈÉẺẼẸÊẾỀỂỄỆÍÌỈĨỊÓÒỎÕỌÔỐỒỔỖỘƠỚỜỞỠỢÚÙỦŨỤƯỨỪỬỮỰÝỲỶỸỴáàảãạăắằẳẵặâấầẩẫậđèéẻẽẹêếềểễệíìỉĩịóòỏõọôốồổỗộơớờởỡợúùủũụưứừửữựýỳỷỹỵ",
"A"*17 + "D" + "E"*11 + "I"*5 + "O"*17 + "U"*11 + "Y"*5 + "a"*17 + "d" + "e"*11 + "i"*5 + "o"*17 + "u"*11 + "y"*5
)
def xoa_dau(txt: str) -> str:
if not unicodedata.is_normalized("NFC", txt):
txt = unicodedata.normalize("NFC", txt)
return txt.translate(BANG_XOA_DAU)
hãy xem phần giải thích bên dưới để hiểu về các dạng unicode chuẩn C và D
phiên bản không phụ thuộc vào các dạng unicode cho ai muốn mì ăn liền
BANG_XOA_DAU_FULL = str.maketrans(
"ÁÀẢÃẠĂẮẰẲẴẶÂẤẦẨẪẬĐÈÉẺẼẸÊẾỀỂỄỆÍÌỈĨỊÓÒỎÕỌÔỐỒỔỖỘƠỚỜỞỠỢÚÙỦŨỤƯỨỪỬỮỰÝỲỶỸỴáàảãạăắằẳẵặâấầẩẫậđèéẻẽẹêếềểễệíìỉĩịóòỏõọôốồổỗộơớờởỡợúùủũụưứừửữựýỳỷỹỵ",
"A"*17 + "D" + "E"*11 + "I"*5 + "O"*17 + "U"*11 + "Y"*5 + "a"*17 + "d" + "e"*11 + "i"*5 + "o"*17 + "u"*11 + "y"*5,
chr(774) + chr(770) + chr(795) + chr(769) + chr(768) + chr(777) + chr(771) + chr(803) # 8 kí tự dấu dưới dạng unicode chuẩn D
)
def xoa_dau_full(txt: str) -> str:
return txt.translate(BANG_XOA_DAU_FULL)
cần package unicodedata
(có sẵn trong python):
import unicodedata
cho 1 string trong python như sau:
raw_txt = "“Đạo đức kinh”"
nếu bạn cho rằng raw_txt
rất bình thường thì bạn đã lầm, bởi vì dấu là 1 kí tự riêng
ví dụ thay vì là 1 kí tự ứ
(U+1EE9
) văn bản lại là chuỗi:
- 3 kí tự
u
(U+0075
) +◌̛
(dấu móc ưU+031B
) +◌́
(dấu sắcU+0301
), do khi crawl text online dạng HTML làứ
, lưu ý thứ tự củaU+031B
vàU+0301
không quan trọng 👉 đây gọi là dạng unicode chuẩn D (unicode normal form D) - 2 kí tự
ư
(U+01B0
) +◌́
(dấu sắcU+0301
) 👉 không thuộc về dạng unicode chuẩn nào - 2 kí tự
ú
(U+00FA
) +◌̛
(dấu móc ưU+031B
) 👉 không thuộc về dạng unicode chuẩn nào
bước này ít người biết đến, nếu bỏ qua bước này, kế tiếp khi ta xoá dấu (thay thế ứ
bằng u
) sẽ gặp rắc rối do không biết ứ
là 1 kí tự hay chuỗi 2/3 kí tự
mình phát hiện ra vấn đề này khi crawl text truyện chữ về đọc
dạng unicode chuẩn C là khi các dấu nhập chung thành 1 kí tự duy nhất ứ
(U+1EE9
)
để kiểm tra xem text đã đúng dạng unicode chuẩn C (viết tắt NFC):
unicodedata.is_normalized("NFC", raw_txt) # False
chuyển sang dạng unicode chuẩn C
txt = unicodedata.normalize("NFC", raw_txt) # "“Đạo đức kinh”"
unicodedata.is_normalized("NFC", txt) # True
nhờ vào dạng unicode chuẩn C, thay thế kí tự là “1 kí tự thay thế 1 kí tự” (thay vì “1 kí tự thay thế nhiều kí tự” nếu không đúng chuẩn), cách làm hiệu quả nhất là tạo bảng tương ứng kí tự có dấu - ko dấu với hàm str.maketrans()
có sẵn trong python:
BANG_XOA_DAU = str.maketrans(
"ÁÀẢÃẠĂẮẰẲẴẶÂẤẦẨẪẬĐÈÉẺẼẸÊẾỀỂỄỆÍÌỈĨỊÓÒỎÕỌÔỐỒỔỖỘƠỚỜỞỠỢÚÙỦŨỤƯỨỪỬỮỰÝỲỶỸỴáàảãạăắằẳẵặâấầẩẫậđèéẻẽẹêếềểễệíìỉĩịóòỏõọôốồổỗộơớờởỡợúùủũụưứừửữựýỳỷỹỵ",
"A"*17 + "D" + "E"*11 + "I"*5 + "O"*17 + "U"*11 + "Y"*5 + "a"*17 + "d" + "e"*11 + "i"*5 + "o"*17 + "u"*11 + "y"*5
)
xoá dấu với hàm .translate()
có sẵn trong python:
txt.translate(BANG_XOA_DAU) # "“ Dao duc kinh”"
lệnh thay thế kí tự trên toàn chuỗi được thực thi đúng 1 lần duy nhất
ngoài ra parameter thứ 3 của str.maketrans
là 1 string của các kí tự muốn xoá, ta có thể đưa vào 8 kí tự dấu vào đó (xem code đầu bài)
điểm mạnh là tốc độ, do thực thi lệnh thay thế kí tự trên toàn chuỗi đúng 1 lần duy nhất (giống như trên), nhưng còn nhiều thiếu sót
- package
unidecode
: vốn được dùng để chuyển kí tự unicode về bảng ASCII, vậy nên sẽ làm mất các kí tự khác, ví dụ ở trên 2 kí tự“ ”
sẽ thành" "
; phương pháp duy nhất ko bị ảnh hưởng bởi dạng unicode chuẩn C hay D
from unidecode import unidecode
txt = "“Đạo đức kinh”"
unidecode(txt) # '" Dao duc kinh"'
-
thuật toán FlashText với package
flashtext
: hứa hẹn tốc độ nhanh hơn cả.replace()
vàRegEx
, nhưng không thể thực thi thay thế kí tự (có thể thay thế từ - word) -
thuật toán Aho-Corasick với package
fsed
hoặccyac
: trong số các package sử dụng thuật toán Aho-Corasick, chỉ có 2 package này có chức năng thay thế kí tự, còn lại chỉ có chức năng tìm, tốc độ cũng rất tốt
xem kết quả benchmark ở notebook phía dưới
- khởi tạo chuỗi mới
BANG_XOA_DAU_1 = {"Á": "A", "À": "A", …}
txt = "“Đạo đức kinh”"
txt = "".join([BANG_XOA_DAU_1.get(s, s) for s in txt])
về cơ bản thì tương đương với phương pháp mình trình bày ở trên, nhưng chậm hơn do sử dụng vòng lặp for
- dùng
.replace()
:
BANG_XOA_DAU_2 = {"Á": "A", "À": "A", …} # total 134 items
txt = "“Đạo đức kinh”"
for k, v in BANG_XOA_DAU_2.items():
txt = txt.replace(k, v)
lệnh thay thế kí tự trên toàn chuỗi (.replace()
) được thực thi 134 lần
- dùng
RegEx
: viết code ngắn hơn,tốc độ nhanh hơn.replace()
import re
BANG_XOA_DAU_3 = { # compile and save regex object for reuse is more efficient
"A": re.compile("[ÁÀẢÃẠĂẮẰẲẴẶÂẤẦẨẪẬ]"),
"a": re.compile("[áàảãạăắằẳẵặâấầẩẫậ]"),
… # total 14 items
}
txt = "“Đạo đức kinh”"
for k, v in BANG_XOA_DAU_3.items():
txt = v.sub(k, txt)
lệnh thay thế kí tự trên toàn chuỗi (re.sub()
) được thực thi 14 lần
không tồn tại dạng này đối với kí tự Đđ
dấu đổi nguyên âm
̆ (dấu ă)U+0306 |
̂ (dấu mũ âêô)U+0302 |
̛ (dấu móc ơư)U+031B |
dấu thanh điệu
́ (dấu sắc)U+0301 |
̀ (dấu huyền)U+0300 |
̉ (dấu hỏi)U+0309 |
̃ (dấu ngã)U+0303 |
̣ (dấu nặng)U+0323 |
tham khảo thêm: https://vietunicode.sourceforge.net/charset/vietcharset.html
ÁU+00C1 |
ÀU+00C0 |
ẢU+1EA2 |
ÃU+00C3 |
ẠU+1EA0 |
|
ĂU+0102 |
ẮU+1EAE |
ẰU+1EB0 |
ẲU+1EB2 |
ẴU+1EB4 |
ẶU+1EB6 |
ÂU+00C2 |
ẤU+1EA4 |
ẦU+1EA6 |
ẨU+1EA8 |
ẪU+1EAA |
ẬU+1EAC |
ĐU+0110 |
|||||
ÈU+00C8 |
ÉU+00C9 |
ẺU+1EBA |
ẼU+1EBC |
ẸU+1EB8 |
|
ÊU+00CA |
ẾU+1EBE |
ỀU+1EC0 |
ỂU+1EC2 |
ỄU+1EC4 |
ỆU+1EC6 |
ÍU+00CD |
ÌU+00CC |
ỈU+1EC8 |
ĨU+0128 |
ỊU+1ECA |
|
ÓU+00D3 |
ÒU+00D2 |
ỎU+1ECE |
ÕU+00D5 |
ỌU+1ECC |
|
ÔU+00D4 |
ỐU+1ED0 |
ỒU+1ED2 |
ỔU+1ED4 |
ỖU+1ED6 |
ỘU+1ED8 |
ƠU+01A0 |
ỚU+1EDA |
ỜU+1EDC |
ỞU+1EDE |
ỠU+1EE0 |
ỢU+1EE2 |
ÚU+00DA |
ÙU+00D9 |
ỦU+1EE6 |
ŨU+0168 |
ỤU+1EE4 |
|
ƯU+01AF |
ỨU+1EE8 |
ỪU+1EEA |
ỬU+1EEC |
ỮU+1EEE |
ỰU+1EF0 |
ÝU+00DD |
ỲU+1EF2 |
ỶU+1EF6 |
ỸU+1EF8 |
ỴU+1EF4 |
|
áU+00E1 |
àU+00E0 |
ảU+1EA3 |
ãU+00E3 |
ạU+1EA1 |
|
ăU+0103 |
ắU+1EAF |
ằU+1EB1 |
ẳU+1EB3 |
ẵU+1EB5 |
ặU+1EB7 |
âU+00E2 |
ấU+1EA5 |
ầU+1EA7 |
ẩU+1EA9 |
ẫU+1EAB |
ậU+1EAD |
đU+0111 |
|||||
èU+00E8 |
éU+00E9 |
ẻU+1EBB |
ẽU+1EBD |
ẹU+1EB9 |
|
êU+00EA |
ếU+1EBF |
ềU+1EC1 |
ểU+1EC3 |
ễU+1EC5 |
ệU+1EC7 |
íU+00ED |
ìU+00EC |
ỉU+1EC9 |
ĩU+0129 |
ịU+1ECB |
|
óU+00F3 |
òU+00F2 |
ỏU+1ECF |
õU+00F5 |
ọU+1ECD |
|
ôU+00F4 |
ốU+1ED1 |
ồU+1ED3 |
ổU+1ED5 |
ỗU+1ED7 |
ộU+1ED9 |
ơU+01A1 |
ớU+1EDB |
ờU+1EDD |
ởU+1EDF |
ỡU+1EE1 |
ợU+1EE3 |
úU+00FA |
ùU+00F9 |
ủU+1EE7 |
ũU+0169 |
ụU+1EE5 |
|
ưU+01B0 |
ứU+1EE9 |
ừU+1EEB |
ửU+1EED |
ữU+1EEF |
ựU+1EF1 |
ýU+00FD |
ỳU+1EF3 |
ỷU+1EF7 |
ỹU+1EF9 |
ỵU+1EF5 |
Xịn xò quá nè, còn giải thích đầy đủ nữa.