FTP接続の悩みを解決!Python ftplib.all_errorsで賢くエラー処理

2025-06-06

具体的には、ftplib.all_errorsには以下の例外クラスが含まれています。

  • EOFError: 予期せずFTP接続が閉じられた場合に発生します。
  • OSError: オペレーティングシステムレベルのエラー(ネットワーク接続の問題など)が発生した場合に発生します。
  • ftplib.error_proto: FTPプロトコルの仕様に合わない応答がサーバーから返された場合に発生します。
  • ftplib.error_perm: 永続的なエラー(5xx番台の応答コード、例:権限不足)が発生した場合に発生します。
  • ftplib.error_temp: 一時的なエラー(4xx番台の応答コード)が発生した場合に発生します。
  • ftplib.error_reply: FTPサーバーからの予期せぬ応答があった場合に発生します。

ftplib.all_errorsを使う理由

通常、Pythonで例外処理を行う際には、特定の例外クラスを捕捉するためにexcept SomeException as e:のように記述します。しかし、ftplibを使用する場合、FTP関連の多くの異なる例外を個別に捕捉するのは手間がかかります。

ftplib.all_errorsを使うことで、これらのFTP接続に関するあらゆるエラーを一括して捕捉することができます。これにより、コードの記述量を減らし、より堅牢なエラーハンドリングを実現できます。

import ftplib

try:
    ftp = ftplib.FTP('ftp.example.com')
    ftp.login('user', 'password')
    # FTP操作(ファイルのアップロード、ダウンロード、ディレクトリ変更など)
    ftp.cwd('/some/directory')
    with open('local_file.txt', 'rb') as fp:
        ftp.storbinary('STOR remote_file.txt', fp)
    ftp.quit()
except ftplib.all_errors as e:
    print(f"FTPエラーが発生しました: {e}")
    # エラーに応じた処理を行う
except Exception as e:
    print(f"予期せぬエラーが発生しました: {e}")
    # FTP関連以外の予期せぬエラーを捕捉


ftplib.all_errorsで捕捉される一般的なエラーとその対処法

ftplib.all_errorsには、ftplibが生成する固有の例外(error_reply, error_temp, error_perm, error_proto)に加え、ネットワーク関連の汎用的なエラーであるOSErrorや、接続が予期せず閉じられた場合に発生するEOFErrorが含まれます。

接続エラー (OSError / ftplib.error_reply / ftplib.error_perm)

よくある原因

  • ネットワーク接続の問題
    クライアントのインターネット接続に問題がある。
  • FTPサーバーがダウンしている
    サーバー自体が稼働していない。
  • ファイアウォールによるブロック
    クライアント側またはサーバー側のファイアウォールが接続をブロックしている。
  • ポート番号の間違い
    通常FTPは21番ポートを使用しますが、異なるポートで稼働している場合がある。
  • ホスト名またはIPアドレスの間違い
    FTPサーバーのアドレスが間違っている。

エラーメッセージ例

  • ftplib.error_reply: 530 Login authentication failed. (ログイン認証失敗)
  • [Errno 8] nodename nor servname provided, or not known (ホスト名解決不可)
  • [Errno 111] Connection refused (接続拒否)

トラブルシューティング

  • 認証情報の確認
    ユーザー名とパスワードが正しいか再確認します。特にパスワードは大文字小文字の区別や特殊文字に注意します。
  • ネットワーク接続の確認
    インターネットに接続できるか、他のWebサイトにアクセスできるかなどを確認します。
  • FTPサーバーの稼働状況確認
    サーバー管理者に連絡して、FTPサーバーが正常に稼働しているか確認してもらいます。
  • ファイアウォール設定の確認
    自身のPCのファイアウォールや、企業のネットワーク環境でのファイアウォールがFTP接続をブロックしていないか確認します。必要に応じて許可設定を追加します。サーバー側管理者にも確認します。
  • ホスト名・ポート番号の確認
    FTPサーバーの正しいホスト名(またはIPアドレス)とポート番号を再確認します。

認証エラー (ftplib.error_perm: 530 Login authentication failed., 500 OOPS: priv_sock_get_cmd)

