บทที่ 4 การประมวลผลข้อมูลจากไฟล์#
ข้อมูลที่เราต้องการนำมาประมวลผลมักจะถูกจัดเก็บในรูปของไฟล์ เมื่อเราได้ไฟล์มาแล้วเราจะต้องเขียนโปรแกรมเพื่อเปิดไฟล์ขึ้นมา และทำความสะอาดข้อมูล (เช่น นำส่วนที่ไม่ใช่ข้อมูลจริง ๆ ออกไป หรือนำส่วนที่ไม่ใช่ข้อความออกไป) จากนั้นนำมาตัดคำ หรือใช้เครื่องมือการประมวลผลภาษาอื่น ๆ มาช่วย
ในบทนี้เราจะเรียนรู้วิธีการเปิดไฟล์ และเขียนข้อมูลลงไฟล์ รวมถึงการใช้นิพจน์ปรกติ (regular exprssion) หรือเรกเอกซ์ (RegEx) ซึ่งต่อไปจะเรียกอย่างย่อว่า “เรกเอกซ์” ในการทำความสะอาดข้อมูล และดึงเฉพาะส่วนของข้อมูลที่เราต้องการทำไปวิเคราะห์ต่อไป
ไฟล์#
ไฟล์สามารถถูกแบ่งออกได้เป็น 2 ประเภท
ไฟล์ไบนารี (binary file) เป็นไฟล์ที่เราไม่สามารถเปิดอ่านเป็นตัวหนังสือได้ ข้อมูลถูกจัดเก็บรหัสประเภทอื่น ที่จะต้องใช้โปรแกรมเฉพาะเจาะจงในการเปิดอ่านและประมวลผล เช่น ไฟล์ภาพ ไฟล์เพลง ไฟล์ .docx ซึ่งต้องใช้โปรแกรม Microsoft Word ในการเปิดประมวลผล หรือ .xlsx ซึ่งต้องใช้โปรแกรม Microsoft Excel ในการเปิดประมวลผล ไฟล์เหล่านี้มีไลบรารีภาษาไพทอนที่สามารถใช้เปิดเพื่อนำข้อมูลมาประมวลผลได้เช่นกัน
ไฟล์ที่มนุษย์อ่านได้ (human-readable file) เป็นไฟล์ที่เก็บข้อความไว้สามารถใช้ IDE ในการเปิดอ่านได้โดยตรง ไฟล์เหล่านี้เป็นไฟล์ที่ไม่ได้เก็บอะไรไว้เลยนอกเหนือจากข้อความอย่างเดียว ในบทนี้เราจะรับมือกับไฟล์ประเภทนี้เพียงอย่างเดียว ตัวอย่างเช่น .html .js .csv .json
ไฟล์ทั้งสองแบบมักจะมีนามสกุลของไฟล์ (file extension) เพื่อบ่งบอกว่าควรจะเปิดขึ้นมาใช้ได้อย่างไร เช่น .docx .xlsx .png เป็นต้น แต่ที่จริงแล้วเราสามารถตั้งนามสกุลไฟล์เป็นอะไรก็ได้ตามแต่ใจเรา แต่ถ้าไฟล์เป็นข้อความดิบล้วน ๆ เรามักจะตั้งว่าเป็น .txt
ทั้งนี้การตั้งชื่อและสกุลของไฟล์นั้นไม่มีผลต่อตัวข้อมูลที่เก็บในไฟล์ใด ๆ ทั้งสิ้น แต่อาจจะทำให้เกิดความสับสน ถ้าเราจะตั้งสกุลของไฟล์ที่เก็บข้อความเป็น .mytext ก็สามารถทำได้และไม่ได้มีผลต่อข้อความที่เก็บอยู่ในไฟล์ แต่ก็จะไม่เป็นมาตรฐาน เมื่อส่งไฟล์ต่อให้คนอื่น ก็จะไม่ทราบแน่ชัดว่าไฟล์นั้นเก็บอะไรไว้อยู่ จะต้องเปิดด้วยวิธีใด หรือจะตั้งหลอกว่าเป็น .docx ก็ได้เช่นกัน แต่ว่าผู้ที่นำไฟล์ไปใช้ต่อก็อาจจะนำไปใช้ผิดวิธี พอเปิดด้วย Microsoft Word ก็อาจจะไม่ได้ หรือไม่ได้ผลตามที่ควรจะเป็น
ในบทนี้เราจะเรียนรู้คำสั่งที่ใช้ในการเปิดไฟล์โดยทั่วไป แต่จะเน้นไปที่ไฟล์ข้อความ (text file) เท่านั้นเพราะเป็นระบบที่ใช้ในการเก็บข้อมูลที่เป็นข้อความ (text data) อย่างเป็นสากล
การระบุพาท#
วิถี หรือ พาท (path) หรือชื่อพาท (pathname) เป็นสตริงที่ไว้ระบุที่อยู่ของไฟล์ที่อยู่บนเครื่องของเรา เพื่อที่เราจะได้เขียนคำสั่งได้ถูกต้องว่าไฟล์ที่เราต้องการจะเปิดนั้นอยู่ที่ไหนบนเครื่อง หรือไฟล์ที่เราต้องการจะสร้างนั้นมีชื่อว่าอะไรอยู่ไหน เพราะว่าไฟล์ต่าง ๆ ถูกเก็บอยู่ในโฟลเดอร์ (folder) ซึ่งในโฟลเดอร์นั้นอาจจะมีโฟลเดอร์อื่น ๆ ซ้อนกันอยู่ เราสามารถเปรียบเทียบการจัดเก็บไฟล์กับการจัดเรียงเอกสารในตู้เก็บเอกสาร โดยที่แต่ละโฟลเดอร์ในระบบไฟล์ทำหน้าที่เหมือนกับลิ้นชักในตู้เอกสาร เมื่อเราต้องการเข้าถึงไฟล์ที่ต้องการ เราต้องระบุเส้นทางที่ถูกต้องจากโฟลเดอร์หลักไปยังโฟลเดอร์ย่อยจนถึงไฟล์ที่ต้องการ
พาทเต็ม#
พาทเต็ม (full path หรือ absolute path) คือการเขียนพาทแบบเต็ม ๆ ประกอบไปด้วย 3 ส่วนสำหรับ Windows และ 2 ส่วนสำหรับระบบปฏิบัติการ (Operating system: OS) MacOS และ Linux
(Windows เท่านั้น) ชื่อไดรฟ์ตามด้วยเครื่องหมาย :
ชื่อโฟลเดอร์และโฟลเดอร์ย่อย ๆ คั่นด้วยเครื่องหมาย \ สำหรับ Windows และ / สำหรับ OS อื่น ๆ
ชื่อไฟล์
ตัวอย่าง (Windows)
พาทเต็ม |
ไดรฟ์ |
โฟลเดอร์ |
ชื่อไฟล์ |
---|---|---|---|
|
C |
|
|
|
C |
|
|
ตัวอย่าง (MacOS)
พาทเต็ม |
โฟลเดอร์ |
ชื่อไฟล์ |
---|---|---|
|
|
|
|
|
|
พาทสัมพัทธ์#
การระบุพาทเต็มค่อนข้างยืดยาว พิมพ์แล้วมีโอกาสผิดสูง พาทสัมพัทธ์ (relative path) เป็นทางเลือกที่สะดวกกว่า เพราะว่าอ้างอิงจากตำแหน่งปัจจุบันของผู้ใช้ไปยังตำแหน่งของไฟล์ พาทสัมพัทธ์จะใช้พาทของโฟลเดอร์ที่เรารันโค้ดมาต่อกันกับพาทสัมพัทธ์เพื่อให้กลายเป็นพาทแบบเต็ม เช่น ถ้าเรารันโค้ดที่โฟลเดอร์ /Users/te/Prog NLP
และสั่งให้เปิดไฟล์ที่พาทสัมพัทธ์ data/raw_text.zip
จะถูกแปลงให้กลายเป็น /Users/te/Prog NLP/data/raw_text.zip
โดยอัตโนมัติ
นอกจากนั้นเรายังสามารถระบุให้ถอยลงไปหนึ่งโฟลเดอร์หรือหลาย ๆ โฟลเดอร์ได้โดยการใช้ ..
เช่น ถ้าเรารันโค้ดที่โฟลเดอร์ /Users/te/Prog NLP
พาทสัมพัทธ์ |
ถูกแปลงเป็นพาทเต็ม |
---|---|
|
|
|
|
|
|
การอ่านเขียนไฟล์ข้อมูล#
ไฟล์ถูกจัดเก็บไว้ในฮาร์ดดิสก์ ซึ่งเป็นหน่วยความจำถาวรที่ทำหน้าที่เก็บรักษาข้อมูลไว้อย่างถาวร ฮาร์ดดิสก์แต่ละเครื่องมีขนาดความจุไม่เท่ากัน โดยไฟล์ขนาด 1 เมกะไบต์ (megabyte: MB) หรือเท่ากับ 1,024,000 ไบต์ สามารถเก็บตัวอักษรได้ประมาณ 500,000 - 1,000,000 ตัว ขึ้นอยู่กับรูปแบบการเข้ารหัสตัวอักษร (encoding) ที่ใช้ เมื่อเราต้องการนำข้อมูลในไฟล์มาประมวลผล จะต้องคัดลอกข้อมูลจากฮาร์ดดิสก์ไปยังแรม (RAM) ซึ่งเป็นหน่วยความจำชั่วคราว ข้อมูลในแรมจะหายไปทันทีเมื่อปิดเครื่อง ในปัจจุบันเครื่องคอมพิวเตอร์สำนักงานทั่วไปมักจะมีแรมมีความจุอยู่ที่ 4 - 16 กิกะไบต์ (gigabyte: GB) ซึ่ง 1GB เท่ากับ 1,024 MB หรือ 1,073,741,824 ไบต์ ซึ่งเล็กกว่าความจุของฮาร์ดดิสก์มาก ๆ เครื่องคอมพิวเตอร์สำนักงานในปัจจุบันมักจะมีฮาร์ดดิสก์ที่มีความจุอยู่ที่ 250 - 500 กิกะไบต์
คุณลักษณะ |
แรม |
ฮาร์ดดิสก์ |
---|---|---|
หน้าที่ |
ใช้เก็บข้อมูลชั่วคราวเพื่อใช้ในการประมวลผล |
ใช้เก็บข้อมูลที่ต้องการเก็บไว้ถาวร |
ความเร็ว |
เร็วมาก |
เร็วแต่ช้ากว่าแรม |
ความจุ |
4 - 16 GB |
250 - 500 GB |
การเก็บข้อมูล |
ข้อมูลหายไปเมื่อปิดเครื่อง |
ข้อมูลยังคงอยู่เมื่อปิดเครื่อง |
ราคา (บาท/GB) |
แพง |
ถูก |
การอ่านเขียนไฟล์ข้อมูล (File Input/Output หรือ File I/O) เป็นกระบวนการสำคัญในการจัดการข้อมูลในระบบคอมพิวเตอร์ ซึ่งหมายถึงการอ่านไฟล์จากฮาร์ดดิสก์มาลงในแรม และการเขียนข้อมูลจากแรมกลับไปยังฮาร์ดดิสก์ การประมวลผลข้อมูลทำได้บนข้อมูลที่อยู่ในแรมเท่านั้น เนื่องจากแรมมีความเร็วสูงกว่าฮาร์ดดิสก์มากเมื่อต้องมาใช้รวมกันกับโปรแกรมที่เราเขียนขึ้น ทำให้การเข้าถึงและประมวลผลข้อมูลทำได้รวดเร็วขึ้น
หากไฟล์มีขนาดใหญ่มาก ๆ เราอาจไม่สามารถโหลดไฟล์ทั้งหมดลงในแรมได้ในครั้งเดียว เราจึงต้องมีรูปแบบการอ่านไฟล์ที่หลากหลายเพื่อให้เหมาะสมกับขนาดของไฟล์และขนาดของหน่วยความจำที่มีอยู่ เช่น การอ่านไฟล์แบบบรรทัดต่อบรรทัด จะช่วยให้เราสามารถจัดการกับไฟล์ขนาดใหญ่ได้อย่างมีประสิทธิภาพ และลดความเสี่ยงที่จะเกิดข้อผิดพลาดจากการที่หน่วยความจำไม่เพียงพอในการจัดการไฟล์ขนาดใหญ่ ยกตัวอย่างเช่น ในการประมวลผลข้อมูลจากไฟล์ข้อมูลขนาดใหญ่ เช่น ไฟล์ฐานข้อมูลหรือไฟล์ล็อก (log file) ซึ่งอาจจะมีขนาดใหญ่หลายกิกะไบต์ เราอาจเลือกใช้วิธีการอ่านไฟล์แบบบรรทัดต่อบรรทัด เพื่อให้สามารถประมวลผลข้อมูลได้ทีละส่วน โดยไม่ต้องโหลดไฟล์ทั้งหมดลงในแรมพร้อมกัน
อ่านไฟล์ไล่ทีละบรรทัด#
ไพทอนมีคำสั่ง open()
เป็นฟังก์ชันแบบบิวท์อินเพื่อใช้เปิดไฟล์ คำสั่งนี้ต้องการพาทเป็นอาร์กิวเมนต์เพื่อระบุว่าจะเปิดไฟล์ใด และไฟล์นั้นอยู่ที่ไหน ซึ่งจะใช้พาทเต็มหรือพาทสัมพัทธ์ก็ได้
สมมติว่าเรามีไฟล์ชื่อว่า lydia.txt ซึ่งเป็นข้อมูลเนื้อเพลงที่เราขูดออกมาจากหน้าเว็บ จึงมีความไม่สมบูรณ์อยู่บ้าง
ไม่ว่างจริง ๆ
อะหรือว่า
มีคนอื่น
ต่อสายเธอทั้งคืน
ก็เจอแต่ฮืมฝากข้อความ
หาย
ไปเลย
เราสามารถใช้คำสั่ง open
และเมท็อด .readline
เพื่อแสดงผลบรรทัดที่มีข้อความอยู่ดังนี้
song_file = open('lydia.txt')
for line in song_file:
if line != '\n': # \n เป็นตัวอักขระลงท้ายบรรทัด
print(line)
song_file.close()
ซึ่งจะแสดงผลดังนี้
ไม่ว่างจริง ๆ
อะหรือว่า
มีคนอื่น
ต่อสายเธอทั้งคืน
ก็เจอแต่ฮืมฝากข้อความ
หาย
ไปเลย
โปรแกรมตัวอย่างข้างบนใช้คำสั่ง open
ซึ่งคืนค่าตัวอ่านไฟล์ จากนั้นเราจะสามารถวนซ้ำไปบนแต่ละบรรทัดที่อยู่ในไฟล์ซึ่งโปรแกรมจะมองหาสัญลักษณ์ \n
(newline) ที่อยู่ในไฟล์เป็นจุดที่แบ่งบรรทัดในข้อความ และคืนค่าสตริงนำไปเก็บไว้ในตัวแปรเพื่อไปใช้ในลูปต่อไป เมื่อเราเปิดไฟล์แล้วควรจะปิดไฟล์ทุกครั้ง เนื่องจากเมื่อเราเปิดไฟล์แล้ว ระบบจัดการไฟล์ของเครื่องคอมพิวเตอร์ของเราอาจจะปิดกั้น (lock) ไฟล์ไฟล์นั้นไว้ เพื่อป้องกันไม่ให้โปรแกรมมาเขียนทับในระหว่างที่เราอ่านอยู่ ดังตัวอย่างข้างบน เราปิดไฟล์ด้วยเมท็อด .close()
สตริงที่ได้มาจากการวนซ้ำไปบนไฟล์จะลงท้ายด้วย \n
เสมอเพราะว่าเป็นตัวแบ่งบรรทัด และคำสั่ง print
จะเติม \n
เข้าไปให้อีกตัวด้วย เพราะฉะนั้นในตัวอย่างข้างบนเราจะได้บรรทัดว่าง ระหว่างเนื้อร้องแต่ละวรรค (\n\n
ติดกัน)
ถ้าหากอยากแสดงผลให้สวยงามขึ้น เราต้องใช้คำสั่ง .strip()
เพื่อเอา \n
ออกไปจากสตริงก่อนจะเรียก print
song_file = open('lydia.txt')
for line in song_file:
if line != '\n': # \n เป็นตัวอักขระลงท้ายบรรทัด
print(line.strip())
song_file.close()
หรือเราจะลัดโดยการไม่เก็บตัวอ่านไฟล์ไว้ในตัวแปรเลยก็ได้ ดังนี้
for line in open('lydia.txt'):
if line != '\n': # \n เป็นตัวอักขระลงท้ายบรรทัด
print(line.strip())
ซึ่งจะแสดงผลดังนี้
ไม่ว่างจริง ๆ
อะหรือว่า
มีคนอื่น
ต่อสายเธอทั้งคืน
ก็เจอแต่ฮืมฝากข้อความ
หาย
ไปเลย
เปิดไฟล์ด้วยคำสั่ง with
#
วิธีนี้เป็นวิธีที่ปลอดภัยที่สุดในเปิดปิดไฟล์ เพราะเป็นการป้องกันการลืมปิดไฟล์ไปในตัว การใช้คำสั่ง with
เป็นการจำกัดว่าเราจะใช้งานไฟล์นั้นจากจุดใดถึงจุดใด ตัวอย่าง เช่น
with open('lydia.txt') as songfile:
for line in songfile:
if line != '\n': # \n เป็นตัวอักขระลงท้ายบรรทัด
print(line.strip())
สังเกตว่า with
เป็นคำสงวนในภาษาไพทอน และบังคับให้เราใช้ไฟล์ได้ในเฉพาะโค้ดบล็อก ที่อยู่ใต้ with
เท่านั้นโค้ดที่ไม่ได้อยู่ในโค้ดบล็อกนั้นจะไม่สามารถใช้ไฟล์นั้นได้อีกต่อไป เนื่องจาก with
จะปิดไฟล์ให้โดยอัตโนมัติหลังจากรันโค้ดที่อยู่ในโค้ดบล็อกนั้นเสร็จสิ้นแล้ว หากเราพยายามอ่านจากไฟล์นั้นอีกจะได้ข้อผิดพลาดดังนี้
ValueError: I/O operation on closed file.
การเปิดปิดไฟล์ในวิธีนี้มีข้อดีคือ เราจำกัดการเข้าถึงไฟล์ที่เปิดให้อยู่ในโค้ดบล็อกเท่านั้น เราสามารถอ่านหรือเปลี่ยนแปลงไฟล์ในช่วงโค้ดที่จำกัด ทำให้เราไม่ลืมปิดไฟล์ และทำให้เรารู้ว่าไฟล์ถูกเปลี่ยนแปลงโดยส่วนไหนของโค้ดของเราบ้าง
อ่านไฟล์และถ่ายข้อมูลทั้งหมดใส่สตริง: .read()
#
เราสามารถเก็บข้อมูลทั้งหมดในไฟล์ใส่สตริงโดยใช้เมท็อด .read()
song_lyrics = open('lydia.txt').read()
print(song_lyrics)
ผลที่ได้ออกมาคือ
ไม่ว่างจริง ๆ
อะหรือว่า
มีคนอื่น
ต่อสายเธอทั้งคืน
ก็เจอแต่ฮืมฝากข้อความ
หาย
ไปเลย
คำสั่งนี้ค่อนข้างสะดวกถ้าเราต้องการประมวลผลข้อความทั้งไฟล์ โดยเฉพาะอย่างยิ่งเวลาเราต้องการขูดข้อมูลออกจากไฟล์จำนวนหลายไฟล์
ข้อควรระวังคือ การใช้คำสั่งนี้เป็นการคัดลอกข้อมูลจากไฟล์ซึ่งอยู่ในฮาร์ดดิสก์ของเครื่องคอมพิวเตอร์ซึ่งมักจะมีความจุมาก ๆ เช่น 250 GB นำมาใส่หน่วยความจำชั่วคราว ซึ่งมักจะมีความจุไม่มาก เช่น 16 GB ซึ่งต้องแบ่งให้กับโปรแกรมอื่น ๆ หรือตัวแปรอื่น ๆ ที่อยู่ในโปรแกรมของเราเอง ดังนั้นหากเราใช้คำสั่ง .read()
เป็นการนำข้อมูลทั้งหมดในไฟล์นั้นมาเก็บใส่ตัวแปรซึ่งเก็บข้อมูลไว้ในแรม ถ้าไฟล์นั้นมีขนาดใหญ่กว่าหรือเทียบเท่าความจุของแรมที่เหลือ เครื่องคอมพิวเตอร์อาจจะหยุดทำงานหรือค้าง เนื่องจากหน่วยความจำถูกนำไปเก็บข้อมูลจากไฟล์จนไม่เหลือให้กับโปรแกรมอื่น ๆ ที่เรารันอยู่บนเครื่อง
ถ้าหากไฟล์ที่ต้องการประมวลผลมีขนาดใหญ่ และมีหลายบรรทัด แนะนำให้ใช้ for
เพื่อค่อยลำเลียงข้อมูลเข้ามาประมวลผลทีละบรรทัด หน่วยความจำแรม ของเครื่องเก็บข้อมูลไว้แค่ทีละหนึ่งบรรทัดเท่านั้น พอประมวลผลเสร็จหนึ่งบรรทัด พื้นที่หน่วยความจำก็จะถูกแทนที่ด้วยข้อมูลจากบรรทัดถัดไป เช่นนี้ไปเรื่อย ๆ
อ่านไฟล์เพียงบางบรรทัด: .readline()
#
ในบางกรณีเราไม่ต้องการอ่านไฟล์ทั้งไฟล์ ทุกบรรทัด เราสามารถใช้เมท็อด .readline()
ในการดึงข้อมูลออกมาทีละบรรทัด สมมติว่าเราต้องการเขียนโปรแกรมที่ประมวลผลเพียงสามบรรทัดแรกเท่านั้น
with open('lydia.txt') as songfile:
line1 = songfile.readline()
line2 = songfile.readline()
print (line1)
print (line2)
ผลลัพธ์ที่ได้ คือ
ไม่ว่างจริง ๆ
อะหรือว่า
ข้อสังเกตคือตัวเปิดไฟล์ ซึ่งเก็บอยู่ในตัวแปร songfile
จะเก็บสถานะว่าเราอ่านไฟล์ไปถึงบรรทัดใดแล้ว เมื่อเราเรียก .readline
ครั้งต่อมาก็จะอ่านไฟล์ต่อจากการเรียก .readline
ครั้งที่แล้ว
ดังนั้นหากเราต้องการอ่านไฟล์แค่บรรทัดสุดท้าย เราต้องไล่อ่านตั้งแต่บรรทัดแรกไปจนถึงบรรทัดสุดท้าย เนื่องจากระบบจัดเก็บไฟล์ของเครื่องคอมพิวเตอร์ไม่ได้รองรับการอ่านข้อมูลจากท้ายไฟล์มายังต้นไฟล์ การอ่านไฟล์ต้องเริ่มต้นจากต้นไฟล์ (ตัวอักษรตัวแรกของไฟล์) เสมอ
อ่านไฟล์และถ่ายข้อมูลทั้งหมดใส่ลิสต์: .readlines()
#
เราสามารถอ่านไฟล์แล้วเก็บใส่ลิสต์ที่สมาชิกแต่ละตัวเป็นข้อความแต่ละบรรทัดที่อยู่ในไฟล์ คำสั่งนี้เป็นการรวม .read()
ซึ่งอ่านไฟล์ทั้งไฟล์จะย้ายมาเก็บในตัวแปร และ .readline()
ซึ่งอ่านไฟล์โดยการแบ่งบรรทัด เพราะฉะนั้นมีข้อควรระวังในการใช้เหมือนการใช้ .read
ก็คือห้ามเปิดไฟล์ที่มีขนาดใหญ่เกินไป เพราะเครื่องคอมพิวเตอร์อาจจะมีหน่วยความจำไม่พอในการเก็บข้อมูล
lines = open('lydia.txt').readlines()
lines[0] #--> 'ไม่ว่างจริง ๆ\n'
lines[1] #--> 'อะหรือว่า\n'
เขียนสตริงใส่ไฟล์#
เมื่อเราประมวลผลข้อมูลเสร็จแล้ว เรามักจะต้องบันทึกข้อมูลจากสตริงลงไฟล์ โดยเฉพาะอย่างยิ่งถ้าสตริงเริ่มมีขนาดใหญ่เกินกว่าความจุของแรม เราต้องทยอยถ่ายข้อมูลใส่ไฟล์ซึ่งอาศัยอยู่ในฮาร์ดดิสก์
วิธีการเขียนสตริงลงไฟล์ ให้เราเปิดไฟล์ขึ้นมาโดยระบุ mode
เป็น w
(write) แล้วใช้เมท็อด .write
เช่น
with open('my_fav_song.txt', mode='w') as f:
f.write('ไม่ว่างจริง ๆ\n')
f.write('อะหรือว่ามีคนอื่น\n')
ข้อควรระวังในการเขียนไฟล์มีดังนี้
ต้องตั้ง
mode='w'
เพราะว่าไพทอนจะเปิดไฟล์เพื่ออ่านไฟล์ไปโดยปริยาย ถ้าไม่ตั้งเป็นโหมดเขียน ไพทอนจะพยายามหาไฟล์นั้นเพื่อเปิดขึ้นมาอ่าน แต่ไฟล์นั้นไม่ได้มีอยู่ก่อนแล้ว เครื่องก็จะคืนค่าFileNotFoundError
เขียนได้เฉพาะสตริงเท่านั้น ถ้าพยายาม
.write
ตัวเลขหรือลิสต์ จะได้TypeError
ต้องใส่ไว้ท้ายสตริง
\n'
ถ้าหากต้องการแบ่งข้อความเป็นบรรทัด ไม่ยาวพืดเดียว
เขียนดิกชันนารี หรือลิสต์ใส่ไฟล์โดยใช้ไลบรารี json
#
ถ้าข้อมูลของเราอยู่ในรูปของดิกชันนารี ลิสต์ ตัวเลข และสตริง เราสามารถจัดเก็บในรูปแบบของ JSON (อ่านว่า เจซอน หรือ เจสัน) ซึ่งจะแปลงโครงสร้างข้อมูลให้เป็นสตริง เพื่อที่จะนำเขียนลงไฟล์ในรูปแบบที่มนุษย์อ่านได้
ไพทอนมีไลบรารี json
ที่มีฟังก์ชันที่ช่วยในการอ่านและเขียน JSON เช่น
import json
lyrics = ['Lydia', 'ไม่ว่างจริง ๆ', 'หรือว่ามีคนอื่น', 'ต่อสายเธอทั้งคืน']
with open('my_fav_song.json', mode='w') as f:
json.dump(lyrics, f)
เรามักจะตั้งสกุลของไฟล์เป็น .json เพื่อทำให้เราและคนอื่น ๆ ทราบว่าจะต้องเปิดไฟล์ด้วยวิธีใด ถ้าหากเราลองเปิดไฟล์โดยใช้โปรแกรมแก้ไขข้อความ เช่น VSCode หรือ Notepad++ หรือ Sublime เราจะเห็นไฟล์ดังนี้
["Lydia", "\u0e44\u0e21\u0e48\u0e27\u0e48\u0e32\u0e07\u0e08\u0e23\u0e34\u0e07 \u0e46", "\u0e2b\u0e23\u0e37\u0e2d\u0e27\u0e48\u0e32\u0e21\u0e35\u0e04\u0e19\u0e2d\u0e37\u0e48\u0e19", "\u0e15\u0e48\u0e2d\u0e2a\u0e32\u0e22\u0e40\u0e18\u0e2d\u0e17\u0e31\u0e49\u0e07\u0e04\u0e37\u0e19"]
ซึ่งดูแล้วไม่เหมือนกับไฟล์ที่มนุษย์อ่านได้เท่าไร เราจะอ่านออกเฉพาะตัวลาตินของภาษาอังกฤษ ที่จริงตัวหนังสือภาษาไทยถูกจัดเก็บในรูปของรหัส \uXXXX
ซึ่งเครื่องจะใช้การถอดรหัสแบบยูนิโค้ด (unicode) ในการแปลว่า 0e44
คือ ไ
โดยการเปิดหาในสมุดรหัสทีละตัว ที่ JSON จัดเก็บข้อมูลแบบนี้โดยปริยาย เนื่องจากว่าในสมัยก่อนเครื่องคอมพิวเตอร์บางเครื่องสามารถอ่านหรือแสดงผลได้แค่ตัวอักษรแบบ ascii ซึ่งประกอบไปด้วยอักษรภาษาอังกฤษ ตัวเลข และเครื่องหมายวรรคตอนบางตัวเท่านั้น ไม่สามารถอ่านและแสดงผลตัวอักษรไทย emoji หรือตัวอักษรในระบบการเขียนอื่น ๆ ได้
หากเราต้องการให้ไฟล์เจซอนสามารถอ่านตัวภาษาไทย หรือตัวอักษรประเภทอื่น ๆ ที่ไม่ใช่ตัวลาตินได้ เราต้องตั้ง ensure_ascii=False
ซึ่งแปลว่าไม่ต้องจัดเก็บเป็นตัว ascii
import json
lyrics = ['Lydia', 'ไม่ว่างจริง ๆ', 'หรือว่ามีคนอื่น', 'ต่อสายเธอทั้งคืน']
with open('my_fav_song.json', mode='w') as f:
json.dump(lyrics, f, ensure_ascii=False)
หากต้องการเปิดไฟล์ json ให้ใช้ฟังก์ชัน json.load
with open('my_fav_song.json') as f:
lyrics = json.load(f)
วิธีนี้ใช้ได้ผลไม่ว่าตอนจัดเก็บเราจะจัดเก็บแบบโดยตั้ง ensure_ascii
ด้วยหรือไม่ เพราะเครื่องคอมพิวเตอร์ในปัจจุบันสามารถประมวลผลยูนิโค้ดได้เป็นปกติ
นิพจน์ปรกติ#
นิพจน์ปรกติ (regular expression) โปรแกรมเมอร์เรียกกันย่อ ๆ ว่าเรกเอกซ์ (regex อ่านว่า /regeks/) เป็นภาษาในการระบุเขียนจับแพตเทิร์นของตัวอักษรที่เราต้องการตรวจหา เพื่อที่จะได้ดึงหรือแก้ไขข้อมูลที่อยากได้โดยสะดวก ตัวอย่างเช่น
ตรวจหาเบอร์โทรศัพท์ซึ่งมีแพตเทิร์นว่า มีตัวเลขทั้งหมด 10 ตัวแต่ตัวแรกต้องเป็น 0 อีก 9 ตัวเป็นอะไรก็ได้ แต่ถ้าตัวเลขมีทั้งหมด 9 ตัว ตัวแรกไม่ต้องเป็น 0 ก็ได้
ตรวจหาอีเมลของพนักงานทุกคนที่มาจากภาครัฐ ซึ่งมีแพตเทิร์นว่า ข้างหน้าเครื่องหมาย @ เป็นตัวภาษาอังกฤษหรือจุดหรือตัวเลขอะไรก็ได้ แต่ข้างหลังเครื่องหมาย @ ต้องเป็นชื่อองค์กรอะไรก็ได้แต่ต้องลงท้ายด้วย .go.th
ตรวจหาอักษรย่อขององค์กรที่เป็นภาษาไทย ซึ่งมีแพตเทิร์นว่า ต้องเป็นตัวอักษรไทย 2 - 4 ตัวและลงท้ายด้วย . แพตเทิร์นเหล่านี้ยากที่จะอธิบายให้เป็นตัวอักษร เรกเอกซ์เป็นภาษาสื่อกลางที่ช่วยให้เราอธิบายแพตเทิร์นที่เราต้องการให้ได้อย่างชัดเจน
เรกเอกซ์มีความสำคัญมากๆ ในการทำงานเกี่ยวกับการประมวลผลภาษาธรรมชาติ เพราะทำให้เราสามารถดึงข้อมูลที่มีแพตเทิร์นเด่นชัดในภาษา (เช่น อีเมล์ เบอร์โทรศัพท์ ชื่อ ตัวย่อ) ออกมาจากข้อความที่เราต้องการประมวลผลได้สะดวก ความสำคัญของเรกเอกซ์อีกประการหนึ่งคือ การทำความสะอาดข้อมูล ข้อมูลภาษาที่เราได้รับมามักจะมีข้อความแปลกปลอมหลากหลายประเภทที่จำเป็นต้องกรองออกก่อน ตัวอย่างเช่น ข้อความที่ดาวน์โหลดมาจากทวิตเตอร์อาจจะมีข้อความที่เป็น hashtag หรือ mention ที่เราต้องการกรองออกไป หรือข้อความที่มีลิงก์ที่เราต้องการแปลงเป็นชื่อเว็บไซต์ หรือข้อความที่มีตัวอักษรพิเศษที่เราต้องการลบออกไป เป็นต้น
สัญลักษณ์ที่ใช้ในนิพจน์ปรกติ#
เรกเอกซ์มีความพิเศษกว่าสตริงปกติ คือการตีความหมายสัญลักษณ์พิเศษต่าง ๆ ที่ช่วยระบุแพตเทิร์นที่ซับซ้อนและยืดหยุ่นมาก เช่น
[a-z.]+@[a-z]+\.com
เป็นเรกเอกซ์สำหรับแพตเทิร์นของ email address ที่ลงท้ายด้วย .com\d\d\d-\d\d\d-\d\d\d\d
เป็นเรกเอกซ์ สำหรับแพตเทิร์นของหมายถึงเลขโทรศัพท์ เช่น 123-123-1234
สัญลักษณ์ที่ใช้ในเรกเอกซ์มีอยู่ 5 ประเภท ได้แก่
อักขระปกติ#
อักขระปกติ (literal character) คือตัวอักษรที่เราต้องการให้เรกเอกซ์จับคู่กับตัวอักษรนั้น ๆ ที่รูปเดียวกัน ดังนั้นอักขระปกติสามารถเป็นตัวอักษรอะไรก็ได้ในระบบยูนิโค้ด แต่ตัวอักษรบางตัวจะมีความหมายพิเศษในเรกเอกซ์ทำให้เราต้องทำการหนี (escape) โดยการเพิ่ม \ เข้าไปข้างหน้า เพื่อบอกว่าเป็นตัวอักษรปกติ ไม่ใช่อักขระพิเศษ ตัวอักษรที่ต้องทำการหนี (escape) ได้แก่ \
+
*
?
^
$
(
)
[
]
{
}
|
และ .
อักขระพิเศษ#
อักขระพิเศษ (metacharacter)เป็นหัวใจสำคัญของการเขียนเรกเอกซ์เพื่อระบุแพตเทิร์น เพราะอักขระพิเศษใช้แทนกลุ่มของตัวอักษร อักขระพิเศษมีหลายตัว แต่ในบทนี้เราจะเรียนรู้เพียงอักขระพิเศษที่ใช้บ่อย ๆ ซึ่งเราควรจะจำให้ได้ทั้งหมด ดังนี้
สัญลักษณ์ |
แทนตัวอะไรบ้าง |
---|---|
. |
ตัวอะไรก็ได้ |
\w |
(word) a-z A-Z 0-9 และ _ (และสำหรับภาษาไพทอน \w แทนตัวอักษรที่ใช้ประกอบเป็นคำในทุกภาษา) |
\W |
อะไรก็ได้ที่ไม่ใช่ \w |
\d |
(digit) ตัวเลข 0-9 |
\D |
อะไรก็ได้ที่ไม่ใช่ \d |
\s |
(space) ช่องว่างรวมถึง \t (แท็บให้ขึ้นย่อหน้าใหม่) \n (newline บ่งบอกการขึ้นบรรทัดใหม่) และ \r (newline แบบของ Windows) |
\S |
อะไรก็ได้ที่ไม่ใช่ \s |
\b |
ตัวแบ่งคำ (แบบภาษาอังกฤษ) |
^ |
หน้าสุดของสตริง |
$ |
ท้ายสุดของสตริง |
ข้อสังเกตอย่างหนึ่งเพื่อให้ช่วยจำได้ง่ายขึ้น คือ \w \d และ \s จะจับคู่กับเวอร์ชั่นที่เป็นตัวพิมพ์ใหญ่ซึ่งเป็นเวอร์ชันที่เป็นนิเสธของ \w \d และ \s ตามลำดับ เช่น \w หมายถึง a-z A-Z 0-9 และ _ แต่ \W เป็นนิเสธของ \w จึงจับกับอะไรก็ได้ที่ไม่ใช่ a-z A-Z 0-9 และ _ ดังนั้น \W จะจับคู่กับตัวอักษรพิเศษ และช่องว่าง แต่ไม่จับคู่กับตัวอักษรทั่วไป เป็นต้น
ตัวอย่างเรกเอกซ์ที่ใช้อักขระพิเศษ 1#
\d\d\d-\d\d\d-\d\d\d\d
เป็นเรกเอกซ์ สำหรับแพตเทิร์นของหมายเลขโทรศัพท์ เริ่มต้นด้วยตัวเลข (\d) 3 ตัว ตามด้วยเครื่องหมายขีดกลาง แล้วตามด้วยตัวเลข (\d) 3 ตัว ตามด้วยเครื่องหมายขีดกลาง แล้วตามด้วยตัวเลข (\d) 4 ตัว
สตริง |
จับคู่กับเรกเอกซ์ |
คำอธิบาย |
---|---|---|
123-123-1234 |
✔️ |
|
123-123-12345 |
บางส่วน |
จับคู่กับ 123-123-1234 เพราะว่ามีตัวเลข 5 อยู่ข้างท้าย |
a123-123-12345 |
บางส่วน |
จับคู่กับ 123-123-1234 เพราะว่ามีตัวเลข a นำหน้า |
a23-123-1234 |
❌ |
เพราะว่า \d ตัวแรกไม่จับกับ a |
๑๒๓-๑๒๓-๑๒๓๔ |
❌ |
เพราะว่า \d จับกับเลขอารบิกเท่านั้น |
ตัวอย่างเรกเอกซ์ที่ใช้อักขระพิเศษ 2#
^\d\d\d-\d\d\d-\d\d\d\d$
เป็นเรกเอกซ์ สำหรับแพตเทิร์นของหมายเลขโทรศัพท์เหมือนกับตัวอย่างข้างต้น แต่ว่าเราใช้เครื่องหมาย ^ เพื่อเพิ่มเงื่อนไขเพิ่มเติมว่าตัวเลขตัวแรกจะต้องอยู่ที่หน้าสุดของสตริง และใช้เครื่องหมาย $ เพื่อเพิ่มเงื่อนไขว่าตัวเลขตัวสุดท้ายจะต้องอยู่ที่ท้ายสุดของสตริง
สตริง |
จับคู่กับเรกเอกซ์ |
คำอธิบาย |
---|---|---|
123-123-1234 |
✔️ |
|
123-123-12345 |
❌ |
เลข 4 ไม่ใช่ตัวสุดท้ายของสตริง |
a123-123-12345 |
❌ |
เลข 1 ไม่ได้อยู่หน้าสุดของสตริง |
ตัวอย่างเรกเอกซ์ที่ใช้อักขระพิเศษ 3#
\w\.\w\.\w\.
เป็นเรกเอกซ์สำหรับแพตเทิร์นของตัวย่อที่มีสามตัวอักษร แต่แต่ละตัวอักษรคั่นด้วยจุด เช่น a.b.c.
a.b.c.
a.b.c.
เป็นต้น อย่าลืมว่าเราต้องทำการหนี .
โดยการใช้ \.
เนื่องจาก .
เป็นอักขระพิเศษ
สตริง |
จับคู่กับเรกเอกซ์ |
คำอธิบาย |
---|---|---|
U.S.A. |
✔️ |
|
ABC |
❌ |
เพราะว่าไม่มีจุดคั่นตามที่ระบุในแพตเทิร์น |
พ.ร.บ. |
✔️ เฉพาะไพทอน |
เพราะว่า \w จับคู่กับตัวอักษรไทยเมื่อใช้ในภาษาไพทอน |
อักขระพิเศษถูกออกแบบมาตอนที่ชุดอักขระมีเพียงแค่ตัวลาตินและเครื่องหมายวรรคตอนเท่านั้น เพราะฉะนั้นถึงแม้ว่า \w ถูกตีความหมายมาให้จับกับตัวอักษรที่ประกอบขึ้นมาเป็นคำ แต่ว่า \w ครอบคลุมถึงสตริงไม่ได้ใช้ได้ตัวลาตินสำหรับภาษาไพทอนเท่านั้น
การจัดเซตของตัวอักษร#
อักขระพิเศษมีข้อจำกัด เพราะมีการจัดกลุ่มมาแน่นอนอยู่แล้ว เช่น ตัวเลข (\d) ตัวอักษรลาติน (\w) ช่องว่าง (\s) แต่ถ้าหากเราต้องการจัดกลุ่มตัวอักษรตามที่เราต้องการเอง เราสามารถทำได้โดยการใช้เครื่องหมายวงเล็บเหลี่ยม []
เพื่อจับกลุ่มตัวอักษร และเครื่องหมายขีดตั้ง |
เพื่อจัดกลุ่มแพตเทิร์น
เครื่องหมาย []
ใช้ในการระบุว่าตัวอักษรใดบ้างที่จะจับคู่กับแพตเทิร์น เช่น [abc]
แปลว่าตัวอักษร a หรือ b หรือ c แต่ถ้าหากเราต้องการให้จับคู่กับตัวอักษรที่ไม่ใช่ a หรือ b หรือ c เราสามารถใช้เครื่องหมาย ^
นำหน้าได้ เช่น [^abc]
แปลว่าตัวอักษรอะไรก็ได้ที่ไม่ใช่ a หรือ b หรือ c สรุปได้ดังนี้
สัญลักษณ์ |
แทนตัวอะไรบ้าง |
ตัวอย่าง |
---|---|---|
|
a e หรือ i |
|
|
ตัวอักษร 1 ตัวที่ไม่ใช่ a e หรือ i |
|
|
aaa หรือ bb หรือ c |
|
เครื่องหมาย -
ที่อยู่ใน []
ใช้สำหรับการไล่ตัวอักษร เช่น
[a-z]
แปลว่าตัวอักษรที่อยู่ใน a ถึง z[0-9]
แปลว่าตัวอักษรที่อยู่ใน 0 ถึง 9 ซึ่งความหมายเหมือนกับ \d
ตัวอย่างการใช้งาน
สัญลักษณ์ |
แทนตัวอะไรบ้าง |
ตัวอย่าง |
---|---|---|
|
b c หรือ d |
|
|
ตัวเลขหนึ่งตัวที่อยู่ในช่วง 0-9 |
|
|
ตัวอักษรหนึ่งตัวที่อยู่ในช่วง a-z หรือ 0-9 |
|
หากต้องการเขียนสัญลักษณ์ที่ให้จับกับเฉพาะตัวอักษรไทย เราจะต้องสร้างเซ็ตที่ไล่ตัวอักษรไทยให้ครบทั้งพยัญชนะ สระ และวรรณยุกต์ ลำดับในการไล่ตัวอักษรในเรกเอกซ์จะอิงจากรหัสในระบบยูนิโค้ด ซึ่งจะไล่ตัวอักษรไทยตามตารางต่อไปนี้
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
A |
B |
C |
D |
E |
F |
|
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
U+0E0x |
ก |
ข |
ฃ |
ค |
ฅ |
ฆ |
ง |
จ |
ฉ |
ช |
ซ |
ฌ |
ญ |
ฎ |
ฏ |
|
U+0E1x |
ฐ |
ฑ |
ฒ |
ณ |
ด |
ต |
ถ |
ท |
ธ |
น |
บ |
ป |
ผ |
ฝ |
พ |
ฟ |
U+0E2x |
ภ |
ม |
ย |
ร |
ฤ |
ล |
ฦ |
ว |
ศ |
ษ |
ส |
ห |
ฬ |
อ |
ฮ |
ฯ |
U+0E3x |
ะ |
ั |
า |
ำ |
ิ |
ี |
ึ |
ื |
ุ |
ู |
ฺ |
฿ |
||||
U+0E4x |
เ |
แ |
โ |
ใ |
ไ |
ๅ |
ๆ |
็ |
่ |
้ |
๊ |
๋ |
๋ |
ํ |
๎ |
๏ |
U+0E5x |
๐ |
๑ |
๒ |
๓ |
๔ |
๕ |
๖ |
๗ |
๘ |
๙ |
๚ |
๛ |
ดังนั้นเซตของตัวอักษรที่ใช้บ่อยสำหรับภาษาไทย มีดังนี้
[ก-๛]
จะจับกับตัวอักษรไทยทุกตัวโดยรวมถึงพินทุ นิคหิต ฟองมัน และสัญลักษณ์เงินบาท[ก-ฮ]
จะจับกับพยัญชนะไทยทุกตัว[ก-์]
จะจับกับพยัญชนะ สระ และวรรณยุกต์ทุกตัว และยังรวมถึงสัญลักษณ์เงินบาท และเครื่องหมายไปรยาลน้อย[ก-์๐-๙]
จะจับกับพยัญชนะ สระ และวรรณยุกต์ทุกตัว เลขไทยทุกตัว และยังรวมถึงสัญลักษณ์เงินบาท และเครื่องหมายไปรยาลน้อย
ตัวบอกปริมาณ#
ตัวบอกปริมาณ (quantifier) ใช้เพื่อกำหนดจำนวนครั้งที่อักขระหรือกลุ่มอักขระสามารถปรากฏในสตริงที่เรากำลังตรวจสอบ เราสามารถระบุปริมาณของตัวอักษรหรือกลุ่มของตัวอักษรโดยการใช้ตัวอักษรร่วมกับตัวบอกปริมาณ การใช้ตัวบอกปริมาณมี 7 แบบ ได้แก่
ตัวบอกปริมาณ |
ความหมาย |
---|---|
+ |
อย่างน้อยหนึ่งตัว (≥ 1) |
* |
กี่ตัวก็ได้ หรือไม่มีสักตัวก็ได้ (≥ 0 ) |
? |
1 ตัว หรือไม่มีก็ได้ (0 หรือ 1) |
{n} |
n ตัวถ้วน |
{m,n} |
อย่างน้อย m ตัว อย่างมาก n ตัว |
{n,} |
อย่างน้อย n ตัว |
{,n} |
อย่างมาก n ตัว |
ตัวอย่าง
เรกเอกซ์ |
แปลว่า |
ตัวอย่างที่จับคู่ |
---|---|---|
\d+ |
ตัวเลขอย่างน้อยหนึ่งตัว |
1113333 |
[ก-ฮ]{2} \d{4} |
ตัวอักษรไทย 2 ตัว ตามด้วยตัวเลข 4 ตัว (เลขทะเบียนรถ) |
ตอ 6465 |
ตัวอย่างเรกเอกซ์ที่ใช้ตัวบอกปริมาณ 1#
\w{3,}\.\w@chula\.ac\.th
เป็นเรกเอกซ์สำหรับแพตเทิร์นของ email address ที่โดเมนคือ chula.ac.th
เริ่มต้นด้วยตัวอักษร (\w) 3 ตัวขึ้นไป
ตามด้วยเครื่องหมายจุด ซึ่งเครื่องหมายจุดต้องมีการหนีโดยการใช้
\.
เพราะ.
เป็นอักขระพิเศษตามด้วยตัวอักษร (\w) หนึ่งตัว
ตามด้วยเครื่องหมาย @
ตามด้วยคำว่า chula.ac.th ซึ่งเครื่องหมายจุดทั้งสองตัวต้องมีการหนีโดยการใช้
\.
เพราะ.
เป็นอักขระพิเศษ
สตริง |
จับกับเรกเอกซ์ |
คำอธิบาย |
---|---|---|
✔️ |
||
❌ |
หน้าจุดจะต้องมีตัวอักษรอย่างน้อย 3 ตัว |
|
❌ |
เพราะว่าต้องมีจุดตามด้วยตัวอักษร 1 ตัวก่อน @ |
|
❌ |
เพราะว่าหลังจุด ก่อน @ มีตัวอักษรได้เพียง 1 ตัว |
การอ้างอิงกลับ (Backreference)#
การอ้างอิงกลับ คือการใช้ค่าของกลุ่มอักขระที่จับคู่ได้ก่อนหน้า (capturing group) ในส่วนอื่นของเรกเอกซ์ กลุ่มที่จับคู่ได้จะถูกเก็บในหน่วยความจำชั่วคราวและสามารถเรียกใช้งานซ้ำได้โดยใช้ตัวบอกการอ้างอิงกลับ
กลุ่มที่จับคู่ได้ถูกสร้างขึ้นด้วยการใช้วงเล็บ ( )
เพื่อระบุส่วนของสตริงที่เราต้องการจับคู่และเก็บไว้ จากนั้นจึงมีการอ้างอิงกลับด้วยหมายเลขกลุ่ม \1
เพื่ออ้างอิงกลับถึงจับกลุ่มด้วย ()
คู่แรก \2
เพื่ออ้างอิงกลับถึงจับกลุ่มด้วย ()
คู่ที่สอง
ตัวอย่างการอ้างอิงกลับ 1#
(\w+) \1
แปลว่าแพตเทิร์นที่ขึ้นตัวด้วยตัวอักษรอย่างน้อยหนึ่งตัว ซึ่งให้เรียกว่ากลุ่มที่ 1 เนื่องจากเราจัดกลุ่มไว้โดยการใส่ในวงเล็บ จากนั้นตามด้วยช่องว่างหนึ่งช่อง และตามด้วยแพตเทิร์นที่เราจับคู่ไปแล้วในกลุ่มที่หนึ่ง ซึ่งเราใช้ \1
ในการอ้างถึงกลับไปยังกลุ่มที่หนึ่ง ดังนั้นเรกเอกซ์นี้จะจับคู่กับสตริงที่มีคำที่ซ้ำกันอยู่ติดกัน เช่น
hello hello
หรือ bye bye
หรือ hi hi
แต่ว่าไม่จับคู่กับ hello hi
เพราะว่า \1
จะต้องจับคู่กับสตริงที่จับคู่กับเรกเอกซ์ใน ()
แรก
ตัวอย่างการอ้างอิงกลับ 2#
สมมติว่าเราต้องการจับคู่ข้อความที่มีแพตเทิร์น ดังนี้ มอร์นิ่งค่ะ มอร์นิ่ง
ดีค่ะ ดี
จบค่ะ จบ
แยกย้ายค่ะ แยกย้าย
แต่ไม่จับคู่กับ มอร์นิ่งค่ะ คุณ
ดีค่ะ จบ
จบค่ะ แยกย้าย
เราสามารถใช้การจัดกลุ่มเพื่ออ้างอิงกลับได้ดังนี้
^([ก-์]+)ค่ะ \1$
คำอธิบาย
^
แปลว่าต้องขึ้นต้นสตริง([ก-์]+)
แปลว่าตัวอักษรไทยอย่างน้อยหนึ่งตัว ซึ่งเราใส่ไว้ในวงเล็บเพื่อบ่งบอกว่าการจับกลุ่ม ทำให้สามารถอ้างอิงกลับได้ค่ะ
เป็นสตริงปกติ\1
แปลว่าอ้างถึงกลุ่มที่หนึ่ง ซึ่งกลุ่มก็คือสตริงที่ถูกจับคู่ด้วย([ก-์]+)
นิพจน์ปรกติในภาษาไพทอน#
คำสั่งที่เกี่ยวกับเรกเอกซ์อยู่ในโมดูล re
ของไพทอน คำสั่งที่จำเป็นต้องทราบสำหรับการใช้เรกเอกซ์มีอยู่ 5 คำสั่ง ได้แก่
ฟังก์ชัน |
จุดประสงค์การใช้ |
---|---|
|
ตรวจสอบว่าเรกเอกซ์จับคู่กับต้นสตริงหรือไม่ |
|
ตรวจสอบว่าเรกเอกซ์จับคู่กับส่วนใดส่วนหนึ่งของสตริงหรือไม่ |
|
หาสตริงย่อยที่เรกเอกซ์จับคู่ได้ทั้งหมด |
|
แทนที่ส่วนที่เรกเอกซ์จับคู่ได้ ด้วยสตริงอีกสตริงหนึ่ง |
|
หั่นสตริงออกเป็นลิสต์ โดยใช้เรกเอกซ์เป็นตัวบ่งบอกส่วนที่ใช้หั่นสตริง |
ทั้ง 5 คำสั่งเป็นคำสั่งที่ต้องเราเขียนเรกเอกซ์เพื่อระบุแพตเทิร์นที่เราต้องการหา ในภาษาไพทอนเรามักจะระบุเรกเอกซ์โดยการใช้สตริงดิบ หรืออาร์สตริง (raw string หรือ r string) อาร์สตริงมีวิธีการสร้างเหมือนกับสตริงทั่วไปเพียงแต่ใช้ตัวอักษร r
ก่อนเครื่องหมาย '
หรือ ""
ที่เราใช้นำหน้าสตริงทั่วไป เช่น เรกเอกซ์สำหรับหาหมายเลขโทรศัพท์ประเทศไทย 10 หลัก จะเขียนได้เป็น
phone_regex = r'\d\d\d-\d\d\d-\d\d\d\d'
ที่จริงแล้วเราจะใช้สตริงธรรมดาแทนก็ได้ แต่ว่ามีอักขระพิเศษบางสัญลักษณ์ที่สตริงธรรมดา และอาร์สตริงมีความหมายต่างกัน เช่น ‘\b’ ในสตริงธรรมดาหมายถึง backspace แต่ในอาร์สตริงหมายถึงตัวแบ่งคำ ดังนั้นเราจึงควรใช้อาร์สตริงในการเขียนเรกเอกซ์แทน
re.match
#
คำสั่งนี้ใช้สำหรับการจับคู่สตริงกับแพตเทิร์นที่ระบุด้วยเรกเอกซ์ โดยจะพยายามจับคู่จากจุดเริ่มต้นของสตริงเท่านั้น หากตรงตามแพตเทิร์น ฟังก์ชันจะส่งคืนค่าอ็อบเจกต์ Match
หากไม่ตรงจะส่งคืน None
ตัวอย่าง
import re
match = re.match(r'นาย([ก-์]+)พล ([ก-์]+)', 'นายณัฐพล โคกสูงเนิน')
if not match:
print ('ไม่ใช่ชื่อตามแพตเทิร์นนี้')
ถ้าหากเราต้องการทราบด้วยว่าแพตเทิร์นไปจับคู่กับส่วนใดของสตริงบ้าง เราต้องใช้อ็อบเจกต์ Match
ที่คืนค่ามา และใช้เมท็อดที่ชื่อว่า .group
ซึ่งจะคืนค่าสตริงที่จับคู่กับกลุ่มแพตเทิร์นที่ระบุไว้กลับมาให้
ตัวอย่าง
import re
match = re.match(r'นาย([ก-์]+)พล ([ก-์]+)', 'นายณัฐพล โคกสูงเนิน')
if not match:
print ('ชื่อไม่จับคู่กับแพตเทิร์นนี้')
else:
print (match.group(0)) # นายณัฐพล โคกสูงเนิน
print (match.group(1)) # ณัฐ
print (match.group(2)) # โคกสูงเนิน
ข้อสังเกตแรกคือเราใช้ ()
ในเรกเอกซ์ข้างต้นเพื่อเป็นการจัดกลุ่มที่เราสามารถอ้างถึงได้ภายหลังด้วยคำสั่ง .group
โดย กลุ่มที่ 0 จะอ้างถึงทั้งแพตเทิร์น ส่วนเลขกลุ่มต่อ ๆ ไปจะเรียงจากซ้ายไปขวา
re.search
#
คำสั่งนี้คล้ายคลึงกับ re.match
เกือบทุกประการ เพียงแต่ว่าจะคำสั่งนี้จะสแกนหาทั้งสตริง เพื่อหาว่าแพตเทิร์นไปจับคู่กับส่วนใดส่วนหนึ่งของสตริงหรือไม่ ไม่ได้จำกัดแค่ตัวอักษรแรกของสตริงเหมือนกับ .match
import re
match = re.match(r'นาย([ก-์]+)พล ([ก-์]+)', 'ได้เข้าพบนายณัฐพล โคกสูงเนิน') # None เพราะไม่ได้จับคู่ได้ตั้งแต่ต้นสตริง
match = re.search(r'นาย([ก-์]+)พล ([ก-์]+)', 'ได้เข้าพบนายณัฐพล โคกสูงเนิน') # จับคู่ได้
print (match.group(0)) # นายณัฐพล โคกสูงเนิน
print (match.group(1)) # ณัฐ
print (match.group(2)) # โคกสูงเนิน
อ็อบเจกต์ Match
ที่คืนค่ากลับมามีวิธีการใช้เช่นเดิม
re.findall
#
คำสั่งนี้ใช้ในสถานการณ์ที่เราทราบว่าแพตเทิร์นที่เราระบุในเรกเอกซ์นั่นเกิดขึ้นซ้ำกันหลาย ๆ ครั้ง และเราต้องการดึงข้อความที่จับคู่กับเรกเอกซ์ทั้งหมดมาเก็บไว้ในลิสต์ ซึ่งต่างจาก re.search
และ re.match
ซึ่งจะให้ส่วนของสตริงที่จับคู่มาแค่ส่วนเดียวเท่านั้น
สมมติว่าเราต้องการหาคำที่ลงท้ายด้วย -s ทั้งหมด
import re
pattern = r'\w+s'
sentence = 'James misses the stitches that Carlos fixes'
s_words = re.findall(pattern, sentence)
s_words # --> ['James', 'misses', 'stitches', 'Carlos', 'fixes']
สมมติว่าเราต้องการหาคำที่ลงที่เราเติม -es ต่อท้ายเพื่อเปลี่ยนคำนามภาษาอังกฤษให้เป็นพหูพจน์ หรือเปลี่ยนคำกริยาภาษาอังกฤษให้เป็นรูป present tense สำหรับประธานที่เป็นบุรุษที่สามเอกพจน์ กล่าวคือเราต้องการหาคำที่ลงท้ายด้วย -xes -ches -shes -zes -ses ในประโยค ` เท่านั้น เช่น
import re
pattern = r'\w+(x|tch|sh|z|s)es'
sentence = 'James misses the stitches that Carlos fixes'
es_words = re.findall(pattern, sentence)
es_words # --> ['s', 'tch', 'x']
ถ้าหากเรกเอกซ์มีการใช้ ()
หนึ่งครั้ง เราคืนค่ามาเฉพาะส่วนที่อยู่ใน ()
เท่านั้นซึ่งต่างจากตอนที่เราใช้เรกเอกซ์แบบไม่มี ()
ซึ่งจะสตริงที่จับคู่แบบเต็ม ๆ กลับมาให้
แต่ถ้าหากเรกเอกซ์มีการใช้ ()
มากกว่าหนึ่งครั้ง เราคืนค่ามาทุกกลุ่ม กลายเป็นลิสต์ของทูเปิล เช่น
import re
pattern = r'(\w+(x|tch|sh|z|s)es)'
sentence = 'James misses the stitches that Carlos fixes'
es_words = re.findall(pattern, sentence)
es_words # --> [('misses', 's'), ('stitches', 'tch'), ('fixes', 'x')]
ในทั้งสองตัวอย่างให้สังเกตว่าคำสั่งนี้จะคืนค่ามาเป็นลิสต์ของสตริง หรือลิสต์ของทูเปิล ไม่ได้คืนค่ามาเป็นอ็อบเจกต์ Match
เหมือนคำสั่ง re.match
และ re.search
re.sub
#
คำสั่งนี้ย่อมาจากภาษาอังกฤษ substitute แปลว่าการแทนค่า คำสั่งนี้ไว้สำหรับการแทนที่ส่วนของสตริงที่จับคู่กับเรกเอกซ์ด้วยสตริงอื่น ๆ หรือสตริงว่างก็ได้ ซึ่งเป็นคำสั่งที่ใช้บ่อยในการประมวลผลข้อความ
re.sub(เรกเอกซ์ที่ต้องการใช้จับคู่, สตริงที่จะมาใช้แทนค่า, สตริงที่ต้องทำการจับคู่และแทนค่า)
ตัวอย่างการใช้งานเบื้องต้น#
สมมติว่าเราต้องการแทนที่คำว่า “สวัสดี” ด้วยคำว่า “สวัสดีครับ” ในประโยค ในกรณีนี้เราไม่จำเป็นต้องใช้อักขระพิเศษของเรกเอกซ์เลย สามารถใช้สตริงเป็นแพตเทิร์นได้เลย
import re
text = 'สวัสดีทุกท่านที่มาร่วมงานในวันนี้'
text = re.sub(r'สวัสดี', 'สวัสดีครับ', text)
text # --> 'สวัสดีครับทุกท่านที่มาร่วมงานในวันนี้'
สมมติว่าเราต้องการแทนที่คำว่า “คะ” และ “ค่ะ” ด้วยคำว่า “ครับ” ในกรณีนี้เราจะใช้สัญลักษณ์ |
ในเรกเอกซ์เพื่อบอกว่าเราต้องการแทนที่คำว่า “คะ” หรือ “ค่ะ” ด้วยคำว่า “ครับ”
import re
text = 'สวัสดีค่ะ ชื่อนัทนะคะ ยินดีรับใช้ค่ะ'
text = re.sub(r'คะ|ค่ะ', 'ครับ', text)
text # --> 'สวัสดีครับ ชื่อนัทนะครับ ยินดีรับใช้ครับ'
ตัวอย่างการใช้งานแบบมีการอ้างอิงกลับ#
สมมติว่าเราต้องการเปลี่ยนคำนามที่เป็นรูปพหูพจน์ทีเกิดจากการเติม -es ต่อท้ายคำ ให้กลายเป็นคำนามที่เป็นรูปเอกพจน์ ในกรณีนี้เราจะใช้สัญลักษณ์ ()
ในเรกเอกซ์เพื่อบอกว่าเราต้องการแทนที่ส่วนที่จับคู่กับเรกเอกซ์ด้วยสตริงอื่น ๆ ซึ่งสตริงนั้นจะเป็นส่วนที่อยู่ใน ()
ซึ่งเราสามารถอ้างถึงได้ด้วย .group
ของอ็อบเจกต์ Match
ที่คืนค่ากลับมาจาก re.sub
ได้
import re
text = 'James misses the stitches that Carlos fixes'
text = re.sub(r'(\w+)(x|tch|sh|z|s)es', r'\1\2', text)
text # --> 'James miss the stitch that Carlos fix'
re.split
#
คำสั่งนี้ใช้ในการหั่นสตริงออกเป็นลิสต์ โดยใช้เรกเอกซ์เป็นตัวบ่งบอกส่วนที่ใช้หั่นสตริง คำสั่งนี้คล้ายคลึงกับสตริงเมท็อดที่ชื่อว่า .split
แต่ว่า re.split
มีความยืดหยุ่นกว่า เพราะว่า str.split
ต้องระบุตัวแบ่ง (delimiter) เป็นสตริงไม่สามารถระบุเป็นแพตเทิร์นที่ซับซ้อนได้ re.split
ใช้เรกเอกซ์ในการกำหนดตัวแบ่ง
สมมติว่าเราต้องการหั่นสตริงด้านล่างออกเป็นลิสต์ โดยใช้เครื่องหมาย ,
หรือ ;
หรือช่องว่างเป็นตัวบ่งบอกส่วนที่ใช้หั่นสตริง ในกรณีนี้เราจะใช้เรกเอกซ์[,;\s]+
ซึ่งหมายถึงเครื่องหมาย ,
หรือ ;
หรือช่องว่างหนึ่งช่องหรือมากกว่า ในการหั่นสตริงออกเป็นลิสต์ดังนี้
import re
data = "Apples; Oranges, Bananas ;Grapes,Watermelons;Pineapples"
items = re.split(r'[,;\s]+', data)
items # --> ['Apples', 'Oranges', 'Bananas', 'Grapes', 'Watermelons', 'Pineapples']
สรุป#
ในบทนี้เราได้เรียนรู้เกี่ยวกับไฟล์และเรกเอกซ์ โดยได้เรียนรู้เกี่ยวกับวิธีการเข้าถึงไฟล์ ความแตกต่างของพาทเต็มและพาทสัมพัทธ์ วิธีการเขียนไฟล์ และอ่านไฟล์ด้วยคำสั่งต่าง ๆ ทั้ง open
with
.read()
.readline()
.readlines()
และการจัดเก็บข้อมูลให้อยู่ในรูป json
ซึ่งเป็นหนึ่งในเรื่องที่ต้องรู้ก่อนจะดำเนินการจัดการข้อมูลต่อไป รวมถึงยังได้เรียนเรื่องเรกเอกซ์ซึ่งมีประโยชน์มากในการจัดการทำความสะอาดข้อมูล
เรกเอกซ์เป็นเครื่องมือที่ใช้ในการกำหนดแพตเทิร์นในสตริง โดยใช้อักขระพิเศษต่าง ๆ เช่น ^ $ . + * ? [ ] { } | ( ) \w \W \d \D \s \S \b
นอกจากนี้ เรายังได้ศึกษาวิธีการใช้งานตัวบ่งบอกปริมาณ ในเรกเอกซ์ซึ่งประกอบด้วยสัญลักษณ์ +
, *
, ?
, {n}
, {m,n}
, {n,}
, และ {,n}
สำหรับการระบุจำนวนครั้งที่ต้องการค้นหา และได้ศึกษาวิธีการใช้งาน การอ้างอิงกลับเพื่ออ้างถึงกลุ่มย่อยที่จับคู่ได้ก่อนหน้านี้ในแพตเทิร์นอีกด้วย
ภาษาไพทอนมีโมดูล re
ในการทำงานกับเรกเอกซ์คำสั่งที่ใช้บ่อย ๆ ในการประมวลข้อมูล ได้แก่
re.match
: ใช้ตรวจสอบว่าแพตเทิร์นที่กำหนดตรงกับจุดเริ่มต้นของสตริงหรือไม่re.search
: ใช้ค้นหาแพตเทิร์นที่ปรากฏครั้งแรกในสตริงre.findall
: ใช้ค้นหาและคืนค่ารายการของแพตเทิร์นที่พบทั้งหมดในสตริงre.sub
: ใช้แทนที่แพตเทิร์นที่ตรงกับค่าที่กำหนดre.split
: ใช้แบ่งสตริงตามแพตเทิร์นที่กำหนด
คำสั่งเหล่านี้ถูกนำมาใช้ในสถานการณ์ต่าง ๆ เพื่อประมวลผลและจัดการข้อมูลที่มีรูปแบบซับซ้อนหรือไม่แน่นอน