もう迷わない!Python ftplib.FTP.sendcmd()を使ったFTPプログラミング実践ガイド

2025-06-06

Pythonのftplibモジュールは、FTP (File Transfer Protocol) クライアントを実装するための機能を提供します。その中で、ftplib.FTP.sendcmd() メソッドは、FTPサーバーに生のコマンドを送信し、その応答を受け取るために使用されます。

sendcmd() は、FTPプロトコルの詳細な動作を理解している場合に非常に役立ちます。通常、ftplibの他の高レベルなメソッド(例: storbinary(), retrbinary(), cwd() など)は、内部的にsendcmd()を使用してFTPコマンドを構築し、送受信しています。しかし、これらの高レベルなメソッドでサポートされていない、特定のFTPコマンドや、より低レベルな制御が必要な場合に、sendcmd()を直接使用することができます。

構文

sendcmd(command)
  • command: FTPサーバーに送信する文字列形式のコマンドです。例えば、'LIST', 'CWD /path/to/dir', 'TYPE I' などです。

動作

  1. コマンドの送信: sendcmd() は、指定された command をFTPサーバーに送信します。コマンドの末尾には、FTPプロトコルで要求される改行コード(CRLF)が自動的に追加されます。
  2. 応答の受信: コマンドを送信した後、FTPサーバーからの応答を待ちます。FTPサーバーは通常、ステータスコードとメッセージを含む応答を返します。
  3. 応答の解析: sendcmd() は、サーバーからの応答の最初の行を返します。この応答には、通常、3桁の数字からなるステータスコードと、それに続くテキストメッセージが含まれます。

戻り値

FTPサーバーからの応答の最初の行(文字列)を返します。この文字列は通常、FTPステータスコードで始まり、その後ろにメッセージが続きます。

例:

  • '550 Requested action not taken.'
  • '200 Command okay.'

エラー処理

FTPサーバーからの応答がエラーを示すステータスコード(例: 4xx, 5xx)である場合、sendcmd() は通常、ftplib.Error またはそのサブクラスの例外(例: ftplib.all_errors) を発生させます。例外の種類は、特定のFTPエラーコードや状況によって異なります。

使用例

一般的なFTP操作ではあまり直接使われませんが、以下のような場合に有用です。

  1. 標準的でないFTPコマンドの実行: ftplibのラッパーメソッドが存在しない、特殊なFTPコマンドを実行したい場合。
  2. 詳細なプロトコルデバッグ: FTPセッション中に何が起こっているかを正確に把握したい場合。
  3. カスタムFTPクライアントの構築: より低レベルでFTPプロトコルを制御したい場合。
from ftplib import FTP

try:
    with FTP('ftp.example.com') as ftp:
        ftp.login(user='your_username', passwd='your_password')

        # 'SYST' コマンドを送信して、サーバーのOSタイプを取得する
        # このコマンドは通常、ftplib.FTP.sendcmd() を使って実行される
        response = ftp.sendcmd('SYST')
        print(f"SYST コマンドの応答: {response}") # 例: 215 UNIX Type: L8

        # 'NOOP' コマンドを送信して、サーバーが応答するかどうかを確認する
        # これはセッションをアクティブに保つためにも使われることがある
        response = ftp.sendcmd('NOOP')
        print(f"NOOP コマンドの応答: {response}") # 例: 200 Command okay.

        # 存在しないディレクトリにCWDを試みる (エラーの例)
        try:
            response = ftp.sendcmd('CWD /nonexistent_directory')
            print(f"CWD コマンドの応答: {response}")
        except Exception as e:
            print(f"CWD コマンドのエラー: {e}") # 例: ftplib.error_perm: 550 Failed to change directory.

except Exception as e:
    print(f"FTP接続またはコマンド実行中にエラーが発生しました: {e}")



ftplib.FTP.sendcmd() に関連する一般的なエラーとトラブルシューティング

ftplib.FTP.sendcmd() を使用する際に発生する可能性のあるエラーは、主にFTPプロトコルの特性とサーバー側の挙動に起因します。

