home changes contents help options

163:ファイルに一行挿入する

一時ファイルに書き留める

いきなりファイルを開いて書き始めると、当然ながら元からあるデータを上書きしてしまいます。 そのため、まず「元ファイルの start_line 行目に data を挿入してある」一時ファイルを作成します。 できあがったら、その一時ファイルを元ファイルにリネームするか、一時ファイルからデータを読みこみ元ファイルに書き込みます。 今回はリネームする方向でつくってみます。

以下の insert_line(org_name, start_line, data) 関数は org_name ファイルの start_line 行目に data を挿入します。:

   import tempfile
   import os
   import stat

   def _rename(f1, f2):
       u"""
       f1 を f2 にリネームする。
       Windows 上ではリネーム先にファイルが存在すると
       os.rename が WindowsError 例外となる。
       これを防ぐためリネーム先ファイルを削除する。
       """
       if os.name == 'nt':
           os.remove(f2)
           os.rename(f1, f2)
       else:
           os.rename(f1, f2)

   def _insert_line(temp_file, org_name, start_line, data):
       u"""
       一時ファイルに挿入位置の前、つまり start_line 行目までをコピーする。
       その後 data を挿入し、残りデータをコピーする。

       start_line が元ファイルの行数より大きい場合、 data は挿入されない。
       start_line が負の値の場合 0 とみなされる。
       """
       org_f = open(org_name, 'r')
       try:
           for i in xrange(start_line):
               temp_file.write(org_f.next())
           temp_file.write(data)
           while True:
               temp_file.write(org_f.next())
       except StopIteration:
           pass
       finally:
           org_f.close()
       temp_file.flush()

   def insert_line(org_name, start_line, data):
       u"""
       org_name ファイルの start_line 行目に data を挿入する。

       まず、元ファイル org_name の属性を保持する。
       そして元ファイルの start_line 行目に data を挿入してある一時ファイルを作成する。
       完成したらその一時ファイルを元のファイルにリネームし、属性を元に戻す。
       """
       st = os.stat(org_name)
       mode = stat.S_IMODE(st.st_mode)

       temp = tempfile.mkstemp(text=True, dir=os.path.dirname(org_name))
       temp_file = os.fdopen(temp[0], 'w')
       temp_name = temp[1]

       try:
           _insert_line(temp_file, org_name, start_line, data)
           temp_file.close()
           _rename(temp_name, org_name)
       except:
           # 処理に失敗したら一時ファイルを削除する
           temp_file.close()
           os.remove(temp_name)
           raise

       os.chmod(org_name, mode)

正常動作時、一時ファイルは元のファイル名にリネームし残します。そのため、一時ファイルは tempfile.TemporaryFile?, tempfile.NamedTemporaryFile? クラスに頼らず tempfile.mkstemp にて作成します。 mkstemp で作成した一時ファイルの「自動で削除されたりはしない」という性質は使いにくいものですが、 今回の目的には合致します。

一時ファイル作成後からリネームが完了するまでの処理は try 文の内で行い、例外が発生したら 一時ファイルを削除します。

今回作成した insert_line 関数はファイルがみつからない、ファイルに読み書き権限が無いなどの例外が 発生した場合、素直に例外を送出します。一時ファイルの削除のために例外を捉えた場合も再送出しています。 この insert_line 関数から送出された例外を処理する際には:

   try:
       insert_line('foo.txt', 1, 'bar')
   except EnvironmentError, e:
       # エラー処理を記述
       print e.errno, e.strerror, e.filename

のように EnvironmentError を捕まえるとよいでしょう。

tempfile.mkstemp の戻り値はファイルオブジェクトではなく、一時ファイルを指し示すファイルディスクリプタ値とファイル名のタプルです。 ファイルディスクリプタ値で示されている一時ファイルを操作するには os.fdopen 関数にてファイルオブジェクトを得て、 これのメソッド経由で行うのがわかりやすいです。 os.write 関数などで直接操作する方法もあります。

一時ファイルが出来上がったら、これに start_line 行目までのデータを書き込みます。その後 data を書き、 残りのデータを書き込みます。

元ファイルから 1 行ずつデータを読み込むのにはファイルオブジェクトの readline メソッドか next メソッドを使います。 ファイル終端に達したときの動作が異なります。 readline は空文字列を返し、next は StopIteration? 例外を送出します ( next メソッドはイテレータ型がもつメソッドです。ファイルオブジェクトはそれ自身がイテレータでもあります。)。 今回は next メソッドを用いています。

ファイルから 1 行読み込む操作を start_line 回繰り返すには for i in xrange(start_line) とします。

残りのデータを読み込む無限ループは while True で作ります。

ファイルの属性を変更するには os.chmod 関数を、 ファイルの属性を得るには os.statstat.S_IMODE 関数を使います。

積み残し課題

Windows の os.rename はリネーム先が存在する場合必ず WindowsError? で失敗します。 このためファイルを削除してからリネームとしています。もし、この間に何かおこるとファイルが消失する可能性があります。 より安全にするために削除ではなく退避するようにしたほうがよいかもしれません。

ファイルの属性を変更する os.chmod 関数は Mac OS 9 以前など使えない環境があります。:

   if hasattr(os, 'chmod'):
       os.chmod(org_name, mode)

万全を喫するならば chmod 関数があるかどうかを確認したほうがよいかもしれません。