Pythonファイル転送の決定版!ftplib.FTPと進化する代替技術

2025-06-06

以下に主な特徴と使い方を説明します。

ftplib.FTP の主な機能

  • 接続の終了: FTPセッションを終了します。
  • ディレクトリ操作:
    • cwd(): カレントディレクトリを変更します。
    • mkd(): 新しいディレクトリを作成します。
    • rmd(): ディレクトリを削除します。
    • nlst() / dir(): ディレクトリの内容(ファイルやサブディレクトリのリスト)を取得します。
  • ファイル転送:
    • storbinary() / storlines(): ローカルファイルをFTPサーバーにアップロードします。バイナリファイルとテキストファイルに対応しています。
    • retrbinary() / retrlines(): FTPサーバーからファイルをダウンロードします。バイナリファイルとテキストファイルに対応しています。
  • 認証(ログイン): ユーザー名とパスワードを使用してFTPサーバーにログインします。
  • FTPサーバーへの接続: 指定したホストとポートに接続します。

ftplib.FTPクラスのインスタンスを作成し、様々なメソッドを呼び出してFTP操作を行います。

from ftplib import FTP

# FTPサーバーへの接続情報
FTP_HOST = "your_ftp_host.com"
FTP_USER = "your_username"
FTP_PASS = "your_password"

try:
    # 1. FTPオブジェクトの作成と接続
    # ホスト名、ユーザー名、パスワードをコンストラクタで指定すると、
    # 自動的にconnect()とlogin()が呼ばれます。
    with FTP(FTP_HOST, FTP_USER, FTP_PASS) as ftp:
        print(f"FTPサーバーに接続しました: {FTP_HOST}")
        print(ftp.getwelcome()) # サーバーからのウェルカムメッセージを表示

        # ファイル名やディレクトリ名のエンコーディングを指定 (推奨)
        ftp.encoding = 'utf-8'

        # 2. ディレクトリの移動
        ftp.cwd("/path/to/remote/directory")
        print(f"カレントディレクトリを {ftp.pwd()} に変更しました。")

        # 3. ファイルリストの取得
        print("\n現在のディレクトリのファイルとディレクトリ:")
        files = ftp.nlst() # ファイル名のみのリストを取得
        for f in files:
            print(f)
        
        # より詳細なリストを取得したい場合は dir() を使用 (lsコマンドの出力形式)
        # ftp.dir(print) # 各行を直接表示する場合

        # 4. ファイルのアップロード (例: ローカルの test.txt をアップロード)
        local_file_to_upload = "local_test.txt"
        remote_filename_upload = "remote_test.txt"
        try:
            with open(local_file_to_upload, 'rb') as f:
                ftp.storbinary(f"STOR {remote_filename_upload}", f)
            print(f"\nファイル '{local_file_to_upload}' を '{remote_filename_upload}' としてアップロードしました。")
        except FileNotFoundError:
            print(f"エラー: ローカルファイル '{local_file_to_upload}' が見つかりません。")

        # 5. ファイルのダウンロード (例: リモートの remote_download.txt をダウンロード)
        remote_file_to_download = "remote_download.txt"
        local_filename_download = "downloaded_file.txt"
        try:
            with open(local_filename_download, 'wb') as f:
                ftp.retrbinary(f"RETR {remote_file_to_download}", f.write)
            print(f"\nファイル '{remote_file_to_download}' を '{local_filename_download}' としてダウンロードしました。")
        except Exception as e:
            print(f"エラー: ファイル '{remote_file_to_download}' のダウンロード中にエラーが発生しました: {e}")

        # 6. ディレクトリの作成 (例: 新しいディレクトリ new_dir を作成)
        # new_dir_name = "new_dir_from_python"
        # ftp.mkd(new_dir_name)
        # print(f"\nディレクトリ '{new_dir_name}' を作成しました。")

        # 7. 接続の終了 (withステートメントを使用すると自動的にcloseされる)
        # ftp.quit()
        print("\nFTPセッションを終了しました。")

except Exception as e:
    print(f"FTP操作中にエラーが発生しました: {e}")

  • FTP_TLS: SSL/TLSによる暗号化されたFTP接続(FTPS)を使用したい場合は、ftplib.FTP_TLSクラスを使用します。これは通常のFTPよりも安全です。
  • バイナリモードとテキストモード: ファイルの転送には、バイナリモード(storbinary, retrbinary)とテキストモード(storlines, retrlines)があります。通常、画像や実行ファイルなどの非テキストデータはバイナリモードで、テキストファイルはテキストモードで転送します。特に指定がない限り、バイナリモードを使用するのが安全です。
  • エラーハンドリング: try...except ブロックを使用して、FTP操作中に発生する可能性のあるネットワークエラーや認証エラーなどを適切に処理することが重要です。ftplibモジュールは、ftplib.all_errorsなどの例外を提供しています。
  • with ステートメントの使用: 上記の例のようにwith FTP(...) as ftp:と記述することで、FTPセッションの終了時(エラーが発生した場合も含む)に自動的に接続が閉じられるため、ftp.quit()を明示的に呼び出す必要がなくなります。


ftplib.FTPはFTPサーバーとの通信を行うため、ネットワーク、認証、ファイルシステムなど、様々な要因でエラーが発生する可能性があります。ここでは、よくあるエラーパターンとそれぞれの対処法を説明します。

