
Python difflib.get_close_matches: เดาคำที่ผู้ใช้พิมพ์ผิด แล้วถามว่า "หมายถึงอันนี้ไหม?"
เคยพิมพ์ git stauts แล้วได้คำตอบกลับมาว่า “หมายถึง status ใช่ไหม” ไหมครับ ฟีเจอร์เล็กๆ แบบนี้ช่วยให้คนใช้ไม่หงุดหงิด ใน Python ไม่ต้องลงไลบรารีเพิ่ม เพราะไลบรารีมาตรฐานมี difflib.get_close_matches ให้ใช้อยู่แล้ว
วันนี้ผมจะใช้มันกับ 2 งาน: แนะนำชื่อจังหวัดเมื่อพิมพ์ผิด และแนะนำคำสั่งในโปรแกรม CLI
WHY: ทำไมต้องแนะนำคำ ไม่ใช่ตอบ error เฉยๆ
- ฟอร์มที่ตอบแค่ “ไม่พบจังหวัด” ทำให้คนต้องเดาเองว่าพิมพ์ผิดตรงไหน
- คำไทยพิมพ์ผิดง่าย เช่น สลับ ใ กับ ไ หรือลืมไม้ไต่คู้
- การ “แนะนำ” ดีกว่า “แก้ให้เลย” เพราะผู้ใช้ยังเป็นคนยืนยันเอง ไม่มีการเปลี่ยนข้อมูลแบบเงียบๆ
HOW: ฟังก์ชันนี้ทำงานยังไง
รูปแบบคือ get_close_matches(word, possibilities, n=3, cutoff=0.6)
wordคือคำที่ผู้ใช้พิมพ์possibilitiesคือรายการคำที่ถูกต้องทั้งหมดnคือจำนวนคำแนะนำสูงสุด (ค่าเริ่มต้น 3)cutoffคือคะแนนความเหมือนขั้นต่ำ ตั้งแต่ 0 ถึง 1 (ค่าเริ่มต้น 0.6) คำที่ได้คะแนนต่ำกว่านี้จะถูกตัดทิ้ง
เบื้องหลังมันใช้ SequenceMatcher ให้คะแนนด้วยค่า ratio() คิดง่ายๆ ว่าเป็นสัดส่วนตัวอักษรที่ตรงกันเมื่อเทียบสองคำ ผลลัพธ์เรียงจากคำที่เหมือนที่สุดก่อน
ตัวอย่างจริงตอนผมรัน: "เชียงไหม่" เทียบกับ "เชียงใหม่" ได้ ratio ราว 0.89 เพราะต่างกันแค่ตัวเดียว จึงผ่าน cutoff 0.6 สบายๆ
จุดที่ต้องรู้ก่อนใช้
- ภาษาไทยเทียบทีละ “ตัวอักษร Unicode” สระบน สระล่าง และวรรณยุกต์นับเป็นตัวแยก คำสั้นมากๆ ผิดหนึ่งตัวจะได้คะแนนตกเยอะ
- ถ้าตั้ง cutoff ต่ำไป จะได้คำแนะนำมั่วๆ ถ้าสูงไป คำที่พิมพ์ผิดนิดเดียวก็ไม่ถูกแนะนำ ค่าเริ่มต้น 0.6 เป็นจุดเริ่มที่ดี
- คำที่พิมพ์ไม่ครบ เช่น
"เชียง"จะได้ทั้ง เชียงราย และ เชียงใหม่ เป็นเรื่องดี ให้ผู้ใช้เลือกเอง แต่ถ้าดัน cutoff ขึ้นเป็น 0.8 จะไม่เหลือสักคำ - ไม่เจอคำที่ใกล้พอ จะได้ลิสต์ว่าง
[]ต้องเตรียมข้อความตอบกลับกรณีนี้ด้วย - เทียบตัวพิมพ์เล็ก/ใหญ่ตรงตัว ถ้าเป็นภาษาอังกฤษ ควร
.lower()ทั้งสองฝั่งก่อน - รายการคำหลายหมื่นคำ ฟังก์ชันนี้จะช้าลงเพราะต้องเทียบทุกคำ เหมาะกับรายการขนาดเล็กถึงกลาง เช่น จังหวัด เมนู หรือคำสั่ง
โค้ดพร้อม unittest
import difflib
import unittest
PROVINCES = ["กรุงเทพมหานคร", "เชียงใหม่", "เชียงราย", "ขอนแก่น",
"ภูเก็ต", "นครราชสีมา", "สงขลา"]
COMMANDS = ["status", "commit", "checkout", "cherry-pick", "push", "pull"]
def suggest(word, choices, n=3, cutoff=0.6):
"""คืนคำที่ใกล้เคียง ถ้าพิมพ์ถูกอยู่แล้วคืนคำนั้นคำเดียว"""
word = word.strip()
if word in choices:
return [word]
return difflib.get_close_matches(word, choices, n=n, cutoff=cutoff)
def reply(word, choices):
hits = suggest(word, choices, n=1)
if not hits:
return f"ไม่พบ '{word}' ลองพิมพ์ใหม่อีกครั้งครับ"
if hits[0] == word.strip():
return f"ตกลง: {word.strip()}"
return f"หมายถึง '{hits[0]}' ใช่ไหมครับ?"
class SuggestTest(unittest.TestCase):
def test_thai_typo(self):
self.assertEqual(suggest("เชียงไหม่", PROVINCES), ["เชียงใหม่"])
self.assertEqual(suggest("ภูเกต", PROVINCES), ["ภูเก็ต"])
def test_exact_match_wins(self):
self.assertEqual(suggest(" สงขลา ", PROVINCES), ["สงขลา"])
def test_cutoff_controls_noise(self):
# พิมพ์ไม่ครบ ได้ 2 ตัวเลือก ให้ผู้ใช้เลือกเอง
self.assertEqual(sorted(suggest("เชียง", PROVINCES)), ["เชียงราย", "เชียงใหม่"])
# cutoff สูงขึ้น ตัดคำที่เหมือนไม่พอทิ้ง
self.assertEqual(suggest("เชียง", PROVINCES, cutoff=0.8), [])
def test_cli_commands(self):
self.assertEqual(suggest("stauts", COMMANDS), ["status"])
self.assertEqual(suggest("chekcout", COMMANDS), ["checkout"])
self.assertEqual(suggest("xyz", COMMANDS), [])
def test_ratio_explains_score(self):
ratio = difflib.SequenceMatcher(None, "เชียงไหม่", "เชียงใหม่").ratio()
self.assertGreater(ratio, 0.85)
def test_reply_messages(self):
self.assertEqual(reply("เชียงไหม่", PROVINCES), "หมายถึง 'เชียงใหม่' ใช่ไหมครับ?")
self.assertEqual(reply("สงขลา", PROVINCES), "ตกลง: สงขลา")
self.assertTrue(reply("abc", PROVINCES).startswith("ไม่พบ"))
if __name__ == "__main__":
unittest.main()
ตอนตรวจบทความ ผมรันโค้ดนี้ด้วย python3 -I -B บน Python 3.14 ของเครื่องนี้ เทสต์ผ่านทุกข้อ
ใช้ตรงไหนในงานจริง
| งาน | ตัวอย่าง |
|---|---|
| ฟอร์มที่อยู่ | พิมพ์จังหวัด/อำเภอผิด แล้วแนะนำชื่อที่ถูก |
| โปรแกรม CLI | พิมพ์คำสั่งผิด แล้วถามว่าหมายถึงคำสั่งไหน |
| ช่องค้นหาเมนู | ร้านอาหารที่มีเมนูไม่กี่ร้อยรายการ |
| ไฟล์ config | key สะกดผิด เช่น timout แนะนำ timeout |
ข้อควรระวัง: อย่าแก้ข้อมูลให้อัตโนมัติในงานที่ผิดแล้วเสียหาย เช่น ชื่อผู้รับโอนเงิน ให้ถามผู้ใช้ยืนยันทุกครั้ง
Recap
- ลองใช้
difflib.get_close_matchesก่อนลงไลบรารี fuzzy matching ตัวใหญ่ - เริ่มที่
cutoff=0.6แล้วค่อยปรับตามข้อมูลจริง - เตรียมข้อความตอบเมื่อได้ลิสต์ว่าง
- ภาษาอังกฤษให้
.lower()ทั้งสองฝั่งก่อนเทียบ - ให้ผู้ใช้ยืนยันคำที่แนะนำ อย่าแก้ข้อมูลเงียบๆ
- ก๊อปเทสต์ด้านบนไปรันบน Python ของคุณ ดูว่าผลตรงกันไหม