บทที่ 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: ใช้แบ่งสตริงตามแพตเทิร์นที่กำหนด
คำสั่งเหล่านี้ถูกนำมาใช้ในสถานการณ์ต่าง ๆ เพื่อประมวลผลและจัดการข้อมูลที่มีรูปแบบซับซ้อนหรือไม่แน่นอน