วิธีสกัดตำบล อำเภอ จังหวัด ออกจาก“ที่อยู่” ด้วย Python

Thachaparn Bunditlurdruk
incubate.co.th
Published in
3 min readApr 23, 2020

ขั้นตอนการจัดการและทำความสะอาดข้อมูลมักเป็นขั้นตอนที่คนทำงานเกี่ยวกับข้อมูลต้องเจอ ก่อนจะสามารถนำข้อมูลไปใช้วิเคราะห์ หา insight ต่าง ๆ ได้ โดยเฉพาะข้อมูลที่เป็นลักษณะข้อความนั้นมีความยากไม่ต่างจากข้อมูลที่เป็นตัวเลขเลย

อย่างงานของเราวันนี้จะมาสกัดตำบล/เเขวง อำเภอ/เขต และจังหวัดออกมาจากที่อยู่ภาษาไทย ที่ตั้งแต่เลขที่บ้านยันรหัสไปรษณีย์เป็น string ก้อนเดียวกัน ให้สะดวกต่อการนำไปวิเคราะห์ต่อได้อย่างไร

Challenges

ข้อมูลที่อยู่ที่เรากำลังจะจัดการเกิดจากการพิมพ์โดยมนุษย์ลงใน input ช่องเดียว ดังนั้นอาจทำให้เกิดหน้าตาของที่อยู่ที่หลากหลายมาก แล้วแต่คนกรอกข้อมูลเลย ซึ่งการใช้ regular expression หาชื่อตำบล อำเภอ จังหวัด หรือหาองค์ประกอบเหล่านี้โดยการอิงจากตำแหน่งก้อนที่อยู่ อาจจะไม่สามารถสกัดข้อมูลออกมาได้ง่ายขนาดนั้นเสมอไป เช่น

  1. การสะกดชื่อตำบล อำเภอ จังหวัดที่อาจไม่ตรงกับข้อมูลกรมการปกครอง สาเหตุอาจเกิดจากการสะกดผิดเพราะพิมพ์พลาด หรือความไม่รู้ — regular expression ก็เป๋ไปเลย
  2. บางครั้งชื่อจังหวัดสามารถไปปรากฏที่ชื่อถนนได้ ถ้าใช้ regular expression หาเลยอาจจะได้ติดมา 2 จังหวัด หรือดีไม่ดี อาจจะได้จังหวัด อำเภอ ตำบล ที่ไม่สอดคล้องกัน เช่น ถนนเพชรบุรี แขวงมักกะสัน เขตราชเทวี กรุงเทพมหานคร
  3. ข้อมูลตำบล อำเภอ จังหวัด อาจจะไม่ได้มาพร้อมหน้าพร้อมตาครบ 3 ส่วน หรือเรียงกันสวยงามแบบนี้เสมอไป บางที่อยู่อาจจะกรอกมาแค่จังหมด ไม่มีตำบล/เขตก็ได้

Solution Overview

จากปัญหาของแต่ละวิธีที่คุยกันไป เราเลยนำแต่ละวิธีมารวมกัน แล้วเสริมทัพฟังก์ชั่นการเช็คตัวสะกดเข้าไป โดยตอนแรกเราจะเริ่มหาด้วยวิธีง่ายที่สุดและยืดหยุ่นน้อยที่สุดก่อน คือหาจากคำบอกที่อยู่ อย่างคำว่าตำบล/แขวง อำเภอ/เขต จังหวัด เป็นต้น จากนั้นถ้ามีส่วนไหนที่หาไม่เจอ เราก็จะเอา string ก้อนสุดท้ายก่อนที่จะถึง 3 ระดับไปหาจาก dictionary ที่ทำขึ้นว่าในแต่ละจังหวัดมีอำเภอและตำบลอะไรบ้าง ถ้ายังไม่เจออีก เราก็สันนิษฐานว่าอาจจะเป็นเพราะสะกดผิด ให้ส่งเข้าฟังก์ชั่นแก้ไขตัวสะกด แล้วกลับมาหาจาก dictionary ใหม่

Overview
Solution Overview

Step by step

