同じファイル名なのに別物?Python pathlibでシンボリックリンクと実体を見分ける

Python

この記事について
この記事は、生成AIを活用した自動生成フローで作成しています。Python公式ドキュメントのpathlib、tempfile、osを参考に、架空のCSVでファイルの入口と参照先を比較する方法を整理しました。

検証ステータス:📘 公式仕様を参考に手順を整理しました。掲載コードの実行結果はまだ確認していません。

同じファイル名なのに別物?Python pathlibでシンボリックリンクと実体を見分ける

「昨日と同じreport.csvを開いたのに、今日は違うデータが出てきた」。ファイル名だけで処理対象を特定していると、このような現象の原因が見つからないことがあります。名前が同じでも、作業フォルダーが違う場合や、シンボリックリンクの参照先が変更された場合は、実際に読み込むファイルが変わるためです。

Pythonの標準ライブラリpathlibを使うと、入口のパス、リンクかどうか、解決後のパスを別々に確認できます。今回は業務フォルダーを触らず、使い捨ての一時フォルダーで二つのCSVを作り、リンク先だけを切り替える実験をします。実行可能な環境と、リンク作成が制限される環境の違いも扱います。

最初に知っておきたい「名前」と「場所」

ファイル名はファイルを見つけるためのラベルです。相対パスのreports/latest.csvは、現在の作業フォルダーを基準にした道順を表します。別の場所から同じスクリプトを実行すると、相対パスが違う場所を指すことがあります。これはファイルが勝手に移動したわけではなく、道順の出発点が変わった結果です。

シンボリックリンクは、別のパスを指す入口です。たとえばlatest.csvという名前のリンクを、月曜日はreport_old.csv、火曜日はreport_new.csvへ向ける運用が考えられます。入口の名前は一定でも、参照先が違えば読み取る内容が変わります。通常のファイルとリンクを、エクスプローラーに表示される名前だけで見分けるのは難しい場合があります。

Path.is_symlink()は入口がリンクかどうかを調べます。Path.resolve()は相対パスやリンクを解決した絶対パスを返します。Path.read_text()はファイルの内容を文字列として読みます。これらは似た機能ではありません。入口の性質、参照先の位置、実際の内容を別の観点で観察するために使い分けます。

対象環境と実験の安全性

項目条件
対象Python 3.10以降を想定
追加ライブラリ不要
作業場所tempfileで作る一時フォルダー
実データ使用しない。架空のCSVだけ
Windowsの注意シンボリックリンク作成には権限・設定の制約がある
未確認Windows/macOS/Linux各実機での掲載コードの実行結果

まずPythonのバージョンを確認します。Windowsならターミナルでpython –version、環境によってはpy –versionを使います。macOSやLinuxではpython3 –versionが一般的です。リンクを作れない場合でも権限を上げたり、セキュリティ設定を変更したりする必要はありません。失敗時の表示を確認するだけで十分です。

安全な実験:入口を変えずに参照先を変える

次のコードを作業用のcheck_link.pyとして保存し、Pythonで実行します。TemporaryDirectory()の中だけに架空のCSVを作るので、既存の業務ファイルを上書きしません。実行後は一時フォルダーが後片付けされます。リンク作成が許可されない環境では、その理由を表示してリンクの比較を省略します。

from pathlib import Path
from tempfile import TemporaryDirectory

with TemporaryDirectory() as folder:
    root = Path(folder)
    old = root / "report_old.csv"
    new = root / "report_new.csv"
    link = root / "latest.csv"

    old.write_text("id,value\n1,old\n", encoding="utf-8")
    new.write_text("id,value\n1,new\n", encoding="utf-8")

    try:
        link.symlink_to(old)
    except (OSError, NotImplementedError) as error:
        print("リンク作成不可:", type(error).__name__)
    else:
        print("入口:", link.name)
        print("リンクか:", link.is_symlink())
        print("解決先:", link.resolve().name)
        print("内容:", link.read_text(encoding="utf-8").strip())

        link.unlink()
        link.symlink_to(new)
        print("変更後の解決先:", link.resolve().name)
        print("変更後の内容:", link.read_text(encoding="utf-8").strip())

リンクを作れる環境で期待する結果は、最初の解決先がreport_old.csvで、読み取ったCSVの値がoldになることです。リンクを作り直した後は解決先がreport_new.csvとなり、値はnewになります。どちらの操作でも入口の名前latest.csvは変わりません。ここが実験の主題です。以下は期待結果であり、今回の実機測定ではありません。

観察点最初変更後
入口の名前latest.csvlatest.csv
解決先の名前report_old.csvreport_new.csv
CSVのvalueoldnew

コードのold.write_text()とnew.write_text()は、それぞれ別の内容のCSVを用意します。link.symlink_to(old)は入口を古いCSVへ向けます。link.resolve().nameは、解決したファイルの名前を表示します。link.read_text()はリンクを通して実際の内容を読みます。その後、link.unlink()で作成した入口だけを取り除き、新しいCSVへリンクを張り直します。削除するのは一時フォルダー内のリンクであり、参照先のCSVではありません。