ftplib.error_reply: 予期せぬサーバー応答

  • トラブルシューティング:
    1. コマンドの確認: 送信しているFTPコマンドが正しい構文であるか、RFC 959 (FTPプロトコル仕様) やサーバーのドキュメントで確認します。大文字小文字の区別や引数の形式に注意してください。
    2. サーバーのログ確認: 可能であれば、FTPサーバー側のログを確認し、サーバーがどのようなコマンドを受け取り、どのような応答を返したかを確認します。
    3. より高レベルのメソッドの利用: sendcmd() は低レベルなため、可能であれば ftplib.FTP が提供するより抽象化されたメソッド(ftp.cwd(), ftp.nlst(), ftp.storbinary() など)を使用することを検討してください。これらのメソッドは内部で適切な sendcmd() 呼び出しを処理し、エラーハンドリングも行います。
    4. デバッグレベルの設定: ftp.set_debuglevel(1) を設定して、送受信されるFTPコマンドと応答をコンソールに出力し、問題の特定に役立てます。
  • 考えられる原因:
    • 不正なコマンド文字列: sendcmd() に渡したコマンド文字列が、FTPプロトコルに準拠していない、またはサーバーが認識しないコマンドである。FTPコマンドは大文字小文字を区別する場合がある(通常は大文字)。
    • サーバーの特殊な実装: 一部のFTPサーバーは、標準RFCに厳密に従わない独自の拡張や挙動を持つことがあります。
    • セッション状態の不一致: コマンドを実行する現在のFTPセッションの状態が、そのコマンドに適していない場合(例: ログイン前にファイル転送コマンドを送信しようとする)。

ftplib.error_perm: 永久的なエラー (5xx ステータスコード)

  • トラブルシューティング:
    1. FTPユーザーの権限確認: 使用しているFTPアカウントに、実行しようとしている操作の権限があることを確認します。FTPクライアント(例: FileZilla)などで手動で操作を試すと良いでしょう。
    2. パスの正確性: ファイルやディレクトリのパスが、FTPサーバー上での正確なパスと一致していることを確認します。相対パスではなく、絶対パスを試すことも有効です。
    3. 存在確認: 操作対象のファイルやディレクトリがサーバー上に実際に存在するか確認します。
    4. ファイルタイプ: TYPE A (ASCII) または TYPE I (Binary) の設定が正しいか確認します。特に SIZE コマンドなどでバイナリモードが必要な場合があります。
    5. FTPS/Passive Mode: サーバーがFTPSを要求している場合は ftplib.FTP_TLS を使用し、必要に応じて ftp.prot_p() を呼び出します。また、ファイアウォールなどの問題でデータチャネルが確立できない場合は、ftp.set_pasv(True) でパッシブモードを使用していることを確認してください(通常はデフォルトで有効)。
  • 考えられる原因:
    • 権限不足: ログインしているユーザーに、指定された操作(例: ディレクトリ作成、ファイル削除、ファイル転送)を行う権限がない。
    • ファイル/ディレクトリが存在しない: 指定したファイルやディレクトリが存在しない。
    • 不正なパス: パスが正しくない、またはサーバーのファイルシステム構造と合致しない。
    • ディスク容量不足: サーバー側のディスク容量が不足している。
    • セキュアな接続の要件: サーバーがFTP-SSL/TLS (FTPS) を要求しているにもかかわらず、平文のFTP接続を試みている。

ftplib.error_temp: 一時的なエラー (4xx ステータスコード)

  • トラブルシューティング:
    1. 再試行: 一時的なエラーであるため、少し時間をおいてから操作を再試行することで解決することがあります。
    2. サーバーの状態確認: FTPサーバーの管理者に問い合わせ、サーバーの状態(負荷、接続数など)を確認します。
    3. 接続タイムアウトの設定: ftplib.FTP オブジェクトの初期化時に timeout パラメータを設定し、適切なタイムアウト時間を確保します。
  • 考えられる原因:
    • サーバーの負荷: サーバーが一時的に過負荷状態にある。
    • 同時接続数の制限: FTPサーバーの最大接続数に達している。
    • タイムアウト: サーバーからの応答が一定時間内にない。