ftplib.error_perm: 530 Login authentication failed (認証失敗)

エラーメッセージの例

ftplib.error_perm: 530 Login authentication failed.

原因

  • IPアドレス制限
    サーバー側で接続元のIPアドレスが制限されている。
  • アカウントのロックアウト
    連続したログイン試行失敗により、アカウントが一時的または永続的にロックされている。
  • 匿名FTPが許可されていない
    匿名FTPを試みているが、サーバー側で許可されていない。
  • ユーザー名またはパスワードの誤り
    最も一般的な原因です。

トラブルシューティング

  • サーバー管理者に問い合わせ
    アカウントのロックアウトやIPアドレス制限が疑われる場合は、サーバー管理者に連絡してください。
  • 匿名FTPの設定確認
    匿名FTPが必要な場合は、サーバーがそれを許可しているか確認してください。
  • FTPクライアントでのテスト
    FileZillaなどのGUIベースのFTPクライアントを使用して、同じ認証情報で接続できるか試してください。これで接続できれば、Pythonコードの問題である可能性が高いです。
  • ユーザー名とパスワードの再確認
    スペルミス、大文字小文字、余分なスペースがないか正確に確認してください。

socket.gaierror: [Errno 8] nodename nor servname provided, or not known (ホスト名解決失敗)

エラーメッセージの例

socket.gaierror: [Errno 8] nodename nor servname provided, or not known

原因

  • DNS解決の問題
    お使いのネットワーク環境でDNSサーバーが正しく設定されていない、またはホスト名がDNSで解決できない。
  • ホスト名の誤り
    FTPサーバーのホスト名(例: ftp.example.com)が間違っている、または存在しない。

トラブルシューティング

  • ネットワーク設定の確認
    ローカルネットワークのDNS設定を確認してください。
  • IPアドレスでの接続試行
    ホスト名の代わりにFTPサーバーのIPアドレスを直接指定して接続できるか試してください。これで接続できれば、DNSの問題である可能性が高いです。
  • pingコマンドで確認
    コマンドプロンプトやターミナルで ping your_ftp_host.com を実行し、IPアドレスが解決され、応答があるか確認してください。
  • ホスト名の再確認
    FTPサーバーのホスト名が正しいことを確認してください。

socket.timeout: timed out (タイムアウト)

エラーメッセージの例

socket.timeout: timed out

原因

  • ポートの誤り
    標準ではないポートを使用している場合、そのポートが開いていない。
  • サーバーがダウンしている
    FTPサーバーが稼働していない。
  • ファイアウォール
    クライアント側またはサーバー側のファイアウォールがFTP接続をブロックしている。
  • ネットワークの遅延
    サーバーからの応答が設定されたタイムアウト時間内にない。

トラブルシューティング

  • Telnetコマンドでポート確認
    コマンドプロンプトやターミナルで telnet your_ftp_host.com 21 を実行し、ポート21が開いているか確認してください(220などの応答があれば開いています)。
  • サーバーの状態確認
    FTPサーバーが稼働しているか、管理者や提供元に確認してください。
  • ファイアウォールの確認
    お使いのPCのファイアウォール(Windows Defender Firewall, macOS Firewallなど)やネットワーク機器(ルーターなど)がFTPポート(通常21番)への接続をブロックしていないか確認してください。サーバー側のファイアウォール設定も確認が必要です。
  • タイムアウト時間の延長
    ftp = FTP(timeout=60) のように、FTPコンストラクタでタイムアウト時間を長く設定してみてください(デフォルトは無限大ですが、ネットワークによっては明示的な設定が必要な場合があります)。

ftplib.error_proto: 500 OOPS: priv_sock_get_cmd または 500 OOPS: vsf_sysutil_bind (サーバー側の問題)

エラーメッセージの例

ftplib.error_proto: 500 OOPS: priv_sock_get_cmd
ftplib.error_proto: 500 OOPS: vsf_sysutil_bind

原因
これらは主にFTPサーバー側の問題を示すエラーコードです。

  • サーバーのメモリ不足
    サーバーのリソースが不足している。
  • ファイルシステムの権限問題
    サーバーがファイルを書き込む/読み込むための十分な権限を持っていない。
  • FTPサーバーソフトウェアの設定ミス
    サーバーソフトウェア(vsftpdなど)の内部的な設定に問題がある。

トラブルシューティング

  • サーバー管理者に連絡
    これらのエラーはクライアント側では解決が難しいです。FTPサーバーの管理者またはホスティングプロバイダーに連絡し、サーバーログを確認してもらう必要があります。

ftplib.error_perm: 550 Permission denied (権限エラー)

エラーメッセージの例

ftplib.error_perm: 550 Permission denied

原因

  • 存在しないパス
    存在しないディレクトリに移動しようとしたり、存在しないファイルをダウンロード/削除しようとしたりしている。
  • 読み取り権限がない
    ファイルのダウンロードやディレクトリリストの取得を試みているが、対象に読み取り権限がない。
  • 書き込み権限がない
    ファイルのアップロードやディレクトリ作成を試みているが、指定されたディレクトリに書き込み権限がない。

