การทำงานกับไฟล์ใน Python

การทำงานกับไฟล์ใน Python

บทความนี้อธิบายการทำงานกับไฟล์ใน Python

โดยคำนึงถึง "ความปลอดภัย" "ประสิทธิภาพ" และ "ความอ่านง่าย" คู่มือนี้อธิบายแนวคิดทีละขั้นตอน ตั้งแต่พื้นฐานจนถึงเทคนิคใช้งานจริง

YouTube Video

การทำงานกับไฟล์ใน Python

การจัดการไฟล์เป็นทักษะพื้นฐานที่ขาดไม่ได้ ตั้งแต่สคริปต์ขนาดเล็กไปจนถึงแอปพลิเคชันขนาดใหญ่

การเปิดและปิดไฟล์

ก่อนอื่น มาดูตัวอย่างของการอ่านและเขียนไฟล์ข้อความกัน การใช้คำสั่ง with (context manager) ช่วยให้แน่ใจว่าไฟล์จะถูกปิดอย่างถูกต้อง

โค้ดต่อไปนี้จะเปิดไฟล์ข้อความ อ่านเนื้อหา และประมวลผลทีละบรรทัด

1# Open and read a text file safely using a context manager.
2with open("example.txt", "r", encoding="utf-8") as f:
3    # Read all lines lazily (as an iterator) and process each line.
4    for line in f:
5        print(line.rstrip())
  • การระบุ encoding="utf-8" อย่างชัดเจน ช่วยลดปัญหาที่ขึ้นกับแพลตฟอร์ม
  • แม้ใช้ไฟล์ขนาดใหญ่ for line in f ก็ยังใช้หน่วยความจำอย่างมีประสิทธิภาพ

การเขียน (ทับและต่อท้าย)

โปรดใส่ใจโหมดการเขียนเมื่อเขียนลงไฟล์ w สำหรับเขียนทับ, a สำหรับต่อท้ายข้อความ ขณะเขียนไฟล์ควรใช้คำสั่ง with ด้วยเช่นกัน

โค้ดต่อไปนี้แสดงตัวอย่างพื้นฐานของการเขียนทับและต่อท้ายไฟล์

1# Write (overwrite) to a file.
2with open("output.txt", "w", encoding="utf-8") as f:
3    f.write("First line\n")
4    f.write("Second line\n")
5
6# Append to the same file.
7with open("output.txt", "a", encoding="utf-8") as f:
8    f.write("Appended line\n")
  • โค้ดนี้เขียนข้อความไปยัง output.txt ก่อน แล้วจึงต่อท้ายข้อความเพิ่มในไฟล์เดียวกัน โหมด "w" จะเขียนทับไฟล์ ในขณะที่ "a" จะต่อท้ายบรรทัดใหม่ลงในข้อมูลเดิม
  • หากต้องการล้างบัฟเฟอร์ ให้เรียกใช้ flush() แต่ปกติเนื้อหาจะถูกล้างอัตโนมัติเมื่อจบ context
  • หากหลายโปรเซสหรือเธรดอาจเขียนพร้อมกัน คุณควรพิจารณาการควบคุมสิทธิ์ เช่น การล็อกไฟล์

การอ่านและเขียนข้อมูลไบนารี

ไฟล์ภาพและไฟล์บีบอัดควรจัดการในโหมดไบนารี (rb หรือ wb) ต่างจากโหมดข้อความ encoding จะถูกละเลยในไบนารี

โค้ดต่อไปนี้อ่านไฟล์ไบนารีแล้วคัดลอกไปยังไฟล์อื่นในโหมดไบนารี

1# Read and write binary files.
2with open("image.png", "rb") as src:
3    data = src.read()
4
5with open("copy.png", "wb") as dst:
6    dst.write(data)
  • เมื่อจัดการกับไฟล์ไบนารีขนาดใหญ่ ควรหลีกเลี่ยงการอ่านทั้งหมดในครั้งเดียวด้วย read(); ควรอ่านหรือเขียนเป็นส่วน ๆ เพื่อประหยัดหน่วยความจำ

ตัวอย่างการจัดการไฟล์ขนาดใหญ่ด้วยการแบ่งเป็นส่วน ๆ