OSError / socket.error (接続に関するエラー)

  • トラブルシューティング:
    1. ホスト名/IPアドレス/ポートの確認: 正しいFTPサーバーのホスト名、IPアドレス、ポート番号を使用しているか確認します。
    2. Ping/Telnetでの疎通確認: コマンドプロンプトやターミナルから ping ftp.example.comtelnet ftp.example.com 21 を実行し、FTPサーバーへの基本的なネットワーク疎通があるか確認します。
    3. ファイアウォールの設定: クライアントPCやネットワークのファイアウォール設定を確認し、FTP (ポート21) の通信が許可されていることを確認します。サーバー側のファイアウォールも確認します。
    4. VPN/プロキシ: VPNやプロキシを使用している場合、それらがFTP接続に影響を与えていないか確認します。
  • 考えられる原因:
    • ホスト名/IPアドレスの誤り: 指定したFTPサーバーのホスト名またはIPアドレスが間違っている。
    • ポート番号の誤り: FTPの標準ポート (21) 以外を使用しているにもかかわらず、指定を誤っている。
    • ファイアウォール: クライアント側またはサーバー側のファイアウォールが接続をブロックしている。
    • サーバーがダウンしている: FTPサーバーが停止しているか、到達不能である。
    • DNS解決の問題: ホスト名がIPアドレスに解決できない。

ftplib.error_proto: プロトコル違反エラー

  • トラブルシューティング:
    1. サーバーのログ確認: サーバーが何を出力しているのかを詳細に確認します。
    2. デバッグレベルの設定: set_debuglevel(1) で生の応答を確認します。
    3. サーバーの実装確認: サーバーが標準FTPプロトコルに準拠しているか、ドキュメントやベンダーに確認します。
  • 考えられる原因:
    • 非常に不正なサーバー応答: FTPサーバーが完全に不正なフォーマットで応答を返している。
  • RFC 959 (FTP Protocol): FTPプロトコルの詳細に興味がある、または複雑な問題をデバッグする必要がある場合は、RFC 959 を参照すると、各コマンドや応答コードの意味を深く理解できます。
  • 手動でのFTPクライアントテスト: コマンドラインFTPクライアント(Windowsの ftp コマンド、Linuxの ftp、またはGUIクライアントのFileZillaなど)を使用して、同じFTPサーバー、同じユーザー、同じパスで同様の操作が成功するかどうかをテストします。これにより、問題がPythonスクリプト側にあるのか、FTPサーバーの設定やネットワーク環境にあるのかを切り分けられます。
  • デバッグレベルの活用: ftp.set_debuglevel(1) (またはより高い値) を設定することで、PythonスクリプトがFTPサーバーとどのようなコマンドをやり取りし、どのような応答を受け取っているかを詳細に確認できます。これは問題の特定に非常に役立ちます。
  • try...except ブロックの使用: ftplib の操作は常に try...except ftplib.all_errors as e: のような形でエラーを捕捉し、適切に処理することが重要です。これにより、プログラムのクラッシュを防ぎ、エラーメッセージから原因を特定しやすくなります。


ftplib.FTP.sendcmd() は、FTPサーバーに直接コマンドを送信し、その応答を受け取るための低レベルなメソッドです。通常、ftplibモジュールが提供するより高レベルなメソッド(storbinary()nlst() など)を使うことが推奨されますが、特定のシナリオでは sendcmd() が非常に役立ちます。

基本的なコマンドの送信と応答の確認

最も基本的な使用法は、サーバーに単純なFTPコマンドを送信し、その応答を確認することです。

from ftplib import FTP

FTP_HOST = 'ftp.dlptest.com' # テスト用の公開FTPサーバー
FTP_USER = 'dlptest'
FTP_PASS = 'dlptest'

