nikkie-ftnextの日記

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

Pythonのcsvモジュールのドキュメントにあるopen(..., newline="")のススメ、やっと意味分かった...(改行文字を変換するか否かの指定)

はじめに

映画『聲の形』を久しぶりに観ました、nikkieです。

Pythoncsvモジュールのドキュメントで、長いことよく分からなかったのですが、ようやく意味が取れた箇所があります。
その点について共有します。

目次

ずっと分からなかった、openのnewline引数への言及

組み込み関数opennewline引数について、以下のように述べられています。
https://docs.python.org/ja/3/library/csv.html#id3 の箇所です。

csv モジュールは独自 (universal) の改行処理を行うため、newline='' を指定することは常に安全です。

私とPythonとの出会いはPyNyumonまで遡ります1
そのテキストの中でcsvモジュールも触りました。
pynyumon/2_scraping.md at 9d3a9cfc33b78043ea79bd00d4507eb1abea7402 · pynyumon/pynyumon · GitHub

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

そのときにドキュメントも見たのですが、「csvモジュールのドキュメントでオススメされているnewline引数の指定がないのはなぜなんだろう」と引っかかりました。
これが長いこと未解決だったんですよね(理解するのに必要な知識が揃っていなかった感じです)

勘違いしていた:指定の有無でCSVファイルのフィールドに改行を含められる/られないというわけではない

フィールドに改行文字を含んだCSVファイルを用意して読み込んでみましょう2
結論から言うと、newline引数を指定してもしなくてもこのファイルは読み込めます(指定しない場合はデフォルト値のNoneが渡ります)。

商品番号,商品名,価格,在庫数
001,"ノートパソコン
(15.6インチ)","100,000円",10
002,"スマートフォン
(Android)","50,000円",20

※この記事でのPythonのバージョンは 3.10.9 です。

openのnewline引数を指定

>>> import csv
>>> with open("products.csv", newline="") as f:
...   reader = csv.reader(f)
...   for row in reader:
...     print(row)
...
['商品番号', '商品名', '価格', '在庫数']
['001', 'ノートパソコン\n(15.6インチ)', '100,000円', '10']
['002', 'スマートフォン\n(Android)', '50,000円', '20']

openのnewline引数を指定しない

>>> with open("products.csv") as f:
...   reader = csv.reader(f)
...   for row in reader:
...     print(row)
...
['商品番号', '商品名', '価格', '在庫数']
['001', 'ノートパソコン\n(15.6インチ)', '100,000円', '10']
['002', 'スマートフォン\n(Android)', '50,000円', '20']

newline引数を指定してもしなくても、フィールドに改行文字を含むCSVを読み込めます
readerlistに変換して等しいか比較すると、Trueが返ります。

>>> with open("products.csv") as f:
...   rows1 = list(csv.reader(f))
...
>>> with open("products.csv", newline="") as f:
...   rows2 = list(csv.reader(f))
...
>>> rows1 == rows2
True

理解のポイント:openしたファイル中の改行文字の扱い

組み込み関数openのドキュメント(読み込みについて)

https://docs.python.org/ja/3/library/functions.html#open

newline はストリームから受け取った改行文字をどのようにパースするかを決定します。 None, '', '\n', '\r', または '\r\n' のいずれかを指定できます。

読み込みの挙動の説明に目を通します。

ストリームからの入力の読み込み時、newline が None の場合、ユニバーサル改行モードが有効になります。

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

ユニバーサル改行モードとはなにか、用語集にヒントがあります。

用語集 「universal newlines」

訳すなら「ユニバーサル改行」でしょうか

https://docs.python.org/ja/3/glossary.html#term-universal-newlines

(略)以下のすべてを行末と認識します: Unix の行末規定 '\n'、Windows の規定 '\r\n'、古い Macintosh の規定 '\r'。

行末の原語は「ending a line」でした。

脱線:Wikipedia 「改行コード」

https://ja.wikipedia.org/wiki/%E6%94%B9%E8%A1%8C%E3%82%B3%E3%83%BC%E3%83%89

タイプライターから来ているんですよね

  • '\r'はCR(キャリッジリターン)
    • 紙を固定して移動する装置(キャリッジ)を元の位置に戻す(リターン、つまり紙の左端に印字装置が来る)こと

  • '\n'はLF(ラインフィード)
    • 紙を必要な行(ライン)だけ上に送る(フィード、つまり下の行に印字装置が来る)こと

用語集を補足すると

  • Unix の行末規定 '\n'(LF)
  • Windows の規定 '\r\n'(CRLF)
  • 古い Macintosh の規定 '\r'(CR)