よくある原因

  • ログイン試行回数の制限
    短時間に何度もログインに失敗し、一時的にブロックされている。
  • 匿名ログインの制限
    サーバーが匿名ログインを許可していない、または特定のIPアドレスからの匿名ログインを制限している。
  • ユーザー名またはパスワードの間違い
    最も一般的な原因です。

トラブルシューティング

  • サーバー管理者に確認
    ログインポリシー(匿名ログインの可否、パスワードの要件、ロックアウトポリシーなど)について確認します。
  • FTPクライアントでのテスト
    FileZillaなどの一般的なFTPクライアントツールを使用して、同じ認証情報で接続できるか試します。これにより、Pythonコードの問題か、認証情報やサーバー設定の問題かを切り分けられます。
  • 認証情報の再確認
    ユーザー名とパスワードを再度確認し、入力ミスがないか慎重に確認します。

ファイル操作エラー (ftplib.error_perm / ftplib.error_reply)

FTPサーバー上のファイルやディレクトリを操作しようとしたときに発生します。

よくある原因

  • ディスク容量不足
    サーバー側のディスク容量が不足している(アップロード時など)。
  • ファイルが存在しない、または使用中
    ダウンロードしようとしたファイルがサーバー上にない、または他のプロセスによってロックされている。
  • 権限不足
    ユーザーがそのファイルやディレクトリに対する操作(読み込み、書き込み、削除など)の権限を持っていない。
  • ファイルのパスの間違い
    存在しないファイルやディレクトリを指定している。

エラーメッセージ例

  • ftplib.error_perm: 553 Could not create file. (ファイルの作成ができなかった、権限不足やディスク容量不足の可能性)
  • ftplib.error_perm: 550 /path/to/nonexistent_file: No such file or directory. (指定のファイルまたはディレクトリが存在しない)
  • ftplib.error_perm: 550 Failed to open file. (ファイルを開けなかった)

トラブルシューティング

  • サーバーのディスク容量確認
    アップロード時にこのエラーが出る場合、サーバー側のディスク容量が不足していないかサーバー管理者に確認します。
  • ファイルの存在確認
    存在しないファイルを操作しようとしていないか確認します。ftp.nlst()ftp.dir()でディレクトリ内容をリストして確認できます。
  • 権限の確認
    ログインしているFTPユーザーが、目的の操作を行うための十分な権限を持っているか確認します。サーバー管理者に問い合わせる必要がある場合があります。
  • パスの確認
    FTPサーバー上のファイルやディレクトリの正確なパスを確認します。FTPクライアントで確認するのが確実です。

タイムアウトエラー (OSError: [Errno 10060] A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond.など / EOFError)

よくある原因

  • アイドルタイムアウト
    FTPサーバー側で設定されたアイドルタイムアウトに達し、接続が強制的に切断された。特にデータ転送中に長時間無応答になった場合に発生しやすいです。
  • FTPサーバーの応答遅延
    サーバーが過負荷状態にあるか、一時的に応答が遅れている。
  • ネットワークの遅延
    ネットワークの状態が悪く、応答に時間がかかりすぎている。

トラブルシューティング

  • Keep-Aliveメッセージの送信
    一部のFTPサーバーでは、データ転送中に制御接続がアイドル状態になるとタイムアウトすることがあります。ftplibには直接的なKeep-Alive機能はありませんが、データ転送中に適度にftp.voidcmd('NOOP')のようなコマンドを送信することで、制御接続のタイムアウトを防げる場合があります。ただし、これはFTPサーバーの実装に依存します。
  • サーバー負荷の確認
    サーバーが過負荷状態でないか、管理者に確認します。
  • ネットワーク環境の改善
    可能な限り安定したネットワーク環境で実行します。
  • タイムアウト値の調整
    ftplib.FTPオブジェクトの初期化時やconnect()メソッドでtimeout引数を指定して、タイムアウト時間を延長してみます。ただし、無限に長く設定すると処理がハングアップする可能性があるので注意が必要です。
    import ftplib
    # タイムアウトを30秒に設定
    ftp = ftplib.FTP('ftp.example.com', timeout=30)
    