print(f"FTPサーバー {FTP_HOST} に接続中...")
try:
    with FTP(FTP_HOST) as ftp:
        print("ログイン中...")
        ftp.login(user=FTP_USER, passwd=FTP_PASS)
        print("ログイン成功!")

        # SYST コマンドを送信してサーバーのシステムタイプを取得
        # 'SYST' はサーバーのOSやシステム情報を問い合わせるコマンドです。
        # 応答は通常 '215 <システム情報>' の形式で返されます。
        print("\n--- SYST コマンドの実行 ---")
        try:
            response_syst = ftp.sendcmd('SYST')
            print(f"SYST コマンド応答: {response_syst}")
            if response_syst.startswith('215'):
                print("サーバーシステム情報の取得に成功しました。")
            else:
                print(f"SYST コマンドが予期せぬ応答を返しました: {response_syst}")
        except Exception as e:
            print(f"SYST コマンドの実行中にエラーが発生しました: {e}")

        # NOOP コマンドを送信してサーバーの活性をチェック
        # 'NOOP' はサーバーに何もさせずに、応答を期待するコマンドです。
        # セッションがタイムアウトするのを防ぐためにも使われることがあります。
        # 応答は通常 '200 Command okay.' です。
        print("\n--- NOOP コマンドの実行 ---")
        try:
            response_noop = ftp.sendcmd('NOOP')
            print(f"NOOP コマンド応答: {response_noop}")
            if response_noop.startswith('200'):
                print("NOOP コマンドが正常に処理されました。")
            else:
                print(f"NOOP コマンドが予期せぬ応答を返しました: {response_noop}")
        except Exception as e:
            print(f"NOOP コマンドの実行中にエラーが発生しました: {e}")

        print("\nログアウト中...")
        ftp.quit()
        print("ログアウト成功。")

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

エラー応答の処理 (例: 存在しないディレクトリへの移動)

sendcmd() を使う場合、エラー応答(4xx や 5xx のステータスコード)を適切に処理することが重要です。ftplib は通常、これらのエラーに対して例外を発生させます。

from ftplib import FTP, error_perm

FTP_HOST = 'ftp.dlptest.com'
FTP_USER = 'dlptest'
FTP_PASS = 'dlptest'

print(f"FTPサーバー {FTP_HOST} に接続中...")
try:
    with FTP(FTP_HOST) as ftp:
        print("ログイン中...")
        ftp.login(user=FTP_USER, passwd=FTP_PASS)
        print("ログイン成功!")

        # 存在するディレクトリへの移動 (成功例)
        print("\n--- 存在するディレクトリへの移動 (PUB) ---")
        try:
            # sendcmd() は、内部で 'CWD /PUB' コマンドを送信
            # 成功すると '250 CWD command successful.' のような応答
            response_cwd_pub = ftp.sendcmd('CWD /PUB')
            print(f"CWD /PUB 応答: {response_cwd_pub}")
            if response_cwd_pub.startswith('250'):
                print("ディレクトリ '/PUB' への移動に成功しました。")
            else:
                print(f"CWD /PUB が予期せぬ応答を返しました: {response_cwd_pub}")
        except Exception as e:
            print(f"CWD /PUB の実行中にエラーが発生しました: {e}")

        # 存在しないディレクトリへの移動 (失敗例)
        print("\n--- 存在しないディレクトリへの移動 (/nonexistent_dir) ---")
        try:
            # 存在しないディレクトリに CWD コマンドを送信
            # これは通常、'550 Requested action not taken.' のようなエラーを返します。
            # ftplib.error_perm 例外が発生します。
            response_cwd_nonexistent = ftp.sendcmd('CWD /nonexistent_dir_12345')
            print(f"CWD /nonexistent_dir 応答: {response_cwd_nonexistent}")
        except error_perm as e:
            # error_perm は 5xx 系エラーを示します
            print(f"エラー発生 (ftplib.error_perm): {e}")
            print("これは期待されるエラーです。存在しないディレクトリのためアクセスが拒否されました。")
        except Exception as e:
            print(f"CWD /nonexistent_dir の実行中に予期せぬエラーが発生しました: {e}")

        print("\nログアウト中...")
        ftp.quit()
        print("ログアウト成功。")

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

デバッグレベルの設定と詳細なログの確認