図で見る「同じ名前なのに違う内容」

次の図は、入口と実体を区別するためのものです。左側のlatest.csvは固定されていますが、リンク先の選択が変わると右側の内容も変わります。ファイル名だけをログに残した場合、どちらを読んだのか分からなくなる理由がここにあります。

flowchart LR
    A["latest.csv という入口"] --> B{"リンク先"}
    B -->|"変更前"| C["report_old.csv"]
    B -->|"変更後"| D["report_new.csv"]
    C --> E["old を読む"]
    D --> F["new を読む"]

図で重要なのは、リンクを変更した後もプログラム側の入口名を変える必要がない点です。これは最新版への切替に便利ですが、障害調査では見落としやすい特徴でもあります。入口の名前だけでなく、解決先と読み込んだデータの特徴も記録すると、調査の手掛かりになります。

もう一つの比較:リンクではない通常ファイル

同じlatest.csvという名前で通常のファイルを作った場合、Path.is_symlink()はFalseを返します。Path.resolve()は通常、そのファイル自身の絶対パスを返します。リンクの場合と違い、別の場所を指す入口として扱う必要はありません。ただし、通常ファイルでも別プロセスが内容を書き換えることはあるため、ファイル名だけで内容が固定されるわけではありません。

またPath.absolute()とPath.resolve()も区別します。absolute()は絶対パスを得る操作で、リンクの解決を主目的とするものではありません。リンク先を調べるときはresolve()を使います。リンクが壊れている場合は、is_symlink()がTrueでもexists()がFalseになることがあります。存在しないという結果だけで「リンクではない」と結論づけないでください。

エラーが出たら原因を三つに分ける

リンクを作る時点でPermissionErrorが出たら、まずOSと実行権限を確認します。Windowsではリンク作成に追加の条件が関係する場合があります。権限を上げて無理に成功させる必要はありません。実験の目的はリンクの性質を理解することであり、管理者権限を得ることではないからです。

FileNotFoundErrorが出る場合は、Path.cwd()で現在の作業フォルダーを表示し、相対パスの基準が期待どおりか確認します。リンク先のファイルが削除されている可能性もあるため、入口のis_symlink()と参照先の存在を分けて調べます。見えているファイル名が同じでも、実行場所が異なれば参照するパスは異なります。

UnicodeDecodeErrorが出たら、パスの問題ではなくファイルの文字コードを疑います。今回のCSVはUTF-8で書き、UTF-8で読みますが、外部から受け取るCSVが同じ文字コードとは限りません。パス解決が成功しても内容の解釈に失敗することはあります。原因を混ぜないため、まず解決先を確かめ、その後で文字コードやCSV形式を調べます。

一時フォルダーの後片付けでエラーが出る場合は、別の処理がファイルを開いたままにしていないかを確認します。共有フォルダーや同期フォルダーでは、権限・ファイルロック・同期遅延が重なることがあります。原因が分からない段階で大量のファイルを削除したり、リンクを一括変更したりしないでください。

業務で使うときの判断

日次バッチで最新版ファイルを取り込むなら、入口のパス、resolve()で確認した解決先、処理時刻をログに残すと障害調査がしやすくなります。ただし、パスには利用者名や部署名が含まれる場合があるため、公開ログへ無条件に出力しないことも大切です。必要な情報だけを内部ログへ記録し、保存期間と閲覧権限を決めます。

さらに重要なのは、resolve()がファイル内容の正しさまで保証するわけではない点です。必要な列名、件数、サイズ、場合によってはハッシュ値を確認し、想定したデータが届いたかを別途検査します。リンクを確認してから実際に開くまでに別プロセスがリンクを切り替える可能性もあります。これは検査時点と利用時点のずれであり、共有領域での安全なファイルオープンには追加の設計が必要です。

小さな個人用スクリプトなら、最初から複雑な安全機構を組み込む必要はありません。まず作業フォルダー、リンクの有無、解決先、読み取った内容を分けて考えるだけで、多くの取り違えを説明できます。ファイル名は入口のラベルであって、内容の保証ではない。この違いが、同じ名前のファイルを扱う業務での大切な発見です。

公式資料

文書情報

記事タイトル
同じファイル名なのに別物?Python pathlibでシンボリックリンクと実体を見分ける
作成日
更新日
Source URL
https://papanda925.com/?p=18621

ライセンス: 本記事のうち、当サイトが権利を有する本文・自作図表は、特記なき限り CC BY 4.0 で利用できます。生成AIを活用して作成・編集した内容を含みます。コードについて、別途ライセンス表示またはリンク先GitHubリポジトリのライセンスがある場合は、その条件を優先します。引用・第三者資料・画像・商標等は本ライセンスの対象外です。 利用ポリシー

タイトルとURLをコピーしました