PythonでCSVファイルを処理する 1

ある区間ごとの平均を求めたいという要求に対してPythonで実装をします。

公式の情報 (そのまま)

CSV ファイルの読み書き
CSV File Reading and Writing

csv ファイルから読み込まれた各行は、文字列のリストとして返されます。

QUOTE_NONNUMERIC フォーマットオプションが指定された場合を除き、データ型の変換が自動的に行われることはありません (このオプションが指定された場合、クォートされていないフィールドは浮動小数点数に変換されます)。

Each row read from the csv file is returned as a list of strings.

No automatic data type conversion is performed unless the QUOTE_NONNUMERIC format option is specified (in which case unquoted fields are transformed into floats).

上記の下部に記載の最も簡単な例 (そのまま)

sample.py

import csv
with open('some.csv', newline='') as f:
    reader = csv.reader(f)
    for row in reader:
        print(row)

とりあえず実現していくための説明

import csv

import csv as c
とするとc.reader()となります。

公式の説明も、importing (csv) moduleのような自己言及な感じになってしまっています。
初心者の人でも、なんとなくでも、importは間違いなく扱えると思うので、詳細は割愛します。

(moduleやpackageの説明は情報が多量になります。)

with open(‘some.csv’, newline=”) as f:

PEP 343 — The “with” Statement

初心者の人は、withを「おまじない」と捉えることが難しいようです。

上記のAbstractに結論が書いてあります。

まず、ファイルをオープンして、必要な処理が完了したら、クローズします。
ファイルをオープンしているときに、ハードウェアの故障でもなんでもいいですが、問題が発生したとします。ハードウェアがエラーを返した、または無応答によるタイムアウトが発生したとします。OSのbugのせいかもしれません。
例外が発生した、と言います。例外を捕まえて、適切な処理をさせることを例外処理と言います。
open()をつかうときに、withを使うと、必ず最後にclose()の処理がされます、ということです。
(突然、PCの電源を切ればclose()処理は起きません。)

一瞬だったり、長くても数分で完了するスクリプトを書く人がほとんどだと思いますので、withのおかげで助かったみたいな経験のある人はまず居ないと思います。
このくらいの短いスクリプトで入力ファイル1つで出力ファイルも無い場合は、closeしなくても期待の動作になるので、恩恵がなかなか見えません。
ライブラリの用に誰かに提供してどう使われるかわからない場合は、withを徹底した方が良いでしょう。

(例外処理も説明が長くなります。)

今居るフォルダーの中のsome.csvをopenして、スクリプト内でfという名前で扱います。

reader = csv.reader(f)

csvクラスライブラリのreader()関数にsome.csvを渡します。

少し突っ込んだ説明をしておくと、some.csvはストレージのどこかに保存されていて、fはファイルの先頭を指し示しています。そうでなければ成立しないですよね。スクリプトは、ファイルの内容をメモリに持ってきます。

for row in reader:

readerは、上の説明にあるように文字列のListです。Pythonのfor文は、inの右側のListを展開して、forの右側の変数に入れます。

print(row)

rowをprintします。

リスト内包表記 / List Comprehensions (そのまま)

5. データ構造
5. Data Structures

リスト内包表記はリストを生成する簡潔な手段を提供しています。主な利用場面は、あるシーケンスや iterable (イテレート可能オブジェクト) のそれぞれの要素に対してある操作を行った結果を要素にしたリストを作ったり、ある条件を満たす要素だけからなる部分シーケンスを作成することです。

例えば、次のような平方のリストを作りたいとします:


>>>
>>> squares = []
>>> for x in range(10):
...     squares.append(x**2)
...
>>> squares
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

これはループが終了した後にも存在する x という名前の変数を作る (または上書きする) ことに注意してください。以下のようにして平方のリストをいかなる副作用もなく計算することができます:


squares = list(map(lambda x: x**2, range(10)))