再びのopen関数のドキュメント(読み込みについて)

ストリームからの入力の読み込み時、newline が None の場合、ユニバーサル改行モードが有効になります。入力中の行は '\n', '\r', または '\r\n' で終わり、呼び出し元に返される前に '\n' に変換されます。

newline引数がデフォルト値のNoneのとき、ユニバーサル改行が有効で、3種類の改行は'\n'に変換されるという記載です。

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

newline引数に空文字列を渡すとき、ユニバーサル改行が有効ですが、改行は変換されないと言っているんですね。

ちょっと例で確認してみましょう。

openのnewline引数の指定の有無で、読み込んだCSVファイルの改行文字の扱いの違いを見る例

フィールドに改行文字を含むCSV、3種類の改行文字で用意します。

% python csv_newline_example.py
newline引数を指定しない
['id', 'text', 'ratio']
['001', 'hogehoge', '0.8']
['002', 'fuga\npiyo', '0.6']
['003', 'foo\nbar', '0.7']
['004', 'spam\nham', '0.3']

newline引数に空文字列を指定
['id', 'text', 'ratio']
['001', 'hogehoge', '0.8']
['002', 'fuga\npiyo', '0.6']
['003', 'foo\rbar', '0.7']
['004', 'spam\r\nham', '0.3']

見てください、この違い!
open("example.csv")newline引数を指定しない(つまりデフォルト値のNoneが渡る)場合は、どの改行文字も'\n'(LF)に変換されています!
それに対してopen("example.csv", newline="")newline引数に空文字列を指定すると、改行文字は変換されていません。元のままです!

openのnewline引数を空文字で指定するか否かの違いは、CSVのフィールドに含まれる改行文字が変換されるかどうかということだったのです。

書き込みの場合(三度openのドキュメント)

書き込みの場合も確認しましょう。

ストリームへの出力の書き込み時、newline が None の場合、全ての '\n' 文字はシステムのデフォルトの行セパレータ os.linesep に変換されます。
newline が '' または '\n' の場合は変換されません。

  • newline引数がデフォルト値のNoneのとき
    • '\n'はos.linesepに変換される
    • macOSでは'\n'
    • '\n'から'\n'への変換、つまり、変換されないのと同じ
>>> import os
>>> os.linesep
'\n'
  • newline引数が空文字列のとき
    • '\n'は変換されない

つまり、macOSでは、newline引数を指定してもしなくても、書き込み時に'\n'が別の文字に変換されることはありません。
よって、結果として、書き込み結果は同じになります。

先ほどのスクリプトで書き込む実装箇所を書き換えて実行しても

-with open("example.csv", "w") as f:
+with open("example.csv", "w", newline="") as f:
    writer = csv.writer(f)
    writer.writerows(rows)

csv.readerの要素(行)の出力結果は変わりませんし、csv.writerが書き込むファイルも差分はありません。

% python csv_newline_example.py
newline引数を指定しない
['id', 'text', 'ratio']
['001', 'hogehoge', '0.8']
['002', 'fuga\npiyo', '0.6']
['003', 'foo\nbar', '0.7']
['004', 'spam\nham', '0.3']

newline引数に空文字列を指定
['id', 'text', 'ratio']
['001', 'hogehoge', '0.8']
['002', 'fuga\npiyo', '0.6']
['003', 'foo\rbar', '0.7']
['004', 'spam\r\nham', '0.3']
% diff bkup_example.csv example.csv
% 

終わりに

Pythoncsvモジュールのドキュメントにある、open関数にnewline=""(空文字列)を指定するすゝめ、ようやく意味が分かったというアウトプットでした。

空文字列を指定したとき、読み込んだファイル中の改行文字は変換されません
なので、'\r'(CR)を含んでいた場合はCRのまま読み込めます。
これはCSVのフィールドに含まれる改行文字にも影響します。
この引数を指定しないとCR(厳密にはユニバーサル改行の3種)はLF('\n')に変換されます。

書き込みでは、newline=""はLFを変換するかどうかの指定です。
os.linesepがLFであれば、openのnewline引数を指定してもしなくても書き込む内容は変わりません。

意味が分かったので、今までよりも積極的にnewline=""と指定していけそうです!
ユニバーサル改行についてはPEPもあるみたいですよ〜(積まれました)


  1. 私、csv.readerのこと、何も分かってなかったんだな... 〜Dialectなるものを完全理解〜 - nikkie-ftnextの日記
  2. こういうとき、ChatGPTは本当に頼りになる