สำหรับไฟล์ขนาดใหญ่มากจนไม่พอดีกับหน่วยความจำ ให้แบ่งอ่านและเขียนเป็นส่วน ๆ ตามขนาดคงที่ เนื่องจากเป็นงานที่ขึ้นกับ I/O การปรับขนาดบัฟเฟอร์จึงมีประโยชน์

โค้ดต่อไปนี้คัดลอกไฟล์ทีละ 64KB ทำงานได้รวดเร็วและใช้หน่วยความจำน้อย

1# Copy a large file in chunks to avoid using too much memory.
2CHUNK_SIZE = 64 * 1024  # 64 KB
3
4with open("large_source.bin", "rb") as src,      open("large_dest.bin", "wb") as dst:
5    while True:
6        chunk = src.read(CHUNK_SIZE)
7        if not chunk:
8            break
9        dst.write(chunk)
  • คุณสามารถเลือกขนาดของแต่ละส่วนตามลักษณะของเครือข่ายหรือดิสก์ของคุณ สำหรับ SSD ขนาด chunk ที่ใหญ่ขึ้นเล็กน้อยมักจะให้ผลลัพธ์ดีกว่า

ตัวอย่างการใช้ argument ชื่อ buffering

โดยการระบุ argument buffering ในฟังก์ชัน open() คุณสามารถควบคุมขนาด buffer ที่ Python ใช้งานภายในได้ สิ่งนี้จะช่วยให้คุณปรับประสิทธิภาพการรับส่งข้อมูลเข้า/ออกได้ดียิ่งขึ้น

 1# Explicitly set the internal buffer size to 128 KB for faster I/O.
 2CHUNK_SIZE = 64 * 1024
 3BUFFER_SIZE = 128 * 1024  # 128 KB internal buffer
 4
 5with open("large_source.bin", "rb", buffering=BUFFER_SIZE) as src,      open("large_dest.bin", "wb", buffering=BUFFER_SIZE) as dst:
 6    while True:
 7        chunk = src.read(CHUNK_SIZE)
 8        if not chunk:
 9            break
10        dst.write(chunk)
  • หากคุณตั้งค่า buffering เป็น 0 การรับส่งข้อมูลจะไม่มีการ buffer; หากตั้งเป็น 1 ระบบจะใช้ line buffering ส่วนค่าตั้งแต่ 2 ขึ้นไปจะใช้ buffer ตามขนาดที่ระบุไว้ในหน่วยไบต์
  • โดยทั่วไปค่านี้ที่ตั้งมาเป็นค่าเริ่มต้นก็เพียงพอแล้ว เนื่องจากระบบปฏิบัติการจะจัดการการเก็บแคชได้อย่างมีประสิทธิภาพ แต่การปรับ parameter นี้อาจเป็นประโยชน์สำหรับไฟล์ขนาดใหญ่มากหรืออุปกรณ์พิเศษบางอย่าง

การจัดการไฟล์สมัยใหม่ด้วย Pathlib

โมดูล pathlib มาตรฐาน ช่วยให้การจัดการ path เข้าใจง่ายกว่าเดิม ช่วยให้โค้ดอ่านง่ายและปลอดภัยกว่าการใช้ path แบบสายอักขระ

 1from pathlib import Path
 2
 3path = Path("data") / "notes.txt"
 4
 5# Ensure parent directory exists.
 6path.parent.mkdir(parents=True, exist_ok=True)
 7
 8# Write and read using pathlib.
 9path.write_text("Example content\n", encoding="utf-8")
10content = path.read_text(encoding="utf-8")
11print(content)
  • โค้ดนี้สาธิตวิธีสร้างโฟลเดอร์ด้วย pathlib เขียนลงไฟล์ข้อความ และอ่านเนื้อหากลับมา เมื่อใช้วัตถุ Path คุณสามารถจัดการเส้นทางได้อย่างปลอดภัยและเป็นธรรมชาติ
  • Path มีฟังก์ชันอำนวยความสะดวก เช่น iterdir() และ glob() คุณสามารถเขียนโค้ดโดยไม่ต้องกังวลเรื่อง path separator บนแต่ละระบบปฏิบัติการ

ไฟล์และโฟลเดอร์ชั่วคราว (tempfile)

