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本)と運用記録