PythonでFTPダウンロード!retrbinary()を使った進捗表示・再開の実践例

2025-06-06

ftplib.FTP.retrbinary() は、Python の ftplib モジュールが提供する FTP クライアントのメソッドの一つで、FTP サーバーからバイナリモードでファイルを受信するために使用されます。

FTP には、ASCII モードとバイナリモードという2つの転送モードがあります。

  • バイナリモード
    画像、圧縮ファイル(.zip、.exeなど)、実行ファイルなどのバイナリファイルを転送する際に使用されます。このモードを使わないとファイルが破損する可能性があります。
  • ASCII モード
    テキストファイルを転送する際に使用されます。異なるOS間での改行コードの違いなどをFTPが自動的に変換してくれます。

retrbinary() メソッドは、このバイナリモードでのファイル受信を担当します。

retrbinary() の引数

retrbinary() メソッドは、通常以下の引数を取ります。

  1. cmd (str)
    サーバーに送信する FTP コマンド文字列です。ファイルをダウンロードする場合、通常 "RETR filename" の形式を使用します。filename はサーバー上のファイルパスです。パスの区切り文字には / を使用します。
  2. callback (callable)
    データを受信するたびに呼び出されるコールバック関数です。この関数は、受信したデータの塊 (バイト列) を引数として受け取ります。例えば、ファイルに書き込む場合は、open('ファイル名', 'wb').write のようにファイルオブジェクトの write メソッドを渡すことができます。
  3. blocksize (int, オプション)
    データを読み取る際のブロックサイズ(チャンクサイズ)を指定します。デフォルトは 8192 バイトです。このサイズでデータがコールバック関数に渡されます。
  4. rest (int, オプション)
    ファイル転送を中断した位置から再開する場合に、転送開始位置をバイト単位で指定します。これは REST コマンドとしてサーバーに送信されます。

以下は、retrbinary() を使って FTP サーバーからバイナリファイルをダウンロードする基本的な例です。

from ftplib import FTP

ftp = None
try:
    # FTPサーバーに接続
    ftp = FTP('your_ftp_host.com') # ここにFTPサーバーのホスト名を入力
    
    # ログイン (匿名ログインの場合)
    ftp.login('anonymous', '[email protected]') # ユーザー名とパスワードを入力 (匿名の場合、メールアドレスを渡すことが多い)

    # ダウンロードするサーバー上のファイルパス
    remote_file_path = 'path/to/your/binary_file.zip'
    
    # 保存するローカルのファイルパス
    local_file_path = 'downloaded_file.zip'

    # バイナリモードでファイルを受信
    with open(local_file_path, 'wb') as f:
        ftp.retrbinary(f"RETR {remote_file_path}", f.write)
    
    print(f"'{remote_file_path}' を '{local_file_path}' としてダウンロードしました。")

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

finally:
    if ftp:
        ftp.quit() # FTP接続を閉じる


よくあるエラーとトラブルシューティング

ftplib.error_perm: 550 Failed to open file. または ftplib.error_perm: 550 File not found.

原因

  • 現在のディレクトリが違う
    ftp.cwd() で移動したディレクトリが意図するものと異なる。
  • パスの誤り
    ファイルパスの指定が間違っている(大文字・小文字、スラッシュ / の有無など)。特に、Windows環境で \ を使用している場合、FTPでは / が正しいです。
  • 権限不足
    FTP ユーザーにそのファイルを読み取る権限がない。
  • ファイルが存在しない
    指定したリモートファイルパスがサーバー上に存在しない。

トラブルシューティング

  • 現在のディレクトリの確認
    ftp.pwd() で現在の作業ディレクトリを確認し、必要であれば ftp.cwd('/path/to/directory') で正しいディレクトリに移動します。
  • 権限の確認
    FTP サーバー管理者に、該当ユーザーがそのファイルをダウンロードする権限を持っているか確認します。
  • ファイルパスの確認
    • FTP クライアント(FileZillaなど)で同じパスでアクセスして、ファイルが存在するか、名前が正しいかを確認します。
    • ftp.nlst()ftp.dir() を使って、サーバー上のファイルリストを取得し、正しいファイル名とパスを確認します。
    • RETR コマンドのファイル名とパスが、サーバー上のそれと完全に一致しているか(大文字・小文字含む)確認します。
    • "RETR " + filename のように、RETR とファイル名の間にスペースが入っていることを確認します。スペースがないと、サーバーがコマンドを正しく解釈できません。

