HOME Python 書き込む

Python を使った FTP アップロード


1. 初めに

紫藤は xyzzy という emacs 風のエディタを使って HTML を書いています。 このエディターから抜け出さないでファンクションキーひとつでファイルをアップロードするスクリプトを 書いてみました。xyzzy Lisp を使ってアップロードの 入力ファイルを作るスクリプトを書き、 入力ファイルに基づいてアップロードを行うスクリプトを Python で書きました。 ここでは Python で書いた部分について解説します。

100 行ほどの短いスクリプトで書くことができます。 このスクリプトは短いのですが、それなりのテクニックを使っているので、 皆様の参考になればと思い、紹介します。

2. 仕様

2.1. 入出力ファイルを作るディレクトリ

入力ファイルおよび出力ファイルは環境変数 TEMP または TMP で定義されたディレクトリに 作成します。両方とも定義されていなければ C:\\ に作成します。

2.2. 入力ファイル

入力ファイル名は upload.inp です。

書式は、
("server.name", "user_name", "password", "sub-folder", ("filename1", "filename2", ........,))
のようにタプルでくくって、 サーバー名、ユーザー名、パスワード、サーバー上の保存するディレクトリ( HOME からの相対パス)、 および転送するファイルのリストです。 このようにしておくと eval を使って変数を簡潔に読み込むことができます。 入力ファイルは読み終わったらすぐに削除します。

2.3. 出力ファイル

出力ファイル名は upload.log です。

成功したか、どこでエラーが起きたかを書き込みます。

2.4. 動作

