Pythonでテキストファイル入力とか

Pythonによるテキストファイル読み込みをまとめておく。

osモジュールとpathlibモジュール

withのopenにファイル名を渡すとファイルを開く

with open('in.csv', 'r') as fi:
    buf = fi.read()
    print(buf)

mode引数(mode='r')のほか、encoding引数(encoding='utf-8')もあるが、UTF-8が当たり前の時代なので気にしない場合も多い(正常にデコードできないとUnicodeDecodeError例外が発生するので適宜キャッチする)。

実際にプログラムを書いていると、ファイルのパスについて複雑な処理をしたくなることがある。例えば下のように書く(unix系を想定)

import os
targetdir = os.path.join('data', 'category1', 'dog')
print(type(targetdir))
# -> <class 'str'>
print(targetdir)
# -> data/category1/dog
os.makedirs(targetdir, exist_ok=True)
print(os.path.isdir(targetdir))
# -> True
filestem_cute_dog = 'cute_dog_info.csv'
filename_cute_dog = os.path.join(targetdir, filestem_cute_dog)
with open(filename_cute_dog, mode='w') as fo:
    fo.write('cute\n')
print(os.path.isfile(filename_cute_dog))
# -> True

パスの操作にはosモジュールを使う方法とpathlibを使う方法がある。どちらか一方が使えばとりあえずはコーディングができる。pathlibの場合は下のように書く。

import pathlib
targetdir = pathlib.Path('data') / 'category1' / 'dog'
print(type(targetdir))
# -> <class 'pathlib.PosixPath'>
print(targetdir)
# -> data/category1/dog
targetdir.mkdir(exist_ok=True, parents=True)
print(targetdir.is_dir())
# -> True
filestem_cute_dog = 'cute_dog_info.csv'
path_cute_dog = targetdir / filestem_cute_dog
with path_cute_dog.open(mode='w') as fo:
    fo.write('cute\n')

osモジュールを用いる場合は、パスを文字列として扱うため、事あるごとにosモジュールの関数を呼び出す必要がある。これはパスのオブジェクトと文字列のオブジェクトの間を行ったり来たりしているような処理に見えて、少し気持ち悪い。pathlibの場合はパスはあくまでパスオブジェクトとして扱うため、いちいちモジュールの関数を呼び出さずに、オブジェクトのメソッドで処理できる。

pathlibとosとで関数名が違ったり細かい挙動が違ったりということがあるので注意が必要である。

ファイルの列挙

入力にディレクトリを指定して処理する場合は多い。 下に示すようなごちゃごちゃとしたフォルダ構成で、パスを順番に処理したいとする。

in1
├── copy_of_in2
│   ├── hoge.txt
│   ├── readme_new.txt
│   └── readme_old.txt
├── in2
│   ├── hoge.txt
│   ├── in5
│   │   └── neko_ha_imasu.txt
│   ├── readme.txt
│   └── readme_new_2.txt
├── in3.csv
└── in3_new
    └── foo.csv

pathlibのパスにはiterdir()というメソッドがあり、ディレクトリ直下のパスを列挙できる。

import pathlib


def do_something(input_dir):
    for input_file in input_dir.iterdir():
        print(input_file)
        # do something here


do_something(pathlib.Path('in1'))
# in1/copy_of_in2
# in1/in2
# in1/in3.csv
# in1/in3_new

イテレータを好む文脈ではこの構文が使いやすいが、そうでない場合はパスを一括で取得した方が使い勝手が良い。

def do_something(input_dir):
    path_list = list(input_dir.iterdir())
    for input_file in path_list:
        print(input_file)
        # do something here

再帰的にすべてのファイルを処理したければglobを用いる。globは、globモジュールの中のglob(glob.glob)とpathlib.Pathオブジェクトのメンバ関数としてのglob(pathlibのglob)があり、これらの挙動は若干異なる。なおosモジュールの方ではos.walk()というものもある。

path_list = list(input_dir.glob('**/*'))

ファイルだけ列挙したい場合などは次のように適宜絞る。

path_list = [_p for _p in input_dir.glob('**/*') if _p.is_file()]

もう少し実用的な例として、tqdmと組み合わせる書き方に使用することができる。先にファイルをリスト化しておくことでファイル総数に対する進捗率が確認できるようになる。

import pathlib
import tqdm


def do_something(input_dir):
    path_list = list(input_dir.glob('**/*'))
    pbar_file_list = tqdm.tqdm(
        path_list, desc='Progress of SOMETHING', unit='file')
    for input_file in pbar_file_list:
        ...
        # do something here


do_something(pathlib.Path('in1'))

ファイルの読み込み

pathlibで1行ずつ読み込む場合は例えば

import pathlib
input_file = pathlib.Path('input.txt')
for line in input_file.open('r'):
    print(line, end='')

lineには行末の'\n'が含まれるので、printで改行が二重にならないようにend引数を指定している。 改行を消したい場合、strip()を使う場合もあるが、改行以外の文字も消えてしまうため、単に末尾要素を削除する方が良いだろう。

line = line[:-1]

read_text()を用いるとファイルを丸ごと読み込むことができる。

import pathlib
input_file = pathlib.Path('input.txt')
content = input_file.read_text()
print(content)

この読み込みは手軽で使いやすい。例えば改行で区切って空行を無視してリスト化するには

input_file = pathlib.Path('input.txt')
lines = [line for line in input_file.read_text().split('\n') if line != '']
print(lines)

tsvファイルなら

input_file = pathlib.Path('input.txt')
lines = [line.split('\t') for line in input_file.read_text().split('\n')]
print(lines)

読み込みが重くなってくると色々と試す必要が出てくる。 例えばDataFrameのread_csvを用いる。

import pandas as pd
import pathlib
input_file = pathlib.Path('input.csv')
df = pd.read_csv(input_file, header=None, index_col=0)
print(df)

csvをfor文で回して処理する場合はcsvモジュールという選択もある。

import csv
import pathlib

input_file = pathlib.Path('input.csv')

for columns in csv.reader(input_file.open('r')):
    first_col, second_col, *rest = columns
    first_col = int(first_col)
    second_col = float(second_col)
    print(f'{first_col = !r}\t{second_col = !r}')