接続切断 (EOFError)

よくある原因

  • ネットワークの問題
    接続途中にネットワークが途切れた。
  • FTPサーバーが予期せず接続を閉じた
    サーバー側の問題(クラッシュ、再起動など)、またはサーバーが設定したタイムアウトに達したため。
  • 接続の再試行
    短時間での一時的な切断であれば、一定時間待ってから接続を再試行するロジックを実装します。
  • try...finallyブロックでのquit()/close()
    FTP操作の最後にftp.quit()(またはftp.close())が確実に呼び出されるように、finallyブロックを使用します。
    import ftplib
    
    ftp = None # 初期化
    try:
        ftp = ftplib.FTP('ftp.example.com')
        ftp.login('user', 'password')
        # FTP操作
    except ftplib.all_errors as e:
        print(f"FTPエラーが発生しました: {e}")
    finally:
        if ftp: # ftpオブジェクトが作成されている場合のみ
            try:
                ftp.quit() # または ftp.close()
            except ftplib.all_errors as e:
                print(f"FTP切断時にエラーが発生しました: {e}")
    
  • パッシブモード/アクティブモード
    FTP接続にはアクティブモードとパッシブモードがあります。デフォルトではパッシブモードが使用されますが、特定のネットワーク環境(特にファイアウォールが多い環境)では問題が発生する場合があります。ftp.set_pasv(False)でアクティブモードに切り替えて試すことも検討できます。
  • Pythonのバージョン
    使用しているPythonのバージョンとftplibのバージョン(Python標準ライブラリなので通常はPythonバージョンに依存)が、FTPサーバーの要件や既知のバグと関係ないか確認します。
  • エラーメッセージの解析
    ftplib.all_errorsで捕捉された例外オブジェクトには、多くの場合、詳細なエラーメッセージが含まれています。特にFTPサーバーからの応答コード(例: 550, 530など)は、エラーの原因を特定する上で非常に役立ちます。これらのコードはFTPプロトコルの標準で定義されており、検索することで具体的な意味を調べられます。
    except ftplib.all_errors as e:
        print(f"エラー内容: {e}")
        # 例外オブジェクトがerror_replyの場合、HTTPステータスコードのようにエラーコードが含まれることがあります。
        if isinstance(e, ftplib.error_reply):
            print(f"FTPサーバー応答: {e.args[0]}") # 応答コードとメッセージ
    
  • FTPクライアントでの再現
    Pythonスクリプトで発生するエラーが、FileZillaなどのGUIベースのFTPクライアントでも再現するかどうかを試すことは非常に重要です。FTPクライアントで再現できる場合は、サーバー側の問題や認証情報、ネットワーク環境の問題である可能性が高いです。再現しない場合は、Pythonコードに問題がある可能性が高いです。
  • 詳細なログ出力
    ftplib.FTPオブジェクトのset_debuglevel(level)メソッドを使用して、デバッグレベルを設定することで、FTPコマンドのやり取りを詳細に確認できます。これにより、どのコマンドでエラーが発生しているか、サーバーからの応答は何かなどが分かり、問題の特定に役立ちます。
    ftp = ftplib.FTP('ftp.example.com')
    ftp.set_debuglevel(2) # 詳細なデバッグ情報を出力
    


基本的なエラーハンドリング

import ftplib
import socket # ネットワーク関連の一般的なエラーも捕捉するため