ftplib.error_perm: 530 Login incorrect. または ftplib.error_perm: 530 Not logged in.

原因

  • 匿名ログインの許可がない
    匿名ログイン(ftp.login('anonymous', '[email protected]'))を試みているが、サーバーが許可していない。
  • ログイン情報の誤り
    ユーザー名やパスワードが間違っている。

トラブルシューティング

  • 匿名ログインの可否確認
    サーバーが匿名ログインを許可しているか確認します。許可していない場合は、有効なユーザー名とパスワードでログインする必要があります。
  • ログイン情報の再確認
    ユーザー名とパスワードを再度確認し、入力ミスがないか確認します。

ftplib.error_temp: 421 Service not available, closing control connection. または ftplib.error_temp: 425 Can't open data connection.

原因

  • タイムアウト
    接続が確立される前にタイムアウトした。
  • サーバー負荷
    サーバーが一時的に過負荷になっている。
  • ネットワーク設定の問題
    NAT ルーターなどのネットワーク設定が FTP のデータ接続を妨げている。
  • ファイアウォール/ポートブロック
    クライアント側またはサーバー側のファイアウォールが FTP のデータ接続(通常パッシブモードではランダムなポート)をブロックしている。

トラブルシューティング

  • 再試行ロジックの実装
    一時的なネットワークの問題の場合、何度かリトライすることで成功する可能性があります。
  • タイムアウト設定の調整
    ftp.connect(host, timeout=xx)ftplib.FTP(timeout=xx) でタイムアウト値を長く設定してみます。
  • ファイアウォールの設定確認
    • クライアント側のファイアウォール(Windows Defender, macOS Firewall, セキュリティソフトなど)で、Python プロセスや FTP 関連のポート(21番だけでなく、パッシブモードで使われるデータポート範囲)が許可されているか確認します。
    • サーバー側のファイアウォール設定を管理者に確認します。

TypeError: argument 2 must be callable

原因

  • retrbinary()callback 引数には、データチャンクを受け取って処理する「呼び出し可能オブジェクト(関数やメソッド)」を渡す必要があります。しかし、ファイルオブジェクトそのものを渡してしまったり、誤った引数を渡している場合に発生します。

トラブルシューティング

  • 正しいコールバック関数の指定
    • ファイルを書き込む場合、open(local_file_path, 'wb').write のように、ファイルオブジェクトの write メソッドを渡します。
    • カスタムの処理を行いたい場合は、lambda chunk: print(len(chunk)) のように、1つのバイト列引数を受け取る関数を定義して渡します。
# 悪い例 (TypeError):
# with open(local_file_path, 'wb') as f:
#     ftp.retrbinary(f"RETR {remote_file_path}", f) # f そのものではなく、f.write を渡す

# 良い例:
with open(local_file_path, 'wb') as f:
    ftp.retrbinary(f"RETR {remote_file_path}", f.write) 

ダウンロードしたファイルが破損している、またはサイズが0になる

原因

  • ローカルファイルのオープンモードの誤り
    open()wb (書き込みバイナリモード) ではなく w (書き込みテキストモード) を使用している。
  • 部分的なダウンロード
    ネットワークの中断、サーバー側の問題、またはスクリプトが途中で終了したため、ダウンロードが完了していない。
  • 転送モードの誤り
    バイナリファイルなのに retrlines() を使用している(テキストモードで転送されているため、改行コードの変換などで破損する)。

トラブルシューティング

  • エラーハンドリングと完了確認
    • try...except ftplib.all_errors as e: で FTP 関連のすべてのエラーをキャッチし、ダウンロードが途中で中断されていないか確認します。
    • ダウンロード後に、リモートファイルのサイズとローカルファイルのサイズを比較して、完全にダウンロードされたかを確認します。ftp.size(remote_file_path) でリモートファイルのサイズを取得できます。
  • ローカルファイルのオープンモード確認
    open(local_file_path, 'wb') のように、必ずバイナリ書き込みモード ('wb') で開いていることを確認します。
  • retrbinary() の使用確認
    バイナリファイルであることを確認し、必ず retrbinary() を使用しているか確認します。

接続がハングアップする、または非常に遅い

