Skip to content

Instantly share code, notes, and snippets.

@phineas-pta
Last active October 31, 2024 06:39
Show Gist options
  • Save phineas-pta/05cad38a29fea000ab6d9e13a6f7e623 to your computer and use it in GitHub Desktop.
Save phineas-pta/05cad38a29fea000ab6d9e13a6f7e623 to your computer and use it in GitHub Desktop.
xoá dấu tiếng Việt với Python

xoá dấu tiếng Việt với Python

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)

1. giải thích quá trình

cần package unicodedata (có sẵn trong python):

import unicodedata

cho 1 string trong python như sau:

raw_txt = "“Đạo đức kinh”"

bước 1: đưa về dạng unicode chuẩn C (unicode normal form C)

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ắc U+0301), do khi crawl text online dạng HTML là ứ, lưu ý thứ tự của U+031BU+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ắc U+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

bước 2: xoá dấu cực nhanh

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)

2. so sánh với các cách xoá dấu thường sử dụng khác

2.1. cài thêm package phụ trợ

đ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()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ặc cyac: 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

2.2. xử lí kí tự bằng công cụ có sẵn trong python

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

3. phụ lục: bảng unicode codepoint kí tự tiếng Việt

3.1. dạng unicode chuẩn D

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

3.2. dạng unicode chuẩn C

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
Display the source blob
Display the rendered blob
Raw
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BANG_DANH_DAU = {
"vni": {
"Á": "A1" , "À": "A2" , "Ả": "A3" , "Ã": "A4" , "Ạ": "A5" ,
"Ă": "A8", "Ắ": "A81", "Ằ": "A82", "Ẳ": "A83", "Ẵ": "A84", "Ặ": "A85",
"Â": "A6", "Ấ": "A61", "Ầ": "A62", "Ẩ": "A63", "Ẫ": "A64", "Ậ": "A65",
"Đ": "D9",
"É": "E1" , "È": "E2" , "Ẻ": "E3" , "Ẽ": "E4" , "Ẹ": "E5" ,
"Ê": "E6", "Ế": "E61", "Ề": "E62", "Ể": "E63", "Ễ": "E64", "Ệ": "E65",
"Í": "I1" , "Ì": "I2" , "Ỉ": "I3" , "Ĩ": "I4" , "Ị": "I5" ,
"Ó": "O1" , "Ò": "O2" , "Ỏ": "O3" , "Õ": "O4" , "Ọ": "O5" ,
"Ô": "O6", "Ố": "O61", "Ồ": "O62", "Ổ": "O63", "Ỗ": "O64", "Ộ": "O65",
"Ơ": "O7", "Ớ": "O71", "Ờ": "O72", "Ở": "O73", "Ỡ": "O74", "Ợ": "O75",
"Ú": "U1" , "Ù": "U2" , "Ủ": "U3" , "Ũ": "U4" , "Ụ": "U5" ,
"Ư": "U7", "Ứ": "U71", "Ừ": "U72", "Ử": "U73", "Ữ": "U74", "Ự": "U75",
"Ý": "Y1" , "Ỳ": "Y2" , "Ỷ": "Y3" , "Ỹ": "Y4" , "Ỵ": "Y5" ,
"á": "a1" , "à": "a2" , "ả": "a3" , "ã": "a4" , "ạ": "a5" ,
"ă": "a8", "ắ": "a81", "ằ": "a82", "ẳ": "a83", "ẵ": "a84", "ặ": "a85",
"â": "a6", "ấ": "a61", "ầ": "a62", "ẩ": "a63", "ẫ": "a64", "ậ": "a65",
"đ": "d9",
"é": "e1" , "è": "e2" , "ẻ": "e3" , "ẽ": "e4" , "ẹ": "e5" ,
"ê": "e6", "ế": "e61", "ề": "e62", "ể": "e63", "ễ": "e64", "ệ": "e65",
"í": "i1" , "ì": "i2" , "ỉ": "i3" , "ĩ": "i4" , "ị": "i5" ,
"ó": "o1" , "ò": "o2" , "ỏ": "o3" , "õ": "o4" , "ọ": "o5" ,
"ô": "o6", "ố": "o61", "ồ": "o62", "ổ": "o63", "ỗ": "o64", "ộ": "o65",
"ơ": "o7", "ớ": "o71", "ờ": "o72", "ở": "o73", "ỡ": "o74", "ợ": "o75",
"ú": "u1" , "ù": "u2" , "ủ": "u3" , "ũ": "u4" , "ụ": "u5" ,
"ư": "u7", "ứ": "u71", "ừ": "u72", "ử": "u73", "ữ": "u74", "ự": "u75",
"ý": "y1" , "ỳ": "y2" , "ỷ": "y3" , "ỹ": "y4" , "ỵ": "y5" ,
},
"telex": {
"Á": "AS" , "À": "AF" , "Ả": "AR" , "Ã": "AX" , "Ạ": "AJ" ,
"Ă": "AW", "Ắ": "AWS", "Ằ": "AWF", "Ẳ": "AWR", "Ẵ": "AWX", "Ặ": "AWJ",
"Â": "AA", "Ấ": "AAS", "Ầ": "AAF", "Ẩ": "AAR", "Ẫ": "AAX", "Ậ": "AAJ",
"Đ": "DD",
"É": "ES" , "È": "EF" , "Ẻ": "ER" , "Ẽ": "EX" , "Ẹ": "EJ" ,
"Ê": "EE", "Ế": "EES", "Ề": "EEF", "Ể": "EER", "Ễ": "EEX", "Ệ": "EEJ",
"Í": "IS" , "Ì": "IF" , "Ỉ": "IR" , "Ĩ": "IX" , "Ị": "IJ" ,
"Ó": "OS" , "Ò": "OF" , "Ỏ": "OR" , "Õ": "OX" , "Ọ": "OJ" ,
"Ô": "OO", "Ố": "OOS", "Ồ": "OOF", "Ổ": "OOR", "Ỗ": "OOX", "Ộ": "OOJ",
"Ơ": "OW", "Ớ": "OWS", "Ờ": "OWF", "Ở": "OWR", "Ỡ": "OWX", "Ợ": "OWJ",
"Ú": "US" , "Ù": "UF" , "Ủ": "UR" , "Ũ": "UX" , "Ụ": "UJ" ,
"Ư": "UW", "Ứ": "UWS", "Ừ": "UWF", "Ử": "UWR", "Ữ": "UWX", "Ự": "UWJ",
"Ý": "YS" , "Ỳ": "YF" , "Ỷ": "YR" , "Ỹ": "YX" , "Ỵ": "YJ" ,
"á": "as" , "à": "af" , "ả": "ar" , "ã": "ax" , "ạ": "aj" ,
"ă": "aw", "ắ": "aws", "ằ": "awf", "ẳ": "awr", "ẵ": "awx", "ặ": "awj",
"â": "aa", "ấ": "aas", "ầ": "aaf", "ẩ": "aar", "ẫ": "aax", "ậ": "aaj",
"đ": "dd",
"é": "es" , "è": "ef" , "ẻ": "er" , "ẽ": "ex" , "ẹ": "ej" ,
"ê": "ee", "ế": "ees", "ề": "eef", "ể": "eer", "ễ": "eex", "ệ": "eej",
"í": "is" , "ì": "if" , "ỉ": "ir" , "ĩ": "ix" , "ị": "ij" ,
"ó": "os" , "ò": "of" , "ỏ": "or" , "õ": "ox" , "ọ": "oj" ,
"ô": "oo", "ố": "oos", "ồ": "oof", "ổ": "oor", "ỗ": "oox", "ộ": "ooj",
"ơ": "ow", "ớ": "ows", "ờ": "owf", "ở": "owr", "ỡ": "owx", "ợ": "owj",
"ú": "us" , "ù": "uf" , "ủ": "ur" , "ũ": "ux" , "ụ": "uj" ,
"ư": "uw", "ứ": "uws", "ừ": "uwf", "ử": "uwr", "ữ": "uwx", "ự": "uwj",
"ý": "ys" , "ỳ": "yf" , "ỷ": "yr" , "ỹ": "yx" , "ỵ": "yj" ,
}
}
def xoa_dau_sang_vni_telex(txt: str, kieu_go: str) -> str:
kieu_go = kieu_go.lower()
if kieu_go not in BANG_DANH_DAU:
raise Exception("kiểu gõ ko hợp lệ")
for k, v in BANG_DANH_DAU[kieu_go].items():
txt = txt.replace(k, v)
return txt
xoa_dau_sang_vni_telex(txt, "vni") # "“D9a5o d9u71c kinh”"
xoa_dau_sang_vni_telex(txt, "telex") # "“DDajo dduwsc kinh”"
@atom-tr
Copy link

atom-tr commented May 6, 2024

Xịn xò quá nè, còn giải thích đầy đủ nữa.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment