今回はファイル処理をメインに取り扱います。実際の業務で使うアプリケーションやサービスは、なんらかの形でファイルを利用する場合が多いです。たとえばCSV(カンマ区切りの表)を読み込んだり、書き出したり……。また、アプリケーションの状態(設定など)やログを残すためにファイルを利用することもあります。

ファイルにはバイナリ(01)で構成される画像ファイルや、テキストで構成されるテキストファイルがあります。バイナリのファイルがどのようなものかについても軽く触れますが、初心者はあまり操作しないと思うので、テキストファイルが話の中心となります。そのため、テキストファイルを扱うために必要なテキスト処理についても扱います。なお、日本語テキストの処理などについては別途扱います。

テキストを生成する方法

テキスト処理は要するに、文字列型の処理です。第5回で簡単に扱ったのですが、テキストファイルの処理では文字列型の処理が必須となるので少し発展させて復習します。まず、文字列は以下のように定義するのでした。

text1 = 'hello python'

text2 = '''hello
world
python'''

ひとつめに関しては今さらいうこともないですが、2つめに関しては複数行でテキストをプログラム中で定義する方法でしたね。記号「'」の代わりに記号「"」を使うことも可能ですが、文字列の前後で統一されている必要があります。

文字列の結合に関しては「+」記号でできますが、数字などを結合するときは「文字列に変換」してから結合するのでした。ほかの型から文字列型への変換にはstr関数を使います。

print('hello ' + 'world') # hello world
print('hello ' + str(5))   # hello 5

結合の代わりに、文字列にテキストや数字を埋め込むという手法で文字列を生成することも可能です。

>>> 'hello {} {}'.format('python', 5)
'hello python 5'

文字列のformat関数(メソッド)の引数に {} に対応する文字列なり数値なりを与えています。このformat関数の使い方を詳細に伝えるとそれだけで連載2~3回分になってしまいますので、詳しくはこちらのドキュメントをご参照ください。結合より埋め込みのほうがコードがきれいになる場合が多いので、積極的に活用してもらいたいです。

文字列のフォーマットに関わるところでは、ほかには数値の整形をしたいことがよくあります。たとえば、1,2……というように連番でテキストを表示なり書き込みする場合、なにも配慮しないと次のように桁数が違うとガタガタになってしまいます。

1: some text
2: some text
……
9: some text
10: some text
11: some text
……

次のように0で揃えられているときれいですね。

01: some text
02: some text
……
09: some text
10: some text
11: some text
……

このような場合には以下の方法で文字列の数字に「0詰め」をすると便利です。zfillで桁数を指定したり、先のformat関数に出力の細かい指定をしたりしています。

print('5'.zfill(5))       # 00005
print(str(101).zfill(5))  # 00101

print('hello {0:05d} world'.format(5))  # hello 00005 world

最後に文字列で使われる特殊記号についてお話します。特殊記号はプログラム中で意味を持ってしまう特別な記号のことです。たとえば「'」という記号は文字列を作成する際に利用する特別な記号です。そのほかにはビープ音なども記号に分類されます。これらは文法的な理由やそもそもそれを表現する記号がキーボードのキーにないことから、「これは XX ですよ」という特別なルールにもとづいて文字列に表記します。そのルールに利用されるのがエスケープ記号と呼ばれるもので半角のバックスラッシュ「\」(英語キーボード)か、半角の円記号「\」(日本語キーボード)を利用します。このエスケープ記号の後に特別な文字を続けることで、それが特別な意味を持つのです。

たとえば「'」とビープ音は以下の用に記載できます。

print('escape sample1 \'.')
print('escape sample2 \a.')

ほかには改行とエスケープ記号自身あたりをよく使います。

print('escape sample1 \n.')
print('escape sample1 \\.')

エスケープ記号一覧はこちらのページの中央付近に記載されています。なお、記事掲載時から時間が経ってリンク切れしている場合は、適当に検索するなどして調べてみてください。

テキストを加工する方法

テキストの生成について取り扱ったので、次はそのテキストを加工する方法について扱います。基礎的な機能を順に紹介していきます。これ以外にも多数の機能がありますが、必要になった時点で調べて覚えていけばよいでしょう。

まず、文字列中の「文字」の取得ですが、以下のように [X] で位置を指定して行います。

>>> text = 'hello world python'
>>> print(text[4])
o

>>> print(text[100])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: string index out of range

>>> print(text[-4])
t

この位置の指定はリストの要素の数え方と同じで0から始まります。先頭から0、1、2……と数えていくと4はoに対応していますね。範囲を超えてしまうとエラーになります。

面白いのがこの値をマイナスにできるところです。このように指定すると後ろ側から取得してきます。この際、0からではなく-1、-2、-3……とカウントすることに注意してください。

文字列から「文字列」を取得するには、以下のように行います。

>>> text = 'hello world python'
>>> print(text[6:11])
world
>>> print(text[-12:-7])
world

これは「スライシング」と呼ばれるテクニックで、[X:Y]とあるとXからYまで取得という意味になります。[X:Y] と指定する際はX < Yとなるようにしてください。先ほどと同じように、範囲指定にもマイナス値を利用できます。

前と後ろを指定するのではなく、Xより前、Xより後という指定の仕方も可能です。

>>> print(text[6:])
world python
>>> print(text[:11])
hello world
>>> print(text[:])
hello world python

見ていただくとわかるように [X:Y] の片方を省略しています。そうすると先頭から、もしくは末尾までという意味になります。あまり使いどころはありませんが、両方とも省略すると、文字列のすべてが取得されます。

次に文字列の置き換えです。テキストエディタなどである特定のキーワードを別のキーワードに置き換えることがあるかと思いますが、それと同じ要領です。

>>> text = 'hello world python'
>>> print(text.replace('o', '0'))
hell0 w0rld pyth0n
>>> print(text.replace('world', 'WORLD'))
hello WORLD python

>>> print(text)
hello world python

文字列.replace(置き換える文字列, 置き換えられる文字列)とすると、変換された文字列が返されます。例にもあるように、元の文字列自体は変化していないので注意してください。

文字列の検索もそれほど難しくはありません。検索には「存在の確認」と「位置の確認」の2つの使い方があり、それぞれ次のようになります。

>>> text = 'hello world python'
>>> 'wor' in text
True
>>> 'w0r' in text
False

>>> text.find('wor')
6
>>> text.find('w0r')
-1
>>> text.find('o')
4

inについてはlistでの使い方とほぼ同じですね。find については最も左側にあるマッチした位置を返します。そのため、'o'は何個もありますが、一番左の位置となっています。マッチしない場合は-1が返ってきます。

それほど使う場面は多くないのですが、前側を指定した数だけ飛ばして途中から検索したり、右側から探索をすることも可能です。

>>> text.find('o', 10)
16

>>> text = 'hello world python'
>>> text.rfind('o')
16

次に文字列の前後からの特定の文字の削除です。よく利用するのは、前後の空白や改行コード、タブなどを取り除く場合などでしょう。

>>> text = ' hello world \n'
>>> text.strip()
'hello world'
>>> text.strip(' hell')
'o world \n'

strip関数に引数を指定しないと、文字列の前後の空白とタブ、改行が取り除かれます。引数に文字列を指定すると、その文字列が取り除かれます。

また、特定の区切りで文字列を分割して文字列のリストにすることも可能です。「,」記号で要素が区切られたCSV(Excel出力)やログの解析あたりでよく使うテクニックです。

>>> text = '1, taro, 35, male'
>>> text.split(',')
['1', ' taro', ' 35', ' male']

text = '''1, taro, 35, male
2, jiro, 29, male
3, hanako, 23, female'''

for line in text.split('\n'):
    elems = line.split(',')
    print('{} {}'.format(elems[1].strip(), elems[2].strip()))

# taro  35
# jiro  29
# hanako  23

分割の逆で文字列を「特定の文字列」で結合していくことも可能です。2次元配列(リストにリストが入っている)に格納された情報をCSV形式でファイルに書き出したりする際に便利な手法です。書式は「結合に使う文字列.join(文字列のリスト)」となります。

>>> l = ['1', 'taro', '35', 'male']
>>> ', '.join(l)
'1, taro, 35, male'

