LoginSignup
5
4

More than 1 year has passed since last update.

PythonでCSVを書き出すときに空文字列とNoneを出し分ける

Last updated at Posted at 2023-04-21

先日、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を指定せずにやってみます。
このオプションは数値以外の値をクォートするかどうかを制御するオプションで、指定しない場合はクォートされなくなります。

QUOTE_NONNUMERICなし
writer = csv.writer(dest, dialect=csv.excel)
writer1.writerow([1, "QUOTE_NONNUMERICなし", "空文字列->", "", None, "<-None"])
結果
1,QUOTE_NONNUMERICなし,空文字列->,,,<-None

空文字列もNonea,,bのように何もない表現になりました。
加えて(当たり前ですが)通常の文字列もダブルクォートがつかなくなりました。

パターン2: QUOTE_NONNUMERICあり

今度はQUOTE_NONNUMERICを指定してみます(=数値以外はクォートする)。
一般的にはこのオプションを指定してCSV生成することが多いのではないでしょうか(個人的な想像です)。

QUOTE_NONNUMERICあり
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"
5
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
4