Python基礎講座 17 ファイル読み書き 第1回 File Read Write Vol 1

読了 13分

今回の講座では、ファイルを読み書きする方法について勉強していきましょう。

開発をしていると、ファイルからデータを読み込んで処理したり、ある結果物をファイルに残したり、プログラムに問題が発生したらエラーログをファイルに残したりするケースが非常に多くあります。

Pythonで主に扱うファイル形式

  • TEXT
  • CSV
  • JSON
  • YAML
  • EXCEL
  • PDF
  • Image

Pythonで主に扱うファイルには、一般的なテキストファイル、CSV、JSON、YAML、EXCEL、PDF、imageファイルなどがあります。今回の講座は基礎講座ですので、一般的なテキストファイルを読み書きする方法についてのみ勉強していきましょう。

まずはファイルを読み込む方法について見ていきます。

ファイルを読み込むためには、まずファイルを開く必要があります。ファイルを開くためには、組み込み関数のopen関数を使う必要があります。open関数は複数のパラメータを持っていますが、今はfilemodeencodingの3つのパラメータだけ覚えておいてください。

Pythonコード
open(file='data.txt', mode='r', encoding='utf8')

fileパラメータにはファイルパスが入ります。ファイルの絶対パスや、Pythonファイルからの相対パスを入力してください。もしファイルがPython実行ファイルと同じフォルダにあれば、ファイル名だけ入力していただいても構いません。

次は、modeパラメータに入れることができるオプションについて見ていきましょう。

ファイル読み書きモード

モード説明
r読み込み専用モードであり、デフォルトオプションのため省略していただいても構いません。
w書き込み専用モードであり、同じ名前のファイルがなければ新しいファイルを生成し、あれば上書きをします。上書きをすると既存のファイルのデータが全部消えるのでご注意ください。
x書き込み専用モードですが、同じ名前のファイルが存在するとエラーを発生させます。wモードの安全モードと考えていただければと思います。
a追記モードであり、同じ名前のファイルがなければ新しいファイルを生成し、あれば最下部から続けて書き込むモードです。
tテキストモードでデフォルトモードのため省略可能です。
bバイナリモードで、PDFや画像ファイルなどのように一般的なテキストファイルではなく、バイナリで作られたファイルを開くときに使うモードです。
r+読み込みと書き込みを同時に行えるモードです。
w+読み込みと書き込みを同時に行えるモードです。

今回の講座では、r、w、x、aの4つのモードについてのみ見ていきます。

encodingパラメータは"cp932"がデフォルト値として定義されており、韓国語や中国語、日本語のようなUnicode文字が入ったファイルを読み書きするには"utf8"オプションを使う必要があります。

下のように複数の国名、首都名、地域名が入ったデータをcountry.txtという名前のテキストファイルに保存したあと、Pythonを通して読み込んでみましょう。

country.txt
韓国,ソウル,アジア
日本,東京,アジア
中国,北京,アジア
イギリス,ロンドン,ヨーロッパ
フランス,パリ,ヨーロッパ
イタリア,ローマ,ヨーロッパ

まずopen関数を使って返された値を確認してみましょう。fileパラメータにはファイルパスを渡す必要がありますが、下のファイルはPython実行ファイルと同じフォルダにあるためファイル名だけを入力します。読み込みモードであるrモードを使い、韓国語はUnicode文字なのでencodingにはutf8オプションを使います。

Pythonコード
file = open(file='country.txt', mode='r', encoding='utf8')
print(file)
結果
<_io.TextIOWrapper name='country.txt' mode='r' encoding='utf8'>

open関数がTextIOWrapperというオブジェクトを返したことが分かります。このオブジェクトは非常に多くのメソッドを持っていますが、その中でもreadメソッドを使ってみましょう。

Pythonコード
file = open(file='country.txt', mode='r', encoding='utf8')
print(file.read())
file.close()
結果
韓国,ソウル,アジア
日本,東京,アジア
中国,北京,アジア
イギリス,ロンドン,ヨーロッパ
フランス,パリ,ヨーロッパ
イタリア,ローマ,ヨーロッパ

