nikkie-ftnextの日記

イベントレポートや読書メモを発信

Pythonのcsv.readerでCSVの中のダブルクォートがどう扱われるか理解する第一歩

はじめに

私的最強概念 こころおねえちゃん 🤗🤗🤯🤯🤯
nikkieです。

時として厄介な形式のCSVファイルを扱わなければならないことってありますよね。
csvモジュールを使って挑んでいったのですが、読み込んだCSVでは、ダブルクォートが閉じるまでは区切り文字で分割されないという動きに翻弄されました。
この動きを理解するまでをアウトプットします。

目次

復習:Dialectと書式化パラメタ

csv.readerはDialect(書式化パラメタのグループ)を指定していることを学びました。

csv.readerのデフォルトのDialectはcsv.excelです。
https://docs.python.org/ja/3/library/csv.html#csv.excel

CSVではなくTSVを読み込むときは、csv.readerdelimiter="\t"と指定し、書式化パラメタを上書きします1

標準ライブラリcsvのテスト内容をサンプルとして理解していく

csvモジュールのテストコードからCSV中のダブルクォートの理解が深まりました。
https://github.com/python/cpython/blob/v3.11.3/Lib/test/test_csv.py#L349-L363

テストコードではcsv.readerをデフォルトのcsv.excel Dialectで使い、必要に応じてfmtparamsを使ってパラメタを上書きします2

csv.excel Dialectでは、delimiterがカンマ(",")です。
https://github.com/python/cpython/blob/v3.11.3/Lib/csv.py#L54-L56

以降の対話例は Python 3.10.9 で動作確認しています。

CSV中のダブルクォートはquotecharなるものだった!

ダブルクォートを含んだ行('1,",3,",5')を考えます。
https://github.com/python/cpython/blob/v3.11.3/Lib/test/test_csv.py#L350

>>> import csv
>>> reader = csv.reader(['1,",3,",5'])
>>> list(reader)
[['1', ',3,', '5']]

この行は(カンマを4つ含んでいますが)3つに分割されました。
1の後のカンマ(delimiter)と、5の前のカンマ(delimiter)の2箇所で分割されています。
3の前後のカンマ(delimiter)では分割されていません。

この挙動の理解の鍵は、Dialectのquotecharです。
https://docs.python.org/ja/3/library/csv.html#csv.Dialect.quotechar

delimiter や quotechar といった特殊文字を含むか、改行文字を含むフィールドをクオートする際に用いられる 1 文字からなる文字列です。デフォルトでは '"' です。

csv.excelDialectのquotechar属性の値はダブルクォート('"')です。
https://github.com/python/cpython/blob/v3.11.3/Lib/csv.py#L57

動きとしては、CSVの行でquotechar以降delimiterは(quotecharが閉じるまでは)delimiterとして扱われないわけですね。
これで',3,'とdelimiterを含んだ部分ができた動きを説明できます。

quotechar閉じない場合は、quotechar以降のdelimiterでは分割されません。
quotechar以降が一切分割されないことになります。

>>> reader = csv.reader(['1,",3,,5'])
>>> list(reader)
[['1', ',3,,5']]

quotecharは変えられる

quotecharは書式化パラメタなので、delimiter同様、別の値(文字列)に変えられます
ダブルクォートから@(アットマーク)に変える例です。
ref: https://github.com/python/cpython/blob/v3.11.3/Lib/test/test_csv.py#L362

>>> reader = csv.reader(['1,",@,3,@,5'], quotechar="@")
>>> list(reader)
[['1', '"', ',3,', '5']]

ダブルクォートは今回はquotecharではないので、続くカンマ(delimiter)で分割されました。
@(quotechar)以降のカンマ(delimiter)はdelimiterとして扱われないため、',3,'という分割になっていますね。

ダブルクォートを含んでも区切り文字で分割するには

ダブルクォートを含んだ行('1,",3,",5')がすべてのカンマ(delimiter)で分割される方法をテストコードを参考に探します。
この行であれば 1 / " / 3 / " / 5 と5つに分かれてほしいです。

1️⃣ quotecharにNoneを指定する

quotecharにNoneを指定すると、すべてのdelimiterで分割されます。
https://github.com/python/cpython/blob/v3.11.3/Lib/test/test_csv.py#L351-L352

>>> reader = csv.reader(['1,",3,",5'], quotechar=None)
>>> list(reader)
[['1', '"', '3', '"', '5']]

CSV中にquotecharがないという指定と認識しています。

2️⃣ 代わりにquotingで指定する

テストコードを見ると別のやり方に気づきました。
quotingという引数も指定できるようです。
https://github.com/python/cpython/blob/v3.11.3/Lib/test/test_csv.py#L353-L354

>>> reader = csv.reader(['1,",3,",5'], quoting=csv.QUOTE_NONE)
>>> list(reader)
[['1', '"', '3', '"', '5']]

quotingも書式化パラメタの1つです。
https://docs.python.org/ja/3/library/csv.html#csv.Dialect.quoting

クオートがいつ writer によって生成されるか、また reader によって認識されるかを制御します。

csv.excel Dialectでは、QUOTE_MINIMALに指定されています。
https://github.com/python/cpython/blob/v3.11.3/Lib/csv.py#L61

これはcsvモジュールに定義された定数の1つです。
https://docs.python.org/ja/3/library/csv.html#csv.QUOTE_MINIMAL
writerに対して指示すると説明されていますね。
(readerに対しては指示はないのでしょうか?)

この値を別の定数QUOTE_NONEに変えます。
https://docs.python.org/ja/3/library/csv.html#csv.QUOTE_NONE

reader に対しては、クオート文字の特別扱いをしないように指示します。

ダブルクォートを(quotecharとして)特別扱いしないことで、すべてのカンマ(delimiterで分割されたわけですね!

終わりに

csv.readerでCSVの中のダブルクォートがどう扱われるか、少し深まった理解をアウトプットしました。

  • csv.readerはデフォルトで 、CSVの中のダブルクォートをquotecharとして扱う
    • csv.excel Dialectのquotecharがダブルクォート
  • CSVファイルの行をパースするとき、quotecharが再度quotecharで閉じるまで、その中に含まれるdelimiterでは分割されない
  • 分割されるように動きを変えるには2つのやり方がある
    • quotecharにNoneを指定する
    • quotingにcsv.QUOTE_NONEを指定する

CSVにはRFCがあるということだけ知っています。
この動きはRFCに沿っているんじゃないかと思うので、一度RFCを確認するともっと自信が持てるかもな〜と思いました。


  1. Dialectを"excel-tab"に変えてもよいです
  2. 詳細はテストのヘルパーメソッド_read_testをどうぞ! https://github.com/python/cpython/blob/v3.11.3/Lib/test/test_csv.py#L292-L295