先日、PythonでCSVを出力する処理を書いていたのですが、空文字列とNone
をいい感じに出し分けられないかと試行錯誤したので、そのやり方をメモがてら記事にしたいと思います。
どうしたかったか
ちょっとわかりにくいかもしれませんが、次のような出し分けがしたかったのです(「CSVでの表現」の真ん中の部分)。
値 | CSVでの表現 |
---|---|
"" (空文字列) |
1,2,"",4,5 |
None |
1,2,,4,5 |
空文字列の時はダブルクォートだけになって、None
の時は文字通り何もなしの表現にしたかったわけです。
しかし、ごく普通にcsv
モジュールのcsv.writer
を使って生成すると、このような出し分けはできません。
通常、CSVでここまでの使い分けが必要なことはあまりないと思いますが、今回はたまたまそういう要件があったのでした。
読む側が区別して読み込めるのかという話もあるので、頑張って処理するかどうかは要件に合わせて決めるとよいでしょう。
何でもかんでも本記事のように処理したほうがよい、ということではありません。
実現方法
このStack Overflowの回答にズバリな実現方法が書いてありました。
r - Python: write CSV with QUOTE_NONNUMERIC, except for None - Stack Overflow
以下は私が少し修正したコードですが、このような感じで処理すると空文字列とNone
の出し分けができました。
class _EmptyString(int):
def __str__(self):
return ""
EmptyString = _EmptyString()
def replace_none_with_emptystring(row: Iterable[Any]) -> Iterable[Any]:
"""row内の値がNoneだったらEmptyStringに置き換える"""
return [value if value is not None else EmptyString for value in row]
writer = csv.writer(dest, dialect=csv.excel, quoting=csv.QUOTE_NONNUMERIC)
writer.writerow(replace_none_with_emptystring(["空文字列->", "", None, "<-None"]))
"空文字列->","",,"<-None"
空文字列とNone
が出し分けられていますね。
仕組み
正直なところ私はPythonにもcsv
モジュールの実装にも詳しくないので、実際のところはよくわからないのですが、おそらく「int
を継承して__str__
で空文字列を返すクラス」を使うことで、「数値型なのでクォートしない」かつ「文字列表現は空文字列」という値を作り出しているのかなと想像しました。
以下はおまけです。
通常の処理だとどうなるのか
せっかくなので、普通にcsv
モジュールを使ってCSVを生成するとどうなるかを紹介したいと思います。
パターン1: QUOTE_NONNUMERICなし
まずはQUOTE_NONNUMERIC
を指定せずにやってみます。
このオプションは数値以外の値をクォートするかどうかを制御するオプションで、指定しない場合はクォートされなくなります。
writer = csv.writer(dest, dialect=csv.excel)
writer1.writerow([1, "QUOTE_NONNUMERICなし", "空文字列->", "", None, "<-None"])
1,QUOTE_NONNUMERICなし,空文字列->,,,<-None
空文字列もNone
もa,,b
のように何もない表現になりました。
加えて(当たり前ですが)通常の文字列もダブルクォートがつかなくなりました。
パターン2: QUOTE_NONNUMERICあり
今度はQUOTE_NONNUMERIC
を指定してみます(=数値以外はクォートする)。
一般的にはこのオプションを指定してCSV生成することが多いのではないでしょうか(個人的な想像です)。
writer = csv.writer(dest, dialect=csv.excel, quoting=csv.QUOTE_NONNUMERIC)
writer.writerow([2, "QUOTE_NONNUMERICあり", "空文字列->", "", None, "<-None"])
2,"QUOTE_NONNUMERICあり","空文字列->","","","<-None"
空文字列は期待通り""
になりましたが、Noneも""
になってしまいました。
全部入りのサンプルコード
# 空文字列とNoneを区別するCSV出力のサンプル
import csv
import sys
from typing import IO, Any, Iterable
# See: https://stackoverflow.com/questions/62233980/python-write-csv-with-quote-nonnumeric-except-for-none
class _EmptyString(int):
def __str__(self):
return ""
EmptyString = _EmptyString()
def replace_none_with_emptystring(row: Iterable[Any]) -> Iterable[Any]:
"""row内の値がNoneだったらEmptyStringに置き換える"""
return [value if value is not None else EmptyString for value in row]
def write_csv(dest: IO[str]):
"""何かのCSVを書き出す処理"""
# csv.QUOTE_NONNUMERICなしの場合
writer1 = csv.writer(dest, dialect=csv.excel)
writer1.writerow([1, "QUOTE_NONNUMERICなし", "空文字列->", "", None, "<-None"])
# => 1,QUOTE_NONNUMERICなし,空文字列->,,,<-None
# csv.QUOTE_NONNUMERICありの場合
writer2 = csv.writer(dest, dialect=csv.excel, quoting=csv.QUOTE_NONNUMERIC)
writer2.writerow([2, "QUOTE_NONNUMERICあり", "空文字列->", "", None, "<-None"])
# => 2,"QUOTE_NONNUMERICあり","空文字列->","","","<-None"
# かつ、EmptyString対応の場合
writer2.writerow(
replace_none_with_emptystring([3, "None対策", "空文字列->", "", None, "<-None"])
)
# => 3,"対策","空文字列->","",,"<-None"
def main(_argv: list[str]) -> int:
write_csv(sys.stdout)
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv))
出力結果
1,QUOTE_NONNUMERICなし,空文字列->,,,<-None
2,"QUOTE_NONNUMERICあり","空文字列->","","","<-None"
3,"None対策","空文字列->","",,"<-None"