บทความนี้เป็นส่วนหนึ่งของมินิซีรีส์ “เล่นกับ Big Data ภาษาไทย ต้องเข้าใจ Unicode” โดยผู้อ่านสามารถเข้าถึงภาคแรกของบทความนี้ได้ที่นี่ครับ : เล่นกับ Big Data ภาษาไทย ต้องเข้าใจ Unicode : Part 1

ในบทความที่แล้วนั้น เราได้ทำความรู้จักกับ Unicode ซึ่งเป็นมาตรฐานสากลเพื่อช่วยให้การสื่อสารด้วยข้อความภาษาต่าง ๆ ทั่วโลกเกิดขึ้นได้อย่างเสมอภาคและเป็นระบบ และยังได้เรียนรู้กับ ASCII representation, Unicode representation, encoding and decoding สำหรับบทความนี้ เราจะดูกันว่า ความรู้เบื้องต้นเหล่านี้สามารถช่วยเราแก้ปัญหาต่าง ๆ ในสถานการณ์จริงได้อย่างไร ผมขอเสนอสองตัวอย่าง คือ เวลาเราจัดการกับ JSON เพื่อเขียนข้อมูลลงเป็นไฟล์ และ เวลาเราสร้าง BeautifulSoup object เพื่อทำ web scraping ครับ

จัดการกับ JSON เพื่อเขียนข้อมูลลงเป็นไฟล์

สมมติว่าเรามี JSON data ที่เรียบง่ายมากอันหนึ่งที่เราต้องการเขียนเก็บลงเป็นไฟล์ (serialization) ตาม Code Block 1

c = {'ไม่ระบุเพศ': 205, 'หญิง': 1040, 'ชาย': 510}

# dict
type(c)
import json
with open('test.json', 'w') as json_file:
    json.dump(c, json_file)

ถ้าเราลองแปลง c ไปเป็น stream ให้ file pointer เขียนข้อมูลลงไฟล์ด้วยฟังก์ชัน json.dump ดูตาม Code Block 2 เราจะได้ผลลัพธ์ในไฟล์ test.json ดังนี้

{"\u0e44\u0e21\u0e48\u0e23\u0e30\u0e1a\u0e38\u0e40\u0e1e\u0e28": 205,
"\u0e2b\u0e0d\u0e34\u0e07": 1040,
"\u0e0a\u0e32\u0e22": 510}

ทำใมเราได้ข้อความที่อ่านไม่ออก? เนื่องจาก json.dump มีพฤติกรรมตั้งต้น (default) ของฟังก์ชันนี้ ว่าจะแปลง JSON data เป็นข้อความ (ให้ file pointer) ที่มีแต่ตัวอักขระ ASCII เท่านั้น นั่นหมายความว่า ตัวอักขระประเภทอื่น ๆ จะถูก “escaped” หรือ แปลงให้อยู่ในรูปที่เขียนได้ด้วยอักขระ ASCII (เราแปลง “ช” เป็น “\\u0e0a” ทุกตัวเป็นอักขระ ASCII หมด ทั้ง “\”, “u”, “0”, “e”, “a”)

แต่เดี๋ยวก่อน ผู้อ่านสังเกตเห็นไหมครับ ว่าจริง ๆ แล้วเราอ่านผลของ json.dump ข้างต้นออก? ตัวอักษรภาษาไทยต่าง ๆ ไม่ได้อยู่ในกลุ่มอักขระ ASCII จึงถูกแปลงให้อยู่ในรูปของ Unicode code points นั่นเอง! ในบทความภาคแรก เราได้เรียนรู้ถึงเครื่องมือที่ช่วยให้เราดู Unicode code points ของตัวอักษรภาษาไทยต่าง ๆ ได้ ดังเช่นที่เราแปลง “ช” เป็น U+0E0A หรือ “\\u0e0a” หากเขียนตามแบบที่ json.dump ใช้

รู้อย่างนี้แล้ว วิธีแก้ไขก็คือ เราปรับพารามิเตอร์ให้ json.dump ให้สนับสนุนการ serialize ที่มีตัวอักขระนอกเหนือจาก ASCII ด้วย แล้วเราก็เลือกเปิดไฟล์สำหรับการเขียนไฟล์ (writing operation) ที่สนับสนุน Unicode (ซึ่งเราจะใช้ UTF-8) ตาม Code Block 3 เราก็จะได้ผลลัพธ์ที่ต้องการแล้วครับ

