Python に埋めた JavaScript の \n が、ブラウザに届く前に改行へ化ける
Python は通常の文字列の \n を展開する
Python の通常の文字列では、バックスラッシュと n の2文字は、1文字の改行として扱われます。JavaScript のコードを文字列として Python に書くと、この展開がそのまま起きます。
Python 側で数えた値は次のとおりです。通常の文字列 "(t) => t.split('\n')" は長さ19で、中に本物の改行が1文字入ります。raw 文字列にした r"(t) => t.split('\n')" は長さ20で、バックスラッシュと n の2文字が残ります。
ブラウザに渡る JavaScript が壊れる
展開されると、JavaScript の文字列リテラルが行をまたぎます。ブラウザは Invalid or unexpected token の SyntaxError を返し、page.evaluate は失敗します。
書いた人の目には、コードは正しく見えます。壊れるのは、Python が文字列を作ったあとの姿だからです。
2026-09-06 に起きたこと。マガジン追加が0件で止まった
経緯
この日、note の公開設定画面で記事をマガジンへ追加する処理を、「一覧の全件に入れる」形に変えました(01:24 のコミット)。同じ日に、「あとで読む」などを除外する判定を JavaScript で足しました。
12:33 に、あるアカウントの無人実行が「マガジン追加 0件」で止まりました。一覧の規模は、自分のマガジン10本と共同運営40本で50本です。公開設定の一覧には「あとで読む」が加わって51本でした。
原因
除外判定の JavaScript を、Python の通常の文字列に埋めていました。その中の split('\n') の \n が、Python の側で本物の改行に展開されました。
ブラウザが受け取った JavaScript は文字列リテラルが行をまたいだ形になり、SyntaxError になりました。
被害は止まった範囲にとどまった
公開はされていません。0件のまま公開しないよう検品が止め、その記事は下書きのまま残りました。
node --check と模擬DOMの検証をすり抜けた理由
node --check はファイルの中身を見る
ファイルの中身を node --check に通すと、エラーになりません。ファイルの上では \n はバックスラッシュと n の2文字で、node には正しいエスケープに見えるからです。
node --check が見ているのは、Python が展開する前の姿です。実際にブラウザへ届くのは、展開したあとの姿です。
模擬DOMの検証も同じ場所を見ていなかった
事前に、模擬DOMでロジックを検証していて、それも通っていました。ここでも検証したのは、除外判定のロジックそのものです。
2つの検証に共通する穴は、「Python が展開したあとの文字列」を見ていない点でした。ロジックが正しくても、運ぶ過程で文字列が変われば、実行時には別のコードになります。
直し方は2通り。検算は page.evaluate の返り値で行う
書き方は2通りある
09-06 の 12:58 の修正では、埋め込む JavaScript を raw 文字列(r"""...""")にしました。コードには「ここを通常の文字列にすると split の \n が本物の改行になる。node --check は通ってしまうので検算は page.evaluate で行う」という注記を残しました。
正しい書き方は、次の2通りです。
- raw 文字列にする
- 通常の文字列で、バックスラッシュを二重にする。
'\\n'のほか、正規表現なら\\/\\S\\sも同じ扱いです
同じファイルに、両方の書き方が実在します。2026-10-23 に数えた値では、そのスクリプトは1,968行、page.evaluate と evaluate_handle の呼び出しが49か所です。raw の三重引用符の文字列は1か所、通常の文字列の中で \\n を使っている箇所が6か所でした。page.evaluate を使うスクリプトは、scripts 配下に5本あります。
f 文字列に差し込むときの注意
raw 文字列で作った JavaScript を、f 文字列に差し込んで使っている箇所もあります。差し込まれた値は再展開されないので、raw で作った中身はそのまま保たれます。
一方、f 文字列の側に JavaScript の波括弧を直接書くなら、{{ }} に二重化する必要があります。
検算の方法
確かめる対象は、ファイルの中身でなく、埋め込んだ JavaScript を page.evaluate に実際に渡した結果です。返り値を見ます。
- 渡す先は about:blank で足ります。ログインは要りません
- 見るのは、SyntaxError が出ないことと、期待した値が返ることです
埋め込んだ JavaScript が複数あるなら、それぞれを同じ方法で渡します。
例外を飲んでも、止まった理由は残す
理由が残っていなかった
09-06 の失敗では、もう一つ問題がありました。例外が except Exception に飲まれ、戻り値は (0, -1) になっていました。止まった理由は、どこにも記録されていませんでした。
無人運転では、画面を見ている人がいません。理由が残らないと、「0件で止まった」という結果しか手元に残りません。
専用の変数に理由を残す
修正では、0件で終わった理由を専用の変数(LAST_MAGAZINE_ERROR)に残し、報告に出すようにしました。中身は、例外の型名と、メッセージの1行目(120字まで)です。
理由の文字列を、成果物の変数に入れない
ここに落とし穴があります。理由の文字列を、追加結果を持つ成果物の変数に入れてはいけません。入れると、検品の真偽判定(bool)が真になり、0件のまま公開を通してしまいます。
そのため、表示用の変数を別に分けました。
同時に塞いだ、待ち時間の穴
同じ日に、別の穴も塞ぎました。マガジンのタブは、公開設定を開いて0.5秒ほど後に出ます(実測)。以前は1回引いて無ければ諦める作りでした。
いまは250ミリ秒おきに最大40回、つまり10秒まで待ちます。待っても出なければ「マガジンのタブが10秒待っても出ない」を理由に残します。
まとめ
Python に埋めた JavaScript の事故から、運用に残した点を並べます。
- Python の通常の文字列では
\nが本物の改行になる。JavaScript に埋めるときは raw 文字列にするか、バックスラッシュを二重にする - node --check はファイルの中身しか見ない。Python が展開したあとの文字列は検出できない
- 検算は、埋め込んだ JavaScript を page.evaluate に渡した返り値で行う。about:blank で足りる
- 例外を飲む経路には、型名とメッセージの1行目を残す専用の変数を置く
- 理由の文字列は、成果物の変数に入れない。検品の真偽判定が通ってしまう
- 画面の要素を待つ処理には、待つ上限と、出なかったときの理由を持たせる
どの対策も、実行結果を見ない確認では気づけなかったものです。効果の大きさを示す数字は持っていないので、ここでは起きた事実と直した内容だけを書きました。
出典: relmea の note 下書き作成・公開の自動化スクリプトの変更履歴(2026-09-06 のコミット2本)と運用記録