トラブルシューティング

  • サーバー側の権限設定
    サーバー管理者に連絡し、FTPユーザーに適切なディレクトリに対する読み書き権限が付与されているか確認してもらってください。特に共有ホスティングの場合、ルートディレクトリなどへの書き込みは制限されていることが多いです。
  • リモートパスの確認
    ftp.cwd()などで指定しているパスが正しいか、そして実際に存在するか確認してください。
  • FTPクライアントでの権限確認
    FileZillaなどのFTPクライアントで、同じユーザーで同じ操作を行い、権限があるか確認してください。

ftplib.error_temp: 421 Service not available, remote server has closed connection (接続終了)

エラーメッセージの例

ftplib.error_temp: 421 Service not available, remote server has closed connection

原因

  • 接続数の上限
    サーバーが許可する同時接続数を超えている。
  • サーバーの過負荷
    サーバーがリクエストを処理しきれず、接続を切断した。
  • アイドルタイムアウト
    FTPサーバーが一定時間操作がないと接続を切断するように設定されている。

トラブルシューティング

  • サーバー管理者に連絡
    サーバーの負荷状況や接続制限について確認してもらう必要があります。
  • 操作間隔の短縮
    可能であれば、FTP操作間の待機時間を短くしてみてください。
  • 再接続のロジックを実装
    短時間で再接続を試みるリトライロジックを実装することを検討してください。

エラーメッセージの例
直接的なエラーメッセージではなく、ファイル名が文字化けしたり、ファイルが見つからないというエラーにつながったりすることがあります。

原因

トラブルシューティング

  • サーバー側のエンコーディング確認
    サーバー管理者にFTPサーバーがどのエンコーディングを使用しているか確認してください。
  • ftp.encoding の設定
    Python 3.xの場合、FTPオブジェクト作成後に ftp.encoding = 'utf-8' のように、サーバーが使用していると思われるエンコーディングを明示的に設定してください。一般的なFTPサーバーはUTF-8を使用していることが多いです。

パッシブモード(Passive Mode)とアクティブモード(Active Mode)の問題

エラーメッセージの例
特定の明示的なエラーメッセージが出ないこともありますが、データ転送(storbinary, retrbinary など)が途中で止まったり、タイムアウトしたりすることがあります。

原因

  • ファイアウォールによるブロック
    • アクティブモード
      クライアントがデータポートをオープンし、サーバーがそのポートに接続しようとするため、クライアント側のファイアウォールがサーバーからの着信接続をブロックすることがよくあります。
    • パッシブモード
      サーバーがデータポートをオープンし、クライアントがそのポートに接続します。これは通常、クライアント側のファイアウォールで問題を起こしにくいですが、サーバー側のファイアウォールがパッシブモード用のポート範囲をブロックしている可能性があります。
  • ファイアウォール設定の確認
    クライアント側およびサーバー側のファイアウォール設定を確認し、FTPのデータポート(パッシブモードの場合は動的なポート範囲、アクティブモードの場合はクライアントが指定するポート)が適切に開いているか確認してください。
  • アクティブモードの試行
    環境によってはアクティブモード(ftp.set_pasv(False))が機能する場合もありますが、クライアント側のファイアウォール設定が必要です。通常は推奨されません。
  • パッシブモードの使用
    ftplib.FTPはデフォルトでパッシブモードを使用しようとします(ftp.set_pasv(True))。ほとんどのケースでパッシブモードが推奨されます。もし問題が発生している場合は、明示的に ftp.set_pasv(True) を呼び出してみてください。

全体的なデバッグのヒント

  • デバッグ出力の有効化
    ftplib.FTPにはデバッグレベルを設定する機能があります。
    import sys
    ftp = FTP(FTP_HOST, FTP_USER, FTP_PASS)
    ftp.set_debuglevel(2) # 0:なし, 1:簡単なメッセージ, 2:詳細なコマンドと応答
    # 以降の操作でFTPのやり取りが標準出力に表示されます
    
    これにより、クライアントとサーバー間のFTPコマンドと応答を詳細に確認でき、問題の特定に役立ちます。
  • サーバーログの確認
    最も重要なデバッグツールの一つです。FTPサーバー側のログファイルを確認することで、何が起こっているのかの詳細な情報を得ることができます。
  • 詳細なエラーメッセージの確認
    try...except ftplib.all_errors as e: のようにして、発生した正確なエラーメッセージを捕捉し、表示してください。


準備: 共通の接続情報とモジュールのインポート

以下の例では、共通のFTP接続情報を使用します。your_ftp_host.comyour_usernameyour_password は、実際のFTPサーバーの情報に置き換えてください。

from ftplib import FTP
import os
import sys

# --- FTP接続情報の設定(実際のものに置き換えてください) ---
FTP_HOST = "your_ftp_host.com"
FTP_USER = "your_username"
FTP_PASS = "your_password"

# --- ローカルファイル・ディレクトリの準備(テスト用) ---
# アップロードするダミーファイルを作成
with open("local_upload_test.txt", "w", encoding="utf-8") as f:
    f.write("これはテスト用のアップロードファイルです。\n")
    f.write("ftplibを使ったPythonプログラムで作成されました。\n")

# ダウンロードしたファイルを保存するディレクトリ
DOWNLOAD_DIR = "downloaded_files"
os.makedirs(DOWNLOAD_DIR, exist_ok=True) # 存在しない場合は作成

# デバッグレベルの設定(必要に応じてコメントを解除)
# ftp.set_debuglevel(2) # 0:なし, 1:簡単なメッセージ, 2:詳細なコマンドと応答

基本的なFTPサーバーへの接続と切断

