บทที่ 4
การประมวลผลข้อมูลจากไฟล์#

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

ในบทนี้เราจะเรียนรู้วิธีการเปิดไฟล์ และเขียนข้อมูลลงไฟล์ รวมถึงการใช้นิพจน์ปรกติ (regular exprssion) หรือเรกเอกซ์ (RegEx) ซึ่งต่อไปจะเรียกอย่างย่อว่า “เรกเอกซ์” ในการทำความสะอาดข้อมูล และดึงเฉพาะส่วนของข้อมูลที่เราต้องการทำไปวิเคราะห์ต่อไป

ไฟล์#

ไฟล์สามารถถูกแบ่งออกได้เป็น 2 ประเภท

  1. ไฟล์ไบนารี (binary file) เป็นไฟล์ที่เราไม่สามารถเปิดอ่านเป็นตัวหนังสือได้ ข้อมูลถูกจัดเก็บรหัสประเภทอื่น ที่จะต้องใช้โปรแกรมเฉพาะเจาะจงในการเปิดอ่านและประมวลผล เช่น ไฟล์ภาพ ไฟล์เพลง ไฟล์ .docx ซึ่งต้องใช้โปรแกรม Microsoft Word ในการเปิดประมวลผล หรือ .xlsx ซึ่งต้องใช้โปรแกรม Microsoft Excel ในการเปิดประมวลผล ไฟล์เหล่านี้มีไลบรารีภาษาไพทอนที่สามารถใช้เปิดเพื่อนำข้อมูลมาประมวลผลได้เช่นกัน

  2. ไฟล์ที่มนุษย์อ่านได้ (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

  1. (Windows เท่านั้น) ชื่อไดรฟ์ตามด้วยเครื่องหมาย :

  2. ชื่อโฟลเดอร์และโฟลเดอร์ย่อย ๆ คั่นด้วยเครื่องหมาย \ สำหรับ Windows และ / สำหรับ OS อื่น ๆ

  3. ชื่อไฟล์

ตัวอย่าง (Windows)

พาทเต็ม

ไดรฟ์

โฟลเดอร์

ชื่อไฟล์

C:\Downloads\data.zip

C

\Downloads

data.zip

C:\Documents\Prog NLP\example.ipynb

C

\Documents\Prog NLP

example.ipynb

ตัวอย่าง (MacOS)

พาทเต็ม

โฟลเดอร์

ชื่อไฟล์

/Users/te/Downloads/data.zip

/Users/te/Downloads

data.zip

/Users/te/Prog NLP/example.ipynb

/Users/te/Prog NLP

example.ipynb

พาทสัมพัทธ์#

การระบุพาทเต็มค่อนข้างยืดยาว พิมพ์แล้วมีโอกาสผิดสูง พาทสัมพัทธ์ (relative path) เป็นทางเลือกที่สะดวกกว่า เพราะว่าอ้างอิงจากตำแหน่งปัจจุบันของผู้ใช้ไปยังตำแหน่งของไฟล์ พาทสัมพัทธ์จะใช้พาทของโฟลเดอร์ที่เรารันโค้ดมาต่อกันกับพาทสัมพัทธ์เพื่อให้กลายเป็นพาทแบบเต็ม เช่น ถ้าเรารันโค้ดที่โฟลเดอร์ /Users/te/Prog NLP และสั่งให้เปิดไฟล์ที่พาทสัมพัทธ์ data/raw_text.zip จะถูกแปลงให้กลายเป็น /Users/te/Prog NLP/data/raw_text.zip โดยอัตโนมัติ

นอกจากนั้นเรายังสามารถระบุให้ถอยลงไปหนึ่งโฟลเดอร์หรือหลาย ๆ โฟลเดอร์ได้โดยการใช้ .. เช่น ถ้าเรารันโค้ดที่โฟลเดอร์ /Users/te/Prog NLP

พาทสัมพัทธ์

ถูกแปลงเป็นพาทเต็ม

../data/raw_text.zip

/Users/te/data/raw_text.zip

../../data/raw_text.zip

/Users/data/raw_text.zip

../../../data/raw_text.zip

/data/raw_text.zip

การอ่านเขียนไฟล์ข้อมูล#

ไฟล์ถูกจัดเก็บไว้ในฮาร์ดดิสก์ ซึ่งเป็นหน่วยความจำถาวรที่ทำหน้าที่เก็บรักษาข้อมูลไว้อย่างถาวร ฮาร์ดดิสก์แต่ละเครื่องมีขนาดความจุไม่เท่ากัน โดยไฟล์ขนาด 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')

ข้อควรระวังในการเขียนไฟล์มีดังนี้

  1. ต้องตั้ง mode='w' เพราะว่าไพทอนจะเปิดไฟล์เพื่ออ่านไฟล์ไปโดยปริยาย ถ้าไม่ตั้งเป็นโหมดเขียน ไพทอนจะพยายามหาไฟล์นั้นเพื่อเปิดขึ้นมาอ่าน แต่ไฟล์นั้นไม่ได้มีอยู่ก่อนแล้ว เครื่องก็จะคืนค่า FileNotFoundError

  2. เขียนได้เฉพาะสตริงเท่านั้น ถ้าพยายาม .write ตัวเลขหรือลิสต์ จะได้ TypeError

  3. ต้องใส่ไว้ท้ายสตริง \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 สรุปได้ดังนี้

สัญลักษณ์

แทนตัวอะไรบ้าง

ตัวอย่าง

[aei]

a e หรือ i

b[aei]d จับกับ bad bed bid

[^abc]

ตัวอักษร 1 ตัวที่ไม่ใช่ a e หรือ i

b[^aei]d จับกับ bbd bcd bdd

aaa|bb|c

aaa หรือ bb หรือ c

bea|ook จับกับ bea ook

เครื่องหมาย - ที่อยู่ใน [] ใช้สำหรับการไล่ตัวอักษร เช่น

  • [a-z] แปลว่าตัวอักษรที่อยู่ใน a ถึง z

  • [0-9] แปลว่าตัวอักษรที่อยู่ใน 0 ถึง 9 ซึ่งความหมายเหมือนกับ \d

ตัวอย่างการใช้งาน

สัญลักษณ์

แทนตัวอะไรบ้าง

ตัวอย่าง

[b-d]

b c หรือ d

[b-d]ad จับกับ bad cad dad

[0-3]

ตัวเลขหนึ่งตัวที่อยู่ในช่วง 0-9

1[0-2] จับกับ 10 11 12

[a-z0-9]

ตัวอักษรหนึ่งตัวที่อยู่ในช่วง a-z หรือ 0-9

a[a-z0-9] จับกับ aa ab a1 a9

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

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 ซึ่งเครื่องหมายจุดทั้งสองตัวต้องมีการหนีโดยการใช้ \. เพราะ . เป็นอักขระพิเศษ

สตริง

จับกับเรกเอกซ์

คำอธิบาย

attapol.t@chula.ac.th

✔️

te.t@chula.ac.th

หน้าจุดจะต้องมีตัวอักษรอย่างน้อย 3 ตัว

attapol@chula.ac.th

เพราะว่าต้องมีจุดตามด้วยตัวอักษร 1 ตัวก่อน @

attapol.th@chula.ac.th

เพราะว่าหลังจุด ก่อน @ มีตัวอักษรได้เพียง 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 คำสั่ง ได้แก่

ฟังก์ชัน

จุดประสงค์การใช้

re.match

ตรวจสอบว่าเรกเอกซ์จับคู่กับต้นสตริงหรือไม่

re.search

ตรวจสอบว่าเรกเอกซ์จับคู่กับส่วนใดส่วนหนึ่งของสตริงหรือไม่

re.findall

หาสตริงย่อยที่เรกเอกซ์จับคู่ได้ทั้งหมด

re.sub

แทนที่ส่วนที่เรกเอกซ์จับคู่ได้ ด้วยสตริงอีกสตริงหนึ่ง

re.split

หั่นสตริงออกเป็นลิสต์ โดยใช้เรกเอกซ์เป็นตัวบ่งบอกส่วนที่ใช้หั่นสตริง

ทั้ง 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.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: ใช้แบ่งสตริงตามแพตเทิร์นที่กำหนด

คำสั่งเหล่านี้ถูกนำมาใช้ในสถานการณ์ต่าง ๆ เพื่อประมวลผลและจัดการข้อมูลที่มีรูปแบบซับซ้อนหรือไม่แน่นอน