Python | ファイル・OS 操作:tarfile

Python Python
スポンサーリンク

概要(tarfile は「フォルダを丸ごと束ねて圧縮・展開」できる標準機能)

tarfile は .tar、.tar.gz、.tar.bz2、.tar.xz などのアーカイブを作成・追記・一覧・解凍できる標準ライブラリです。zipfile と違い「フォルダ構造をそのまま束ねる」のが得意で、圧縮方式はモードで指定します。重要なのは「モードの選び方(w:gz など)」「相対パス化で再現性を高める」「安全な展開(パストラバーサル対策)」の3点です。with 文で開閉を自動化し、pathlib と組み合わせると安全で読みやすいコードになります。


基本の操作(ここが重要)

新規作成(圧縮あり)とファイル追加

import tarfile

# gzip 圧縮の tar.gz を新規作成
with tarfile.open("archive.tar.gz", mode="w:gz") as tar:
    # ファイルを追加(アーカイブ内の名前は自動で元パス)
    tar.add("project/README.md")
    tar.add("project/src/app.py")
Python

「w:gz」が gzip 圧縮(.tar.gz)、「w:bz2」が bzip2(.tar.bz2)、「w:xz」が xz(.tar.xz)、「w」は無圧縮(.tar)です。拡張子とモードを一致させると、後で扱う側が迷いません。

追記・読み込み・一覧取得

import tarfile

# 既存アーカイブへ追記
with tarfile.open("archive.tar.gz", mode="a:gz") as tar:
    tar.add("project/config.yaml")

# 読み込みと一覧
with tarfile.open("archive.tar.gz", mode="r:gz") as tar:
    print([m.name for m in tar.getmembers()])  # ファイル名のリスト
Python

追記では同名が重複し得るため、アーカイブ内の配置規則(サブフォルダ、ユニーク名)を事前に決めておくと混乱しません。

展開(解凍)

from pathlib import Path
import tarfile

out = Path("extracted")
out.mkdir(parents=True, exist_ok=True)

with tarfile.open("archive.tar.gz", "r:gz") as tar:
    tar.extractall(out)
Python

extractall は「中身をすべて書き出す」動作です。安全対策は後述します。


圧縮とパス設計(方式・相対化・メタデータ)

圧縮方式の選び方

  • テキスト中心でサイズを小さくしたいなら xz(w:xz)や bz2(w:bz2)が効きますが遅めです。
  • 一般的でバランスが良いのは gzip(w:gz)。
  • ビルド成果物やバイナリ中心で速度重視なら無圧縮(w)も選択肢です。

圧縮率と速度はトレードオフです。用途(配布、バックアップ、CIのキャッシュなど)で選びましょう。

相対パスで格納して再現性を高める

from pathlib import Path
import tarfile

root = Path("project")
with tarfile.open("project.tar.gz", "w:gz") as tar:
    for p in root.rglob("*"):
        if p.is_file():
            # ルートからの相対パスで格納(絶対や環境依存を避ける)
            tar.add(p, arcname=p.relative_to(root).as_posix())
Python

arcname を相対化すると、どこで作っても展開後の構成が同じになり、再現性(deterministic builds)が上がります。

メタデータ(パーミッション・時刻)の扱い

tar は UNIX 系のパーミッションやタイムスタンプを保持します。格納時に TarInfo を使えば、属性を明示設定できます。

import tarfile, io, time

with tarfile.open("conf.tar", "w") as tar:
    data = io.BytesIO(b"hello\n")
    info = tarfile.TarInfo("config/hello.txt")
    info.size = len(data.getvalue())
    info.mtime = int(time.time())
    info.mode = 0o644
    tar.addfile(info, data)
Python

安全な解凍(パストラバーサル対策と部分抽出)

ZIP/TAR スリップ対策(意図外の場所へ書き出さない)

アーカイブ内の名前に「../」が紛れていると、展開先の外へ書き出される恐れがあります。展開先ディレクトリ配下に限定するガードを入れます。

from pathlib import Path
import tarfile

def safe_extract(tar_path, outdir):
    outdir = Path(outdir).resolve()
    with tarfile.open(tar_path, "r:*") as tar:
        for m in tar.getmembers():
            dest = (outdir / m.name).resolve()
            if outdir not in dest.parents and dest != outdir:
                raise ValueError(f"危険なパス: {m.name}")
        tar.extractall(outdir)

safe_extract("archive.tar.gz", "extracted")
Python

「r:*」は圧縮方式を自動判別します。Python 3.12 以降は filter 引数で安全に寄せられます(例:filter=”data”)が、バージョンを跨ぐなら自前ガードが安心です。

特定ファイルだけ取り出す・内容を直接読む

import tarfile