def basic_ftp_operation(host, user, password, remote_path):
    ftp = None # ftpオブジェクトの初期化
    try:
        print(f"Connecting to {host}...")
        ftp = ftplib.FTP(host)
        # タイムアウトを設定することも可能です (例: 10秒)
        # ftp.connect(host, 21, timeout=10)

        print(f"Logging in as {user}...")
        ftp.login(user, password)

        print(f"Changing directory to {remote_path}...")
        ftp.cwd(remote_path)

        print("Listing directory contents:")
        # ディレクトリの内容をリスト表示
        files = ftp.nlst()
        for f in files:
            print(f"- {f}")

        print("FTP operation successful!")

    except ftplib.all_errors as e:
        # ftplibが生成する全てのFTP関連エラーを捕捉
        print(f"### FTP ERROR OCCURRED: {e}")
        print("Please check host, username, password, and file/directory paths.")
    except socket.gaierror as e:
        # ホスト名解決エラー (例: ドメイン名が存在しない)
        print(f"### NETWORK ERROR: Hostname resolution failed for {host}. {e}")
        print("Please check the host name or your network connection.")
    except socket.error as e:
        # その他の一般的なソケットエラー (例: 接続拒否、ネットワーク到達不能)
        print(f"### NETWORK ERROR: Could not connect to {host}. {e}")
        print("Please check your network connection or firewall settings.")
    except Exception as e:
        # 予期せぬその他のエラーを捕捉
        print(f"### UNEXPECTED ERROR: An unhandled error occurred: {e}")
    finally:
        if ftp:
            try:
                print("Quitting FTP connection...")
                ftp.quit()
            except ftplib.all_errors as e:
                # 接続終了時のエラーも念のため捕捉
                print(f"### ERROR during FTP disconnect: {e}")
            except Exception as e:
                print(f"### UNEXPECTED ERROR during disconnect: {e}")

# --- 実行例 ---
# 成功する可能性のある設定(匿名FTPサーバーなど)
# basic_ftp_operation('ftp.dlptest.com', 'dlptest', 'dlptest', '/')

# ホスト名が存在しない場合(socket.gaierror を発生させる)
# basic_ftp_operation('nonexistent.example.com', 'user', 'pass', '/')

# 認証失敗の例(ftplib.error_perm を発生させる)
# basic_ftp_operation('ftp.dlptest.com', 'wronguser', 'wrongpass', '/')

# 存在しないディレクトリへのアクセス例(ftplib.error_perm を発生させる)
# basic_ftp_operation('ftp.dlptest.com', 'dlptest', 'dlptest', '/nonexistent_dir')

# ご自身のFTPサーバー情報に置き換えて試してください
# basic_ftp_operation('your_ftp_host', 'your_username', 'your_password', '/your_remote_path')

解説

  • finallyブロック: ftpオブジェクトが正常に作成された場合、必ずftp.quit()を呼び出して接続を安全に終了させるために使用します。ここでも切断時にエラーが発生する可能性があるので、再度try...exceptを入れています。
  • Exception as e: 上記のどの例外にも当てはまらない、予期せぬエラーを最終的に捕捉するためのものです。
  • socket.gaierrorsocket.error: ftplib.all_errorsOSErrorを含みますが、特定のネットワーク関連のエラー(ホスト名解決失敗、接続拒否など)は、より具体的にsocketモジュールの例外として捕捉すると、エラーの原因がネットワーク側にあることを明確にユーザーに伝えられます。

ファイルアップロード時のエラーハンドリング

ファイルをFTPサーバーにアップロードする際に発生しうるエラーに焦点を当てた例です。

import ftplib
import os

def upload_file_to_ftp(host, user, password, local_filepath, remote_filename):
    ftp = None
    try:
        if not os.path.exists(local_filepath):
            print(f"Error: Local file '{local_filepath}' does not exist.")
            return

        print(f"Connecting to {host}...")
        ftp = ftplib.FTP(host)
        ftp.login(user, password)

        print(f"Uploading '{local_filepath}' as '{remote_filename}'...")
        with open(local_filepath, 'rb') as fp:
            # STORコマンドでファイルをアップロード
            # ftplib.error_perm (権限不足、ディスク容量不足など) が発生しうる
            ftp.storbinary(f'STOR {remote_filename}', fp)

        print(f"File '{local_filepath}' uploaded successfully to '{remote_filename}'!")

    except ftplib.all_errors as e:
        print(f"### FTP UPLOAD ERROR: {e}")
        print("Possible causes: Incorrect remote path, insufficient permissions, or server disk full.")
    except Exception as e:
        print(f"### UNEXPECTED ERROR during upload: {e}")
    finally:
        if ftp:
            try:
                ftp.quit()
            except ftplib.all_errors:
                pass # 切断時のエラーはログに記録しない場合もある

