งูขดตัวต้นฉบับกำลังจัดลูกปัดหน้าตาคล้ายกันจากคนละชุดให้เข้ารางมาตรฐานเดียวบนโต๊ะสีมิ้นต์

Python unicodedata.normalize: เทียบข้อความให้ตรงโลก Unicode

พี่โชว์Engineering

ข้อความสองชุดอาจแสดงเหมือนกันแต่เก็บคนละลำดับ code point เช่นตัวอักษรสำเร็จรูปกับตัวอักษรบวกเครื่องหมายประกอบ ถ้าเอาไปเป็น key โดยไม่ normalize ผู้ใช้เห็นชื่อเดียว แต่ระบบเห็นคนละบัญชีครับ

WHY: หน้าตาเหมือน ไม่ได้แปลว่า byte เหมือน

NFC รวมรูปที่เทียบเท่าตาม canonical equivalence ส่วน NFKC เพิ่ม compatibility mapping เช่นอักขระกว้างเต็มหรือรูปแบบตกแต่งบางชนิด จึงเหมาะกับ identifier มากกว่าเนื้อหาที่ต้องรักษารูปลักษณ์ แต่ policy ต้องตัดสินก่อนเก็บ ไม่ใช่ normalize สลับไปมาหลังข้อมูลอยู่ในฐานแล้ว

import unicodedata
import unittest


def canonical_key(text):
    return unicodedata.normalize("NFKC", text).casefold()


class NormalizeTests(unittest.TestCase):
    def test_composed_and_decomposed_match(self):
        composed = "é"
        decomposed = "e\u0301"
        self.assertNotEqual(composed.encode(), decomposed.encode())
        self.assertEqual(canonical_key(composed), canonical_key(decomposed))

    def test_fullwidth_compatibility(self):
        self.assertEqual(canonical_key("Admin"), "admin")


if __name__ == "__main__":
    unittest.main()

Normalization ไม่ใช่ตัวกรองความปลอดภัยครบชุด

อักษรคนละภาษาอาจหน้าคล้ายกันแต่ไม่ canonically equivalent เช่น Latin กับ Cyrillic บางตัว NFKC จึงไม่แทน confusable detection, script policy หรือการยืนยันเจ้าของชื่อ และไม่ควร normalize password เพราะเปลี่ยน secret ที่ผู้ใช้ตั้งโดยไม่คาดคิด เก็บทั้ง display value กับ normalized key เมื่อ UI ต้องแสดงรูปเดิม

Recap

  • เลือก NFC สำหรับข้อความทั่วไป หรือ NFKC สำหรับ identifier ตาม policy
  • ทำก่อน uniqueness check และทำแบบเดียวทุก write path
  • เก็บ display value แยกจาก comparison key
  • เพิ่ม confusable/script policy ถ้า identifier มีผลด้านสิทธิ์