ファイル読み取り - Python
基本
ファイルを開いて1行ずつ処理
with open(file_path, mode='r', encoding='cp932') as rh: for line in rh: # 1行ずつ何らかの処理 pass
mode
ファイルの開き方「読込/書込/追記」「テキスト/バイナリ」を指定する。'w
'とか'r+b
'とか。(他の指定子もあるっぽいけど、メジャーなところとしては)
区分 | 文字 | 意味 | 備考 |
---|---|---|---|
読み書き | 未指定 | 読込専用。ファイルが存在しないとエラー | いずれか1つのみ指定 |
r | |||
w | 書込専用。既存ファイルは開いた瞬間に上書きされ0byteになる | ||
x | 書込専用。既存ファイルが存在するとエラー | ||
a | 書込専用。既存ファイルが存在すると末尾に追記 | ||
フラグ | + | 読みも書きもできるように開く | |
内容 | 未指定 | テキストモード(str型) | いずれか1つのみ指定 |
t | |||
b | バイナリモード(bytes型) |
encoding
デフォルトの文字エンコードはOSに依る。Windowsならcp932(MicrosoftによるShift-JISの独自拡張)
7.2. codecs — codec レジストリと基底クラス — Python 3.6.1 ドキュメント
エンコード | 指定 |
---|---|
Shift-JIS | cp932, sjis, s_jis |
UTF-8 | utf8 |
UTF-8 BOM付き | utf_8_sig |
といってもファイル内でasciiの英数字記号だけしか使われてないならShiftJISもUTF-8も同じ。
UTF-8(BOM付)のファイルはきちんとutf_8_sig
を指定しないとUnicodeDecodeError
になる。
一方、UTF-8(BOM無し)のファイルをutf_8_sig
で読んでも読める。どちらかわからない場合はsigありで指定しておくとよい。
エンコードの自動判定
chardetというパッケージで自動判定が行えるらしい。
ただし、数点の候補のみから絞れさえすればいいというのであれば、以下のように独自に全通り試した方が、変な誤判定の可能性が無くてよいような気もする。
def get_encoding(path, eachline=False, lookup=('cp932', 'utf_8', 'utf_8_sig')): """ テキストファイルのエンコードを調べる。 :param path: ファイルパス :param eachline: 1行ずつ確かめる(メモリに一度に載りきらないファイルなど。ただし時間かかる) :param lookup: 試すエンコード名候補のiterable :return: eachline=True の時は、エンコード名を返す eachline=False の時は、エンコード名と読み取ったデータを返す """ encoding = None if eachline: for trying_encoding in lookup: try: with open(path, 'r', encoding=trying_encoding) as rh: for line in rh: pass encoding = trying_encoding break except UnicodeDecodeError: continue return encoding else: encoded = None with open(path, 'rb') as rh: data = rh.read() for trying_encoding in lookup: try: encoded = data.decode(trying_encoding) encoding = trying_encoding break except UnicodeDecodeError: continue return encoding, encoded
テキスト形式のバリエーション
CSV
- python標準モジュール'csv'のドキュメント
- pythonでのcsv読み取り方法いろいろ
標準モジュール
CSVと一口に言っても公的にフォーマットが決まっているわけでは無く、バリエーションが様々ある。
- 改行コードはLFかCRLFか
- データを“引用符”で囲うか
- データ内に“,”や改行があって誤読する場合のみ囲う or 常に囲う
- 引用符の中でさらに引用符を使う場合は2つ重ねる
などなど、細かいルールまで気にしなければならない場合は結構面倒。そんな場合は、pythonにはcsvライブラリがあるので、素直にそちらに任せよう。
import csv with open(file_path) as rh: for record in csv.reader(rh): # record に既に分割された値がstr型で入っている pass
pandas
もしくは、データがメモリに載る程度で、読み取った後に統計的なフィルタリングや数値演算を行う場合はpandasがよい。ただし、ヘッダの存在有無指定とか、読み取り後の行列の指定の仕方とか、pandas特有の記述方法がある。
import pandas as pd df = pd.read_csv(file_path) print(df['col1'])
自前実装
そうじゃなくて、対処に困る値もなく、単に“,”でsplitして文字列が得られればいいだけなら、自前でsplit()した方が速い。
with open(file_path) as rh: for line in rh: records = line.strip().split(',') user_id = records[1] # ...
for line in rh
で取得されるline
には、末尾に改行が残ったままとなっている点に注意。そのままsplit
すると最後のカラムの値に改行が付いてしまうので、必要ならstrip
しておく。
沢山あるカラムの最初の方のデータだけ必要なら、maxsplitを指定すると気持ち速くなる。
with open(file_path) as rh: for line in rh: # 5カラム目だけ必要な場合 records = line.split(',', maxsplit=5) # records[0]~[5]まで6つに分割 # records[5]には6カラム目以降のデータがカンマ分割されずに入っている print(records) # => ['col1', 'col2', 'col3', 'col4', 'col5', 'col6,col7,col8,col9,...']
ヘッダなど、1行読み飛ばすにはreadline()
with open(file_path) as rh: rh.readline() # 複数行読み飛ばすならその分だけ繰り返す rh.readline() rh.readline() for line in rh: records = line.strip().split(',') user_id = records[1] # ...
3カラムのcsvを、int型にパースして、1カラム目をキー、2・3カラム目のtupleを値に持つ辞書に変換
with open(file_path) as rh: data = {a: (b, c) for a,b,c in (map(int, line.split(',')) for line in rh)} # 無理に1行で書くと読みにくいので良い子は''for''で回そう
パフォーマンス
数値比較
あるカラムの数値が、基準値より大きいレコードのみ抽出して何かする、などという処理。
この「ある値より大きいかどうか」の判定速度が、データ型でどこまで違うか。
(※そのカラムには全レコード、数値しか入っていないことが保証されているとする)
(SQLを組めば速いのはわかるが、そのためのDBを構築するほどでもない場合)
split()した時点ではstr型で、int型で比較するにはパースする必要があるが、この処理が無視できる軽さではない。
桁が揃っていれば、str型のまま比較しても大小関係は保たれる。
桁が違っていてもzfill()
でゼロ埋めして揃えることが可能。
- 桁まで揃っているカラムで、文字列型のまま比較する
- 桁は揃っていないカラムで、文字列をゼロ埋めして文字列型のまま比較する
- 桁は揃っていないカラムで、int型にパースして比較する
結果、100万回の比較で、以下の通り。
手法 | 時間(秒) |
---|---|
1.文字列のまま | 0.078 |
2.zfill() | 0.218 |
3.int() | 0.281 |
パースすると遅くなる。ゼロ埋めしてもなお文字列型のままの方が4/3ほど速い。
日付比較
日付を表す値('2019/01/01 09:00:00
' など)も、datetimeにパースする処理が重いので文字列比較の方がよい。
桁が揃っているなら、ゼロ埋めの必要も無く文字列でも大小関係は保たれる。
ただし、1桁の月日時などがゼロ埋めされない様式('2019/1/1 9:00:00
' など)の場合は注意。
エクセルで保存したcsvは、とくに指定無い限り、ゼロ埋めされない様式となるので厄介。
この場合でも、各要素を正規表現で抽出後、個別にゼロ埋めして文字列のまま処理した方が明確に速くなる。というか日付のパースはかなり遅い。
手法 | 時間(秒) |
---|---|
1.文字列のまま | 0.078 |
2.年月日をzfill() | 0.187 |
3.datetime.strptime() | 11.388 |
import re def f(d): m = re.fullmatch('(\d{1,4})/(\d{1,2})/(\d{1,2}) (\d{1,2}):(\d{1,2}):(\d{1,2})', d) return m.group(1).zfill(4) + '/' + m.group(2).zfill(2) + '/' + m.group(3).zfill(2) + ' ' \ + m.group(4).zfill(2) + ':' + m.group(5).zfill(2) + ':' + m.group(6).zfill(2)