1. ตามหา “คำบอกที่อยู่”

เราจะนำก้อนที่อยู่มาหาคำบอกที่อยู่ที่เราลิสท์ไว้ใน level_mark โดยเราพยามจะลิสท์ให้ครบทุกแบบที่คนมักจะเขียนเลย ทั้ง “ตำบล” “ต.” ซึ่งนอกจากระดับตำบล อำเภอ จังหวัด โดยจุดสำคัญของ regular expression นี้ก็คือ ด้านหน้าของแต่ละอันต้องมีช่องว่าง เพื่อเอาไว้หลบพวกชื่อเฉพาะที่มีคำเหล่านี้อยู่ เช่น มหาวิทยาลัยกรุงเทพฯ

นอกจากนี้ เราก็เตรียมพวกคำบอกที่อยู่อย่างอาคาร หมู่บ้าน หมู่ ซอย ถนน ไว้เผื่อเก็บข้อมูลที่ตรงตามรูปแบบปกติไว้ด้วย แต่ในบทความนี้เราจะยังไม่ได้โฟกัสมาก เพราะมันมีรูปแบบการเขียนที่ต้องศึกษาอีกเยอะ

ในฟังก์ชั่น split_frontAddrs() เราจะหาคำบอกที่อยู่ที่เราเตรียมไว้ทั้งหมดทีเดียว แล้วหาตำแหน่งของคำเหล่านั้น แล้วทำสัญลักษณ์ | เอาไว้ก่อนหน้าคำพวกนั้น เพื่อเอาไว้เป็นตัวแบ่งที่อยู่ออกเป็นก้อน ๆ

  • ตัวอย่างที่ 1: “888/22 |หมู่ที่ 4 |ถนนเพชรบุรี |แขวงมักกะสัน |เขตราชเทวี |กรุงเทพมหานคร”
  • ตัวอย่างที่ 2: “888/22 |หมู่ที่ 4 |ถนนเพชรบุรี มักกะสัน ราชเทวี |กรุงเทพมหานคร”

จากนั้นเราก็ใช้คำสั่ง split() ด้วย “|” ที่เราทำไว้แบ่งเป็นก้อน แล้วลูปเก็บข้อมูลแต่ละก้อนใน dictionary โดยมี key ก็คือคำบอกที่อยู่ที่แปลงเป็นภาษาอังกฤษแล้ว และ value คือก้อนข้อมูลที่ลบคำบอกที่อยู่ออก โดยกรณีของกรุงเทพ จะมีการจัดการพิเศษขึ้นมาอีกนิดหนึ่งตรงที่เขาจะไม่ค่อยมีคำว่าจังหวัดกำกับ เราเลยต้องใส่ key ให้เอง

สิ่งที่ได้กลับมาจากฟังก์ชันนี้มี 2 อย่าง คือ dictionary ของส่วนประกอบที่อยู่ กับ last_key โดย last_key คือ คำบอกที่อยู่ตัวสุดท้ายที่มีข้อมูล ที่ไม่ใช่ตำบล อำเภอ จังหวัด

2. ตามหาชื่อจังหวัด > ชื่ออำเภอ > ชื่อตำบล ทีละขั้น ๆ

หลังจากหาด้วยวิธีที่เน้นความเป๊ะ ๆ ของคำบอกที่อยู่แล้วยังหาจังหวัด อำเภอ ตำบลไม่ครบ เราก็เริ่มมาหาจากชื่อจังหวัด อำเภอ ตำบลในประเทศไทยแทน โดยเราหาข้อมูลนี้มาจากฐานข้อมูลของกรมการปกครอง* แล้วแปลงให้อยู่ในรูป nested address dict 3 ชั้นแบบนี้

{จังหวัด:{อำเภอ:{set ของตำบล}, อำเภอ:{set ของตำบล}}, จังหวัด:{อำเภอ:{set ของตำบล}}}

และ input ที่เราจะเอามาใช้ search ใน dict ก็คือ ก้อนขวาสุดของ value ของ last_key นั่นเอง เราสามารถเอาก้อนขวาสุดออกมาโดยใช้คำสั่ง .rsplit(“ ”) ที่จะแบ่งด้วยช่องว่าง 1 ครั้งไล่จากทางขวามือ