sendcmd() を使用する際、特に問題が発生した場合やプロトコルの詳細を確認したい場合に、ftp.set_debuglevel() を使うと非常に役立ちます。これにより、PythonがFTPサーバーに送信するコマンドと、サーバーからの生の応答をコンソールに出力できます。

from ftplib import FTP

FTP_HOST = 'ftp.dlptest.com'
FTP_USER = 'dlptest'
FTP_PASS = 'dlptest'

print(f"FTPサーバー {FTP_HOST} に接続中...")
try:
    with FTP(FTP_HOST) as ftp:
        # デバッグレベルを1に設定すると、送受信されるFTPコマンドと応答が表示されます。
        # 2に設定すると、さらに詳細なソケットレベルの情報も表示されます。
        ftp.set_debuglevel(1)
        print("\n--- デバッグレベル1に設定済み ---")

        print("ログイン中...")
        ftp.login(user=FTP_USER, passwd=FTP_PASS)
        print("ログイン成功!")

        # PWD (Print Working Directory) コマンドを送信
        # 現在の作業ディレクトリを問い合わせます。
        # 応答は通常 '257 "<パス>" is current directory.' の形式です。
        print("\n--- PWD コマンドの実行 ---")
        try:
            response_pwd = ftp.sendcmd('PWD')
            print(f"PWD コマンド応答: {response_pwd}")
            if response_pwd.startswith('257'):
                current_dir = response_pwd.split('"')[1] # パスを抽出
                print(f"現在の作業ディレクトリ: {current_dir}")
            else:
                print(f"PWD コマンドが予期せぬ応答を返しました: {response_pwd}")
        except Exception as e:
            print(f"PWD コマンドの実行中にエラーが発生しました: {e}")

        # TYPE I (Binary) コマンドを送信
        # ファイル転送モードをバイナリモードに設定します。
        # 応答は通常 '200 Type set to I.' です。
        print("\n--- TYPE I コマンドの実行 ---")
        try:
            response_type = ftp.sendcmd('TYPE I')
            print(f"TYPE I コマンド応答: {response_type}")
            if response_type.startswith('200'):
                print("転送モードをバイナリに設定しました。")
            else:
                print(f"TYPE I コマンドが予期せぬ応答を返しました: {response_type}")
        except Exception as e:
            print(f"TYPE I コマンドの実行中にエラーが発生しました: {e}")


        print("\nログアウト中...")
        ftp.quit()
        print("ログアウト成功。")

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

sendcmd() を使う上での注意点

  • 高レベルなメソッドの優先: ほとんどのFTP操作では、ftplib.FTP オブジェクトが提供する retrbinary(), storbinary(), cwd(), nlst() などの高レベルなメソッドを使用することが推奨されます。これらのメソッドは内部で sendcmd() を呼び出し、適切なエラーハンドリングやデータチャネルの確立(パッシブモードなど)を自動的に行います。sendcmd() は、これらの高レベルなメソッドでカバーされていない特殊なコマンドを実行する場合や、プロトコルのデバッグに限定して使用するのが一般的です。
  • 改行コード: sendcmd() は、送信するコマンドの末尾に自動的にFTPプロトコルで必要な CRLF (Carriage Return Line Feed) を追加します。自分で追加する必要はありません。
  • エラーハンドリング: sendcmd() は、サーバーからのエラー応答に対して ftplib.error_replyftplib.error_perm などの例外を発生させます。これらの例外を適切に捕捉し、処理することが重要です。
  • 低レベルな操作: sendcmd() はFTPプロトコルの詳細な知識を要求します。コマンドの構文、応答コードの意味、セッションの状態などを正確に理解している必要があります。


はい、ftplib.FTP.sendcmd() はFTPプロトコルを低レベルで操作するための強力なツールですが、多くの場合、より高レベルな代替メソッドが存在し、これらを使用する方がコードが簡潔になり、エラー処理も容易になります。

ftplibモジュールは、一般的なFTP操作のために、sendcmd()を内部で利用する多数の便利なメソッドを提供しています。これらのメソッドを使うことで、FTPコマンドの具体的な応答コードを気にすることなく、より直感的にプログラミングができます。