解説

  • ftp.storbinary(): このメソッドでファイルがアップロードされます。この際に、サーバー側の権限不足(ftplib.error_perm)、ディスク容量不足、または不正なリモートパスなどが原因でエラーが発生する可能性があります。これらのエラーはftplib.all_errorsで捕捉されます。
  • os.path.exists(local_filepath): まず、ローカルファイルが存在するかどうかを確認します。存在しない場合はFTP操作を行う前にエラーメッセージを表示します。

ディレクトリ作成時のエラーハンドリング

FTPサーバー上に新しいディレクトリを作成する際の例です。

import ftplib

def create_ftp_directory(host, user, password, new_dir_path):
    ftp = None
    try:
        print(f"Connecting to {host}...")
        ftp = ftplib.FTP(host)
        ftp.login(user, password)

        print(f"Attempting to create directory: {new_dir_path}")
        # MKDコマンドでディレクトリを作成
        # ftplib.error_perm (親ディレクトリの権限不足、ディレクトリが既に存在する)
        # ftplib.error_reply (サーバーがMKDコマンドをサポートしていないなど)
        ftp.mkd(new_dir_path)

        print(f"Directory '{new_dir_path}' created successfully!")

    except ftplib.error_perm as e:
        # ディレクトリ作成時の権限エラーや、既にディレクトリが存在する場合
        print(f"### FTP DIRECTORY CREATION ERROR (Permission/Existing): {e}")
        print("Possible causes: Insufficient permissions, or directory already exists.")
    except ftplib.all_errors as e:
        # その他のFTP関連エラーを捕捉 (例: error_reply など)
        print(f"### FTP DIRECTORY CREATION ERROR: {e}")
    except Exception as e:
        print(f"### UNEXPECTED ERROR during directory creation: {e}")
    finally:
        if ftp:
            try:
                ftp.quit()
            except ftplib.all_errors:
                pass

# --- 実行例 ---
# create_ftp_directory('ftp.dlptest.com', 'dlptest', 'dlptest', 'test_new_dir')
# 匿名FTPサーバーではディレクトリ作成が許可されていないことが多いので注意
# 通常は専用のFTPアカウントとサーバーで試す必要があります

解説

  • ftplib.all_errorsは残しておき、error_perm以外のFTP関連エラーを捕捉するようにしています。
  • except ftplib.error_perm as e:: この例では、ftplib.error_permftplib.all_errorsより前に捕捉しています。これは、ディレクトリ作成でよくある「権限不足」や「既に存在する」といった特定のエラーに対して、より詳細なメッセージを表示するためです。これにより、ユーザーは一般的なFTPエラーではなく、より具体的なエラー原因を把握しやすくなります。
  • ftp.mkd(): ディレクトリ作成コマンドです。

エラー発生時に、FTPコマンドのやり取りを詳細に確認するためにset_debuglevelを使用する例です。

import ftplib
import sys # デバッグ出力をstderrにリダイレクトするため

def debug_ftp_connection(host, user, password):
    ftp = None
    try:
        print(f"Connecting to {host} with debug mode...")
        # set_debuglevel(1) は基本的な送受信コマンドを表示
        # set_debuglevel(2) はさらに詳細な情報を表示
        ftp = ftplib.FTP(host)
        ftp.set_debuglevel(2) # デバッグレベルを設定
        # デバッグ出力を特定のファイルにリダイレクトすることも可能
        # ftp.set_debuglevel(2, sys.stdout) # 標準出力へ
        # ftp.set_debuglevel(2, open('ftp_debug.log', 'w')) # ファイルへ

        print(f"Logging in as {user}...")
        ftp.login(user, password)

        print("Listing current directory contents:")
        ftp.nlst() # この操作でエラーが発生した場合のデバッグ出力を確認

        print("FTP operation successful!")

    except ftplib.all_errors as e:
        print(f"\n### FTP ERROR OCCURRED: {e}")
        print("Review the debug output above for details on the FTP communication.")
    except Exception as e:
        print(f"\n### UNEXPECTED ERROR: {e}")
    finally:
        if ftp:
            try:
                print("Quitting FTP connection...")
                ftp.quit()
            except ftplib.all_errors:
                pass