最も基本的な接続と、サーバーからのウェルカムメッセージの表示、そしてセッションの終了です。with ステートメントを使うことで、エラー時でも確実に接続が閉じられます。

print("--- 例1: 基本的な接続と切断 ---")
try:
    with FTP(FTP_HOST, FTP_USER, FTP_PASS) as ftp:
        # 接続成功時のウェルカムメッセージ
        print(f"FTPサーバーに接続しました: {FTP_HOST}")
        print(f"サーバーウェルカムメッセージ: {ftp.getwelcome()}")
        
        # ファイル名やディレクトリ名のエンコーディングを指定 (日本語対応のため推奨)
        ftp.encoding = 'utf-8'

        print("接続成功、セッション終了。")

except Exception as e:
    print(f"エラーが発生しました: {e}")

リモートディレクトリの移動とリスト表示

FTPサーバー上のディレクトリを変更し、そのディレクトリ内のファイルやサブディレクトリの一覧を取得します。

print("\n--- 例2: リモートディレクトリの移動とリスト表示 ---")
try:
    with FTP(FTP_HOST, FTP_USER, FTP_PASS) as ftp:
        ftp.encoding = 'utf-8'
        print(f"接続しました: {FTP_HOST}")

        # カレントディレクトリの表示
        current_dir = ftp.pwd()
        print(f"現在のディレクトリ: {current_dir}")

        # ディレクトリの移動(例: public_htmlディレクトリへ)
        # 存在しないディレクトリを指定するとエラーになります
        remote_path = "/public_html" # サーバー上の既存のパスに置き換えてください
        try:
            ftp.cwd(remote_path)
            print(f"ディレクトリを '{remote_path}' に変更しました。")
            print(f"新しいカレントディレクトリ: {ftp.pwd()}")

            # ファイルとディレクトリのリストを取得 (nlst(): ファイル名のみ)
            print("\n--- リモートディレクトリの内容 (nlst) ---")
            for item in ftp.nlst():
                print(f"  - {item}")

            # 詳細なリストを取得 (dir(): ls -l のような形式)
            print("\n--- リモートディレクトリの内容 (dir) ---")
            # lambdaでprintを渡すと、各行を直接出力します
            ftp.dir(lambda line: print(f"  {line}"))

        except Exception as e:
            print(f"ディレクトリ移動またはリスト取得中にエラー: {e}")
            print(f"指定したパス '{remote_path}' が存在しないか、アクセス権がありません。")

except Exception as e:
    print(f"エラーが発生しました: {e}")

ファイルのアップロード (storbinary)

ローカルのファイルをFTPサーバーにアップロードします。バイナリモード ('rb') でファイルを開き、storbinary() メソッドを使用します。

print("\n--- 例3: ファイルのアップロード ---")
local_file_to_upload = "local_upload_test.txt"
remote_upload_path = f"/temp/{os.path.basename(local_file_to_upload)}" # リモートの保存先パス

try:
    with FTP(FTP_HOST, FTP_USER, FTP_PASS) as ftp:
        ftp.encoding = 'utf-8'
        print(f"接続しました: {FTP_HOST}")

        # リモートディレクトリの存在確認と作成(オプション)
        remote_dir = os.path.dirname(remote_upload_path)
        try:
            ftp.mkd(remote_dir) # ディレクトリ作成 (既に存在する場合はエラーになるが、今回は無視)
            print(f"リモートディレクトリ '{remote_dir}' を作成または確認しました。")
        except Exception:
            pass # ディレクトリが既に存在する場合のエラーを無視

        # アップロードファイルを開く
        with open(local_file_to_upload, 'rb') as fp:
            # STOR コマンドでファイルをサーバーに転送
            # STOR [リモートのファイル名]
            ftp.storbinary(f"STOR {remote_upload_path}", fp)
            print(f"'{local_file_to_upload}' を '{remote_upload_path}' にアップロードしました。")

except FileNotFoundError:
    print(f"エラー: ローカルファイル '{local_file_to_upload}' が見つかりません。")
except Exception as e:
    print(f"アップロード中にエラーが発生しました: {e}")

ファイルのダウンロード (retrbinary)

FTPサーバーからファイルをローカルにダウンロードします。バイナリモード ('wb') でファイルを開き、retrbinary() メソッドを使用します。

print("\n--- 例4: ファイルのダウンロード ---")
# 例3でアップロードしたファイルをダウンロードする想定
remote_file_to_download = f"/temp/{os.path.basename('local_upload_test.txt')}"
local_download_path = os.path.join(DOWNLOAD_DIR, "downloaded_test_file.txt")

try:
    with FTP(FTP_HOST, FTP_USER, FTP_PASS) as ftp:
        ftp.encoding = 'utf-8'
        print(f"接続しました: {FTP_HOST}")

        # ダウンロードファイルを開く
        with open(local_download_path, 'wb') as fp:
            # RETR コマンドでファイルをサーバーから取得
            # RETR [リモートのファイル名]
            # fp.write をコールバック関数として渡し、データを少しずつ書き込む
            ftp.retrbinary(f"RETR {remote_file_to_download}", fp.write)
            print(f"'{remote_file_to_download}' を '{local_download_path}' にダウンロードしました。")

except FileNotFoundError:
    print(f"エラー: ローカルの保存先ディレクトリ '{DOWNLOAD_DIR}' が見つかりません。")