原因

  • サーバー負荷
    サーバーが混雑している。
  • 大量のファイル転送
    多数の小さいファイルを転送している場合、個々の転送のオーバーヘッドで全体が遅くなることがある。
  • ネットワーク速度
    クライアントまたはサーバー側のネットワーク速度が遅い。
  • アイドルタイムアウト
    FTP サーバーがアイドル状態の接続を一定時間で切断するため、ダウンロード中にタイムアウトが発生している。
  • ftp.set_debuglevel(2) の活用
    • ftp.set_debuglevel(2) を設定すると、FTP コントロール接続での送受信の詳細なログが出力されます。これにより、どのコマンドで問題が発生しているか、サーバーからの応答コードが何かなどを把握できます。
  • 大きなファイルの分割ダウンロード
    非常に大きなファイルをダウンロードする場合、rest 引数を使用して、部分的にダウンロードし、中断したところから再開するロジックを実装することも検討します。
  • タイムアウト設定の調整
    • ftplib.FTP オブジェクトを作成する際に timeout 引数を設定します。例: ftp = FTP('host', timeout=600) (600秒 = 10分)。
    • FTP サーバー側で設定されているアイドルタイムアウト値を把握し、それよりも長く設定するか、サーバー管理者に調整を依頼します。
import ftplib
import sys

ftp = None
try:
    ftp = ftplib.FTP('your_ftp_host.com')
    ftp.set_debuglevel(2) # デバッグレベルを2に設定して詳細なログを出力
    ftp.login('username', 'password')
    
    remote_file = 'path/to/large_file.zip'
    local_file = 'downloaded_large_file.zip'

    with open(local_file, 'wb') as f:
        print(f"Downloading {remote_file}...")
        # タイムアウトを長く設定
        ftp.retrbinary(f"RETR {remote_file}", f.write, timeout=300) 
    
    print("Download complete.")

except ftplib.all_errors as e:
    print(f"FTP Error: {e}")
except Exception as e:
    print(f"General Error: {e}")
finally:
    if ftp:
        ftp.quit()
  • ftplib.FTP_TLS の使用
    サーバーが FTPS (FTP over SSL/TLS) をサポートしている場合は、ftplib.FTP_TLS を使用することを強く推奨します。これにより、データ転送が暗号化され、セキュリティが向上します。また、一部のファイアウォールは暗号化された接続をよりスムーズに扱う場合があります。
  • ネットワークツールでの診断
    ping, traceroute, telnet (ポート21に接続できるか確認) などのネットワークコマンドを使用して、基本的な接続性を確認します。
  • サーバーログの確認
    可能であれば、FTPサーバー側のログを確認します。サーバー側で何らかのエラーが記録されている場合があります。
  • FTP クライアントとの比較
    FileZillaなどのGUI FTPクライアントで同じ操作(同じホスト、ユーザー、パスワード、ファイルパス)を試してみて、正常に動作するか確認します。FTPクライアントで成功するのに Python コードで失敗する場合、コード側の問題である可能性が高いです。
  • エラーハンドリングの徹底
    try...except ftplib.all_errors as e: を使用して、FTP 関連のすべての例外をキャッチし、エラーメッセージを詳細にログに出力するようにします。


FTP サーバーへの接続には、ホスト名、ユーザー名、パスワードが必要です。匿名 FTP サーバーを使用しない限り、これらの情報は適切なものに置き換えてください。

注意点

  • ダミーサーバーの利用
    実際の FTP サーバーがない場合、ローカルに FTP サーバーを立てるか、python -m pyftpdlib のようなライブラリを使ってダミーサーバーを起動してテストすることができます。
  • エラーハンドリング
    ネットワーク通信では様々なエラーが発生するため、try...except...finally ブロックを使った堅牢なエラーハンドリングが非常に重要です。
  • セキュリティ
    FTP はパスワードやデータが暗号化されないため、セキュリティ上問題がある場合があります。可能であれば、FTPS (FTP over SSL/TLS) を使用できる ftplib.FTP_TLS の利用を検討してください。

例1: 基本的なバイナリファイルのダウンロード

最も基本的な使い方です。FTP サーバーから指定されたファイルをダウンロードし、ローカルに保存します。

from ftplib import FTP
import os

FTP_HOST = 'your_ftp_host.com'    # ★★★ あなたのFTPホスト名に置き換える
FTP_USER = 'your_username'        # ★★★ あなたのFTPユーザー名に置き換える
FTP_PASS = 'your_password'        # ★★★ あなたのFTPパスワードに置き換える

REMOTE_FILE = 'remote/path/to/your_image.jpg' # ★★★ サーバー上のファイルパスに置き換える
LOCAL_FILE = 'downloaded_image.jpg'          # ★★★ 保存するローカルファイル名に置き換える

