silent wrong results
エラーを出さずに間違う検査|日本語が割れる・正規表現が素通り・数の取り違え
検査コードが「異常なし」と出すとき、異常がないのか、読めていないのかを区別できないことがあります。この面は、例外を1つも出さず、数字だけが狂った3つの実例(HTTPで日本語が割れる、正規表現が空白を拾う、出現回数を実体数と取り違える)と、見破る手順の記録です。
このサイトはAnthropicの公式サイトではありません。Claude Codeの機能・料金・提供範囲は変わる可能性があるため、重要な判断の前には公式のドキュメントと自分のアカウント設定を確認してください。
先に結論:3つに共通する見破り方
| 欠陥 | 出方 | 見破る手順 |
|---|---|---|
| HTTPのチャンクで日本語が割れる | 本番と手元が「違う」と出るが、実際は同じ | curl と diff で裏を取る。道具どうしを突き合わせる |
| 正規表現が改行を値として拾う | フィルタが全件素通り。落とした数が0 | 「読めなかった」の件数が取得件数と同じなら異常として止める |
| 出現回数を実体数と取り違える | 「◯人」の数字が膨らむ | 直前の文字列の異なり数と、固有URLの異なり数の2本で検算する |
3つとも、結果が「きれい」なとき(差が0、落とした数が0、きれいな数字)こそ疑う、という同じ姿勢で見つかりました。
読者が確かめる手順
- HTTPで取る道具に
setEncoding("utf8")を入れる。入れないと、チャンクの境目が多バイトの文字の途中に来たとき、その文字が壊れます。d += cは Buffer を文字列にするだけです。 - 差が出たら、先に curl と diff で裏を取る。バイト数まで一致するなら、道具が嘘をついています。
- 値を取る正規表現は、タグの直後の空白を食べる。
[^<]は空白と改行を含むので、>\s*([^<>\s][^<]*?)\s*<のように、文字から始めさせます。ラベルの文字列でなくリンク先のコードを読む方法もあります。 - フィルタが落とした件数を必ず出す。ずっと0なら、働いていないか、そもそも要らないかのどちらかです。0を「きれい」と読みません。
- 「読めなかった」が取得件数と同じなら、異常として止める。警告を出すだけでは読み飛ばされます。
- 「◯人」「◯件」と書く前に、出現回数と実体数を2本で検算する。重複率が5%を超えたものは数えずに落とします。
- 読むのは1ページだけにする。複数ページを足し合わせると、ページをまたいだ重複が入ります。
文字化けそのものの話はストリームの文字化けの記録、WindowsのPowerShellでの文字の扱いはPowerShellの事故にあります。ここでは繰り返しません。ターミナルでの除外パターンが日本語で0件になる件も別の面で扱っているので、ここでは名前を挙げるだけにとどめます。道具が出す誤報全般は検査コードの欠陥を直した記録にあります。
実際に起きた記録
1. 日本語が割れた(2026年9月19日)
あるサイトの全数点検で、本番と手元を突き合わせる道具が3ページを「違う」と報告しました。ところが curl と diff では、バイト数まで同じ(30582バイト対30582バイトなど)でした。20〜30キロバイトのページで1〜2文字が壊れる程度なので、気づきにくいのです。setEncoding("utf8") を足したら0件になりました。同じ書き方は網内のスクリプトに20本以上ありました。語の出現を数える用途では実害が小さく、測り直しても平均は3.6回から3.5回で、結論は変わりませんでした。それでも数字を報告した後に気づいたら、測り直します。直す順は、本番との一致を見る道具から。
2. 正規表現が改行を拾った(2026年8月28日)
外部の店舗一覧の業種を読む正規表現が、18,609行すべてで失敗していました。タグの直後が改行で、[^<]{1,12} が改行を捕まえ、trimで空文字になり、falsyとして「読めなかった」に分類されたため、フィルタが丸ごと素通りしました。例外は出ません。実害は、指定した業種以外の店(実体はソープ3店など)を全部同じ業種として記録していたことです。30店の一覧の内訳を作り直しました。共有の道具と、それを写した別の道具の両方に同じ欠陥がありました。
3. 出現回数と実体数(2026年8月28日)
身長の書式の出現回数を数えて「在籍◯人」と書こうとしたとき、別のセッションから「出現回数は人数ではない」と止められました。指摘は妥当な問題提起でした。そこで測りました。
- 各ヒットの直前28文字の異なり数:181店中180店(99%)は重複なし。重複があったのは1店だけ(出現30・異なり21)。
- 固有URL(プロフィールへのリンク)の異なり数との突き合わせ:両方で数えられた8店が一致または近い(109対106、95対96、85対85など)。
止めた側の数え方(年齢を含む書式)は、一覧の外の体験談や案内文にも出るので、確かに人数ではありませんでした。同じ「出現回数」でも、何を数えているかで信頼度が違います。言い方も実体に合わせました。「在籍数」ではなく「公式サイトの在籍一覧に載っている人数」とし、休みの人も含むこと、501店中180店しか数えていないことを添えました。欠け方は均一ではなく、描画をJavaScriptに頼るサイトほど欠けます。検算は道具に組み込みます。1回測って終わりにすると、データが増えたときに静かに壊れます。
検査コードを書くときの小さな習慣
私たちが直したのは個々の道具より、書き方の習慣でした。取得した本文は必ず文字コードを指定して受ける。正規表現で値を取るときは空白を明示的に食べる。フィルタには「何件落としたか」の出力を付ける。数を書く前に、数えた対象が実体かを別の数え方で突き合わせる。どれも1行か2行で足せるのに、足さないと、数字だけが静かに間違います。間違いに気づくのは、たいてい別の人の一言です。止められたら反論の前に測る、が結論でした。
公式の記載で見る:多バイト文字がチャンクの境目で割れる仕組み(2026年11月15日確認)
1つ目の欠陥(HTTPのチャンクで日本語が割れる)は、Node.jsの公式の説明で仕組みが読めます。
- 受信側の既定:ストリームの
'data'イベントは、readable.setEncoding()で既定の文字コードを指定していれば文字列を、指定していなければBufferを受け取ります。つまり、指定のない受信コードは、バイト列の塊をそのまま持っています。 - 割れを防ぐ仕組み:公式のstring_decoderの説明は、
Bufferを書き込むと内部のバッファが働き、不完全な多バイト文字を含まない文字列だけを返す、と書いています。不完全な分は、次のwrite()かend()が呼ばれるまで保持されます。 - 公式の例:ユーロ記号の3バイト(
0xE2 0x82 0xAC)を1バイトずつ3回に分けて渡すと、最初の2回は空の文字列が返り、最後にend()で記号が出ます。
日本語のひらがな・漢字は、UTF-8では1文字が複数のバイトになります。塊の境目が文字の途中に来る機会は、ページが大きいほど、塊の数が増えるほど増えます。この面の「20〜30キロバイトのページで1〜2文字が壊れる」という実測は、この仕組みの上で説明がつきます。
自作の取得コードを点検するときは、次の3点を順に見ます。
- 受信した塊を、文字列に直す前にバイトのまま扱っているか。
- 文字列に直すなら、
setEncodingかStringDecoderのように、境目の不完全な文字を持ち越す仕組みを通しているか。 - 差が出たときに、
curlの結果とバイト数まで突き合わせる手順が書いてあるか。
出典:Node.js公式ドキュメント「Stream」(Readable stream)、同「String decoder」(2026年11月15日確認)。
よくある質問
検査の結果が全部「異常なし」のときは、安心していいですか。
安心できません。落とした件数や読めなかった件数が0または全件のときは、検査自体が働いていない可能性があります。件数を必ず出して、極端な値で止めます。
setEncodingを入れれば文字化けは全部防げますか。
HTTPの受信で文字が割れる分は防げます。保存時の文字コードや、シェルの出力の化けは別の話なので、関連する面を見てください。
出現回数を実体数に近づけるコツは。
読む範囲を一覧の部分に限り、直前の文字列の異なり数と固有URLの異なり数で検算します。合わないものは数えずに落とします。