CSVの読み書きで意図せず空行が増えたり、行末に謎の\rが残った経験はありませんか。
原因の多くは改行コードの扱いにあります。
本記事では、Python標準のcsv
モジュールで必ず指定すべきnewline=''
について、初学者向けに丁寧に解説し、正しい使い方と再現手順、検証のコツを実例付きでまとめます。
PythonのCSVでnewline=”が必要な理由
余計な空行の原因は改行コードの二重変換
CSVを書き込むとき、Pythonのテキストファイルはデフォルトではnewline=None
です。
この場合、テキスト層が改行をOSの既定形式に変換します。
さらにcsv.writer
は自前でレコード区切り(既定は'\r\n'
)を付与します。
- Windows(既定の改行はCRLF)では、
csv.writer
が出力する'\r\n'
に対して、テキスト層の変換が重なり改行が二重(例:'\r\r\n'
)になります。 - 結果として、読み返すと空行が増える、もしくは行末に余分な\rが残るといった現象が生じます。
解決策は、ファイルを開くときにnewline=''
を渡し、テキスト層での改行変換を無効化することです。
csvモジュールは改行を内部処理するためnewline=”で渡す
csv
モジュールはレコード区切りの制御を自分で行う設計です。
open(..., newline='')
で開くと、生の改行文字がそのままcsv.reader
やcsv.writer
に渡され、混在する改行(CRLFやLF)や、引用符内に含まれる改行を正しく扱えます。
逆にnewline=None
のままだと、テキスト層が改行を勝手に変換するため、csv
の期待とずれて不具合が起こりやすくなります。
よくある症状(空行が増える・行末に\rが残る)
- 書き込み後に読み返すと、行間に空行が挿入されている。
- 行末やセル末尾に謎の
\r
が残ってしまう。 - Excelで開いた際に余計な空の行が見える。
- 引用符内の改行を含むCSVで、行の分割が崩れる。
これらは多くの場合newline=''
を指定していないことが原因です。
書き込み時の正しい開き方とコツ
open(…, newline=”)でダブル改行を防ぐ
最重要ポイントはファイルオープン時にnewline=''
を指定することです。
これでテキスト層の改行変換を止め、csv.writer
の意図通りに出力されます。
# write_csv_correct.py
# 正しい書き込み例: newline='' を必ず指定する
import csv
from pathlib import Path
p = Path("correct.csv")
rows = [
["id", "name", "note"],
[1, "Alice", "Hello"],
[2, "Bob", "Line1\nLine2"], # セル内の改行(引用符で適切にエスケープされる)
]
# 最重要: newline='' を指定する
with p.open("w", encoding="utf-8", newline="") as f:
writer = csv.writer(f) # lineterminator は既定で '\r\n'
writer.writerows(rows)
# バイト列を確認して、余分な '\r' が入っていないことを見る
raw = p.read_bytes()
print(raw)
b'id,name,note\r\n1,Alice,Hello\r\n2,Bob,"Line1\nLine2"\r\n'
上記のように、レコード区切りは'\r\n'
、セル内の改行は'\n'
と、期待どおりに整います。
csv.writerでの改行コード制御(Windowsでも安心)
既定のlineterminator
は'\r\n'
です。
LinuxやmacOS向けにレコード区切りをLFにしたいなら、lineterminator='\n'
を明示します。
このときでもnewline=''
は必須です。
# write_csv_lf.py
# レコード区切りを LF にしたい場合: lineterminator='\n' を使う
import csv
from pathlib import Path
p = Path("correct_lf.csv")
rows = [
["id", "os", "remark"],
[1, "Linux", "LF only"],
[2, "macOS", "LF only"],
]
with p.open("w", encoding="utf-8", newline="") as f:
# レコード区切りのみ LF に変更する
writer = csv.writer(f, lineterminator="\n")
writer.writerows(rows)
print(p.read_bytes())
b'id,os,remark\n1,Linux,LF only\n2,macOS,LF only\n'
Windows上であっても、この指定により正確にLFで出力されます。
追記モード(a)でもnewline=”は必須
追記モードは既存ファイルの末尾に足していくため、ここでnewline=''
を忘れると空行が混入しがちです。
常に明示しましょう。
# append_csv.py
# 追記時も newline='' を忘れない
import csv
from pathlib import Path
p = Path("append.csv")
# 初回作成
with p.open("w", encoding="utf-8", newline="") as f:
writer = csv.writer(f)
writer.writerow(["id", "value"])
# 追記1
with p.open("a", encoding="utf-8", newline="") as f:
writer = csv.writer(f)
writer.writerow([1, "first"])
# 追記2
with p.open("a", encoding="utf-8", newline="") as f:
writer = csv.writer(f)
writer.writerow([2, "second"])
print(p.read_text(encoding="utf-8"))
id,value
1,first
2,second
もし上記の追記でnewline=''
を省くと、環境によっては各追記の間に空行が入ります。
読み込み時の正しい開き方
open(…, newline=”)で改行をそのまま渡す
読み込み時も必ずnewline=''
を付けます。
これにより、改行コードを変換せず生のままcsv.reader
へ渡せるため、混在する改行や、引用符内の改行を適切に扱えます。
# read_csv_correct.py
# 正しい読み込み例: newline='' を必ず指定する
import csv
from pathlib import Path
# 混在する改行とセル内改行を含むCSVを準備(テスト用)
p = Path("mixed_newlines.csv")
p.write_bytes(
b"name,comment\r\n"
b"Alice,Hello\n"
b'Bob,"Line1\r\nLine2"\n'
b"Carol,No issue\r\n"
)
# newline='' を指定して読み込む
with p.open("r", encoding="utf-8", newline="") as f:
reader = csv.reader(f)
for row in reader:
print(row)
['name', 'comment']
['Alice', 'Hello']
['Bob', 'Line1\nLine2']
['Carol', 'No issue']
引用符内の改行も崩れず、混在するCRLFとLFでも安定して読み込めます。
csv.readerで混在した改行(CRLF/LF)を無難に扱う
現実のCSVは、ツールやOSの違いで改行が混在していることが珍しくありません。
newline=''
で開いてcsv.reader
に丸ごと渡せば、レコードの境界をcsvモジュールが一貫して判定します。
自前でsplitlines()
や.read().split('\n')
などで分割すると、引用符内の改行を誤って行境界として扱う危険があるため避けましょう。
# read_csv_dont_split.py
# 悪い例: 行分割を自前でやると、引用符内改行で壊れることがある
import csv
from pathlib import Path
p = Path("bad_split.csv")
p.write_bytes(b'name,comment\r\nBob,"A\r\nB"\r\n')
# 悪い: splitlines() で分割してしまう
lines = p.read_text(encoding="utf-8").splitlines()
print("lines:", lines)
try:
# これは2行に分割された文字列列を reader に渡すため、引用符内改行が行境界と衝突しうる
reader = csv.reader(lines)
print(list(reader))
except Exception as e:
print("パースエラー:", e)
# 良い: newline='' で開いて reader に生データを渡す
with p.open("r", encoding="utf-8", newline="") as f:
reader2 = csv.reader(f)
print("正しい読み:", list(reader2))
lines: ['name,comment', 'Bob,"A', 'B"']
パースエラー: new-line character seen in unquoted field - do you need to open the file in universal-newline mode?
正しい読み: [['name', 'comment'], ['Bob', 'A\nB']]
行分割はcsvに任せるのが鉄則です。
トラブルシューティングとチェック方法
newlineを省いたときの挙動を簡単に再現して確認
Windowsで特に再現しやすい「空行増殖」を、newline
有無で比較してみます。
# reproduce_double_newline.py
# Windowsでの「空行が増える」現象を簡単に比較するスクリプト
# (他OSでも確認できることがあります)
import csv
from pathlib import Path
import sys
def write_csv(path: Path, use_newline_empty: bool):
kwargs = {"encoding": "utf-8"}
if use_newline_empty:
kwargs["newline"] = "" # 正しい
else:
kwargs["newline"] = None # 省略時の既定(ダメな例)
with path.open("w", **kwargs) as f:
w = csv.writer(f) # 既定 lineterminator は '\r\n'
w.writerow(["id", "v"])
w.writerow([1, "x"])
w.writerow([2, "y"])
def show_effect(path: Path):
print("-----", path.name, "-----")
raw = path.read_bytes()
print("raw bytes:", raw) # 改行が \r\r\n になっていないかを観察
# テキストとして読みだした際の行分割結果
txt = path.read_text(encoding="utf-8")
print("text splitlines:", txt.splitlines())
p_bad = Path("bad.csv")
p_ok = Path("ok.csv")
write_csv(p_bad, use_newline_empty=False)
write_csv(p_ok, use_newline_empty=True)
show_effect(p_bad)
show_effect(p_ok)
----- bad.csv -----
raw bytes: b'id,v\r\r\n1,x\r\r\n2,y\r\r\n'
text splitlines: ['id,v', '', '1,x', '', '2,y', '']
----- ok.csv -----
raw bytes: b'id,v\r\n1,x\r\n2,y\r\n'
text splitlines: ['id,v', '1,x', '2,y']
bad.csvでは\r\r\nになり、空行が増殖していることが確認できます。
期待する行数と実際の行数を比較して検証
自動テストやチェックでは、行数を検証すると早期に異常に気づけます。
# validate_row_count.py
# 書き込んだ行数と読み戻した行数を比較する簡易検証
import csv
from pathlib import Path
p = Path("validate.csv")
rows = [
["id", "name"],
[1, "Alice"],
[2, "Bob"],
]
# 正しい書き込み
with p.open("w", encoding="utf-8", newline="") as f:
csv.writer(f).writerows(rows)
# 読み戻して行数を数える
with p.open("r", encoding="utf-8", newline="") as f:
reader = csv.reader(f)
loaded = list(reader)
expected, actual = len(rows), len(loaded)
print(f"expected={expected}, actual={actual}")
print("loaded:", loaded)
expected=3, actual=3
loaded: [['id', 'name'], ['1', 'Alice'], ['2', 'Bob']]
差分がある場合は、まずnewline
の指定を疑うのが近道です。
環境差(Windows/Mac/Linux)と改行コード(CRLF/LF)の注意点
OSとツールによって改行コードは異なります。
知っておくと原因切り分けが容易になります。
- Windows: 既定は
CRLF
('\r\n'
)。最も二重変換の影響を受けやすい。 - Linux / 現行のmacOS: 既定は
LF
('\n'
)。 - Excelや一部ETLツール: CRLFで保存することが多い。
- Gitの設定(
core.autocrlf
やeol
): テキストの改行を自動変換する場合がある。
環境ごとに推奨パターンをまとめます。
シナリオ | ファイルオープン | writerのlineterminator | 備考 |
---|---|---|---|
標準的な書き込み | newline='' | 既定('\r\n' ) | まずはこれでOK |
LFで統一したい | newline='' | '\n' | Linux系ツール連携向け |
追記モード | newline='' | 既定または任意 | 追記でもnewline='' 必須 |
読み込み | newline='' | なし | 混在改行やセル内改行を安全に扱う |
newline='\n'
やnewline='\r\n'
など部分的な指定は、csvの意図と競合しやすく非推奨です。
常にnewline=''
を使い、改行制御はcsv.writer(… lineterminator=…)
側で行うのが安全です。
まとめ
CSVの入出力で起こりがちな空行の増殖や行末の\rの混入は、多くがnewline=''
未指定に起因します。
ポイントは次のとおりです。
- ファイルは必ず
open(..., newline='')
で開く
(追記モードでも必ず指定する) - 書き込み時の改行コードは
csv.writer
のlineterminator
で制御する - 読み込み時も
newline=''
を徹底する
(行分割は自前でせずcsv.reader
に任せる)
こうすることで、Windows/Mac/Linuxの環境差やツール間の互換性問題に強い、堅牢なCSV処理が実現できます。