except Exception as e:
    print(f"ダウンロード中にエラーが発生しました: {e}")
    # ダウンロードに失敗した場合、部分的にダウンロードされたファイルを削除することも検討
    if os.path.exists(local_download_path) and os.path.getsize(local_download_path) == 0:
         os.remove(local_download_path)

ディレクトリの作成と削除

FTPサーバー上に新しいディレクトリを作成し、その後削除します。

print("\n--- 例5: ディレクトリの作成と削除 ---")
new_remote_dir = "/temp/my_new_directory_py" # 新しく作成するディレクトリのパス

try:
    with FTP(FTP_HOST, FTP_USER, FTP_PASS) as ftp:
        ftp.encoding = 'utf-8'
        print(f"接続しました: {FTP_HOST}")

        # ディレクトリの作成
        try:
            ftp.mkd(new_remote_dir)
            print(f"ディレクトリ '{new_remote_dir}' を作成しました。")
        except Exception as e:
            # 既に存在する場合のエラーをキャッチ (ftplib.error_perm: 550 Create directory operation failed.)
            print(f"ディレクトリ '{new_remote_dir}' の作成中にエラーが発生しました: {e}")
            if "550" in str(e) and "exists" in str(e).lower():
                print("(ディレクトリは既に存在している可能性があります)")
            else:
                raise # 予期せぬエラーは再スロー

        # 作成したディレクトリに移動して確認
        ftp.cwd(new_remote_dir)
        print(f"カレントディレクトリを '{ftp.pwd()}' に変更しました。")
        ftp.cwd("..") # 親ディレクトリに戻る

        # ディレクトリの削除
        # 注意: ディレクトリが空でないと削除できません。
        # 中にファイルがある場合は、先にファイルを削除する必要があります。
        try:
            ftp.rmd(new_remote_dir)
            print(f"ディレクトリ '{new_remote_dir}' を削除しました。")
        except Exception as e:
            print(f"ディレクトリ '{new_remote_dir}' の削除中にエラーが発生しました: {e}")
            if "550" in str(e) and "Directory not empty" in str(e):
                print("(ディレクトリが空ではありません。先に中のファイルを削除してください。)")

except Exception as e:
    print(f"エラーが発生しました: {e}")

ファイルの削除と名前変更

FTPサーバー上のファイルを削除したり、名前を変更したりします。

print("\n--- 例6: ファイルの削除と名前変更 ---")
remote_file_to_manipulate = f"/temp/manipulate_this_file.txt" # 操作対象のファイル

try:
    with FTP(FTP_HOST, FTP_USER, FTP_PASS) as ftp:
        ftp.encoding = 'utf-8'
        print(f"接続しました: {FTP_HOST}")

        # テスト用にファイルをアップロード(既に存在する場合はスキップ)
        try:
            with open("local_upload_test.txt", 'rb') as fp:
                ftp.storbinary(f"STOR {remote_file_to_manipulate}", fp)
            print(f"テスト用に '{remote_file_to_manipulate}' をアップロードしました。")
        except Exception as e:
            print(f"テストファイルのアップロード中にエラーが発生 (続行): {e}")

        # ファイルの名前変更
        new_remote_name = f"/temp/renamed_file_by_python.txt"
        try:
            ftp.rename(remote_file_to_manipulate, new_remote_name)
            print(f"ファイル名を '{remote_file_to_manipulate}' から '{new_remote_name}' に変更しました。")
        except Exception as e:
            print(f"ファイルの名前変更中にエラーが発生しました: {e}")
            print(f"(ファイル '{remote_file_to_manipulate}' が存在しないか、権限がありません。)")
            # 名前変更失敗時は、削除対象を元のファイル名から新しいファイル名に切り替える
            remote_file_to_manipulate = new_remote_name # 削除のためにこの名前を使う

        # ファイルの削除
        try:
            ftp.delete(remote_file_to_manipulate)
            print(f"ファイル '{remote_file_to_manipulate}' を削除しました。")
        except Exception as e:
            print(f"ファイルの削除中にエラーが発生しました: {e}")
            print(f"(ファイル '{remote_file_to_manipulate}' が存在しないか、権限がありません。)")

except Exception as e:
    print(f"エラーが発生しました: {e}")

エラーハンドリングとリトライロジック (概念)

FTP操作はネットワークの状態に依存するため、エラーが発生しやすいです。基本的なエラーハンドリングと、簡単なリトライロジックの概念を示します。

import time
from ftplib import FTP, all_errors, error_perm, error_temp

print("\n--- 例7: エラーハンドリングと簡単なリトライ ---")

MAX_RETRIES = 3
RETRY_DELAY_SECONDS = 5

def ftp_connect_with_retry(host, user, password, max_retries=MAX_RETRIES, delay=RETRY_DELAY_SECONDS):
    for i in range(max_retries):
        try:
            print(f"接続試行 {i + 1}/{max_retries}...")
            ftp = FTP(host, user, password, timeout=30) # タイムアウトを設定
            ftp.encoding = 'utf-8'
            print("FTPサーバーに接続しました。")
            return ftp
        except (all_errors, OSError) as e: # OSErrorはソケット関連のエラーも含む
            print(f"接続失敗: {e}")
            if i < max_retries - 1:
                print(f"{delay}秒待機してから再試行します...")
                time.sleep(delay)
            else:
                print("最大再試行回数に達しました。接続を諦めます。")
                raise # 最終的に失敗したら例外を再スロー

