コンテンツにスキップ

第10回:ファイルの操作

前回は例外処理を学び、finally の「例外があってもなくても必ず後片付けする」という考え方に触れた。実は今回学ぶファイル操作でも、この「必ず後片付け(=ファイルを閉じる)」が重要になる。例外処理とファイル操作は地続きの話だ。

ここまで書いてきたプログラムには、ひとつ大きな弱点がある。閉じると全部消える。今回はその対策、データをファイルに保存・読み込みする方法を学ぶ。

プログラムを閉じると全部消える(動機)

変数に入れた値は、プログラムが終わると消える。変数はメモリ上にしかないからだ。

ゲームのセーブ、LINE のトーク履歴、成績…これらはすべてファイルに保存されている。だから次に開いても残っている。やりたいことは 2 つ。

  • セーブ:今の状態をファイルに書き出す'w'
  • ロード:保存した状態をファイルから読み込む'r'
[メモリ上の変数]                    [ファイル:save.txt]
  name = '勇者'  ──書き出し(セーブ)──▶   勇者
  hp   = 60                                60
               ◀──読み込み(ロード)──

ファイルとは

  • テキストファイル:人が読んで理解できる文字の集まり(メモ帳で開ける)
    • これに対し、人が読めない形式をバイナリファイルという
  • データをカンマ区切りで書く形式を CSV 形式とよぶ
    • 1 件分のデータ(1 行)をレコードとよぶ
  • 拡張子:ふつうのテキストは .txt、CSV は .csv(どちらも中身はテキスト)
name,hp,level
勇者,100,5
マーリン,50,3      ← save.csv(一例)
戦士,120,4

ファイルへの書き出し=セーブ

  • open(ファイルパス, 'w', encoding='UTF-8')書き込みモードで開く
  • f.write(文字列) で書き出す。改行 \n自分で付ける
  • 最後に f.close()必ず閉じる

注意点が 2 つ。

  • 'w'まっさらに上書きする(前の中身は消える)
  • write に渡せるのは文字列だけ(数値は str() で文字列にする)
f = open('save.txt', 'w', encoding='UTF-8')
f.write('勇者' + '\n')
f.write('100' + '\n')
f.write('5' + '\n')
f.close()
(save.txt の中身)
勇者
100
5

with 文で安全に閉じる

close() を書き忘れると、書き込みが反映されないことがある

with を使うと、ブロックを抜けるときに自動で閉じてくれる。前回の finally と同じ「必ず後片付け」の考え方だ。以後はこの with の形を基本にする

with open('save.txt', 'w', encoding='UTF-8') as f:
    f.write('勇者' + '\n')
    f.write('100' + '\n')
    f.write('5' + '\n')
# ここを抜けると自動で閉じる(close 不要)

プレ演習10-1

次のコードを打って実行しよう。実行後、同じフォルダにできた save.txt をメモ帳(や Spyder)で開いて中身を確認しよう。

with open('save.txt', 'w', encoding='UTF-8') as f:
    f.write('勇者' + '\n')
    f.write('HP 100' + '\n')
    f.write('レベル 5' + '\n')
print('セーブ完了!')

期待される save.txt の中身:

勇者
HP 100
レベル 5

ファイルの読み込み=ロード

  • open(ファイルパス, 'r', encoding='UTF-8')読み込みモードで開く
  • for line in f:1 行ずつ取り出せる
  • 一度に全部メモリに載せないので、大きなファイルでも扱える

ポイント:各 line には末尾に改行 \n が付いている。だから print では end=''(改行を付け足さない)にする。そうしないと改行が二重になる。

with open('party.txt', 'r', encoding='UTF-8') as f:
    for line in f:
        print(line, end='')
(party.txt の中身が3行なら)
マーリン
50
3

プレ演習10-2

test.txt(中身は自由、3 行以上)を作成し、プログラムと同じフォルダに置こう。下のコードを実行する前に、画面に何が出るかを紙やメモ帳に予測して書こう。そのあと実行して、予測と合っているか確認しよう。

with open('test.txt', 'r', encoding='UTF-8') as f:
    lines = f.readlines()
    for line in lines:
        print(line, end='')
    print(lines[0], end='')
    print(lines[2], end='')
ポイント
  • readlines() はファイル全体を1 回だけリストにする(1 行=1 要素)。
  • ファイルは一度読むと戻れない。だから「全部表示」も「特定の行の取り出し」も、同じ lines から行う(同じ f は 2 回読めない)。
  • インデックスに注意:lines[0] は何行目? lines[2] は何行目?
  • end='' を外すと各行のあとに空行が入る理由は、本文の「\n が二重になる」を見直そう。

セーブデータがない=FileNotFoundError

まだ一度もセーブしていないファイルを読もうとすると、例外が起きる。これは前回の try〜except の出番だ(ファイル操作と例外処理はつながっている)。

try:
    with open('save.txt', 'r', encoding='UTF-8') as f:
        for line in f:
            print(line, end='')
except FileNotFoundError:
    print('セーブデータがありません。はじめから始めます')
(save.txt が無いとき)
セーブデータがありません。はじめから始めます

複数キャラをまとめて=CSV