with tarfile.open("archive.tar.gz", "r:gz") as tar:
    tar.extract("config/app.yaml", path="out")
    f = tar.extractfile("README.md")  # ExFileObject を取得
    content = f.read().decode("utf-8")
    print(content)
Python

extractfile なら「ディスクへ書き出さず中身を読み取る」ことができます。メモリ処理やストリーム応答に向きます。


実務パターン(フォルダ丸ごと圧縮・除外・配布・インメモリ)

フォルダ丸ごと圧縮(除外ルール付き)

from pathlib import Path
import tarfile

root = Path("src")
exclude_dirs = {".git", "__pycache__"}

with tarfile.open("src.tar.xz", "w:xz") as tar:
    for p in root.rglob("*"):
        if any(part in exclude_dirs for part in p.parts):
            continue
        if p.is_file():
            tar.add(p, arcname=p.relative_to(root).as_posix())
Python

不要フォルダを除外して、配布用の軽量アーカイブを作れます。

必要ファイルだけ選んで「ルート直下」に平坦化

import tarfile
from pathlib import Path

files = [Path("README.md"), Path("LICENSE"), Path("dist/app")]
with tarfile.open("release.tar.gz", "w:gz") as tar:
    for f in files:
        tar.add(f, arcname=f.name)  # ルート直下に格納
Python

展開先が迷わない構成にすると、利用者が楽になります。

インメモリで tar を生成(HTTP レスポンス向け)

import tarfile
from io import BytesIO

buf = BytesIO()
with tarfile.open(fileobj=buf, mode="w:gz") as tar:
    tarinfo = tarfile.TarInfo("hello.txt")
    data = b"world\n"
    tarinfo.size = len(data)
    tar.addfile(tarinfo, BytesIO(data))

payload = buf.getvalue()  # bytes をそのまま送信・保存
Python

ディスクを使わず、その場でアーカイブを作れます。


よくある落とし穴の回避(モード・拡張子・巨大サイズ・権限)

モードと拡張子の不一致を避ける

「w:gz なのに .tar 」「w:xz なのに .tar.gz」といった不一致は後工程で混乱します。拡張子とモードを合わせるのを習慣化しましょう。

絶対パスや環境依存のパスを格納しない

arcname に絶対パスを入れると、展開側で予期せぬ場所へ書き出される原因になります。必ず相対化してから格納します。

大量・巨大ファイルの処理時間とメモリ

xz/bz2 は高圧縮ですが遅いので、ビルドやCIでは gzip へ寄せる、分割アーカイブを検討するなど「時間とサイズのバランス」を意識します。ストリーム読み書き(extractfile・addfile)でメモリ節約が可能です。

権限・所有者情報の復元

UNIX 系の権限や所有者情報が復元される場合があります。展開先環境で意図しない属性にならないよう、必要なら展開後に chmod/chown を調整します(特に配布用途)。


例題で身につける(定番から一歩先まで)

例題1:プロジェクトを相対パスで再現性高く圧縮

from pathlib import Path
import tarfile

root = Path("project")
with tarfile.open("project.tar.gz", "w:gz") as tar:
    for p in root.rglob("*"):
        if p.is_file():
            tar.add(p, arcname=p.relative_to(root).as_posix())
Python

例題2:安全展開(配下限定ガード付き)

from pathlib import Path
import tarfile

def safe_extract(tar_path, outdir):
    outdir = Path(outdir).resolve()
    with tarfile.open(tar_path, "r:*") as tar:
        for m in tar.getmembers():
            dest = (outdir / m.name).resolve()
            if outdir not in dest.parents and dest != outdir:
                raise ValueError(f"危険なパス: {m.name}")
        tar.extractall(outdir)

safe_extract("project.tar.gz", "out")
Python

例題3:メモリから文字列ファイルを追加して作成

import tarfile, io

with tarfile.open("conf.tar", "w") as tar:
    content = io.BytesIO(b"key=value\n")
    info = tarfile.TarInfo("config/settings.ini")
    info.size = len(content.getvalue())
    tar.addfile(info, content)
Python

例題4:アーカイブ内テキストを直接読み出す

import tarfile

with tarfile.open("project.tar.gz", "r:gz") as tar:
    f = tar.extractfile("README.md")
    print(f.read().decode("utf-8"))
Python

まとめ

tarfile は「フォルダ構造を保ったまま束ねる」場面で強力です。圧縮方式とモード(w:gz、w:xz など)を正しく選び、arcname を相対化して再現性を確保する。展開はパストラバーサル対策で出力先配下に限定し、必要に応じてメタデータを調整。with 文と pathlib の併用で、結合→安全確認→圧縮/展開の流れを明示すれば、配布・バックアップ・CI のキャッシュまで、短く安全なアーカイブ処理が書けます。

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