Pythonでファイルとパスを扱う|open・with・pathlib・UTF-8の基本

「Pythonでファイルとパスを扱う|open・with・pathlib・UTF-8の基本」の内容を表す技術イラスト

PythonでCSVを変換する、計算結果を保存する、設定ファイルを読む、CAD用データを出力する。こうした処理では、ファイルの内容だけでなく「どのファイルを、どの形式で、どの文字コードで開くか」を明確にする必要があります。

ファイル操作で起きやすい問題は次のとおりです。

  • 実行位置が変わるとファイルが見つからない
  • 読み取りと書き込みのモードを間違える
  • 文字コードが一致せず文字化けや例外が起きる
  • ファイルを閉じ忘れる
  • 既存ファイルを意図せず上書きする
  • テキストとバイナリを混同する

この記事では、with open()とpathlib.Pathを中心に、再利用しやすく環境へ依存しにくいファイル操作を学びます。

目次

今日の到達点

  • open()のファイルモードを使い分けられる
  • withを使ってファイルを確実に閉じられる
  • UTF-8を明示してテキストを読み書きできる
  • テキストモードとバイナリモードの違いを説明できる
  • pathlibでパスを組み立てられる
  • 相対パスと絶対パスの違いを理解できる
  • ファイル不在や文字コード不一致を例外として処理できる

openでファイルを開く

Pythonのopen()はファイルオブジェクトを返します。基本形は次のとおりです。

file = open(
    "report.txt",
    mode="r",
    encoding="utf-8",
)

主な引数は次の3つです。

  • 第1引数:対象ファイルのパス
  • mode:読み取り、書き込みなどのモード
  • encoding:テキストを文字とバイトの間で変換する方式

代表的なモードを整理します。

モード用途ファイルがない場合既存内容
r読み取りエラー保持
w書き込み新規作成消去して書き直す
a追記新規作成末尾へ追加
x新規作成新規作成存在すればエラー
bバイナリ指定他のモードと組み合わせるバイト列を扱う

たとえば"rb"はバイナリ読み取り、"wb"はバイナリ書き込みです。

withでファイルを確実に閉じる

ファイルは使い終わったら閉じる必要があります。with文を使うと、処理中に例外が発生してもブロックを抜ける際にファイルが閉じられます。

with open(
    "report.txt",
    mode="r",
    encoding="utf-8",
) as file:
    text = file.read()

print(text)

次のように手作業でclose()する方法もあります。

file = open("report.txt", encoding="utf-8")
text = file.read()
file.close()

しかし、read()とclose()の間で例外が起きると、close()へ到達しない可能性があります。通常のファイル操作ではwithを基本にします。

テキストファイルを読み込む

ファイル全体を文字列として読むにはread()を使います。

with open(
    "pressure.txt",
    mode="r",
    encoding="utf-8",
) as file:
    text = file.read()

print(text)

複数行を1行ずつ処理するなら、ファイルオブジェクトを直接反復できます。

with open(
    "pressure.txt",
    mode="r",
    encoding="utf-8",
) as file:
    for line_no, line in enumerate(file, start=1):
        value = line.strip()
        print(f"Line {line_no}: {value}")

strip()は行末の改行だけでなく前後の空白も除去します。空白自体に意味があるデータでは、必要に応じてrstrip("\r\n")などを使い、除去範囲を限定します。

大きなファイルはread()で全体をメモリへ読み込まず、1行ずつ処理する方法を検討します。

テキストファイルへ書き込む

wモードは、既存ファイルの内容を消去して先頭から書き込みます。

report = "Force: 14000.0 N\n"

with open(
    "force_report.txt",
    mode="w",
    encoding="utf-8",
    newline="\n",
) as file:
    file.write(report)

write()は改行を自動追加しません。必要な位置へ\nを入れます。

複数行を書き込む例です。

lines = [
    "component,force_n",
    "Cylinder A,14000.0",
    "Cylinder B,7000.0",
]

with open(
    "force_report.txt",
    mode="w",
    encoding="utf-8",
    newline="\n",
) as file:
    file.write("\n".join(lines) + "\n")

出力内容です。

component,force_n
Cylinder A,14000.0
Cylinder B,7000.0

このコードはPython 3.12.14で実行確認済みです。

追記したい場合はaモードを使います。

with open(
    "force_report.txt",
    mode="a",
    encoding="utf-8",
) as file:
    file.write("Cylinder C,21000.0\n")

既存ファイルの上書きを禁止したい場合はxモードを使えます。

with open(
    "new_report.txt",
    mode="x",
    encoding="utf-8",
) as file:
    file.write("New report\n")

同名ファイルがすでに存在するとFileExistsErrorになります。

UTF-8を明示する

テキストモードでencodingを省略した場合、使用される文字コードは環境に依存します。同じコードでもOSや設定によって結果が変わる可能性があります。

公開するコードや複数環境で動かす処理では、文字コードを明示します。

with open("report.txt", encoding="utf-8") as file:
    text = file.read()

書き込み時も同様です。