country.txtファイルの内容がそのまま出力されました。

open関数を使ってファイルを開いて使ったあとは、closeメソッドを使って必ずファイルを閉じる必要があります。プログラムが終了するときにファイルが自動的に閉じられはしますが、プログラムが実行されている間はファイルにロックがかかり、他の人や他のプログラムがファイルにアクセスできなくなります。この点は必ず注意していただきたいので、このような形でファイルにアクセスするよりは、コンテキストマネージャを使うほうが良いです。

Pythonコード
with open(file='country.txt', mode='r', encoding='utf8') as file:
    print(file.read())  # 実行ブロック
# file.close()  # withキーワードを使えばcloseメソッドを使わなくても構いません。

コンテキストマネージャはwithキーワードで始まり、オブジェクトを保存する変数をasキーワードの後ろに置けば良いです。 ここからインデントされた部分が実行ブロックですが、この実行ブロックが終了すると、コンテキストマネージャはファイルを自動的にクローズします。 ファイルオブジェクトはclosedという属性を持っていますが、これを使えばファイルがちゃんと閉じられたかを確認できます。 本当にファイルがちゃんと閉じられたか確認してみましょう。

Pythonコード
with open(file='country.txt', mode='r', encoding='utf8') as file:
    print('[with文の中で実行]' , file.closed)
    print(file.read())

print('[with文の外で実行]' , file.closed)
結果
[with文の中で実行] False
韓国,ソウル,アジア
日本,東京,アジア
中国,北京,アジア
イギリス,ロンドン,ヨーロッパ
フランス,パリ,ヨーロッパ
イタリア,ローマ,ヨーロッパ
[with文の外で実行] True

with文の中ではファイルが開かれている状態であり、with文の外ではファイルが閉じられている状態であることを確認しました。

readメソッドはnというパラメータに整数を渡すことができますが、このパラメータを使うと、その数だけの文字を読み込んで返します。

2を入力して、韓国の2文字だけを出力してみましょう。

Pythonコード
with open(file='country.txt', mode='r', encoding='utf8') as file:
    print(file.read(2))
結果
韓国

ファイルオブジェクトは内部的にカーソルというものが動作し、ファイルを開くと、ファイルの一番先頭にカーソルを位置させます。そしてreadメソッドを実行すると、その数だけカーソルを移動しながら文字を読み込みます。先ほどのように2文字を読み込むと、カーソルは2回移動することになります。そしてまたread関数を実行すると、カーソルが移動した位置から読み込み始めます。

今度はカンマと単語を別々に出力してみましょう。下のコードを実行すると、カーソルがread関数に渡された数だけ移動しながらテキストを読み込みます。

Pythonコード
with open(file='country.txt', mode='r', encoding='utf8') as file:
    print(file.read(2))
    print(file.read(1))
    print(file.read(3))
    print(file.read(1))
    print(file.read(3))
結果
韓国
,
ソウル
,
アジア

それでは、以下のようにreadメソッドを2回実行すると、ファイル全体が2回出力されるでしょうか。実行してみましょう。

Pythonコード
with open(file='country.txt', mode='r', encoding='utf8') as file:
    print(file.read())
    print(file.read())
結果
韓国,ソウル,アジア
日本,東京,アジア
中国,北京,アジア
イギリス,ロンドン,ヨーロッパ
フランス,パリ,ヨーロッパ
イタリア,ローマ,ヨーロッパ

1回しか出力されないことが分かります。理由は、最初にreadメソッドを実行するとカーソルがファイルの最後に位置することになり、2回目のreadメソッドが実行されたときには、もう読み込む文字がないため何も返さずに終了するからです。もしファイル全体を2回読み込みたい場合は、seekメソッドを使ってください。seekメソッドに数値0を渡して実行すると、カーソルがファイルの先頭に移動します。

それではseekメソッドを使ってみましょう。

Pythonコード
with open(file='country.txt', mode='r', encoding='utf8') as file:
    print(file.read())
    file.seek(0)
    print(file.read())