try:
    # 接続を試みる
    with ftp_connect_with_retry(FTP_HOST, FTP_USER, FTP_PASS) as ftp:
        print("FTPセッションが確立されました。")
        
        # 例: 存在しないディレクトリに移動しようとしてエラーを発生させる
        try:
            ftp.cwd("/non_existent_directory_12345")
            print("ディレクトリ移動成功 (このメッセージは表示されないはず)。")
        except error_perm as e:
            print(f"意図したエラー (権限エラー): {e}")
        except all_errors as e:
            print(f"その他のFTPエラー: {e}")
        
        print("操作完了、セッション終了。")

except Exception as e:
    print(f"最終的なエラー処理: {e}")



ここでは、ftplib.FTPの代替となる主な方法をいくつか紹介します。

SFTP (SSH File Transfer Protocol)

FTPの最も直接的な代替手段であり、セキュリティが強化されています。SSH(Secure Shell)プロトコル上で動作するため、データは暗号化され、認証もより安全です。FTPとは互換性がありません。

Pythonでの実装
paramikoライブラリがSFTPクライアントとして広く使われています。

特徴

  • SSH認証
    パスワード認証の他に、SSHキーペアによる認証もサポートします。
  • 単一ポート
    通常、SSHと同じポート(デフォルトで22番)を使用するため、ファイアウォール設定が容易です。
  • 暗号化
    データと認証情報がすべて暗号化されます。

使用例 (Paramiko)

import paramiko
import os

# SFTP接続情報
SFTP_HOST = "your_sftp_host.com"
SFTP_PORT = 22
SFTP_USER = "your_username"
SFTP_PASS = "your_password" # または秘密鍵のパスを指定

LOCAL_FILE_UPLOAD = "local_sftp_upload.txt"
REMOTE_UPLOAD_PATH = f"/temp/{os.path.basename(LOCAL_FILE_UPLOAD)}"
LOCAL_FILE_DOWNLOAD = "downloaded_sftp_file.txt"
REMOTE_DOWNLOAD_PATH = f"/temp/{os.path.basename(LOCAL_FILE_UPLOAD)}" # アップロードしたファイルをダウンロード

# テスト用ファイルの作成
with open(LOCAL_FILE_UPLOAD, "w") as f:
    f.write("This is a test file for SFTP upload.\n")

print("\n--- SFTP (Paramiko) を使ったファイルのアップロード/ダウンロード ---")
try:
    with paramiko.SSHClient() as ssh_client:
        # ホストキーの自動追加 (本番環境ではKnownHostsファイルを使うなど、より厳格な方法が推奨されます)
        ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        
        ssh_client.connect(
            hostname=SFTP_HOST,
            port=SFTP_PORT,
            username=SFTP_USER,
            password=SFTP_PASS,
            # key_filename="/path/to/your/private_key", # 秘密鍵を使う場合
            look_for_keys=False # 秘密鍵ファイルを使わない場合
        )
        print(f"SFTPサーバーに接続しました: {SFTP_HOST}")

        with ssh_client.open_sftp() as sftp_client:
            # ディレクトリの作成 (存在しない場合)
            remote_dir = os.path.dirname(REMOTE_UPLOAD_PATH)
            try:
                sftp_client.mkdir(remote_dir)
                print(f"リモートディレクトリ '{remote_dir}' を作成しました。")
            except IOError: # ディレクトリが既に存在する場合
                print(f"リモートディレクトリ '{remote_dir}' は既に存在します。")
                pass

            # ファイルのアップロード
            sftp_client.put(LOCAL_FILE_UPLOAD, REMOTE_UPLOAD_PATH)
            print(f"'{LOCAL_FILE_UPLOAD}' を '{REMOTE_UPLOAD_PATH}' にアップロードしました。")

            # ファイルのダウンロード
            sftp_client.get(REMOTE_DOWNLOAD_PATH, LOCAL_FILE_DOWNLOAD)
            print(f"'{REMOTE_DOWNLOAD_PATH}' を '{LOCAL_FILE_DOWNLOAD}' にダウンロードしました。")
            
            # リモートファイルのリスト表示
            print("\n--- リモートディレクトリの内容 ---")
            for entry in sftp_client.listdir(remote_dir):
                print(f"  - {entry}")

except Exception as e:
    print(f"SFTP操作中にエラーが発生しました: {e}")

FTPS (FTP Secure)

FTPにSSL/TLS暗号化を追加したものです。ftplib.FTP_TLSクラスを使用します。FTPのコマンドチャネルとデータチャネルの両方を暗号化できますが、ポート設定(特にパッシブモードでのデータポート)が複雑になることがあります。

Pythonでの実装
ftplibモジュール内のFTP_TLSクラス。

特徴

  • 既存FTPインフラとの互換性
    FTPサーバーがFTPSをサポートしていれば、既存のFTP環境を活かしつつセキュリティを向上できます。
  • 暗号化
    FTP通信全体をSSL/TLSで保護します。

使用例 (ftplib.FTP_TLS)

from ftplib import FTP_TLS
import os

# FTPS接続情報 (多くの場合、ポート990 (Implicit SSL) または21 (Explicit SSL) を使用)
FTPS_HOST = "your_ftps_host.com"
FTPS_PORT = 21 # または 990
FTPS_USER = "your_username"
FTPS_PASS = "your_password"

