ファイル読み取り - 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-JIScp932, sjis, s_jis
UTF-8utf8
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

標準モジュール

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()でゼロ埋めして揃えることが可能。

  1. 桁まで揃っているカラムで、文字列型のまま比較する
  2. 桁は揃っていないカラムで、文字列をゼロ埋めして文字列型のまま比較する
  3. 桁は揃っていないカラムで、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)

programming/python/tips/readfile.txt · 最終更新: 2019/01/31 by ikatakos
CC Attribution 4.0 International
Driven by DokuWiki Recent changes RSS feed Valid CSS Valid XHTML 1.0