結果
韓国,ソウル,アジア
日本,東京,アジア
中国,北京,アジア
イギリス,ロンドン,ヨーロッパ
フランス,パリ,ヨーロッパ
イタリア,ローマ,ヨーロッパ
韓国,ソウル,アジア
日本,東京,アジア
中国,北京,アジア
イギリス,ロンドン,ヨーロッパ
フランス,パリ,ヨーロッパ
イタリア,ローマ,ヨーロッパ

ファイルが2回出力されているのが分かります。

今度は、ファイルオブジェクトが持っているreadlinesというメソッドを使ってみましょう。このメソッドはすべての行をリストのアイテムとして保存し、ファイル全体のテキストを1つのリストとして返します。

実行してみましょう。

Pythonコード
with open(file='country.txt', mode='r', encoding='utf8') as file:
    lines = file.readlines()

print(lines)
結果
['韓国,ソウル,アジア\n', '日本,東京,アジア\n', '中国,北京,アジア\n', 'イギリス,ロンドン,ヨーロッパ\n', 'フランス,パリ,ヨーロッパ\n', 'イタリア,ローマ,ヨーロッパ']

1行ずつリストに上手く保存されて返されているのが分かります。このようにリストを作る理由は、ループを回しながらデータを処理するためですが、for反復文を使って出力してみましょう。

Pythonコード
with open(file='country.txt', mode='r', encoding='utf8') as file:
    lines = file.readlines()

for line in lines:
    print(line)
結果
韓国,ソウル,アジア

日本,東京,アジア

中国,北京,アジア

イギリス,ロンドン,ヨーロッパ

フランス,パリ,ヨーロッパ

イタリア,ローマ,ヨーロッパ

実際のファイルのデータとは違って、各行の間に空行が追加されました。この理由は、各リストアイテムの最後に改行文字があり、print関数も改行をするため、各行ごとに改行が2回発生したからです。

この問題は、下のコードのようにprint関数のendパラメータに空文字列を渡して改行をしないようにすることで解決できます。

Pythonコード
with open(file='country.txt', mode='r', encoding='utf8') as file:
    lines = file.readlines()

for line in lines:
    print(line, end='')
結果
韓国,ソウル,アジア
日本,東京,アジア
中国,北京,アジア
イギリス,ロンドン,ヨーロッパ
フランス,パリ,ヨーロッパ
イタリア,ローマ,ヨーロッパ

今度は、地域別に国をフィルタリングする機能を作ってみましょう。まず各行をカンマを基準にスプリットしてリストに保存します。

Pythonコード
with open(file='country.txt', mode='r', encoding='utf8') as file:
    lines = file.readlines()

countries = []

for line in lines:
    countries.append(line.split(','))
    
print(countries)
結果
[['韓国', 'ソウル', 'アジア\n'], ['日本', '東京', 'アジア\n'], ['中国', '北京', 'アジア\n'], ['イギリス', 'ロンドン', 'ヨーロッパ\n'], ['フランス', 'パリ', 'ヨーロッパ\n'], ['イタリア', 'ローマ', 'ヨーロッパ']]

地域名に改行文字が残っていますね。stripメソッドを使って削除しましょう。

Pythonコード
with open(file='country.txt', mode='r', encoding='utf8') as file:
    lines = file.readlines()

countries = []

for line in lines:
    split_items = [x.strip() for x in line.split(',')]
    countries.append(split_items)
    
print(countries)
結果
[['韓国', 'ソウル', 'アジア'], ['日本', '東京', 'アジア'], ['中国', '北京', 'アジア'], ['イギリス', 'ロンドン', 'ヨーロッパ'], ['フランス', 'パリ', 'ヨーロッパ'], ['イタリア', 'ローマ', 'ヨーロッパ']]

きれいに削除されました。それでは、今度はif文を使ってアジアの国だけを出力してみましょう。

Pythonコード
with open(file='country.txt', mode='r', encoding='utf8') as file:
    lines = file.readlines()

countries = []