import json
with open('test.json', 'w', encoding='utf8') as json_file:
    json.dump(c, json_file, ensure_ascii=False)

แล้วเราก็จะได้ผลลัพธ์ในไฟล์ test.json ใหม่ดังนี้ :

{"ไม่ระบุเพศ": 205, "หญิง": 1040, "ชาย": 510}

เวลาเรา load JSON file กลับเข้ามาใน Python environment ก็สามารถทำได้ง่าย ๆ ดัง Code Block 4 ครับ

with open('test.json', 'r', encoding='utf8') as f:
    read_json = json.load(f)

ในการกำหนด UTF-8 ในฟังก์ชัน open นี้ บางครั้งก็เป็น default มาให้เลย แต่ขึ้นอยู่กับบาง OS / platform ค่าตั้งต้นอาจจะไม่ได้ใช้ UTF-8 ก็ได้ ถ้าจะให้แน่ใจ เราควรกำหนดลงไปชัด ๆ เลยตาม Code Block 3 และ 4 ดีกว่าครับ แต่หากใครสงสัยว่าระบบของตนเองใช้ default encoding อะไรอยู่ สามารถตรวจสอบได้ด้วยคำสั่ง import locale; locale.getpreferredencoding() ใน Python ครับ

สร้าง BeautifulSoup object เพื่อทำ Web Scraping จากข้อมูลภาษาไทย

สำหรับผู้อ่านที่ยังไม่คุ้นเคยกับพื้นฐาน web scraping ผู้อ่านสามารถศึกษาบทความที่สอนวิธีการทำ web scraping เบื้องต้นได้ที่บทความ ดึงข้อมูลจากเว็บไซต์ (Data Scraping) ด้วย Python Part 1 ครับ

เวลาที่เราทำ web scraping เราอาจจะต้องดึงข้อมูลจากเว็บที่มีข้อความที่ถูกเข้ารหัสในรูปแบบต่าง ๆ ที่ไม่ใช่ ASCII แล้วเวลาที่เราได้ข้อมูลที่ดึงจากหน้าเว็บมา เราอาจจะได้มาเป็น bytes sequence ที่มีรูปร่างหน้าตาดังตัวอย่างใน Code Block 5

html_content = b'<html><head>\
<title>\xe0\xb8\xa3\xe0\xb8\xab\xe0\xb8\
\xb1\xe0\xb8\xaa\xe0\xb8\xab\xe0\xb8\x99\
\xe0\xb9\x88\xe0\xb8\xa7\xe0\xb8\xa2\xe0\
\xb8\x87\xe0\xb8\xb2\xe0\xb8\x99\xe0\xb8\
\x9a\xe0\xb8\xa3\xe0\xb8\xb4\xe0\xb8\x81\
\xe0\xb8\xb2\xe0\xb8\xa3\xe0\xb8\xaa\xe0\
\xb8\xb8\xe0\xb8\x82\xe0\xb8\xa0\xe0\xb8\
\xb2\xe0\xb8\x9e \xe0\xb8\x81\xe0\xb8\xa2\
\xe0\xb8\x9c.</title></head><body>\
<p class="title">\
<b>Very Important Data!\
\xe0\xb8\x84\xe0\xb9\x89\xe0\xb8\x99\xe0\
\xb8\xab\xe0\xb8\xb2\xe0\xb8\x82\xe0\xb9\
\x89\xe0\xb8\xad\xe0\xb8\xa1\xe0\xb8\xb9\
\xe0\xb8\xa5\xe0\xb8\xa3\xe0\xb8\xab\xe0\
\xb8\xb1\xe0\xb8\xaa\xe0\xb8\xab\xe0\xb8\
\x99\xe0\xb9\x88\xe0\xb8\xa7\xe0\xb8\xa2\
\xe0\xb8\x87\xe0\xb8\xb2\xe0\xb8\x99</b>\
</p></body></html>'

