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かに合わせました。
保存後の本文は、送った内容と完全に一致しています。
参考
- 運営記録 2026-08-17:データ更新で全69行の差分・SHA1で発覚・
newline=""で修正 - 運営記録 2026-10-28:改行コードを元に合わせて差し替え
- 運営記録 2026-11-10:この記事のための小さな再現
- Python 組み込み関数 open() の newline(日本語版、2026年11月10日確認):https://docs.python.org/ja/3/library/functions.html#open
- os.linesep(日本語版、2026年11月10日確認):https://docs.python.org/ja/3/library/os.html#os.linesep
- pathlib の read_text と write_text(2026年11月10日確認):https://docs.python.org/3/library/pathlib.html
- git-config の core.autocrlf(2026年11月10日確認):https://git-scm.com/docs/git-config
10月28日の保存は、サーバーのWAF(攻撃らしい通信を止める仕組み)を一時的に切ってから行いました。
止められた経緯は、別の記事に書いています。
WordPressの保存が「閲覧できません (Forbidden access)」403で止まる。原因はサーバーのWAFだった