[Regex]Sử dụng regex để bắt kí tự Kanji level 1, level 2,…

Ha Nguyen Duc
Chim cu chăm code
Published in
4 min readJan 20, 2020

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 chữ kanji

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 encoding utf8

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))

--

--