ファイル処理の概念

ファイル処理については、プログラミングというよりも「OSのファイル処理の方式」をまず理解しておく必要があります。そのため、最初にファイル処理の概念について説明します。これがわかってしまえば、その利用はさほど難しくありません。なお、プログラムがどのようにファイルを扱うかは、OSの仕組みにもとづいているため、多くのプログラミング言語でさほど変わりません。

ファイル処理がOSにおいてどう実現されているかを抽象化すると以下のようになります。実際はもっと複雑ですが、通常のプログラミングではそこまで意識する必要はないので詳細は割愛します。

ファイル処理の概念

まずご存知のようにOSにはディレクトリがあり、それが階層構造を作っています。ファイルはそのディレクトリのなかに保存されています。OSはこの階層構造を管理しています。ディレクトリやファイルは、サイズなどの情報と共にポインタのようなものを持っていて、それがファイルの実体を指しています。

構造についての話はこれぐらいにして、実際にファイルをどのように処理するか話をしましょう。OSにおけるファイル処理は主に以下のような流れとなります。

OSにおけるファイル処理

まず絶対パス(ルートやCドライブなどからのパス)や相対パス(現在いるディレクトリから指し示すパス)を使ってファイルを指定します。それに対して読み、書き、読み書きなどのモードを指定してファイルをオープンします。そして読み書きなどの必要な処理を繰り返し、処理がすべて完了したらファイルをクローズして終わりです。クローズし忘れないようにしてくださいね。

読み書きなどの具体的な処理はそれほど難しくありません。一言でいってしまえば、「テキストファイルは行ごとに処理する」「バイナリファイルは先頭から何バイトめか(位置)を指定して処理する」ことです。たとえば、テキストファイルで以下のものがあるとします。

world
python
java

この内容にすべて"hello "を加えて画面に表示するというプログラムを書く場合、ループ処理を利用して

  1. 行の内容を取得
  2. hello に行の内容を追加しprint
  3. 次の行に進む

ということを繰り返して処理するのが一般的です。「テキストファイルは行ごとに処理する」のが基本であることを覚えておいてください。

次にバイナリファイルです。バイナリファイルは中身が01から構成されているファイルで、一般的には画像ファイルや音声ファイル、それに加えてアプリケーション特有のファイル(たとえば word など)があります。こちらはテキストと違うのでそもそも行という概念がありません。正直なことをいうと、テキスト処理よりもバイナリファイルの処理は骨が折れます(笑)。ただ、ファイルを読み書きできないかというと、そんなことはありません。そのバイナリファイルの構造を知ってさえいれば操作は可能です。

著者はビットマップ形式の画像ファイルの合成とWAV形式の音声データの加工の経験があるので、それをベースにしてバイナリファイルの処理についてお話をします。

ビットマップは以下の図のように、ピクセルから構成されている画像ファイルです。

ビットマップの画像ファイル イメージ Wikipedia「ビットマップ画像」より

それぞれのピクセルはRGB(赤緑青)で表現されています。それぞれの色は1バイト(0~255)の容量があるので、ようするに1ピクセルは3バイトです。つまりファイルサイズは「縦のピクセル数×横のピクセル数×3」バイトになります。

ここまでわかってしまえば、あとは簡単です。たとえば画像Aに画像Bをオーバーレイ(一部上書き)するとします。この際、Bの画像の黒(RGBが0, 0, 0)は透過させます。すると、以下の図のようにして合成が可能です。

画像Aに画像Bをオーバーレイする

Bの左上は黒なのでAのものを合成画像に利用。その右隣は黒ではないのでBのものを利用。その右隣はA……といった感じでどんどん処理をしていくと、最終的に右の図のようになります。これをファイルに書き込めば、自分でバイナリファイルを作ったことになります。

次にWAV音声ファイルです。これも比較的わかりやすい形式ですが、先ほどのビットファイルと違って「ヘッダ」と「データ」に分かれています。データは先程のビットマップと同じく音声のデータ(波形)を含んでいるだけなので簡単ですが、ヘッダにはデータをどのように表現するかといった情報が含まれています。