ftp = None # ftpオブジェクトの初期化

try:
    print(f"{FTP_HOST} に接続中...")
    ftp = FTP(FTP_HOST)
    
    print(f"ログイン中 ({FTP_USER})...")
    ftp.login(FTP_USER, FTP_PASS)
    
    print(f"リモートファイル '{REMOTE_FILE}' のダウンロードを開始します...")
    
    # ローカルファイルをバイナリ書き込みモード ('wb') で開く
    with open(LOCAL_FILE, 'wb') as fp:
        # retrbinary() を使ってファイルをダウンロード。
        # f.write がコールバック関数として渡され、受信したデータをファイルに書き込む
        ftp.retrbinary(f"RETR {REMOTE_FILE}", fp.write)
    
    print(f"ダウンロード完了: '{LOCAL_FILE}' として保存されました。")
    print(f"ファイルサイズ: {os.path.getsize(LOCAL_FILE)} バイト")

except Exception as e:
    print(f"エラーが発生しました: {e}")
    # 特定の ftplib エラーをハンドルすることも可能
    # 例: except ftplib.error_perm as e:
    #         print(f"FTP パーミッションエラー: {e}")

finally:
    if ftp:
        print("FTP接続を閉じています...")
        ftp.quit()
        print("FTP接続を閉じました。")

解説

  • try...except...finally: 接続エラー、認証エラー、ファイルが存在しないエラーなど、様々な例外を適切に処理するための標準的な Python の慣用句です。finally ブロックは、エラーが発生しても必ず接続を閉じるために重要です。
  • ftp.retrbinary(f"RETR {REMOTE_FILE}", fp.write):
    • 第一引数 f"RETR {REMOTE_FILE}" は、サーバーに送信する FTP コマンドです。RETR は「取得 (Retrieve)」を意味し、その後にダウンロードしたいファイルのパスを指定します。
    • 第二引数 fp.write はコールバック関数です。retrbinary() は、サーバーからデータチャンク(塊)を受信するたびに、このコールバック関数を呼び出し、受信したデータチャンクを引数として渡します。fp.write はそのデータチャンクをファイルに書き込むため、ダウンロードが進行します。
  • open(LOCAL_FILE, 'wb') as fp:: 受信したデータを書き込むためのローカルファイルをバイナリ書き込みモード ('wb') で開きます。バイナリファイルをダウンロードする場合は、必ず 'wb' を使用してください。
  • ftp.login(FTP_USER, FTP_PASS): FTP サーバーにログインします。
  • FTP(FTP_HOST): 指定されたホストに接続します。

例2: ダウンロード進捗の表示

大きなファイルをダウンロードする際に、現在のダウンロード状況をユーザーにフィードバックとして表示する例です。

from ftplib import FTP
import os
import sys

FTP_HOST = 'your_ftp_host.com'
FTP_USER = 'your_username'
FTP_PASS = 'your_password'

REMOTE_FILE = 'remote/path/to/large_archive.zip'
LOCAL_FILE = 'downloaded_archive.zip'

ftp = None
total_bytes_downloaded = 0
file_size = 0

def handle_data(data):
    """
    retrbinary() のコールバック関数。
    受信したデータをファイルに書き込み、進捗を表示する。
    """
    global total_bytes_downloaded
    total_bytes_downloaded += len(data)
    
    fp.write(data) # fp はこの関数の外で定義されているファイルオブジェクト
    
    # 進捗表示
    if file_size > 0:
        progress_percent = (total_bytes_downloaded / file_size) * 100
        # キャリッジリターン('\r')を使って同じ行を上書きする
        sys.stdout.write(f"\rダウンロード中: {total_bytes_downloaded} / {file_size} バイト ({progress_percent:.2f}%)")
        sys.stdout.flush() # バッファをフラッシュして即座に表示
    else:
        sys.stdout.write(f"\rダウンロード中: {total_bytes_downloaded} バイト")
        sys.stdout.flush()