以下の動作をします。
  1. 前回作った upload.log がもしあれば念のため削除します。
  2. 入力ファイルを読み込んで必要な値を変数に代入します。入力ファイルは読み終わったらすぐに削除します。
  3. 次に、サーバーに接続して、ユーザー名とパスワードを使ってログインします。
  4. その後、相対パス名のパスに移動します。その際、ディレクトリがなければ作成します。
  5. 最後に、転送するファイルのリストに従って転送します。TXT, HTML, CSS は ASCII モードで、 その他のファイルは BINARY モードで転送します。
  6. 成功したか、どこでエラーが起きたを upload.log に書き込みます。 (運悪く upload.log を開くときエラーが起きたら何も書き込まれない。)

    3. コード

    コードを [code 1] に示します。
     01:     #! /usr/bin/env python
     02:     
     03:     import os, os.path, sys
     04:     from ftplib import FTP
     05:     from cStringIO import StringIO
     06:     from datetime import datetime
     07:     
     08:     """
     09:     upload.py
     10:     
     11:     by T.Shido
     12:     on July 19, 2005
     13:     
     14:     """
     15:     
     16:     
     17:     INP = 'upload.inp'
     18:     LOG = 'upload.log'
     19:     
     20:     def is_ascii(fname):
     21:         return fname.split('.').pop() in ('txt', 'htm', 'html', 'css')
     22:     
     23:     def write_log(logfile, str, success):
     24:         log = file(logfile, 'w')
     25:         log.write( ((not success) and  'Error:\n\n' or '') + str)
     26:         log.close()
     27:         success or sys.exit(1)
     28:     
     29:     def server_cd(ftp, path, log_name):
     30:         if path!='HOME':
     31:             stdout = sys.stdout
     32:             for d in path.split('/'):
     33:                 sys.stdout = StringIO()
     34:                 try:
     35:                     ftp.dir()
     36:                 except:
     37:                     ftp.close()
     38:                     write_log(log_name, "Cannot get LIST", False)
     39:                     
     40:                 if not d in [line.split(' ').pop()
                                      for line in sys.stdout.getvalue().split('\n') if line and line[0]=='d']:
     41:                     try:
     42:                         ftp.mkd(d)
     43:                     except:
     44:                         ftp.close()
     45:                         write_log(log_name, "Cannot make directory on the server: " + d, False)
     46:                 try:        
     47:                     ftp.cwd(d)
     48:                 except:
     49:                     ftp.close()
     50:                     write_log(log_name, "Cannot change directory on the server: " + d, False)
     51:                     
     52:                 sys.stdout.close()
     53:             sys.stdout = stdout
     54:             
     55:     
     56:     def put(ftp, ls, log_name):
     57:         for fname in ls:
     58:             fbase = os.path.basename(fname)
     59:             (method, mode) = is_ascii(fbase) and ('storlines', 'r') or ('storbinary', 'rb')
     60:             try:
     61:                 f = file(fname, mode)
     62:             except:
     63:                 ftp.close()
     64:                 write_log(log_name, "Cannot open local file: " + fbase, False)
     65:             try:
     66:                 getattr(ftp, method)("STOR " + fbase, f)
     67:             except:
     68:                 ftp.close()
     69:                 write_log(log_name, "Cannot Transfer local file: " + fbase, False)
     70:                 
     71:             f.close()
     72:     
     73:     if __name__ == '__main__':
     74:         dtemp = os.getenv("TEMP") or os.getenv("TMP") or "C:\\"
     75:         log_name = os.path.join(dtemp, LOG)
     76:         os.path.exists(log_name) and os.remove(log_name)
     77:         inp_name = os.path.join(dtemp, INP)
     78:         try:
     79:             inp = file(inp_name)
     80:         except:
     81:             write_log(log_name, "Cannot open input file.", False)
     82:             
     83:         server, user, passw, path, files = eval(inp.read())
     84:         inp.close()
     85:         os.remove(inp_name)
     86:     
     87:         try:
     88:             ftp = FTP(server)
     89:         except:
     90:             write_log(log_name, "Cannot connect to " + server, False)
     91:     
     92:         try:
     93:             ftp.login(user, passw)
     94:         except:
     95:             ftp.close()
     96:             write_log(log_name, "Cannot login: " + server, False)
     97:             
     98:         server_cd(ftp, path, log_name)
     99:         put(ftp, files, log_name)
    100:         ftp.quit()
    101:         write_log(log_name,
    102:                   "Following files are uploaded successfully at\n%s:\n\n" % datetime.now().isoformat(' ') + \
    103:                   "\n".join(files) + "\n",
    104:                   True)
    

    3.1. 読み込むモジュール

    3.1.1. ftplib

    FTP をするためのモジュールです。class FTP をインポートします。 このクラスは、サーバー名を与えて初期化します。FTP でできることは何でも できるようにメソッドがそろっています。

    詳しくは ライブラリリファレンス 11.7 ftplib -- FTPプロトコルクライアント を参照してください。

    3.1.2. cStringIO

    FTP サーバーからの応答は標準出力に出力されるので、 応答を利用しようとすると、標準出力をリダイレクトしなければなりません。 StringIOcStringIO モジュールは文字列をファイルのように扱うためのモジュールで、 標準出力のリダイレクトなどに利用されます。ASCII 文字しか入力されないことがわかっていれば cStringIO の方が高速です。

    詳しくは、 Python ライブラリリファレンス 4.6 StringIO および、 4.7 cStringIO を見てください。

    3.2. 関数

    3.2.1. is_ascii(fname)

    fname がテキストファイルか調べます。 拡張子が 'txt', 'htm', 'html', 'css' のファイルはテキストファイルとみなします。

    3.2.2. write_log(logfile, str, success)

    結果を upload.log に書き込む関数です。Error が起きた時は終了します。

    3.2.3. server_cd(ftp, path, log_name)

    FTP クラスのインスタンス (ftp) と '/' で区切られた相対パス (path) を引数にとり、 サーバーの path で表される パスにいきます。path がなければ作ります。
    1. path が 'HOME' なら何もしません。
    2. sys.stdoutstdout に保存しておきます。
    3. path を '/' で分けて、1つずつ 32 行目までを実行します。個々の要素を d とします。
      1. sys.stdoutStringIO() を代入します。 これで、標準出力がリダイレクトされました。
      2. ftp.dir() で、サーバー上のカレントディレクトリーの 'ls' を取得します。 .dir メソッドは、標準出力にリストを出力するので、プログラム中でその出力を 利用するためには StringIO にリダイレクトする必要があります。
      3. d という名前のディレクトリがサーバーのカレントディレクトリになければ ftp.mkd(d) でディレクトを作ります。
        sys.stdout.getvalue() を使って、StringIO に出力された値を取得しています。 それを1行ずつ分けて、'd' で始まれば、最後の単語をとって、それをまとめてリストにします。 そのリストに d がなければ、サーバー上に d といるディレクトリが無いので作ります。
      4. ftp.cwd(d) でディレクトリを移動します。
      5. sys.stdout を閉じます。
    4. 最後に退避した sys.stdout を元に戻します。

    3.2.4. put(ftp, ls, log_name)

    ls にあるファイルをアップロードする関数です。 59 行目で、ファイルが ASCII か BINARY かによってファイルを開くモードと転送するメソッドを分けています。 ASCII のときは 'storlines' で転送し、BINARY のときは 'storbinary' で転送します。 66 行目の getattr が便利な関数です。これを使うと属性を文字列で指定できるので、 いろいろ便利なことができます。もし、59 行目と getattr を使わないで普通の方法でメソッドを指定すると put は次のように冗長になります。下のコードでは、ほとんど同じことを 2 回繰り返しています。
    01:     def put(ftp, ls):
    02:         for fname in ls:
    03:             fbase = os.path.basename(fname)
    04:             if is_ascii(fbase):
    05:                 try:
    06:                     f = file(fname, 'r')
    07:                 except:
    08:                     ftp.close()
    09:                     write_log(log_name, "Cannot open local file: " + fbase, False)
    10:                 try:
    11:                     ftp.storline("STOR " + fname, f)
    12:                 except:
    13:                     ftp.close()
    14:                     write_log(log_name, "Cannot Transfer local file: " + fbase, False)
    15:                 f.close()
    16:     
    17:             else:
    18:                 try:
    19:                     f = file(fname, 'rb')
    20:                 except:
    21:                     ftp.close()
    22:                     write_log(log_name, "Cannot open local file: " + fbase, False)
    23:                 try:
    24:                     ftp.storbinary("STOR " + fname, f)
    25:                 except:
    26:                     ftp.close()
    27:                     write_log(log_name, "Cannot Transfer local file: " + fbase, False)
    28:                 f.close()
    

    3.3. main

    if __name__ == '__main__': 以下を簡単に説明すると以下のようになります。カッコ内は [code 1] の行番号です。
    1. upload.inp, upload.log のあるディレクトリ dtempos.getenv を使って取得します。(74)
    2. ログファイルのフルパス名 (log_name) を作ります。(75)
    3. 以前のログファイルを念のため消しておきます。(76)
    4. インプットファイルのフルパス名 (inp_name) を作ります (77)
    5. インプットファイルを開きます。(78--81)
    6. インプットファイルからサーバー名 (server)、ユーザー名 (user)、パスワード (passw)、 転送するファイルのリスト (files) を読み取ります。(83)
      入力ファイルの形式を工夫することで、eval を使って一発で読み込むことができます。
    7. 入力ファイルを閉じます。(84)
    8. 入力ファイルを消去します。(85)
      パスワードなどの取り扱いに注意を要する情報が含まれているので。
    9. サーバに接続します。 (87--90)
    10. サーバーにログインします。(92--96)
    11. ファイルを転送するディレクトリに移動します。(98)
    12. ファイルを転送します。(99)
    13. 接続を閉じます。(100)
    14. ログファイルに結果を出力します。(101--104)

    4. その他

    4.1. getattr, setattr, hasattr

    getattr, setattr, hasattr はインスタンスの属性を操作する関数です。 属性を文字列で与えられるので、いろいろな技が効きます。 典型的な例は Lib/lib-tk/ScrolledText.py にある次の部分でしょう。
    01:             # Copy geometry methods of self.frame -- hack!
    02:             methods = Pack.__dict__.keys()
    03:             methods = methods + Grid.__dict__.keys()
    04:             methods = methods + Place.__dict__.keys()
    05:     
    06:             for m in methods:
    07:                 if m[0] != '_' and m != 'config' and m != 'configure':
    08:                     setattr(self, m, getattr(self.frame, m))
    
    まず、widget を配置するメソッドである Pack, Grid, Place の名前空間からメソッド、クラス変数を取り出し、 self.frame におけるそれらの値を self の属性としています。 こうすると、ScrolledText は1つのオブジェクトとして、配置できます。 また、Dive Into Python にもこれらの関数の便利な使い方が 載っています。

    4.2. 接続のタイムアウト

    ここで示したプログラムはバックグラウンドで行われるのでタイムアウトは気にしませんが、 タイムアウトを気にする必要があるときは、socket モジュールの setdefaulttimeout を指定すると良いでしょう。 この関数が呼ばれた後生成するソケットは setdefaulttimeout で指定された秒数でタイムアウトします。

    例:

    import socket
    from ftplib import FTP
    socket.defaulttimeout(30.0)       # 以降作られるソケットは 30 秒後にタイムアウト
    ftp = FTP(fit.site.somewhere)
    ftp.login("me", "mypassward")
    .....................................
    

    5. 終わりに

    例外処理をこまめにやったため 100 行を超えましたが、実質 50 行ほどで アップロード専用 FTP クライアントが書けました。 StringIO, getattr の使い方を参考にして下さい。

    Python ライブラリが完備しているので便利なプログラムを短い行数でかけます。