# --- 実行例 ---
# debug_ftp_connection('ftp.dlptest.com', 'wronguser', 'wrongpass') # 認証失敗をデバッグ
# debug_ftp_connection('nonexistent.example.com', 'user', 'pass') # ホスト名解決失敗をデバッグ
  • ftp.set_debuglevel(level): このメソッドは、FTPコマンドの送受信の詳細を標準エラー出力(または指定したファイル)に表示します。levelが高いほど詳細な情報が出力されます。エラーが発生した際に、どのコマンドで、サーバーからどのような応答があったのかを確認できるため、トラブルシューティングに非常に役立ちます。


個別の例外クラスを捕捉する

ftplib.all_errorsに含まれる主な例外クラス

  • EOFError: 予期せぬ接続終了
  • OSError: オペレーティングシステムレベルのエラー (ネットワーク接続の問題など)
  • ftplib.error_proto: FTPプロトコルの違反
  • ftplib.error_perm: 永続的なエラー (5xx番台の応答コード、権限不足など)
  • ftplib.error_temp: 一時的なエラー (4xx番台の応答コード)
  • ftplib.error_reply: FTPサーバーからの予期せぬ応答 (例: 4xx 一時的なエラー、5xx 永続的なエラー)

コード例

import ftplib
import socket # OSError や EOFError の具体的な原因を特定するため

def handle_individual_errors(host, user, password):
    ftp = None
    try:
        ftp = ftplib.FTP(host)
        ftp.login(user, password)
        print("Login successful.")

        # 存在しないディレクトリへの変更を試みる (ftplib.error_perm を発生させる可能性が高い)
        ftp.cwd("/nonexistent_directory_12345")

        print("Operation successful!")

    except ftplib.error_perm as e:
        # 永続的なエラー(例:権限不足、ファイル/ディレクトリが存在しない)
        print(f"ERROR: Permission/Permanent error occurred: {e}")
        # 例外メッセージからFTP応答コードとメッセージを解析する
        if e.args and isinstance(e.args[0], str) and e.args[0].strip():
            # 例: '550 Failed to change directory.' の '550' を取得
            error_code = e.args[0].split(' ')[0]
            print(f"  FTP Error Code: {error_code}")
            if error_code == '550':
                print("  Reason: Directory might not exist or insufficient permissions.")
        # 特定のログ記録、ユーザーへの指示など

    except ftplib.error_temp as e:
        # 一時的なエラー(例:サーバーが一時的に利用不可)
        print(f"WARNING: Temporary error occurred: {e}")
        print("  Reason: Server might be temporarily overloaded. Consider retrying.")
        # 短い時間待って再試行するロジックをここに実装するなど

    except ftplib.error_reply as e:
        # その他の一般的なFTP応答エラー(error_perm/temp で捕捉されないもの)
        print(f"ERROR: FTP server replied with an unexpected error: {e}")
        print("  Reason: Unhandled FTP server response.")

    except OSError as e:
        # ネットワーク関連のエラー(接続拒否、ホスト不明など)
        print(f"ERROR: Network or OS error occurred: {e}")
        if isinstance(e, socket.gaierror):
            print("  Reason: Hostname could not be resolved. Check host name or network.")
        elif isinstance(e, socket.error):
            print("  Reason: Connection refused or other network issue. Check firewall or server status.")
        # ネットワーク問題に対する特定のエラーハンドリング

    except EOFError as e:
        # 予期せぬ接続終了
        print(f"ERROR: Unexpected end of file/connection: {e}")
        print("  Reason: Connection was likely closed unexpectedly by the server.")

    except Exception as e:
        # 上記以外の予期せぬエラー
        print(f"ERROR: An unhandled exception occurred: {e}")

    finally:
        if ftp:
            try:
                print("Quitting FTP connection...")
                ftp.quit()
            except ftplib.all_errors:
                print("Warning: Error during FTP disconnect.")

# --- 実行例 ---
# 存在しないディレクトリへのアクセスを試みる (ftplib.error_perm)
# handle_individual_errors('ftp.dlptest.com', 'dlptest', 'dlptest')