with open(
    "report.txt",
    mode="w",
    encoding="utf-8",
) as file:
    file.write("設定圧力は14 MPaです\n")

ただし、入力ファイルがUTF-8とは限りません。外部システムや古いソフトウェアが別の文字コードを使用している場合は、仕様を確認して正しいencodingを指定します。

文字化けを避けるために、根拠なくerrors="ignore"を指定してはいけません。読めなかった文字が消え、データ欠損に気づけなくなるためです。

テキストとバイナリの違い

テキストモードでは、ファイルのバイト列を指定した文字コードでstrへ変換します。

with open(
    "report.txt",
    mode="r",
    encoding="utf-8",
) as file:
    text = file.read()

print(type(text))

出力です。

<class 'str'>

バイナリモードでは変換せず、bytesとして扱います。

with open("sample.bin", mode="rb") as file:
    data = file.read()

print(type(data))

出力です。

<class 'bytes'>

画像、PDF、圧縮ファイル、独自バイナリ形式などは通常バイナリとして扱います。バイナリモードではencodingを指定しません。

with open("sample.bin", mode="wb") as file:
    file.write(b"\x00\x01\xff")

テキストファイルでも、文字コードの調査やハッシュ計算など、バイト単位の処理が必要ならバイナリモードを使う場合があります。

pathlibでパスを組み立てる

pathlibは、ファイルシステムのパスをオブジェクトとして扱う標準ライブラリです。

from pathlib import Path

output_dir = Path("output")
report_path = output_dir / "force_report.txt"

print(report_path)

/演算子でパス要素を連結できます。区切り文字を文字列として直接書く必要がありません。

ファイル名、拡張子、親ディレクトリも取得できます。

from pathlib import Path

report_path = Path("output") / "force_report.txt"

print(report_path.name)
print(report_path.stem)
print(report_path.suffix)
print(report_path.parent)

出力例です。

force_report.txt
force_report
.txt
output

ディレクトリを作成する例です。

from pathlib import Path

output_dir = Path("output")
output_dir.mkdir(parents=True, exist_ok=True)
  • parents=True:必要な親ディレクトリも作成する
  • exist_ok=True:すでに存在してもエラーにしない

Pathでテキストとバイナリを読み書きする

短いファイルを一括で扱うなら、Path.read_text()とPath.write_text()も使えます。

from pathlib import Path

report_path = Path("output") / "force_report.txt"

report_path.parent.mkdir(
    parents=True,
    exist_ok=True,
)

report_path.write_text(
    "Force: 14000.0 N\n",
    encoding="utf-8",
    newline="\n",
)

text = report_path.read_text(encoding="utf-8")
print(text, end="")

出力です。

Force: 14000.0 N

バイナリにはread_bytes()とwrite_bytes()があります。

from pathlib import Path

binary_path = Path("sample.bin")
binary_path.write_bytes(b"\x00\x01\xff")

data = binary_path.read_bytes()
print(data)

出力です。

b'\x00\x01\xff'

一括読み込みは簡潔ですが、大容量ファイルや逐次処理にはopen()を使います。

相対パスと絶対パス

相対パスは、基準となる場所から見た位置を表します。

from pathlib import Path

path = Path("data") / "pressure.txt"

絶対パスは、ファイルシステム上の位置を先頭から表します。

from pathlib import Path

path = Path("data") / "pressure.txt"
absolute_path = path.resolve()

print(absolute_path)

相対パスの基準は、通常「スクリプトが置かれた場所」ではなく、プロセスの現在の作業ディレクトリです。IDE、ターミナル、タスクスケジューラなど実行方法が変わると、基準位置も変わる可能性があります。

スクリプト自身と同じディレクトリを基準にする場合は、次のように明示できます。

from pathlib import Path

base_dir = Path(__file__).resolve().parent
data_path = base_dir / "data" / "pressure.txt"

対話環境などでは__file__が定義されない場合があるため、実行形態に合わせて基準を決めます。

公開記事へ、特定ユーザー名を含むPC固有の絶対パスを書かないようにします。環境ごとに変わる位置は、設定、コマンドライン引数、環境変数などから受け取る設計へ発展させられます。

ファイルが存在するか確認する

pathlibでは状態を確認できます。

from pathlib import Path

data_path = Path("data") / "pressure.txt"

print(data_path.exists())
print(data_path.is_file())
print(data_path.is_dir())

ただし、存在確認から実際に開くまでの間に、別の処理がファイルを削除する可能性があります。存在確認だけで安全を保証できるわけではありません。

処理上予想される失敗は、実際の読み込み時にも例外として扱います。

ファイルがない場合を処理する

存在しないファイルをrモードで開くとFileNotFoundErrorが発生します。

from pathlib import Path

data_path = Path("data") / "pressure.txt"

try:
    text = data_path.read_text(encoding="utf-8")
except FileNotFoundError:
    print(f"File not found: {data_path}")
else:
    print(text)

入力必須のファイルがない場合、空データで処理を続けるより、対象パスを示して停止する方が安全な場合があります。