try:
    print(f"{FTP_HOST} に接続中...")
    ftp = FTP(FTP_HOST)
    ftp.login(FTP_USER, FTP_PASS)

    # ダウンロードするファイルのサイズを取得(進捗表示のため)
    try:
        file_size = ftp.size(REMOTE_FILE)
        print(f"リモートファイル '{REMOTE_FILE}' のサイズ: {file_size} バイト")
    except Exception:
        print("ファイルサイズを取得できませんでした。進捗率は表示されません。")
        file_size = 0 # サイズ取得失敗の場合、進捗率なしで続行

    print(f"リモートファイル '{REMOTE_FILE}' のダウンロードを開始します...")
    
    with open(LOCAL_FILE, 'wb') as fp:
        # コールバック関数として handle_data を渡す
        ftp.retrbinary(f"RETR {REMOTE_FILE}", handle_data)
    
    print(f"\nダウンロード完了: '{LOCAL_FILE}' として保存されました。")
    print(f"最終ファイルサイズ: {os.path.getsize(LOCAL_FILE)} バイト")

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

finally:
    if ftp:
        print("FTP接続を閉じています...")
        ftp.quit()
        print("FTP接続を閉じました。")

解説

  • ftp.size(REMOTE_FILE): retrbinary() の前に呼び出し、ダウンロードするファイルの総バイトサイズを取得します。これにより、進捗率(パーセンテージ)を計算できます。size() メソッドは、ファイルが存在しない場合や権限がない場合にエラーを発生させる可能性があるため、try...except で囲むとより堅牢になります。
  • handle_data(data):
    • このカスタム関数が retrbinary() のコールバックとして渡されます。
    • global total_bytes_downloaded: グローバル変数 total_bytes_downloaded を更新して、これまでに受信したバイト数を追跡します。
    • fp.write(data): ここで、受信したデータチャンクをファイルに書き込みます。fpwith open(...) ブロックで開かれたファイルオブジェクトであり、この関数が呼び出されるスコープ(try ブロック内)からアクセスできます。
    • sys.stdout.write('\r...')sys.stdout.flush(): キャリッジリターン (\r) を使うことで、ターミナル上で同じ行を上書きし、リアルタイムの進捗表示を実現します。flush() はバッファを強制的に出力するために必要です。

ネットワークが不安定な場合や、中断したダウンロードを最初からやり直さずに途中から再開したい場合に役立ちます。

from ftplib import FTP
import os
import sys

FTP_HOST = 'your_ftp_host.com'
FTP_USER = 'your_username'
FTP_PASS = 'your_password'

REMOTE_FILE = 'remote/path/to/resumable_large_file.iso'
LOCAL_FILE = 'resumed_download.iso'

ftp = None
total_bytes_downloaded = 0
file_size = 0
resume_point = 0 # 再開点

def handle_data(data):
    global total_bytes_downloaded
    total_bytes_downloaded += len(data)
    fp.write(data)
    
    if file_size > 0:
        progress_percent = ((resume_point + total_bytes_downloaded) / file_size) * 100
        sys.stdout.write(f"\rダウンロード中: {resume_point + total_bytes_downloaded} / {file_size} バイト ({progress_percent:.2f}%)")
        sys.stdout.flush()
    else:
        sys.stdout.write(f"\rダウンロード中: {resume_point + total_bytes_downloaded} バイト")
        sys.stdout.flush()

try:
    print(f"{FTP_HOST} に接続中...")
    ftp = FTP(FTP_HOST)
    ftp.login(FTP_USER, FTP_PASS)

    file_size = ftp.size(REMOTE_FILE)
    print(f"リモートファイル '{REMOTE_FILE}' のサイズ: {file_size} バイト")

    # ローカルに同じ名前のファイルが存在するかチェック
    if os.path.exists(LOCAL_FILE):
        local_file_size = os.path.getsize(LOCAL_FILE)
        if local_file_size < file_size:
            resume_point = local_file_size
            print(f"ローカルファイル '{LOCAL_FILE}' が存在します。{resume_point} バイトからダウンロードを再開します。")
        elif local_file_size == file_size:
            print(f"ファイル '{LOCAL_FILE}' はすでに完全にダウンロードされています。")
            sys.exit(0) # スクリプトを終了
        else:
            print(f"ローカルファイル '{LOCAL_FILE}' のサイズがリモートファイルより大きいです。新規ダウンロードを開始します。")
            os.remove(LOCAL_FILE) # 既存ファイルを削除
            resume_point = 0
    else:
        print(f"ファイル '{LOCAL_FILE}' は存在しません。新規ダウンロードを開始します。")

    # ファイルをバイナリ追記モード ('ab') で開く
    # 再開点がある場合は seek() で追記開始位置を設定
    with open(LOCAL_FILE, 'ab') as fp:
        if resume_point > 0:
            fp.seek(resume_point) # ファイルポインタを再開点に移動
            print(f"ファイルポインタを {resume_point} バイトに設定しました。")

        # retrbinary() の rest 引数に再開点を指定
        ftp.retrbinary(f"RETR {REMOTE_FILE}", handle_data, rest=resume_point)
    
    print(f"\nダウンロード完了: '{LOCAL_FILE}' として保存されました。")
    print(f"最終ファイルサイズ: {os.path.getsize(LOCAL_FILE)} バイト")

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