คุณสามารถสร้างไฟล์ชั่วคราวอย่างปลอดภัยด้วย tempfile ช่วยหลีกเลี่ยงปัญหาด้านการชนของชื่อและ race condition ด้านความปลอดภัย

โค้ดต่อไปนี้แสดงตัวอย่างการสร้างข้อมูลชั่วคราวโดยใช้ไฟล์ temp

 1import tempfile
 2
 3# Create a temporary file that is deleted on close.
 4with tempfile.NamedTemporaryFile(
 5    mode="w+",
 6    encoding="utf-8",
 7    delete=True
 8) as tmp:
 9    tmp.write("temporary data\n")
10    tmp.seek(0)
11    print(tmp.read())
12
13# tmp is deleted here
  • โค้ดนี้สร้างไฟล์ชั่วคราว เขียน/อ่านข้อมูล และลบไฟล์โดยอัตโนมัติเมื่อจบ with block โดยใช้ tempfile.NamedTemporaryFile คุณจะจัดการไฟล์ temp ได้ปลอดภัยและไม่มีปัญหาการชนกัน เนื่องจากกำหนด delete=True ไฟล์จะถูกลบอัตโนมัติ
  • บน Windows อาจไม่สามารถเปิดไฟล์จาก handle อื่นได้ทันที ดังนั้นสามารถตั้งค่า delete=False และจัดการการลบเอง

shutil: การคัดลอก ย้าย และลบไฟล์/โฟลเดอร์ในระดับสูง

การคัดลอก ย้าย หรือลบไฟล์และโฟลเดอร์แบบ recursive ทำได้ง่ายด้วย shutil

 1import shutil
 2
 3# Copy a file preserving metadata.
 4shutil.copy2("source.txt", "dest.txt")
 5
 6# Move a file or directory.
 7shutil.move("old_location", "new_location")
 8
 9# Remove an entire directory tree (use with caution).
10shutil.rmtree("some_directory")
  • shutil.copy2 จะคัดลอก metadata (เช่น เวลาปรับปรุงล่าสุด) ด้วย move จะย้ายไฟล์ให้ แม้ไม่สามารถใช้การ rename ได้
  • rmtree เป็นคำสั่งที่อันตราย ควรตรวจสอบและสำรองข้อมูลก่อนทุกครั้ง

ข้อมูลเมตาของไฟล์ (os.stat) และการจัดการสิทธิ์

ขนาดไฟล์ เวลาแก้ไขล่าสุด และสิทธิ์ต่าง ๆ สามารถอ่านหรือแก้ไขได้ด้วย os และ stat

โค้ดต่อไปนี้แสดงตัวอย่างการอ่านข้อมูลไฟล์ด้วย os.stat และเปลี่ยนสิทธิ์ด้วย os.chmod

 1import os
 2import stat
 3from datetime import datetime
 4
 5st = os.stat("example.txt")
 6print("size:", st.st_size)
 7print("modified:", datetime.fromtimestamp(st.st_mtime))
 8
 9# Make file read-only for owner.
10os.chmod("example.txt", stat.S_IREAD)
  • สิทธิ์การเข้าถึงจะแตกต่างกันระหว่างระบบ POSIX และ Windows หากต้องการให้รองรับหลายแพลตฟอร์ม ควรใช้ API ระดับสูงหรือเขียนโค้ดตรวจสอบเงื่อนไขเพิ่มเติม

การล็อกไฟล์ (ควบคุมสิทธิ์) — ความต่างระหว่าง Unix กับ Windows

ต้องมีการควบคุมสิทธิ์เมื่อหลายโปรเซสเข้าถึงไฟล์เดียวกันพร้อมกัน บน UNIX ใช้ fcntl และบน Windows ใช้ msvcrt

โค้ดต่อไปนี้ใช้ fcntl.flock ใน UNIX เพื่อขอล็อกไฟล์แบบ exclusive ขณะเขียน

 1# Unix-style file locking example
 2import fcntl
 3
 4with open("output.txt", "a+", encoding="utf-8") as f:
 5    fcntl.flock(f, fcntl.LOCK_EX)
 6    try:
 7        f.write("Safe write\n")
 8        f.flush()
 9    finally:
10        fcntl.flock(f, fcntl.LOCK_UN)
  • โค้ดนี้จะขอล็อกไฟล์แบบ exclusive ด้วย fcntl.flock บนระบบ UNIX เพื่อให้เขียนไฟล์ได้อย่างปลอดภัยและป้องกันการเขียนพร้อมกัน หลังประมวลผลเสร็จ ควรปลดล็อกเสมอเพื่อให้โปรเซสอื่นเข้าถึงไฟล์ได้
  • บน Windows ให้ใช้ msvcrt.locking() หากต้องการใช้งานระดับสูงขึ้น ควรพิจารณาไลบรารีเสริมเช่น portalocker หรือ filelock

รูปแบบการเขียนไฟล์แบบ atomic

เพื่อป้องกันไฟล์เสียหายระหว่างอัปเดต ให้เขียนลงไฟล์ชั่วคราวแล้วใช้ os.replace แทนที่เมื่อสำเร็จ

การเขียนลงไฟล์ชั่วคราวก่อนแล้วจึงแทนที่ช่วยป้องกันข้อมูลเสียหากเกิดการพังของโปรแกรม

 1import os
 2from pathlib import Path
 3import tempfile
 4
 5def atomic_write(path: Path, data: str, encoding="utf-8"):
 6    # Write to a temp file in the same directory and atomically replace.
 7    dirpath = path.parent
 8    with tempfile.NamedTemporaryFile(
 9        mode="w",
10        encoding=encoding,
11        dir=str(dirpath),
12        delete=False
13    ) as tmp:
14        tmp.write(data)
15        tmp.flush()
16        tempname = tmp.name
17
18    # Atomic replacement (on most OSes)
19    os.replace(tempname, str(path))
20
21# Usage
22atomic_write(Path("config.json"), '{"key":"value"}\n')

os.replace จะเปลี่ยนไฟล์แบบ atomic ภายในไฟล์ซิสเต็มเดียวกัน โปรดทราบว่า atomicity ไม่รับประกันกรณีข้าม mount point

การเข้าถึงข้อมูลอย่างรวดเร็วด้วย mmap (สำหรับข้อมูลขนาดใหญ่)

เพื่อการเข้าถึงแบบสุ่มในไฟล์ขนาดใหญ่ mmap จะช่วยเพิ่มประสิทธิภาพ I/O โดยทั่วไปจะเกี่ยวข้องกับการดำเนินการข้อมูลไบนารี

โค้ดต่อไปนี้จะแมปไฟล์เข้าหน่วยความจำ แล้วอ่านหรือเขียนข้อมูลตามช่วง byte ที่กำหนด ควรระวังหากต้องเปลี่ยนขนาดไฟล์

1import mmap
2
3with open("data.bin", "r+b") as f:
4    mm = mmap.mmap(f.fileno(), 0)  # map entire file
5    try:
6        print(mm[:20])  # first 20 bytes
7        mm[0:4] = b"\x00\x01\x02\x03"  # modify bytes
8    finally:
9        mm.close()
  • โค้ดนี้จะแมปไฟล์ไบนารีไปยังหน่วยความจำด้วย mmap และอ่าน/เขียนระดับ byte โดยตรง Memory mapping ช่วยให้เข้าถึงข้อมูลขนาดใหญ่แบบสุ่มได้อย่างรวดเร็ว
  • mmap ทำงานได้รวดเร็ว แต่หากใช้งานไม่ถูกต้องอาจเกิดปัญหาความถูกต้องของข้อมูล เรียกใช้ flush() เมื่อต้องการซิงโครไนซ์ข้อมูล

CSV / JSON / Pickle: อ่านและเขียนตามรูปแบบไฟล์

ข้อมูลรูปแบบพิเศษมีโมดูลเฉพาะทางสำหรับแต่ละประเภท ใช้ csv สำหรับ CSV, json สำหรับ JSON และ pickle สำหรับบันทึกอ็อบเจกต์ Python