# 認証失敗 (ftplib.error_perm: 530 Login authentication failed)
# handle_individual_errors('ftp.dlptest.com', 'wronguser', 'wrongpass')

# 存在しないホスト名 (OSError/socket.gaierror)
# handle_individual_errors('nonexistent.example.com', 'user', 'pass')

# 自身で設定したFTPサーバーで試す
# handle_individual_errors('your_ftp_host', 'your_username', 'your_password')

利点

  • コードの可読性向上
    どのexceptブロックがどの種類のエラーを処理しているか明確になります。
  • 詳細なエラーハンドリング
    エラーの種類に応じて異なるログメッセージ、ユーザーへのフィードバック、リカバリー戦略(例:一時的なエラーなら再試行、永続的なエラーなら中止)を実装できます。

欠点

  • 網羅性の課題
    ftplib.all_errorsがカバーする全ての例外を個別に列挙し忘れる可能性があります。
  • コード量増加
    捕捉する例外の種類が多いほど、exceptブロックが増え、コードが冗長になる可能性があります。

特定のFTP応答コードを解析する

ftplibの例外は、FTPサーバーから返された数値の応答コード(例: 200, 550)を含むことがあります。これらのコードを直接解析して、よりきめ細やかなエラー判断を行うことができます。これは、ftplib.all_errorsや個別の例外を捕捉した後に行う追加のステップとして有効です。

コード例

import ftplib

def parse_ftp_response_code(host, user, password, remote_file_path):
    ftp = None
    try:
        ftp = ftplib.FTP(host)
        ftp.login(user, password)
        print("Login successful.")

        # 存在しないファイルの取得を試みる (550 No such file)
        # 正常な操作の場合は、error_reply を発生させない
        ftp.retrbinary(f'RETR {remote_file_path}', open('local_temp.txt', 'wb').write)

        print("File downloaded successfully!")

    except ftplib.error_reply as e:
        # ftplib.error_reply は応答コードを保持することが多い
        print(f"FTP error reply: {e}")
        try:
            # エラーメッセージの最初の部分が応答コードであることが多い
            response_code = int(str(e).split(' ')[0])
            print(f"Detected FTP Response Code: {response_code}")

            if response_code == 530:
                print("  Meaning: Not logged in / Authentication failed.")
                # 認証情報の再入力を促すなど
            elif response_code == 550:
                print("  Meaning: Requested action not taken (e.g., file not found, permission denied).")
                # ファイルパスの確認や権限の確認を促すなど
            elif response_code >= 400 and response_code < 500:
                print("  Meaning: Transient negative completion reply (temporary error).")
            elif response_code >= 500 and response_code < 600:
                print("  Meaning: Permanent negative completion reply (permanent error).")
            else:
                print("  Meaning: Unrecognized FTP response code.")

        except (ValueError, IndexError):
            print("  Could not parse FTP response code from error message.")
        # ここで特定の応答コードに応じた追加の処理を行う

    except ftplib.all_errors as e:
        # その他の ftplib 関連エラーを捕捉
        print(f"General FTP error: {e}")
    except Exception as e:
        print(f"An unexpected error occurred: {e}")
    finally:
        if ftp:
            ftp.quit()

# --- 実行例 ---
# 存在しないファイルへのアクセスを試みる (ftplib.error_reply -> 550)
# parse_ftp_response_code('ftp.dlptest.com', 'dlptest', 'dlptest', 'nonexistent_file_12345.txt')

# 認証失敗 (ftplib.error_reply -> 530)
# parse_ftp_response_code('ftp.dlptest.com', 'wronguser', 'wrongpass', 'any_file.txt')

利点

  • 国際化対応の容易さ
    エラーメッセージの文字列に依存せず、数値コードで判断できるため、異なる言語環境でも安定して動作します。
  • 非常に詳細なエラー判断
    FTPプロトコル標準に則った厳密なエラーハンドリングが可能です。

欠点

  • すべてのエラーに適用できない
    OSErrorEOFErrorなど、FTP応答コードを直接含まない例外もあります。
  • 実装の複雑さ
    全ての関連するFTP応答コードとその意味を把握し、それらに対応するロジックを記述する必要があります。