1 人ぶんを 3 行で書くと、パーティ全員だと行数が膨大になる。1 人=1 行、項目はカンマ区切り(CSV)にすると、1 行=1 レコードで扱える。

読むときは line.split(',') でカンマで分けてリストにする。

# セーブ:1人1行
with open('party.csv', 'w', encoding='UTF-8') as f:
    f.write('勇者,100,5' + '\n')
    f.write('マーリン,50,3' + '\n')

# ロード:カンマで分けて使う
with open('party.csv', 'r', encoding='UTF-8') as f:
    for line in f:
        parts = line.split(',')      # ['勇者', '100', '5\n']
        name = parts[0]
        hp = int(parts[1])           # 末尾に \n が付いても int() はOK
        print(name, 'のHP:', hp)

プレ演習10-3

下は party.csv を読んで「名前 のHP: 100」と表示したいコードだが、3 か所まちがいがある。それぞれ見つけて、正しく動くように直そう(party.csv の中身は 勇者,100,5 などの 1 行 1 キャラ)。

with open('party.csv', 'w', encoding='UTF-8') as f:
    for line in f:
        parts = line.split()
        print(parts[0], 'のHP:', parts[2])
ヒント
  • 1 か所目:このコードは読み込みたいのに、open のモードは何になっている? 本文の「'w' はまっさらに上書き」を見直そう。
  • 2 か所目:split()(引数なし)は何で区切る? カンマで分けるにはどう書く?
  • 3 か所目:勇者,100,5 を分けると [勇者, 100, 5]。HP は何番目の要素?

プレ演習10-4

キャラを 1 体つくってセーブするプログラムを、ゼロから書こう

要件:

  • 名前・HP・レベルをキーボードから入力する
  • save.txt3 行で書き出す
  • 最後に セーブしました と表示する

期待される実行結果:

名前: アリス
HP: 80
レベル: 3
セーブしました
ヒント
  • input で受け取った値は最初から文字列なので、write にそのまま渡せる(str() は不要)。
  • 改行 \n は自分で付けるのを忘れずに。
  • with で開けば close は不要。本文のセーブの形を見直そう。

もっと知りたい人へ(発展)

  • 追記モード 'a'open(..., 'a') は上書きせず、末尾に追加する(戦績ログをためる等に便利)。
  • 一気に全部読むf.read() はファイル全体を 1 つの文字列に、f.readlines() は 1 行ずつのリストにする。
  • 書き込みと読み込みを同時にwith を入れ子にすると、入力ファイルと出力ファイルを両方開ける(フィルタ処理など)。

授業内演習

data2.txt を読み込み、各行ごとの合計を計算して画面に出力するプログラムを作成しなさい。ただし 1 行に書かれている数値は 3 つだが、何行あるかはわからないものとする。

data2.txt は上のリンクからダウンロードし、プログラムと同じフォルダに置くこと。

実行例:

ファイル名を入力してください: data2.txt
合計: 60
合計: 180

確認の観点:

  • 1 行を split() で分けると、要素はいくつになるか
  • 行数が増えても、for line in f: でそのまま対応できるか
  • 存在しないファイル名を入れたら、どんな例外が出るか(前回の FileNotFoundError

基本課題

キーボードから 5 つの実数を入力すると、それら 5 つの数と合計・平均を実行例のように画面とファイルの両方に出力するプログラムを作成しなさい。

実行例:

ファイル名を入力してください>> kihonkadai.txt
1つ目の実数を入力してください>> 2.2
2つ目の実数を入力してください>> 1.8
3つ目の実数を入力してください>> 3.0
4つ目の実数を入力してください>> 2.4
5つ目の実数を入力してください>> 2.8
2.2 1.8 3.0 2.4 2.8
合計: 12.2
平均: 2.44

約束ごと:

  • 実数の入力には float() を使うこと
  • 画面への出力ファイルへの書き出しの両方を行うこと
  • ファイルは with open(..., 'w', encoding='UTF-8') の形で開くこと

応用課題

キーボードから neko.txt を読み込み、実行例のように、検索ワードの個数検索ワードを入力して、各ワードの出現回数を画面とファイルに出力するプログラムを作成しなさい。neko.txt は上のリンクからダウンロードすること。

実行例:

入力ファイル名を入力してください: neko.txt
出力ファイル名を入力してください: kekka.txt
検索するワードの個数を入力してください>> 3
1個目の検索ワードを入力してください>> 猫
2個目の検索ワードを入力してください>> 犬
3個目の検索ワードを入力してください>> 吾輩は
"猫"の出現回数: 263
"犬"の出現回数: 23
"吾輩は"の出現回数: 187

約束ごと:

  • 入力ファイル名と出力ファイル名を、それぞれキーボードから入力すること
  • 出現回数は画面とファイルの両方に出力すること
  • 出力メッセージは実行例のとおりに揃えること("<ワード>"の出現回数: <回数>
  • 外部モジュールを使用しないこと

ヒント:

  • ファイル全体を 1 つの文字列にするには f.read() が使える
  • ある文字列の中に別の文字列が何回出てくるかは、文字列メソッドで数えられる("あいあい".count("あい")2