finally:
    if ftp:
        print("FTP接続を閉じています...")
        ftp.quit()
        print("FTP接続を閉じました。")
  • handle_data 関数内の進捗計算: resume_point を考慮して、総ダウンロードバイト数を正確に表示するように変更されています。
  • ftp.retrbinary(f"RETR {REMOTE_FILE}", handle_data, rest=resume_point):
    • rest 引数に resume_point を渡します。ftplib はこの rest 値を FTP の REST コマンドとしてサーバーに送信します。サーバーはこれを受け取り、指定されたバイトオフセットからファイルの転送を開始します。
  • fp.seek(resume_point):
    • ファイルポインタを resume_point に移動させます。これにより、fp.write() が呼び出されたときに、データが正しい位置に書き込まれるようになります。
  • open(LOCAL_FILE, 'ab') as fp::
    • ローカルファイルをバイナリ追記モード ('ab') で開きます。これにより、既存のファイルがある場合、新しいデータがファイルの末尾に追加されます。
  • 再開点の検出
    • os.path.exists(LOCAL_FILE) でローカルに同じファイルが存在するかを確認します。
    • 存在する場合、os.path.getsize(LOCAL_FILE) でそのサイズを取得し、これが resume_point となります。
    • リモートファイルのサイズと比較して、ローカルファイルが完全にダウンロードされているか、部分的にダウンロードされているか、または破損している(サイズが大きい)かを判断します。


requests ライブラリ (HTTP/HTTPS の場合)