LOCAL_FILE_UPLOAD_FTPS = "local_ftps_upload.txt"
REMOTE_UPLOAD_PATH_FTPS = f"/temp_ftps/{os.path.basename(LOCAL_FILE_UPLOAD_FTPS)}"

with open(LOCAL_FILE_UPLOAD_FTPS, "w") as f:
    f.write("This is a test file for FTPS upload.\n")

print("\n--- FTPS (ftplib.FTP_TLS) を使ったファイルのアップロード ---")
try:
    with FTP_TLS(FTPS_HOST, FTPS_USER, FTPS_PASS) as ftps:
        ftps.encoding = 'utf-8'
        # Explicit FTPS (PORT 21) の場合、認証後にTLSを有効化
        # ftps.prot_p() # データ接続も保護
        # ftps.auth() # サーバーによっては認証を最初に呼び出す必要がある場合も
        
        # Implicit FTPS (PORT 990) の場合は、connect時にTLSが有効になる
        # ftps = FTP_TLS(FTPS_HOST, FTPS_PORT)
        # ftps.login(FTPS_USER, FTPS_PASS)
        
        print(f"FTPSサーバーに接続しました: {FTPS_HOST}")
        ftps.prot_p() # データチャネル保護 (Explicit FTPSの場合必須)

        # ディレクトリ作成
        remote_dir_ftps = os.path.dirname(REMOTE_UPLOAD_PATH_FTPS)
        try:
            ftps.mkd(remote_dir_ftps)
            print(f"リモートディレクトリ '{remote_dir_ftps}' を作成しました。")
        except Exception: # 既に存在する場合など
            print(f"リモートディレクトリ '{remote_dir_ftps}' は既に存在します。")
            pass

        # ファイルのアップロード
        with open(LOCAL_FILE_UPLOAD_FTPS, 'rb') as fp:
            ftps.storbinary(f"STOR {REMOTE_UPLOAD_PATH_FTPS}", fp)
            print(f"'{LOCAL_FILE_UPLOAD_FTPS}' を '{REMOTE_UPLOAD_PATH_FTPS}' にアップロードしました。")

except Exception as e:
    print(f"FTPS操作中にエラーが発生しました: {e}")

注意: FTPSの設定はサーバーによって多様であり、上記コードがそのまま機能しない場合があります。特に、ポート番号やftps.auth()ftps.prot_p()の呼び出し順序はサーバーの設定に依存します。

クラウドストレージサービス (S3, Azure Blob, Google Cloud Storageなど)

近年、企業や開発者にとって主流となっているのが、Amazon S3、Microsoft Azure Blob Storage、Google Cloud Storageといったオブジェクトストレージサービスです。これらはファイル転送プロトコルとは異なり、HTTP(S)ベースのAPIを通じてファイルを管理します。

特徴

  • セキュリティ
    HTTPSによる転送暗号化、サーバーサイド暗号化、きめ細やかなアクセス制御。
  • 豊富な機能
    バージョニング、ライフサイクル管理、アクセス制御(IAM)、イベント通知など。
  • 低コスト
    使用量に応じた課金体系。
  • スケーラビリティ
    ほぼ無限に拡張可能なストレージ容量。
  • 高い耐久性と可用性
    データが複数箇所に冗長化されて保存されます。

Pythonでの実装
各クラウドプロバイダーが公式のPython SDKを提供しています。

  • Google Cloud Storage
    google-cloud-storage
  • Azure Blob Storage
    azure-storage-blob
  • AWS S3
    boto3

使用例 (AWS S3 - boto3)

import boto3
import os

# AWS認証情報とS3バケット名(環境変数またはAWS CLI設定から読み込まれます)
# 環境変数: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION
S3_BUCKET_NAME = "your-unique-s3-bucket-name-12345" # ユニークなバケット名に置き換えてください
LOCAL_FILE_UPLOAD_S3 = "local_s3_upload.txt"
REMOTE_UPLOAD_KEY_S3 = f"uploads/{os.path.basename(LOCAL_FILE_UPLOAD_S3)}"
LOCAL_FILE_DOWNLOAD_S3 = "downloaded_s3_file.txt"
REMOTE_DOWNLOAD_KEY_S3 = f"uploads/{os.path.basename(LOCAL_FILE_UPLOAD_S3)}"

with open(LOCAL_FILE_UPLOAD_S3, "w") as f:
    f.write("This is a test file for S3 upload.\n")