文字コードが違う場合を処理する

指定した文字コードで変換できないバイト列があると、UnicodeDecodeErrorが発生します。

from pathlib import Path

data_path = Path("data") / "pressure.txt"

try:
    text = data_path.read_text(encoding="utf-8")
except FileNotFoundError:
    print(f"File not found: {data_path}")
except UnicodeDecodeError as error:
    print(f"Encoding error: {error}")
else:
    print(text)

この場合は、ファイルの作成元や仕様を確認します。文字コードを推測して無条件に再試行すると、誤変換した文字列を正しいデータとして扱う危険があります。

実務例:計算結果を安全に保存して読む

出力ディレクトリを作成し、推力計算結果をUTF-8のテキストとして保存してから読み戻します。

from pathlib import Path


def build_force_report() -> str:
    lines = [
        "component,force_n",
        "Cylinder A,14000.0",
        "Cylinder B,7000.0",
    ]

    return "\n".join(lines) + "\n"


def save_text(
    output_path: Path,
    text: str,
) -> None:
    output_path.parent.mkdir(
        parents=True,
        exist_ok=True,
    )

    with output_path.open(
        mode="w",
        encoding="utf-8",
        newline="\n",
    ) as file:
        file.write(text)


def load_text(input_path: Path) -> str:
    with input_path.open(
        mode="r",
        encoding="utf-8",
    ) as file:
        return file.read()


report_path = Path("output") / "force_report.txt"
report = build_force_report()

save_text(report_path, report)
loaded_report = load_text(report_path)

print(loaded_report, end="")

実行結果です。

component,force_n
Cylinder A,14000.0
Cylinder B,7000.0

この例では処理を3つへ分けています。

  • build_force_report():出力内容を作る
  • save_text():パスへ保存する
  • load_text():パスから読み込む

データ生成とファイルI/Oを分けると、保存せずに内容だけテストしたり、将来CSVやJSONへ出力方式を変更したりしやすくなります。

よくある失敗

wモードで既存ファイルを消す

wは既存内容を消去します。追記ならa、新規作成だけを許可するならxを検討します。重要ファイルを更新する場合は、一時ファイルへ書いてから置き換える設計も必要です。

encodingを省略する

環境の既定値へ依存すると、別のPCで再現しない可能性があります。扱う仕様がUTF-8ならencoding="utf-8"を明示します。

バックスラッシュを直接連結する

OS固有の区切り文字を手作業で連結すると移植性が下がります。Path("data") / "file.txt"のように組み立てます。

エラーを無視して空文字列を返す

入力ファイルの不在を空データとして扱うと、「本当にデータが0件」なのか「読み込みに失敗した」のか区別できません。原因を保持したまま停止するか、明示的な代替処理を選びます。

機密情報をパスやログへ出す

ローカル固有パスには、ユーザー名、案件名、顧客名などが含まれる場合があります。公開記事や共有ログには、一般化した相対パスだけを掲載します。

練習問題

UTF-8でテキストを保存する

result.txtへPressure: 14.0 MPaと改行を書き込んでください。

from pathlib import Path

result_path = Path("result.txt")
result_path.write_text(
    "Pressure: 14.0 MPa\n",
    encoding="utf-8",
    newline="\n",
)

確認観点は、UTF-8と改行を明示していることです。

拡張子を取得する

data/measurement.csvからファイル名と拡張子を取得してください。

from pathlib import Path

data_path = Path("data") / "measurement.csv"

print(data_path.name)
print(data_path.suffix)

期待される出力です。

measurement.csv
.csv

ファイル不在を処理する

settings.txtが存在しない場合に、対象パスを含むメッセージを表示してください。

from pathlib import Path

settings_path = Path("settings.txt")

try:
    settings = settings_path.read_text(
        encoding="utf-8",
    )
except FileNotFoundError:
    print(f"File not found: {settings_path}")
else:
    print(settings)

まとめ

  • open()はパス、モード、文字コードを指定して使う
  • ファイルはwithで開き、処理後に確実に閉じる
  • rは読み取り、wは上書き、aは追記、xは新規作成である
  • テキストモードはstr、バイナリモードはbytesを扱う
  • UTF-8を使う処理ではencoding="utf-8"を明示する
  • pathlib.PathでOS固有の区切り文字に依存せずパスを組み立てられる
  • 相対パスの基準は現在の作業ディレクトリである
  • FileNotFoundErrorとUnicodeDecodeErrorを原因別に扱う
  • 存在確認だけでは、実際の読み書きが成功する保証にならない
  • データ生成とファイルI/Oを関数として分離すると再利用しやすい

ファイル操作は、単に読み書きできれば完了ではありません。パスの基準、文字コード、上書き規則、異常時の扱いを明示して、別の環境でも再現できる処理にすることが重要です。

参考情報

参考になったらシェアしてください
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

機械設計・油圧・CAD・Python・AIなど、ものづくりに関わる技術を扱っています。工学知識を整理・構造化し、設計や自動化に再利用できる形へ変えていくことを目指しています。

目次