もしくは、以下でも同じです:


squares = [x**2 for x in range(10)]

これはより簡潔で読みやすいです。

5.1.3. List Comprehensions

List comprehensions provide a concise way to create lists.

For example, assume we want to create a list of squares, like:


>>>
>>> squares = []
>>> for x in range(10):
...     squares.append(x**2)
...
>>> squares
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

Note that this creates (or overwrites) a variable named x that still exists after the loop completes. We can calculate the list of squares without any side effects using:


squares = list(map(lambda x: x**2, range(10)))

or, equivalently:


squares = [x**2 for x in range(10)]

which is more concise and readable.

まとめ

下記の書き方に慣れましょう。


squares = [x**2 for x in range(10)]

日本語の検索結果のみだと手詰まりとなる場合も多いので、こういう整備されている場合は英語も読むようにしておくと、そのうち英語で検索するようになります。

newline (そのまま)

ビルトイン / open / newline
Built-in Functions / open / newline

” の場合、ユニバーサル改行モードは有効になりますが、行末は変換されずに呼び出し元に返されます。

If it is ”, universal newlines mode is enabled, but line endings are returned to the caller untranslated.

まとめ

newline=”としておいた方が良いです。

Windowsでしか開発しない、UNIXでしか開発しない、GitHubを介した開発はしない、だとnewlineの指定なしのNoneで問題が出ることはないと思います。
なので、この指定はなんやねん、と思われてしまうと思います。

まずは下記です。

CR : ‘\r’
LF : ‘\n’

UNIXは、改行にLFを使います。
Windowsは、改行にCR LFを使います。
Macは、OS9まで改行にCRを使っていました。

(モジュールを購入して仕様を確認すると文字列の終端を示すためにCRが使われている場合もあります。)

上記の状況ですので、今はLFを見つけたら改行と思って問題が出ることは極めて稀だと思いますが、「上手くいくはず」と思い込んでいると、こんなところで何日も取られてしまうので今でも改行の扱いについては頭の片隅から完全に削除してはいけません。

入力

よくあるやつです。

tmp.csv


0,0.1
10,0.2
20,0.3
30,0.4
40,0.5
50,0.6
60,0.7
70,0.8
80,0.9
90,1.0
100,1.1
110,1.2
120,1.3
130,1.4
140,1.5
150,1.6
160,1.7
170,1.8
180,1.9
190,2.0
200,2.1

少し書き換える

ファイル名をソースコードに埋め込むのは良くないので、実行時引数として取得するように変更します。

001.py

import sys
import csv

argv = sys.argv
if 1 < len(argv):
    print( 'argv[1] :', argv[1], flush=True )

with open( argv[1], newline='' ) as f:
    reader = csv.reader( f )
    for row in reader:
        print( "row :", row )

使い方。


> python3 001.py tmp.csv

実行結果です。


argv[1] : tmp.csv
row : ['0', '0.1']
row : ['10', '0.2']
row : ['20', '0.3']
row : ['30', '0.4']
row : ['40', '0.5']
row : ['50', '0.6']
row : ['60', '0.7']
row : ['70', '0.8']
row : ['80', '0.9']
row : ['90', '1.0']
row : ['100', '1.1']
row : ['110', '1.2']
row : ['120', '1.3']
row : ['130', '1.4']
row : ['140', '1.5']
row : ['150', '1.6']
row : ['160', '1.7']
row : ['170', '1.8']
row : ['180', '1.9']
row : ['190', '2.0']
row : ['200', '2.1']
row : []

もう少し書き換える

rowは要素数2のListでした。これをfor文を使って、Listの中で展開させて、ListのListを作ります。
これで、indexが3つくらいあれば、色々な入力に対してほとんどやりたいことができるようになると思います。

vは、valueでも、vectorでも、どっちの解釈でもいいです。

002.py

import sys
import csv