これは厳密には FTP の代替ではありませんが、多くの「ファイルダウンロード」のユースケースでは、実際には FTP ではなく HTTP/HTTPS を介したダウンロードが利用されます。もし、ダウンロード元が FTP サーバーではなく Web サーバー(例えば、http://example.com/file.zip のような URL)である場合、requests ライブラリがはるかに簡単で強力な選択肢となります。

特徴

  • ストリーミングダウンロードにより、大きなファイルのメモリ効率が良い。
  • プロキシ、認証、リダイレクト、タイムアウトなど、HTTP の高度な機能に対応。
  • HTTPS をサポートし、セキュリティが確保される。
  • 非常にシンプルで使いやすい。


import requests
import os

url = 'https://example.com/some_file.zip' # ★★★ ダウンロードしたいファイルのURLに置き換える
local_filename = 'downloaded_via_http.zip'

try:
    print(f"URL: {url} からダウンロード中...")
    with requests.get(url, stream=True) as r:
        r.raise_for_status() # HTTPエラー (4xx, 5xx) があれば例外を発生させる
        total_size = int(r.headers.get('content-length', 0))
        downloaded_size = 0

        with open(local_filename, 'wb') as f:
            for chunk in r.iter_content(chunk_size=8192): # チャンク単位で読み込み
                f.write(chunk)
                downloaded_size += len(chunk)
                # 進捗表示
                if total_size > 0:
                    progress = (downloaded_size / total_size) * 100
                    print(f"\rダウンロード中: {downloaded_size}/{total_size} バイト ({progress:.2f}%)", end='')
                else:
                    print(f"\rダウンロード中: {downloaded_size} バイト", end='')
                sys.stdout.flush()
    print(f"\nダウンロード完了: {local_filename}")
    print(f"ファイルサイズ: {os.path.getsize(local_filename)} バイト")

except requests.exceptions.RequestException as e:
    print(f"HTTPダウンロードエラーが発生しました: {e}")
except Exception as e:
    print(f"予期せぬエラーが発生しました: {e}")

いつ使うべきか

  • FTP サーバーへのアクセスが不要な場合。
  • ダウンロード元が HTTP または HTTPS サーバーである場合。

paramiko ライブラリ (SFTP の場合)

SFTP (SSH File Transfer Protocol) は、SSH プロトコル上で動作するセキュアなファイル転送プロトコルです。ftplib とは異なり、データが暗号化され、セキュアな環境でのファイル転送に適しています。FTP の代替として最も推奨される方法の一つです。

特徴

  • FTP とは異なり、ファイアウォール設定が比較的容易(SSHポート22番のみ開けばよいことが多い)。
  • 認証方法が豊富(パスワード、SSHキーなど)。
  • SSH を利用するため、セキュリティが非常に高い。


import paramiko
import os
import sys

SFTP_HOST = 'your_sftp_host.com'    # ★★★ あなたのSFTPホスト名に置き換える
SFTP_PORT = 22                      # SFTPの標準ポート
SFTP_USER = 'your_sftp_username'    # ★★★ あなたのSFTPユーザー名に置き換える
SFTP_PASS = 'your_sftp_password'    # ★★★ あなたのSFTPパスワードに置き換える (または鍵認証)

REMOTE_FILE = '/remote/path/to/your_sftp_file.bin' # ★★★ サーバー上のファイルパスに置き換える
LOCAL_FILE = 'downloaded_via_sftp.bin'             # ★★★ 保存するローカルファイル名に置き換える

transport = None
sftp = None

try:
    print(f"{SFTP_HOST}:{SFTP_PORT} に接続中...")
    transport = paramiko.Transport((SFTP_HOST, SFTP_PORT))
    transport.connect(username=SFTP_USER, password=SFTP_PASS)
    
    sftp = paramiko.SFTPClient.from_transport(transport)
    
    print(f"リモートファイル '{REMOTE_FILE}' のダウンロードを開始します...")
    
    # ファイルサイズ取得(進捗表示のため)
    remote_file_stat = sftp.stat(REMOTE_FILE)
    file_size = remote_file_stat.st_size
    print(f"リモートファイルサイズ: {file_size} バイト")

    downloaded_bytes = 0
    with open(LOCAL_FILE, 'wb') as f:
        # callback関数で進捗表示
        sftp.get(REMOTE_FILE, LOCAL_FILE, callback=lambda x, y: print(f"\rダウンロード中: {x}/{y} バイト ({(x/y)*100:.2f}%)", end=''))
        # 上記は get() の簡潔なコールバック例ですが、
        # より柔軟な進捗表示やエラーハンドリングのためには
        # sftp.open() と read() を使う方法もあります。
        # 例:
        # with sftp.open(REMOTE_FILE, 'rb') as remote_f:
        #     while True:
        #         chunk = remote_f.read(8192)
        #         if not chunk:
        #             break
        #         f.write(chunk)
        #         downloaded_bytes += len(chunk)
        #         # 進捗表示ロジック
        #         sys.stdout.write(f"\rダウンロード中: {downloaded_bytes}/{file_size} バイト ({(downloaded_bytes/file_size)*100:.2f}%)")
        #         sys.stdout.flush()

    print(f"\nダウンロード完了: '{LOCAL_FILE}' として保存されました。")
    print(f"ファイルサイズ: {os.path.getsize(LOCAL_FILE)} バイト")

except paramiko.AuthenticationException:
    print("SFTP認証に失敗しました。ユーザー名またはパスワードを確認してください。")
except paramiko.SSHException as e:
    print(f"SSH接続エラーが発生しました: {e}")
except FileNotFoundError:
    print(f"リモートファイル '{REMOTE_FILE}' が見つかりません。")
except Exception as e:
    print(f"予期せぬエラーが発生しました: {e}")

finally:
    if sftp:
        sftp.close()
    if transport:
        transport.close()
    print("SFTP接続を閉じました。")

いつ使うべきか

  • 既存の SSH インフラストラクチャを利用したい場合。
  • FTP サーバーではなく SFTP サーバーが利用可能な場合。
  • セキュアなファイル転送が必要な場合。

wsgiref.util.FileWrapper (DjangoなどのWebフレームワークでファイルを送信する場合)

これは ftplib.FTP.retrbinary() の「代替」というよりも、Web アプリケーションでユーザーにバイナリファイルをダウンロードさせる際の一般的な方法です。ユーザーがブラウザからファイルをダウンロードするようなシナリオで使用します。

特徴

  • 大きなファイルをメモリに一度にロードすることなく効率的に送信できる。
  • Web サーバーからクライアントへのファイルストリーミングに最適化されている。

例 (Flaskを使用)

# from flask import Flask, send_file, Response
# from wsgiref.util import FileWrapper
# import os

# app = Flask(__name__)

# @app.route('/download_large_file')
# def download_large_file():
#     # 実際のファイルパス
#     file_path = '/path/to/your/actual/large_file.zip' # ★★★ サーバー上の実際のファイルパスに置き換える
#     
#     if not os.path.exists(file_path):
#         return "File not found", 404

#     # Flaskのsend_fileを使用するのが最も簡単
#     # return send_file(file_path, as_attachment=True, download_name='large_file.zip')

#     # 大規模なファイルやより細かい制御が必要な場合はFileWrapperを使用
#     # chunk_size は任意で調整
#     wrapper = FileWrapper(open(file_path, 'rb'), 8192) 
#     headers = {
#         'Content-Length': str(os.path.getsize(file_path)),
#         'Content-Type': 'application/zip', # ファイルのMIMEタイプに合わせて変更
#         'Content-Disposition': 'attachment; filename="large_file_wsgiref.zip"'
#     }
#     return Response(wrapper, headers=headers)

# if __name__ == '__main__':
#     # このサーバーは開発用です。本番環境ではGunicornやuWSGIのようなWSGIサーバーを使用してください。
#     app.run(debug=True)

いつ使うべきか

  • 特に大きなファイルを効率的にストリーミング配信したい場合。
  • Python で書かれた Web アプリケーションを通じて、ユーザーにファイルをダウンロードさせたい場合。

これは Python の直接的な FTP 機能ではありませんが、Python から外部のコマンドラインツール(wgetcurl など)を呼び出してファイルダウンロードを実行する方法です。

特徴

  • Python コードがシンプルになる。
  • wgetcurl が提供する多様なオプション(再試行、レート制限、プロキシなど)を利用できる。
  • 既存の堅牢なツールを利用できる。

欠点

  • プラットフォーム依存性が高くなる可能性がある。
  • エラーハンドリングや進捗表示が subprocess モジュールの出力解析に依存するため、複雑になることがある。
  • システムに wgetcurl がインストールされている必要がある。


import subprocess
import os

FTP_URL = 'ftp://your_username:your_password@your_ftp_host.com/remote/path/to/your_file.txt' # ★★★ FTP URLに置き換える
LOCAL_FILE = 'downloaded_via_wget.txt'

try:
    print(f"wget で {FTP_URL} からダウンロード中...")
    # --ftp-no-epsv を追加すると、一部のFTPサーバーでESPV (拡張パッシブモード) の問題が解決されることがある
    # -O は出力ファイル名を指定
    # --show-progress は進捗表示を有効にする (wgetのバージョンによる)
    result = subprocess.run(['wget', '--ftp-no-epsv', '-O', LOCAL_FILE, FTP_URL], 
                            check=True,  # エラー時にCalledProcessErrorを発生させる
                            capture_output=False, # 出力をキャプチャしない (wgetが進捗を直接コンソールに表示するため)
                            text=True) # 出力をテキストとして扱う (通常は必要ないが、デバッグ用)
    
    print(f"ダウンロード完了: {LOCAL_FILE}")
    print(f"ファイルサイズ: {os.path.getsize(LOCAL_FILE)} バイト")

except subprocess.CalledProcessError as e:
    print(f"wget コマンドがエラーを返しました: {e}")
    # print(f"stdout: {e.stdout}") # capture_output=True の場合のみ
    # print(f"stderr: {e.stderr}") # capture_output=True の場合のみ
except FileNotFoundError:
    print("エラー: wget コマンドが見つかりません。システムにインストールされていますか?")
except Exception as e:
    print(f"予期せぬエラーが発生しました: {e}")

いつ使うべきか

  • Python の組み込み FTP 機能では対応しきれない、特定のネットワーク環境やサーバー設定に対応する必要がある場合。
  • 既存のスクリプトでこれらのツールを呼び出したい場合。
  • wgetcurl の高度な機能(特定の認証、ミラーリング、レート制限など)が必要で、それを Python で再実装するのが手間な場合。

ftplib.FTP.retrbinary() は FTP に特化した基本的なダウンロードメソッドですが、現代の多くのユースケースでは、よりセキュアで多機能な代替手段が存在します。

  • 特定の高度な機能や既存ツール活用
    subprocess を使って wgetcurl を呼び出す。
  • Web アプリケーションでの配信
    wsgiref.util.FileWrapper (または Web フレームワークの send_file など)
  • 最も推奨される代替
    • HTTP/HTTPS の場合
      requests
    • SFTP の場合
      paramiko