for line in lines:
    split_items = [x.strip() for x in line.split(',')]
    countries.append(split_items)

print('アジアの国:')
for country in countries:
    if country[2] == 'アジア':
        print('- {}'.format(country[0]))
結果
アジアの国:
- 韓国
- 日本
- 中国

countries変数に保存されたデータを再利用して、ヨーロッパの国を追加でフィルタリングしてみましょう。

Pythonコード
with open(file='country.txt', mode='r', encoding='utf8') as file:
    lines = file.readlines()

countries = []

for line in lines:
    split_items = [x.strip() for x in line.split(',')]
    countries.append(split_items)

print('アジアの国:')
for country in countries:
    if country[2] == 'アジア':
        print('- {}'.format(country[0]))
        
print('\nヨーロッパの国:')
for country in countries:
    if country[2] == 'ヨーロッパ':
        print('- {}'.format(country[0]))
結果
アジアの国:
- 韓国
- 日本
- 中国

ヨーロッパの国:
- イギリス
- フランス
- イタリア

ところで、上のようにreadメソッドやreadlinesメソッドでサイズの大きいファイルを開くと、ファイル内のすべてのテキストをメモリ上に保存するため、メモリリソースを浪費する問題が発生します。メモリだけ多く使うのではなく、プログラムのパフォーマンスも下がります。ファイルを1つ読むのに数ギガバイトもメモリを使い、しかも遅いプログラムは、誰も使おうとしないでしょう。readreadlinesメソッドは小さなファイルを読み込むときに使うのは問題ありませんが、サイズの大きなファイルを読み込むときには別の方法を使う必要があります。

ファイルオブジェクトはfor反復文の中で直接使うことができ、ループを1回回るたびにテキストを1行ずつ返します。この機能を使うと、ファイルのすべてのデータをメモリにロードすることなく、すべてのデータを読み込むことができます。実行してみましょう。

Pythonコード
with open(file='country.txt', mode='r', encoding='utf8') as file:
    for line in file:
        print(line, end='')
結果
韓国,ソウル,アジア
日本,東京,アジア
中国,北京,アジア
イギリス,ロンドン,ヨーロッパ
フランス,パリ,ヨーロッパ
イタリア,ローマ,ヨーロッパ

実際にreadメソッドを使ったときと使わなかったときのメモリ使用量とパフォーマンスを比較してみましょう。比較テストのため、事前に135MB程度のファイルを用意しました。まずtimeパッケージを使って、プログラムの実行時間を測定するコードを作ってみましょう。プログラムが終了する時間から開始した時間を引くと、プログラムの実行時間が秒で出力されます。

まずreadメソッドをテストしてみましょう。

Pythonコード
import time

start = time.time()

with open(file='big_file.txt', mode='r', encoding='utf8') as file:
    text = file.read()
    for line in text.splitlines():
        print(line)
        
end = time.time()
time_took = end - start
print('実行時間: {} 秒'.format(time_took))
結果
line 1
line 2
...
line 9999998
line 9999999
実行時間: 407.7338275909424 秒

read関数のテストは407秒かかりました。下の画像のように、実行中に824.4 MBもの大量のメモリを占有していることを確認しました。

Read関数パフォーマンステスト
Read関数パフォーマンステスト

今度はfileオブジェクトをfor反復文の中で直接使ってみましょう。

Pythonコード
import time

start = time.time()

with open(file='big_file.txt', mode='r', encoding='utf8') as file:
    # text = file.read()
    for line in file:
        print(line, end='')

end = time.time()
time_took = end - start
print('実行時間: {} 秒'.format(time_took))
結果
line 1
line 2
...
line 9999998
line 9999999
実行時間: 405.07602643966675 秒
ファイルオブジェクトパフォーマンステスト
ファイルオブジェクトパフォーマンステスト

どうですか。readメソッドを使うよりもfileオブジェクトを直接for反復文で使うほうが、メモリをはるかに少なく使いながら実行時間も短いことを見ていただきました。

次の講座では、ファイル書き込みについて勉強していきましょう。ありがとうございました。

X