[Regex]Sử dụng regex để bắt kí tự Kanji level 1, level 2,…
Hôm nay mới giải quyết xong một task khá khó (theo mình thấy thế), đang khá phê nên viết lên đây để làm kinh nghiệm cho những ai chưa gặp hoặc đang loay hoay tìm cách giải quyết như mình.
Yêu cầu công việc
Không cho phép người dùng nhập kí tự kanji level 3 trở lên, tức là chỉ cho phép nhập kí tự alphabet, số, kí tự hiragana, katakana, kanji level 1, level 2.
Mọi người có thể xem danh sách kí tự tiếng Nhật và kí tự kanji level 1, 2, 3, 4 ở đây:
Danh sách kí tự không phải kanji
Hướng giải quyết
Dĩ nhiên khi gặp task như thế này thì ai cũng nghĩ đến dùng regex rồi.
Nhìn vào danh sách kí tự bên trên và yêu cầu thì mình thấy có thể dùng regex như thế này:
[0-9a-zA-Z -/:-@]
dùng đoạn này để bắt kí tự alphabet, số và một số kí tự đặc biệt được phép
[ -╂]
đây là các kí tự hiragana, katakana, dấu câu, kí hiệu của tiếng Nhật. Tùy yêu cầu mà mình có thể đổi đoạn regex này (danh sách kí tự lấy theo link này Danh sách kí tự không phải kanji. Các kí tự này nằm trong dải từ (dấu cách trong tiếng Nhật) đến ╂
[亜-腕]
đây là các kí tự kanji level 1. Các kí tự này nằm trong dải từ 亜
đến 腕
[弌-熙]
đây là các kí tự kanji level 2. Các kí tự này nằm trong dải từ 弌
đến 熙
Tổng hợp lại sẽ được regex như dưới đây:
^[0-9a-zA-Z -/:-@ -╂亜-腕弌-熙]+$
Nếu chỉ làm đến đây mà giải quyết được vấn đề thì mọi chuyện khá đơn giản và sẽ không có bài này =)).
Vậy cái regex này có vấn đề gì?
Regex này viết dưới dạng dải (range) ([亜-腕]
, [弌-熙]
, 0-9
,...) và mỗi dải sẽ tính theo mã unicode của kí tự đầu và cuối. Tuy nhiên như mọi người thấy thì mã unicode của các kí tự trong một dải không theo thứ tự tăng dần.
Ví dụ [亜-腕]
có mã unicode [U+4E9C-U+8155]
. Trong khi đó kí tự 鰐
có mã U+9C10 lại nằm ngoài dải trên nên sẽ không match với regex mặc dù nó vẫn là kí tự level 1.
Nhìn vào bảng kí tự kanji, có thể thấy là mã shift_jis
của nó là liên tục, ta có thể dùng mã shift_jis
này để thay thế cho mã unicode của kí tự đó. Khi đó regex nhận được sẽ như dưới đây:
^[0-9a-zA-Z -/:-@\u{8140}-\u{84BE}\u{889F}-\u{9872}\u{989F}-\u{EAA4}]+$
Ném cái này lên regex101 để test thì thấy các kí tự hiragana đều sai cả. Lí do sai ở đây là do regex sẽ coi các kí tự là mã unicode tương ứng của nó (chứ không phải mã shift_jis
), mã unicode của kí tự あ
là U+3042 nằm ngoài tất cả các dải trên.
Để xử lý được vấn đề này thì phải dùng thủ thuật một chút:
- Encode các kí tự đầu vào theo encoding
shift_jis
=> nhận được mã unicode của nó - Chuyển ngược mã unicode với encoding
shift_jis
này về thành string với encodingutf8
Như vậy cả regex và chuỗi cần kiểm tra đều được encode về dạng shift_jis
Dưới đây là code tham khảo gồm cả Swift và Python:
Swift:
let word = "ああい abcz576 :/@"// Chuyển chuỗi đầu vào về dạng unicode escape
var convertedStr = ""
for char in word {
let data = String(char).data(using: .shiftJIS, allowLossyConversion: true) ?? Data()
// Các kí tự 1 byte như a, b, c phải dùng format %04hhX để đưa về dạng 00XX thì mới giải mã được về string bình thường
let format = data.count == 1 ? "%04hhX" : "%02hhX"
let convertedChar = data.map {byte -> String in
return String(format: format, byte)
}.joined()
convertedStr.append("\\u\(convertedChar)")
}// Chuyển chuỗi unicode escape về string tương ứng với mã unicode đó
var mutableStr = NSMutableString(string: convertedStr)
CFStringTransform(mutableStr, nil, "Any-Hex/Java" as NSString, true)
print(mutableStr)// Regex
let regex = "^[0-9a-zA-Z -/:-@\u{8140}-\u{84BE}\u{889F}-\u{9872}\u{989F}-\u{EAA4}]+$"
let predicate = NSRegularExpression(pattern: regex, options: NSRegularExpression.Options.caseInsensitive)
let targetStr = "\u{61}\u{62}"
print(predicate.matches(in: mutableStr as String, options: [], range: NSRange(location: 0, length: mutableStr.length)).first?.range)
Python:
import retargetStr = 'abc あい唖愛 ╂'
// Chuyển chuỗi đầu vào thành chuỗi mới
encodedStr = ''
for letter in targetStr:
// Encode theo dạng shift_jis
encodedBytes = letter.encode('SHIFT_JIS_2004')
// Chuyển bytes vừa encode được về dạng int
encodedInt = int.from_bytes(encodedBytes, "big")
// Lấy kí tự tương ứng với mã vừa lấy được
encodedChar = chr(encodedInt)
encodedStr += encodedChar
print(encodedStr)// Regex
regexp = re.compile(r"^[0-9a-zA-Z -/:-@\u8140-\u84BE\u889F-\u9872\u989F-\uEAA4]+$")
print(regexp.search(encodedStr))