print("\n--- AWS S3 (boto3) を使ったファイルのアップロード/ダウンロード ---")
try:
    s3_client = boto3.client('s3')

    # バケットの作成(一度だけ実行すればよい)
    try:
        s3_client.create_bucket(Bucket=S3_BUCKET_NAME)
        print(f"S3バケット '{S3_BUCKET_NAME}' を作成しました。")
    except s3_client.exceptions.BucketAlreadyOwnedByYou:
        print(f"S3バケット '{S3_BUCKET_NAME}' は既に存在します。")
    except Exception as e:
        print(f"S3バケット作成中にエラー: {e}")

    # ファイルのアップロード
    s3_client.upload_file(LOCAL_FILE_UPLOAD_S3, S3_BUCKET_NAME, REMOTE_UPLOAD_KEY_S3)
    print(f"'{LOCAL_FILE_UPLOAD_S3}' を S3バケット '{S3_BUCKET_NAME}' の '{REMOTE_UPLOAD_KEY_S3}' にアップロードしました。")

    # ファイルのダウンロード
    s3_client.download_file(S3_BUCKET_NAME, REMOTE_DOWNLOAD_KEY_S3, LOCAL_FILE_DOWNLOAD_S3)
    print(f"S3バケット '{S3_BUCKET_NAME}' の '{REMOTE_DOWNLOAD_KEY_S3}' を '{LOCAL_FILE_DOWNLOAD_S3}' にダウンロードしました。")

    # バケット内のオブジェクトリスト
    print("\n--- S3バケット内のオブジェクト ---")
    response = s3_client.list_objects_v2(Bucket=S3_BUCKET_NAME, Prefix='uploads/')
    if 'Contents' in response:
        for obj in response['Contents']:
            print(f"  - {obj['Key']} (サイズ: {obj['Size']} バイト)")
    else:
        print("指定されたプレフィックスにオブジェクトはありません。")

except Exception as e:
    print(f"S3操作中にエラーが発生しました: {e}")

WebDAV (Web-based Distributed Authoring and Versioning)

HTTPプロトコルを拡張し、ウェブサーバー上でファイルの作成、変更、管理を可能にするプロトコルです。WebDAVをサポートするサーバーは、HTTP(S)経由でファイルのアップロード、ダウンロード、移動、削除などを行えます。

Pythonでの実装

  • webdavclient3 (より高機能)
  • easywebdav (軽量で使いやすい)

特徴

  • Webブラウザからのアクセス
    クライアントソフトウェアなしでブラウザからアクセス可能な場合もある。
  • HTTP(S)ベース
    既存のウェブインフラと親和性が高い。

使用例 (easywebdav)
事前に pip install easywebdav でインストールが必要です。

import easywebdav
import os

# WebDAV接続情報
WEBDAV_URL = "https://your_webdav_host.com" # 例: https://webdav.example.com/remote.php/dav/files/username/
WEBDAV_USER = "your_username"
WEBDAV_PASS = "your_password"

LOCAL_FILE_UPLOAD_WEBDAV = "local_webdav_upload.txt"
REMOTE_UPLOAD_PATH_WEBDAV = f"python_test/{os.path.basename(LOCAL_FILE_UPLOAD_WEBDAV)}" # リモートパスはWebDAVサーバーの構造による
LOCAL_FILE_DOWNLOAD_WEBDAV = "downloaded_webdav_file.txt"
REMOTE_DOWNLOAD_PATH_WEBDAV = f"python_test/{os.path.basename(LOCAL_FILE_UPLOAD_WEBDAV)}"

with open(LOCAL_FILE_UPLOAD_WEBDAV, "w") as f:
    f.write("This is a test file for WebDAV upload.\n")

print("\n--- WebDAV (easywebdav) を使ったファイルのアップロード/ダウンロード ---")
try:
    webdav = easywebdav.connect(
        WEBDAV_URL.replace("https://", "").replace("http://", "").split("/")[0], # ホスト名のみ
        username=WEBDAV_USER,
        password=WEBDAV_PASS,
        protocol=WEBDAV_URL.split("://")[0] # "http" or "https"
    )
    
    # リモートパスのベースディレクトリ
    remote_base_dir = "python_test"
    try:
        webdav.mkdir(remote_base_dir)
        print(f"リモートディレクトリ '{remote_base_dir}' を作成しました。")
    except Exception as e:
        if "already exists" in str(e).lower(): # easywebdavが返すエラーメッセージに依存
            print(f"リモートディレクトリ '{remote_base_dir}' は既に存在します。")
        else:
            raise # 予期せぬエラー

    # ファイルのアップロード
    webdav.upload(LOCAL_FILE_UPLOAD_WEBDAV, REMOTE_UPLOAD_PATH_WEBDAV)
    print(f"'{LOCAL_FILE_UPLOAD_WEBDAV}' を '{REMOTE_UPLOAD_PATH_WEBDAV}' にアップロードしました。")

    # ファイルのダウンロード
    webdav.download(REMOTE_DOWNLOAD_PATH_WEBDAV, LOCAL_FILE_DOWNLOAD_WEBDAV)
    print(f"'{REMOTE_DOWNLOAD_PATH_WEBDAV}' を '{LOCAL_FILE_DOWNLOAD_WEBDAV}' にダウンロードしました。")
    
    # リモートファイルのリスト表示
    print("\n--- リモートディレクトリの内容 ---")
    for item in webdav.ls(remote_base_dir):
        print(f"  - {item.name} (サイズ: {item.size} バイト)")

except Exception as e:
    print(f"WebDAV操作中にエラーが発生しました: {e}")

選択肢は、要件、セキュリティ、既存のインフラ、そしてコストによって異なります。

  • WebDAV
    HTTPベースでファイルの管理を行いたい場合。Nextcloudなどのウェブアプリケーションでファイル共有によく使われる。
  • クラウドストレージ
    スケーラビリティ、耐久性、高度な機能、そして将来性が必要な場合。現代の多くのアプリケーションで推奨される選択肢。
  • FTPS
    既存のFTPサーバーを利用しつつ、セキュリティを向上させたい場合。
  • SFTP
    最も安全なファイル転送プロトコルが必要な場合。既存のSSHサーバーを活用できる。