argv = sys.argv
if 1 < len(argv):
    print( 'argv[1] :', argv[1], flush=True )

with open( argv[1], newline='' ) as f:
    reader = csv.reader( f )
    v = [ row for row in reader ]
    print( "v :", v )

    value = 0
    counter = 1
    for i in range(0,len(v)):
        if not v[i]:
            print( "v :", v[i] )
            break

        value += float( v[i][1] )

        if 10 <= counter:
            print( "value :", value / 10 )
            value = 0
            counter = 0

        counter += 1

counterを使わないで、int( v[i][0] )に対して剰余演算を行う方法もあります。
勿論、実際には10という値の埋め込みは禁止です。
ソースコードの先頭の方で決めることができるようにしておくと便利です。この辺りは定石なので、徹底しましょう。

使い方。


> python3 002.py tmp.csv

実行結果です。


argv[1] : tmp.csv
v : [['0', '0.1'], ['10', '0.2'], ['20', '0.3'], ['30', '0.4'], ['40', '0.5'], ['50', '0.6'], ['60', '0.7'], ['70', '0.8'], ['80', '0.9'], ['90', '1.0'], ['100', '1.1'], ['110', '1.2'], ['120', '1.3'], ['130', '1.4'], ['140', '1.5'], ['150', '1.6'], ['160', '1.7'], ['170', '1.8'], ['180', '1.9'], ['190', '2.0'], ['200', '2.1']]
value : 0.55
value : 1.55

完成させる

003.py

import sys
import csv

INTERVAL = 10

argv = sys.argv
# if 1 < len(argv):
    # print( 'argv[1] :', argv[1], flush=True )

with open( argv[1], newline='' ) as f:
    reader = csv.reader( f )
    v = [ row for row in reader ]
    # print( "v :", v )

    value = 0
    counter = 1
    for i in range( 0,len(v) ):
        if not v[i]:
            # print( "v :", v[i] )
            break

        value += float( v[i][1] )

        if INTERVAL <= counter:
            print( v[i][0], ",", v[i][1], ",", value / INTERVAL )
            value = 0
            counter = 0
        else:
            print( v[i][0], ",", v[i][1] )

        counter += 1

使い方。


> python3 003.py tmp.csv | tee out.csv

実行結果です。


0 , 0.1
10 , 0.2
20 , 0.3
30 , 0.4
40 , 0.5
50 , 0.6
60 , 0.7
70 , 0.8
80 , 0.9
90 , 1.0 , 0.55
100 , 1.1
110 , 1.2
120 , 1.3
130 , 1.4
140 , 1.5
150 , 1.6
160 , 1.7
170 , 1.8
180 , 1.9
190 , 2.0 , 1.55
200 , 2.1

よくはないですが、誰でもわかる簡単な実装方法で実装する場合もあります。少しでも規模が大きくなると破綻します。
用意されていなければ、自分でListをCSVの文字列にする関数を作るべきです。v[i]にvalueを割った値をappendした方がわかりやすいでしょう。

総まとめ

順番に説明すると、

  1. データ構造の説明
    • 配列、リスト、スタック、二分木、他、の長所と短所。どう使うべきか。山ほど色々。
  2. (出来ればアルゴリズムの説明)
    • 山ほど色々
  3. 多機能に過ぎるPythonのListの説明
    • 山ほど色々
  4. PythonのCSVの説明
    • これだけでも結構な量

となるでしょうが、CSVに辿りつかないですよね。

なお、上記のif breakはほとんどの場合に意味がありませんが、入力によっては意味があります。

余談

今から仕事でC++始める人がarrayを使って実装して、本番環境のコンパイラではvectorを使うしかなくて全部書き換えていました。
検索する前に、目の前にいる詳しい人達に訊けよ。50過ぎて何やってんだよ。


コメントを残すために、Twitter OAuthを必要としています。ご了承ねがいます。
コメントは、Twitterに影響しません。

Twitter OAuth