โค้ดต่อไปนี้แสดงตัวอย่างพื้นฐานของการอ่าน/เขียน CSV กับ JSON และการใช้งาน Pickle เนื่องจาก Pickle สามารถรันโค้ดได้ อันตรายหากโหลดข้อมูลจากแหล่งที่ไม่น่าเชื่อถือ

 1import csv
 2import json
 3import pickle
 4
 5# CSV write
 6with open("rows.csv", "w", encoding="utf-8", newline="") as f:
 7    writer = csv.writer(f)
 8    writer.writerow(["name", "age"])
 9    writer.writerow(["Alice", 30])
10
11# JSON write
12data = {"items": [1, 2, 3]}
13with open("data.json", "w", encoding="utf-8") as f:
14    json.dump(data, f, ensure_ascii=False, indent=2)
15
16# Pickle write (only for trusted environments)
17obj = {"key": "value"}
18with open("obj.pkl", "wb") as f:
19    pickle.dump(obj, f)
  • ควรใส่ newline="" สำหรับไฟล์ CSV เพื่อป้องกันบรรทัดว่างเกินบน Windows ใช้ ensure_ascii=False เพื่อให้ JSON แสดงอักขระ UTF-8 ได้อ่านง่าย

การอ่านและเขียนไฟล์บีบอัดโดยตรง (gzip / bz2 / zipfile)

การใช้งาน gzip และ zip โดยตรงช่วยประหยัดเนื้อที่ดิสก์ ไลบรารีมาตรฐานมีโมดูลสำหรับการนี้อยู่แล้ว

โค้ดต่อไปนี้คือตัวอย่างการอ่านและเขียนไฟล์ gzip ในรูปแบบข้อความ

1import gzip
2
3# Write gzipped text.
4with gzip.open("text.gz", "wt", encoding="utf-8") as f:
5    f.write("Compressed content\n")
6
7# Read gzipped text.
8with gzip.open("text.gz", "rt", encoding="utf-8") as f:
9    print(f.read())
  • มีการแลกเปลี่ยนกันระหว่างอัตราการบีบอัดและความเร็ว ขึ้นอยู่กับระดับและรูปแบบของการบีบอัด

มาตรการความปลอดภัยและช่องโหว่

สามารถพิจารณาประเด็นต่อไปนี้สำหรับมาตรการความปลอดภัยและช่องโหว่

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

หากรับ input จากผู้ใช้ใน path ไฟล์ ต้อง normalize และตรวจสอบความถูกต้องทุกครั้ง เช่น สามารถใช้ Path.resolve() และเช็คเส้นทาง parent directory

 1from pathlib import Path
 2
 3def safe_path(base_dir: Path, user_path: str) -> Path:
 4    candidate = (base_dir / user_path).resolve()
 5    if base_dir.resolve() not in candidate.parents and base_dir.resolve() != candidate:
 6        raise ValueError("Invalid path")
 7    return candidate
 8
 9# Usage
10base = Path("/srv/app/data")
11
12# Raises an exception: attempted path traversal outside `base`
13safe = safe_path(base, '../etc/passwd')
  • ต้องระวังเป็นพิเศษหากใช้ input ภายนอกเป็น path ใน web app หรือ public API

สรุปรูปแบบที่ใช้บ่อย

  • ควรใช้ with ในการเปิดไฟล์เสมอ (ปิดอัตโนมัติ)
  • ควรระบุ encoding ให้ชัดเจนเมื่อใช้กับข้อมูลข้อความ
  • ควรอ่านเขียนไฟล์ขนาดใหญ่แบบแบ่งส่วน
  • นำระบบล็อกไฟล์มาใช้สำหรับทรัพยากรร่วม
  • สำหรับการอัปเดตที่สำคัญ ให้ใช้รูปแบบ atomic คือ 'เขียนไฟล์ชั่วคราว → os.replace'
  • ควรยืนยันและสำรองข้อมูลทุกครั้งก่อนดำเนินการที่เสี่ยงอันตราย เช่น การลบหรือเขียนทับ
  • ทำให้อยู่ในรูปแบบมาตรฐานและตรวจสอบความถูกต้องเมื่อใช้ข้อมูลภายนอกเป็นเส้นทางไฟล์

สรุป

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

คุณสามารถติดตามบทความข้างต้นโดยใช้ Visual Studio Code บนช่อง YouTube ของเรา กรุณาตรวจสอบช่อง YouTube ด้วย

YouTube Video