ftplib.error_tempや一部のOSErrorEOFErrorは一時的なネットワークの問題やサーバーの負荷によって発生することがあります。このような場合に、ftplib.all_errorsで捕捉した後に、自動的に操作を再試行するロジックを実装することは一般的な代替手段です。

コード例

import ftplib
import time
import socket

def robust_ftp_download(host, user, password, remote_file, local_file, max_retries=3, delay_seconds=5):
    ftp = None
    retries = 0
    while retries < max_retries:
        try:
            print(f"Attempt {retries + 1} of {max_retries} to connect to {host}...")
            ftp = ftplib.FTP(host)
            ftp.login(user, password)
            print("Login successful.")

            print(f"Downloading {remote_file} to {local_file}...")
            with open(local_file, 'wb') as fp:
                ftp.retrbinary(f'RETR {remote_file}', fp.write)
            print("Download successful!")
            return # 成功したら関数を終了

        except ftplib.error_temp as e:
            print(f"WARNING: Temporary FTP error: {e}. Retrying in {delay_seconds} seconds...")
            retries += 1
            time.sleep(delay_seconds)
            if ftp: # 接続が確立されていた場合、切断して再試行
                try: ftp.quit()
                except ftplib.all_errors: pass

        except ftplib.error_perm as e:
            # 永続的なエラーの場合は再試行せず即座に終了
            print(f"ERROR: Permanent FTP error, not retrying: {e}")
            break # ループを抜ける

        except (OSError, EOFError, socket.error) as e:
            print(f"WARNING: Network/Connection error: {e}. Retrying in {delay_seconds} seconds...")
            retries += 1
            time.sleep(delay_seconds)
            if ftp:
                try: ftp.quit()
                except ftplib.all_errors: pass

        except ftplib.all_errors as e:
            # その他の ftplib エラー(未分類のエラー)
            print(f"ERROR: General FTP error: {e}. Not retrying this type of error.")
            break

        except Exception as e:
            print(f"ERROR: An unhandled exception occurred: {e}. Not retrying.")
            break
        finally:
            # 既に上で quit() している可能性もあるため、念のためチェック
            if ftp:
                try:
                    # ftplib.error_tempなどで処理済みの場合、ここで再度quit()を試みる必要はないが、
                    # 確実に閉じたい場合はここにも入れる。ここでは個別のexceptでquitしているため省略。
                    pass
                except ftplib.all_errors:
                    pass

    print(f"Failed to complete operation after {max_retries} retries.")

# --- 実行例 ---
# この例をテストするには、一時的に接続を不安定にできるFTPサーバーが必要です。
# 例として、存在しないファイルをダウンロードしようとすると永続的なエラーになるため、
# リトライは発生しません。
# robust_ftp_download('ftp.dlptest.com', 'dlptest', 'dlptest', 'nonexistent_file.txt', 'downloaded_file.txt')

# 実際にリトライをテストするには、ネットワークを一時的に切断したり、
# サーバーが一時的に応答しなくなる状況をシミュレートする必要があります。

利点

  • 自動回復
    ユーザーやアプリケーションの介入なしに、一時的な障害から回復できます。
  • 堅牢性の向上
    一時的な問題によって処理が中断されるのを防ぎます。

欠点

  • 永続的なエラーへの非対応
    永続的なエラー(例:権限不足、ファイルが存在しない)に対しては再試行しても無意味です。
  • 処理時間の増加
    エラーが発生するたびに待機時間が発生するため、全体の処理時間が長くなる可能性があります。
  • ロジックの複雑化
    リトライ回数、遅延時間、リトライすべきエラーとすべきでないエラーの判断など、ロジックが複雑になります。

ftplib.all_errors手軽さと網羅性を提供するため、多くのユースケースで非常に有効です。しかし、よりきめ細やかなエラーハンドリングが必要な場合は、以下の代替方法を検討してください。

  1. 個別の例外クラスを捕捉する
    エラーの種類に応じて異なる処理を行いたい場合。
  2. FTP応答コードを解析する
    厳密なエラー判断が必要な場合。
  3. リトライロジックを実装する
    一時的なネットワーク問題やサーバー負荷から自動回復したい場合。