主な代替メソッドを以下に示します。

ファイルのアップロード・ダウンロード

ファイルの転送には、sendcmd() ではなく、専用のメソッドを使います。これらはデータ接続の確立(PASVモードなど)も自動で行ってくれます。

  • ftp.retrlines(command, callback=None):
    • リモートファイルをテキストモードでダウンロードします(行ごとに処理)。
    • command'RETR filename' の形式で指定します。
    • callback 関数で、ダウンロードされた各行を処理します。
    • 例: ftp.retrlines('RETR remote_text.txt', print)
  • ftp.retrbinary(command, callback, blocksize=8192, rest=None):
    • リモートファイルをバイナリモードでダウンロードします。
    • command'RETR filename' の形式で指定します。
    • callback 関数で、ダウンロードされたデータブロックを処理します。
    • 例:
      with open('downloaded_file.bin', 'wb') as f:
          ftp.retrbinary('RETR remote_file.bin', f.write)
      
  • ftp.storlines(command, file, callback=None):
    • ローカルファイルをテキストモードでアップロードします(行ごとに処理)。
    • command'STOR filename' の形式で指定します。
    • 例: ftp.storlines('STOR my_local_text.txt', open('my_local_text.txt', 'r'))
  • ftp.storbinary(command, file, blocksize=8192, callback=None, rest=None):
    • ローカルファイルをバイナリモードでアップロードします。
    • command'STOR filename' の形式で指定します。
    • 例: ftp.storbinary('STOR my_local_file.bin', open('my_local_file.bin', 'rb'))

ディレクトリ操作

ディレクトリの変更、作成、削除なども専用のメソッドがあります。

  • ftp.pwd():
    • 現在の作業ディレクトリのパスを取得します (PWD コマンドに対応)。
    • 例: current_path = ftp.pwd()
  • ftp.rmd(pathname):
    • FTPサーバー上のディレクトリを削除します (RMD コマンドに対応)。
    • 例: ftp.rmd('empty_folder')
  • ftp.mkd(pathname):
    • FTPサーバー上に新しいディレクトリを作成します (MKD コマンドに対応)。
    • 例: ftp.mkd('new_folder')
  • ftp.cwd(pathname):
    • FTPサーバー上の現在の作業ディレクトリを変更します (CWD コマンドに対応)。
    • 例: ftp.cwd('/path/to/new_directory')

ファイル操作

ファイルのリネームや削除も代替メソッドを使います。

  • ftp.dir(pathname="", cmd=None):
    • FTPサーバー上のディレクトリの内容を詳細なリストとして取得します (LIST コマンドに対応)。これは ls -l のような形式です。
    • 例: ftp.dir() (結果は標準出力に表示されるか、retrlines と組み合わせて利用)
  • ftp.nlst(pathname=""):
    • FTPサーバー上のディレクトリの内容を単純なファイル名のリストとして取得します (NLST コマンドに対応)。
    • 例: files = ftp.nlst()
  • ftp.mlsd(path="", facts=[]):
    • FTPサーバー上のディレクトリの内容をリストアップします(推奨される方式)。各エントリが辞書形式で返されるため、ファイル名、サイズ、更新日時などの情報を簡単に取得できます。LIST コマンドよりも構造化された情報を提供します。
    • 例:
      for name, attrs in ftp.mlsd('.'): # カレントディレクトリの内容をリストアップ
          print(f"Name: {name}, Type: {attrs.get('type')}, Size: {attrs.get('size')}")
      
  • ftp.size(filename):
    • FTPサーバー上のファイルのサイズを取得します (SIZE コマンドに対応)。
    • 例: file_size = ftp.size('my_file.zip')
  • ftp.delete(filename):
    • FTPサーバー上のファイルを削除します (DELE コマンドに対応)。
    • 例: ftp.delete('file_to_delete.txt')
  • ftp.rename(fromname, toname):
    • ファイルやディレクトリの名前を変更します (RNFR および RNTO コマンドに対応)。
    • 例: ftp.rename('old_filename.txt', 'new_filename.txt')