แต่ ๆ ๆ ๆ ก่อนจะใช้ ต้องเช็ค 2 เงื่อนไขก่อนว่าผ่านไหม

  1. เมื่อตัดก้อนขวาสุดออกไปแล้ว string ที่เหลืออยู่ต้องไม่ใช่คำบอกที่อยู่ เช่น
    - ถนน เพชรบุรี : เพชรบุรีเอามาใช้ไม่ได้ เพราะก้อนที่เหลือคือ “ถนน” ซึ่งเป็นคำบอกที่อยู่ ถ้าเอาเพชรบุรีมาใช้ ข้อมูลถนนจะหายไป
    - ถนน เพชรบุรี มักกะสัน ราชเทวี : ราชเทวีเอามาใช้ได้ เพราะก้อนที่เหลืออยู่ยังมีข้อความอื่นให้เป็นชื่อของ ถนน (value ของ key ถนน)
  2. ก้อนที่นำมาต้องไม่ใช่ตัวเลข หรือมีวงเล็บอยู่ใน string เพื่อป้องกันกรณีอย่าง ถนน เพชรบุรี (ตัดใหม่) หรือ ซอย สุขุมวิท 19 ที่จะเอา (ตัดใหม่) หรือ 19 ไปหาใน dictionary

ถ้าผ่านเกณฑ์ทั้งสองข้อฉลุยแล้ว เราก็เอามาเช็คได้ โดยไอเดียคือเราจะเริ่มเช็คจากระดับจังหวัดก่อน โดยการเอาก้อนนั้นมาเช็คกับ key ชั้นแรกของ dictionary โดยไม่จำเป็นว่าจะต้องมีคำว่าจังหวัดก็ได้(ต่างจากข้อ 1) แต่ถ้ามีข้อมูลจังหวัดอยู่แล้วจากขั้นตอนแรก ก็เอาข้อมูลจังหวัดมาเป็น key ชั้นแรก เพื่อไปสร้าง regular expression ของทุกอำเภอในจังหวัดนั้น เพื่อตามหาอำเภอต่อได้เลย

ในทุก ๆ ขั้นที่หาเจอ เราก็จะลบตัวที่หาเจอออกจากก้อน value ของ last_key ด้วย แล้วพอไปขั้นถัดไปก็เรียก value นี้ไปประกอบเป็น last_element ใหม่

# ตัวอย่างโค๊ดบางส่วน ช่วงการ search ใน address_dict
last_element = last_key + ' ' + component_dict[last_key]
inspect_parts = last_element.rsplit(' ', 1)
province_inspect = inspect_parts[-1]
if inspect_parts[0].strip() != last_key and not re.search("[0-9\-\(\)]", province_inspect): # เรียกฟังก์ชั่นที่เอาไปเช็ค regular expression ของแต่ละระดับ
provinceOutput = province_check(province_inspect)
if provinceOutput[0]:
province = re.sub('(จังหวัด|จ\.)\s*','', provinceOutput[1])
province = province.strip()

'''
if province in ['กรุงเทพ', 'กรุงเทพฯ', 'กทม']:
province = 'กรุงเทพมหานคร'
'''

component_dict['Province'] = province
# สร้าง label ขึ้นมาไว้เช็คว่ามีความมั่นใจจนนำไปใช้ได้ไหม
component_dict['Confirmed_p'] = True
# ลบตัวที่ใช้ regular expression หาเจอออกจากก้อน value ของ last_key เพื่อใช้ในขั้นต่อไป
component_dict[last_key] = re.sub(provinceOutput[1], '',component_dict[last_key], 1).strip()
if amphoe == None and province != None:
last_element = last_key + ' ' + component_dict[last_key]
inspect_parts = last_element.rsplit(' ', 1)
amphoe_inspect = inspect_parts[-1]

ถ้ายังหาไม่เจออีก ก็ไปข้อ 3 เลย

3. หรือว่าไม่เจอเพราะมัน “สะกดผิด” นะ

