Pythonで3行直しただけなのに全行が差分になった(Windows)。原因は改行コードCRLFの自動変換だった

運営者

2026年8月17日、WordPressの記事に埋め込んでいる無料の診断ツールのデータを更新しました。
変わるのは3行だけのはずでした。
ところが、公開中の本文と比べると、全69行が違うと出ます。

どの行も、違うのは行の終わりの見えない1文字だけでした。
原因は改行コードです。
改行コードは、行の終わりを示す見えない文字を指します。
Windowsで昔から使われるのは、CRとLFの2文字を続けるCRLFという形です。
LFの1文字だけで行を終える形も、広く使われています。

公開中の本文はCRLFで、手元のPythonが読み書きのたびに改行を変えていました。
読み書きに newline="" を付けると、差し替わるのは3行だけになりました。

3行のつもりが、69行すべて1文字ずつ違った

このツールは、記事の本文にJavaScript(ページの中で動くプログラム)を直接埋め込んでいます。
データの部分は、部下役のAIがPythonのスクリプトで元のデータファイルから作り直す仕組みです。
この日は新しく分かった2件を元のデータに入れ、配列を作り直しました。
公開中の本文は、ブラウザから、変わる行だけを差し替える形で更新しています。

その配列を公開中の本文と比べると、全69行が違うと出ました。
気づいたきっかけは、SHA1での照合です。
SHA1は、ファイルの中身から作る指紋のような文字列で、1文字でも違えば別の値になります。
中身を見比べると、どの行も1文字ずつ違うだけでした。

公開中はCRLF、読み直した手元はLF

公開中の本文に入っているJavaScriptは、改行がCRLFでした。
一方、WindowsのPythonは、ファイルを開くときの指定しだいで改行を変換します。
open(path) で読むと、CRLFはLFに置き換わります。
open(path, "w") で書いたときは、逆にLFがCRLFになる仕組みです。

Pythonの公式ドキュメントにも、この動きが書かれています。
newline を指定せずに読むと、行末の '\r\n' などは「呼び出し元に返される前に '\n' に変換」されます。
書くときは、'\n' がそのOSの既定の行の区切り(os.linesep)に置き換えられます。
Windowsでは、この区切りが '\r\n' です。

手元の生成物は、読み直した時点で行末がLFになっていました。
これを公開中のCRLFと比べたので、全行で行末のCRが1文字ずつ足りない形になったのです。

newline引数を空にして、元の改行のまま扱う

直し方は、読みにも書きにも newline="" を付けることでした。
公式の説明では、'' を渡すと、読むときの行末は「変換されずに」返ってきます。
書くときも、'' なら変換されません。

# 読む:CRLFはCRLFのまま
with open(path, encoding="utf-8", newline="") as f:
    text = f.read()

# 書く:文字列の改行をそのまま書く
with open(path, "w", encoding="utf-8", newline="") as f:
    f.write(text)

部下役のAIの作業メモにも、手元のHTMLと生成物の読み書きには必ず newline="" を付ける、と残しました。

pathlib の Path.write_text() に newline を渡せるのは、Python 3.10からです。
Path.read_text() のほうは3.13からなので、それより古い版では open() で読むのが確実です。
僕のPCのPythonは3.10でした。

ブラウザの側で行を差し替えるときも、考え方は同じです。
本文を \n で区切って行に分けると、CRLFの行は末尾に \r が残ります。
元の行が \r で終わっていたら、新しい行にも \r を足してから入れます。

const eol = oldLine.endsWith("\r") ? "\r" : "";
lines[i] = newLine + eol;

直したあとは、変わった3行だけを差し替えました。
データの部分のSHA1は、手元の生成物と公開中の記事で一致しています。
公開ページでも、実際に入力欄を操作して、新しいデータどおりの結果が出ることを確かめました。

小さなファイルで、同じ動きを確かめた

この記事を書く前の11月10日に、僕のPC(Windows・Python 3.10)で同じ動きを試しました。
CRLFで3行のファイルを作り、普通に読んだときと newline="" で読んだときを比べたものです。

open("live.js", "wb").write(b"a\r\nb\r\nc\r\n")
print(repr(open("live.js", encoding="utf-8").read()))              # 'a\nb\nc\n'
print(repr(open("live.js", encoding="utf-8", newline="").read()))  # 'a\r\nb\r\nc\r\n'

CRLFで5行のファイルでも試しています。
普通に読んで1行だけ直し、元と比べると、5行すべてが違うと出ました。
newline="" で読んで同じ1行を直すと、違うのは1行だけです。

手元のファイルの改行がどちらかは、バイトのまま数えると分かります。

b = open("live.js", "rb").read()
print("CRLF:", b.count(b"\r\n"), "LFだけ:", b.count(b"\n") - b.count(b"\r\n"))

さっきのファイルなら CRLF: 3 LFだけ: 0 と出ます。
両方の数が0でなければ、1つのファイルに2つの形が混ざっています。

Gitの設定を見直しても、ここは変わらない

全行が差分になる話で検索すると、Gitの core.autocrlf の解説がよく出てきます。
Gitの保存庫ではLF、手元の作業フォルダではCRLFにそろえたいときに使う設定です。
僕のこの作業は、Gitを通していませんでした。
改行を変えていたのはPythonの open() だったので、Gitの設定では直らない形だったと見ています。

10月28日の差し替えでは、改行を元に合わせた

10月28日に、このツールを含む3本のデータと注記を、新しく分かった内容に差し替えました。
このときも、ページ全体は送らずに、変わる部分だけを入れています。
改行コードは、差し替える先の本文がCRLFかLFかに合わせました。
保存後の本文は、送った内容と完全に一致しています。

参考

10月28日の保存は、サーバーのWAF(攻撃らしい通信を止める仕組み)を一時的に切ってから行いました。
止められた経緯は、別の記事に書いています。
WordPressの保存が「閲覧できません (Forbidden access)」403で止まる。原因はサーバーのWAFだった

記事URLをコピーしました