เราต้องการนำข้อมูล html_content นี้ไปสร้าง BeautifulSoup object ซึ่งในขณะนี้ อ่านไม่ออกเลย 😅 แต่จากบทความ Part 1 สังเกตว่าข้อมูลนี้อยู่ในรูปแบบ bytes (สังเกตตัว b หน้าสุด ในค่าของ html_content) แสดงว่าต้องถูกเข้ารหัส (encoded) อยู่แน่ ๆ 🤔 ถ้าเราหาตัวถอดรหัส (decoder) ที่ถูกต้องได้ เราก็จะมีข้อความที่อ่านออก ไปทำการวิเคราะห์ต่อได้

ตอนนี้เรารู้จัก decoder หลายตัวแล้ว เช่น UTF-8, UTF-32, ASCII, แต่นอกจากนี้ยังมีเช่น ISO-8859-1 ที่บทความนี้จะไม่ขอกล่าวถึงในรายละเอียด แต่ ณ ตอนนี้ เราสามารถลองสิ่งต่อไปนี้

# UnicodeDecodeError: can't decode bytes in position 0-3 ...
html_content.decode("utf-32")

# Success!
html_content.decode()

เราจะพบว่า UTF-8 สามารถ decode bytes sequence นี้ออกมาได้สำเร็จ! นั่นหมายความว่า เราสามารถสร้าง BeautifulSoup เพื่อวิเคราะห์ข้อมูลที่ได้มาดัง Code Block 7 และจะได้ผลลัพธ์ออกมาดัง Code Block 8

from bs4 import BeautifulSoup
soup = BeautifulSoup(html_content, 'lxml', from_encoding='UTF-8')
print(soup.prettify())
<html>
 <head>
  <title>
   รหัสหน่วยงานบริการสุขภาพ กยผ.
  </title>
 </head>
 <body>
  <p class="title">
   <b>
    Very Important Data! ค้นหาข้อมูลรหัสหน่วยงาน
   </b>
  </p>
 </body>
</html>

จริง ๆ แล้ว ใน BeautifulSoup จะมีความฉลาดอยู่คือ ถ้าเราลองสร้าง

soup = BeautifulSoup(html_content, 'lxml')

โดยไม่ใบ้ encoding ให้เลย BeautifulSoup ก็สามารถที่จะเดา encoding ที่ใช้ และอาจจะถอดรหัสได้ถูกต้องอยู่ดี แต่การที่เราเข้าใจข้อมูล bytes sequence ที่เราได้มา และสามารถลองผิดลองถูกเพื่อหาการถอดรหัสที่ถูกต้องได้เอง ก็จะทำให้เราสามารถ debug ข้ามผ่านขั้นตอนการเตรียมข้อมูลภาษาไทยนี้ก่อนทำการวิเคราะห์ต่อไปได้อย่างรวดเร็ว

บทสรุป

รูปที่ 1 เว็บไซต์ทางการของ Unicode (home.unicode.org)

Unicode เป็นมาตรฐานสากลที่รองรับการสื่อสารกันทางข้อความด้วยภาษาต่าง ๆ ทั่วโลก ในมินิซีรีส์นี้ เราได้เรียนรู้ถึงหลักการทำงานเบื้องต้น, วิธีที่ Unicode จัดระเบียบอักขระให้เก็บเลขอัตลักษณ์ได้โดยไม่ซ้ำกัน, การแสดงผล การเข้ารหัสและถอดรหัสของ Unicode, มาตรฐานที่เกี่ยวข้องอย่าง ASCII, และสถานการณ์ที่ความรู้เหล่านี้มีประโยชน์ในการใช้งานจริงบน Python

การสร้างความคุ้นเคยกับ concepts เหล่านี้ จะทำให้เราเล่นกับข้อมูลประเภทข้อความผ่าน APIs และ libraries ที่เกี่ยวข้องได้อย่างชำนาญขึ้น สามารถ debug ผลลัพธ์ที่ไม่เป็นไปตามคาดได้รวดเร็วขึ้น เพราะเมื่อเราเจอกับข้อมูลที่อ่านไม่ออก เราจะพอเดาได้ว่ามันคืออะไร (เช่น encoding ข้อความที่อยู่ในชนิดตัวแปร bytes, หรือข้อมูลกำลังอยู่ในรูปแบบ code points) ผมหวังว่าบทความนี้จะช่วยสร้างพื้นฐานที่สำคัญส่วนหนึ่งสำหรับการวิเคราะห์ข้อมูล Big Data ภาษาไทย และข้อมูลข้อความอื่น ๆ ต่อไปครับ 😊

Recommended Posts