ถ้าผ่านมาสองวิธีแล้วยังไม่เจอ เราก็เอาก้อนข้อความเดิมของเราไปเข้าฟังก์ชั่น minimum edit distance เพื่อคำนวณหา “cost” ที่เกิดขึ้นจากการแก้ไข string หนึ่งให้กลายเป็นอีก string หนึ่ง

สำหรับใครที่ไม่คุ้นกับ term นี้ สามารถศึกษาเพิ่มเติมได้ ที่นี่ ก่อนนะ

ให้เราเอา string เดิมก้อนสุดท้ายมาคำนวณหา cost กับทุกจังหวัด แล้วเลือกตัวที่มี cost น้อยที่สุด แปลว่ามีการแก้ไขน้อยที่สุด และค่า cost ต้องน้อยกว่า 3 (ถ้ามากกว่านี้อาจจะเป็นคนละจังหวัดมากกว่าการสะกดผิด เช่น ตาก → ตราด) จากนั้นเราก็เอาตัวที่คาดว่าเป็นตัวที่สะกดถูกไปทำวิธีข้อ 2 ใหม่

พอหาระดับจังหวัดเสร็จแล้วก็ไปต่อที่ระดับอำเภอ หรือตำบลได้เลย

แต่ถ้าในระดับอำเภอ หรือตำบล มาจนถึงขั้นนี้แล้วยังไม่เจออีก … ก็ให้คำนวณหา cost กับ ตำบล หรืออำเภอแบบทั่วประเทศไปเลย

พอเอาทุกส่วนมายำรวมกัน ก็จะออกมาเป็นโค๊ดฉบับเต็ม แบบนี้

Further works

ยอมรับว่าวิธีนี้ก็ยังมีช่องโหว่ให้เกิด bug อีกมาก แต่ก็พยายามที่จะแก้ปัญหา เรื่องการสะกดคำผิด และไม่อิงกับตำแหน่งมากเกินไป วิธีนี้จะใช้ไม่ได้ผลถ้าคนกรอกคำบอกที่อยู่บ้างไม่กรอกบ้างในข้อความเดียวกัน

{'address': '888/22 หมู่ที่ 4 ถนนเพชรบุรี แขวงมักกะสัน ราชเทวี กรุงเทพมหานคร',
'frontmost_cut': '888/22',
'Moo': '4',
'Street': 'เพชรบุรี', # ---> ตัวนี้จะกลายเป็น last key ที่เอาไปทำวิธีที่ 2 แต่ก็ไม่เจออะไรอยู่ดี
'Tambon': 'มักกะสัน ราชเทวี',
'Province': 'กรุงเทพมหานคร',
'Alley': None,
'Building': None,
'Amphoe': None}

หรือถ้ากรอกมาแต่ตำบล หรืออำเภอโดยที่ไม่มีคำบอกที่อยู่ และไม่กรอกจังหวัดมาก็จะไม่สามารถสกัดอำเภอกับตำบลออกมาได้ เพราะวิธีนี้อิงลำดับขั้นจาก จังหวัด > อำเภอ > ตำบล มาก เพราะถ้าไม่มีจังหวัดก็จะไม่รู้ว่าต้องไปหาที่ระดับไหน อาจต้องหาจากแต่ละระดับในสโคปทั้งประเทศไปทีละอันเลย

{'address': '888/22 หมู่ที่ 4 ถนนเพชรบุรี มักกะสัน ราชเทวี',
'frontmost_cut': '888/22',
'Moo': '4',
'Street': 'เพชรบุรี มักกะสัน ราชเทวี',
'Amphoe': None,
'Province': None,

'Alley': None,
'Building': None,
'Tambon': None}

ถ้าใครมีวิธีการสกัดที่อยู่แตกต่างกันอย่างไร ก็เอามาแชร์ หรือเสนอแนะกันได้นะคะ ยังมือใหม่อยู่ เลยมาแชร์ประสบการณ์ที่ลองผิดลองถูกมาค่ะ :)

--

--

Thachaparn Bunditlurdruk
incubate.co.th

An Arts graduate who’s trying to challenge herself with programming