WAV音声ファイルのイメージ

後ろのデータを変えれば当然再生される音も変わりますが、その際に必要に応じてヘッダを変更する必要があります。

最後にバイナリデータの処理のコツを伝えます。それは「プログラムで処理しやすい生(raw)の形式に一旦戻す」ということです。たとえばビットマップであれば編集は簡単ですが、JPEGやPNGを編集するのは非常に難しいです。そのためまずはJPEG → ビットマップに変換してやり、ビットマップで編集を行った後に再度、ビットマップ → JPEGに変換すればよいのです。音声も同じでmp3を直接編集するのではなく、mp3 → wav → 編集 → new wav → mp3とすればよいです。これらの変換には組み込みもしくは外部のライブラリを使用してかまいません。

実際にファイル処理をしてみよう

長くなりましたが、実際に pythonでテキストファイルの処理をどのようにするか紹介します。先ほどの概念さえわかってしまえば非常に簡単です。

world
python
java

と書かれたテキストファイルtext.txtの各行にhelloを加えて表示するサンプルを書いてみます。

f = open('text.txt', 'r')
print(type(f))

for line in f:
    print('hello ' + line)
f.close()

まずファイル 'txt.txt' をモード 'r(読み)' でオープンしています。オープンしたファイルオブジェクトに対してfor文を使うと1行1行取得できるので、行ごとにprintする処理をしています。これを実行すると以下のような出力となります。

<type 'file'>
hello world

hello python

hello java

print文の改行に加えて、もとのテキストの改行も表示されるので1行スペースがあいてしまっていますね。print文の改行をなくすには以下のようにprint文の後に「,」を書けばよいです。

print('hello\n'),
print('world\n'),

ほかにはファイルを丸ごと読む方法もあります。

f = open('text.txt', 'r')
text = f.read()
print(text)

lines = text.split('\n')
print(lines)

f.close()

ファイルオブジェクトに対してread関数を使うことで、その中身をすべて文字列として取得します。それを行ごとに処理したいのであれば、文字列を先に説明した改行コードで分割することで行ごとのリストになるので、それに対して処理を行うことができます。

次に書き込み方法について説明します。書き込みも読み込みと大差ありませんが、ファイルをオープンする際に書き込みモードを指定します。

以下のテキストファイルtext.txtに書き込みをするとします。

hello

書き込みのコードは以下となります。

f = open('text.txt', 'w')
f.write('123')
f.write('456')
f.close()

コードを見てもらうと想像がつくとは思いますが、openの第二引数が書き込みモードの 'w' となっています。そしてファイルオブジェクトにたいしてwriteすることで、実際にファイルに書き込み処理がされています。最後にクローズですね。

するとファイル text.txt は以下のようになりました。

123456

見てもらうとわかるように、もともとのテキストであるhelloが消えていますね。上書きされていることがわかります。ただ、場合によっては「追記(もとの中身を残したまま後ろに加える)」しないといけないこともあります。その場合はモードを 'a' の「追記」にすれば実現できます。モードのみ修正して以下のコードにしてみます。

f = open('text.txt', 'a')
f.write('123')
f.write('456')
f.close()

これを実行すると、

123456123456

となりました。もとの '123456' は残ったままで、その後ろに '123456' が新しく追加されていますね。ファイルのオープンごとに以前の内容が消えないので、アプリケーションのログなどを取る際に便利な手法です。なお、書き込みを「次の行」にする場合は「\n」を書き込めばいいです。

最後に小ネタを話して終わりたいと思います。ファイル処理をする際に心の片隅においていただきたいのが「バッファリング」という処理です。

ご存知かもしれませんが、ハードディスクへのアクセス速度はメモリへのアクセス速度に比べて何桁も遅いです。そのため、ファイルを何度も細かく書くことを繰り返しているとプログラムが非常に低速になってしまいます。この問題を防ぐために、出力があるたびに毎回ディスクに書き込むのではなく、メモリ上の高速な一時領域にデータをおいておき、まとめてそれを書き込むという処理が行われます。こうすることで低速なディスクアクセスの回数が減らせるのでプログラムが高速化されます。これがバッファリングの基本的な概念です。

以下にこれを図で示します。

バッファリングの概念

このディスクへの書き込みは特定のタイミングで発生するようですが、それを強制的に行いたい場合はflush()関数を使います。

f = open('text.txt', 'w')
f.write('123')
f.flush()
f.write('456')
f.close()

closeのタイミングで必ず書き込まれるので、今回のようにopenからcloseまで時間が短い場合はflushは不要です。ただ、openしっぱなしで、なかなかcloseしないようなプログラムは適切なタイミングでflush するように心がけてください。でないと、プログラムが強制終了されてしまった場合などに、ファイルに書き込みがされていない 可能性があります。

以上でファイルに関する基本的な話は終了です。ある特定ディレクトリ配下のすべてのファイルを調べるのに便利なglobや、リソース管理のwith文などもあるのですが今回は割愛します。便利なのである程度レベルがあがったら、ぜひ自分で調べてみてください。

「Pickle」とは

最後に「Pickle」についてご紹介します。PickleはPythonのデータをファイルに保存し、それを読み取って復元する目的で使えます。あるアプリケーションで終了時に保持するデータをPicklで保存し、再度開いた際にPickelで読み取れば、前回終了した際の状態に戻すといった使い方ができます。

「Pickle」のイメージ

Pickle の使い方はそれほど難しくないので、以下にサンプルを載せます。

import pickle

a = {'hello':1, 'world':[1,2,3]}
f1 = open('test.dump', 'wb')      # WRITE
pickle.dump(a, f1)
f1.close()

f2 = open('test.dump', 'rb')
b = pickle.load(f2)               # READ
f2.close()
print(b) ## {'world': [1, 2, 3], 'hello': 1}

まずPickleパッケージをインポートしています。そして書き出すファイルを書き込みモードでオープンし、pickle.dump関数でデータをファイルに書き込んでいます。Pickleで書き込まれるデータはバイナリなので'w'ではなく'wb'でバイナリとしてオープンしています。'w'でもおそらく問題はないと思います。

次に Pickleのデータが書き込まれたファイルから中身をロードしてきています。これには pickle.load 関数を使っています。'wb'と同様に、こちらもバイナリの読み込みなので'rb'でファイルをオープンしています。簡単ですね。


演習1

以下のCSV形式のテキストデータから教科ごとの生徒の平均点を算出してください。

text = '''
lecture\students, 1, 2, 3, 4
math, 80, 70, 75, 54
english, 60, 80, 90, 80
'''

可能なら生徒や教科が増えても対応可能なプログラムにしてください。

演習2

あるテキストファイルAの内容を読み取り、まったく同じ内容をファイルBに書き出すプログラムを書いてください。

演習3

演習2で作ったプログラムを改良し、ファイルBに行番号を書き出すようにしてください。ただし、行番号は最後の行の桁数にあるように0詰めしてください。たとえば以下のようになります。

a
b
c
……
i
j
k
……
z


01 a
02 b
03 c
……
09 i
10 j
11 k
……
26 z

演習4

標準入力で入力されたテキストをpickleでファイルに保存してください。そしてそれをロードして、画面に表示してください。さまざまなデータをPickleで保存して、そのファイルを開いて中身を確認してみてください。

※解答はこちらをご覧ください。


次回は正規表現と日本語の扱いについて解説します。

執筆者紹介

伊藤裕一(ITO Yuichi)

シスコシステムズでの業務と大学での研究活動でコンピュータネットワークに6年関わる。専門はL2/L3 Switching とデータセンター関連技術およびSDN。TACとしてシスコ顧客のテクニカルサポート業務に従事。社内向けのソフトウェア関連のトレーニングおよびデータセンタとSDN関係の外部講演なども行う。

もともと仮想ネットワーク関連技術の研究開発に従事していたこともあり、ネットワークだけでなくプログラミングやLinux関連技術にも精通。Cisco社内外向けのトラブルシューティングツールの開発や、趣味で音声合成処理のアプリケーションやサービスを開発。

Cisco CCIE R&S, Red Hat Certified Engineer, Oracle Java Gold,2009年度 IPA 未踏プロジェクト採択

詳細(英語)はこちら