技術ログ

取得したHTMLに本文が無い — 200 が返っているのに中身が17文字だったサイトの話

公開: 2026-09-13 · 著者: GRAMSHIFT

ブラウザで見える画面と、サーバが返すHTMLは別物

取得したHTMLに本文が無い — 200 が返っているのに中身が17文字だったサイトの話

サイトを巡回してページの状態を記録する道具を作っていて、行が並んだ表を眺めていたときに気づいた。ある行だけ、ほとんどの列が0や空欄になっている。HTTPステータスは200。エラーではない。

ブラウザで同じURLを開くと、当然ながら普通にページが表示される。つまり「取れなかった」のではなく「取れたものの中に本文が無い」。この状態をどう判定して、どう出力に書くかを決めるために、2026-09-13 に日本語・英語の14サイトを同じ条件で取得して数字を並べた。

まず、いちばん極端だった1件

nhk.or.jp のトップページ。

https://www.nhk.or.jp/
  HTTP 200
  HTMLのバイト数    : 5,856
  本文の文字数      : 17   (script/style/noscript を除き、空白も除いた実数)
  空白区切りの語数  : 4
  同一サイトへのリンク: 0 本

5,856バイトのHTMLに、本文が17文字。同じサイトへのリンクが1本も無い。ページの中身はブラウザ上で JavaScript が組み立てているので、サーバが最初に返すHTMLには骨組みしか入っていない。

これを黙って表に流すと、「本文がほぼ無くリンクも無いページ」という行になる。読んだ人は「このサイトに問題がある」と読むだろう。実際には、測り方がそのページの作りに合っていないだけだ。

14サイトを同じ条件で取得した

条件は揃えてある。1回ずつの取得、リダイレクトは追う、User-Agent は素性を明かす自作のボット名(NeverEmptySeoBot/1.0)、JavaScript は実行しない。「本文の文字数」は body から script・style・noscript・template を除いてタグを落とし、空白を全部取り除いた実数。「語数」は同じ本文を空白で区切って数えたもの。

サイト状態HTMLバイト本文文字数語数内部リンクh1
nhk.or.jp2005,85617401
jal.co.jp4033691571501
note.com200144,0831796130
zenn.dev200237,52264516200
pixiv.net20042,7188269680
notion.com200240,9692,115250711
vercel.com200527,9983,113279751
city.yokohama.lg.jp20060,9243,5135091241
mlit.go.jp20063,0334,4503081220
gov.uk20085,4604,614798541
python.org20052,6105,4351,001635
qiita.com200309,7856,0393111840
rakuten.co.jp200382,5987,9548833000
このサイト20082,78111,447699691

ついでに分かったことが1つ。14サイトのうち6サイト(mlit.go.jp・rakuten.co.jp・note.com・zenn.dev・qiita.com・pixiv.net)は、サーバが返したHTMLに h1 が1つも無かった。これも「サイトに h1 が無い」とは限らず、h1 を JavaScript で差し込んでいる場合を含む。同じ数字が2つの意味を持つ、という今回の主題そのままの例になっている。

HTMLのバイト数と本文の量に相関が無いのが、この表のいちばん分かりやすい点だと思う。note.com は144KBのHTMLを返して本文179文字、vercel.com は528KBを返して3,113文字。バイト数で「中身がありそう」と判断するのは無理だ。

「語数が少ない」で判定すると、日本語のページが全滅する

最初に思いつくのは「本文の語数が極端に少なければ空とみなす」だが、これは日本語で壊れる。同じ本文を文字数と語数の両方で測ると、1語あたりの文字数がまったく違う。

  • gov.uk … 4,614文字 ÷ 798語 = 1語あたり 5.8文字
  • python.org … 5,435文字 ÷ 1,001語 = 5.4文字
  • mlit.go.jp … 4,450文字 ÷ 308語 = 14.4文字
  • qiita.com … 6,039文字 ÷ 311語 = 19.4文字
  • このサイト … 11,447文字 ÷ 699語 = 16.4文字

日本語は単語の間に空白を入れないので、空白区切りの語数は本文量をまったく表さない。英語のページで「50語未満なら空」という閾値を決めると、本文4,450文字ある国土交通省のトップページが308語という数字で評価されることになる。今回はたまたま閾値を超えたが、もっと短い日本語ページなら簡単に下回る。形態素解析を入れずに語数を指標にするのは、日本語のサイトを測る道具としては欠陥だ。

条件を2つ重ねた。それでも2件 すり抜けた

そこで、判定は語数だけに頼らず「語数が50未満」かつ「同一サイトへのリンクが0本」の合わせ技にした。本文が取れていないページは、たいてい内部リンクも取れていない(ナビゲーションも JavaScript で作られるため)ので、この2つが同時に成立したときだけ「サーバが返したHTMLは空だった」と報告する。

この条件で14サイトを判定した結果は、nhk.or.jp と jal.co.jp の2件。そして取りこぼしが2件ある

  • note.com … 本文179文字・語数6 だが、内部リンクが13本あるので判定されない
  • zenn.dev … 本文645文字・語数16 だが、内部リンクが20本あるので判定されない

どちらも中身は明らかにブラウザ側で組み立てられている。判定は取りこぼす側に外していることになる。これは意図的にそうしてある。誤って「このページは空だ」と言うほうが、言わずに数字を渡すより害が大きいと判断したからだ。ただし、取りこぼしたことを黙っていていいわけではないので、どの行にも「JavaScript は実行していない」という但し書きを付けて返すようにした。判定に引っかからなかったページでも、本文が少ないなら読む側が理由に思い当たれる。

閾値を上げれば note.com も zenn.dev も捕まる。捕まるが、そのぶん「本文が本当に少ないだけのページ」も巻き込む。この線引きに正解は無いので、線を動かすのではなく、線の位置と根拠(語数と内部リンク数の実数)を出力に一緒に入れることにした。

403 を「空」と一緒に数えない

表の2行目、jal.co.jp は HTTP 403 を返した。369バイト、本文157文字。今回の条件では「空」の側に分類されるが、これは性質がまったく違う。

403 は答えだ。「このクライアントには見せない」と言っている。JavaScript レンダリングのページは「見せているが、この形式では読めない」。この2つを同じ「空」として集計すると、原因も対処も取り違える。前者はUAや経路の問題、後者は読み方の問題だ。

ブラウザで開けば普通に見えるサイトが、素のリクエストには403を返すことは珍しくない。別の日(2026-08-26)にリンク検査の道具を作って有名サイト15件をボットの User-Agent で叩いたときは、15件のうち6件が403を返した(どれもブラウザでは問題なく開く)。403 を「リンク切れ」として報告すると、生きているリンクを消す作業を人にやらせてしまう。ステータスコードが返ってきたなら、それは情報なので、潰さずに最後まで運ぶ。

測った対象が、いつのまにかすり替わっていた

この測定でもう1つ踏んだ。表の pixiv.net の行だ。https://www.pixiv.net/ を要求したのに、リダイレクトを追った結果 https://www.pixiv.net/en/ が返ってきていた。英語版である。日本語版のトップページは測っていない。

この種の事故は数字が合っているので気づけない。表の中では「pixiv.net の本文は826文字」という行として静かに座っている。対策は単純で、最終URL(リダイレクト後)を必ず列として持ち、要求したURLと並べて出す。今回は列に出ていたので、表を作る段階で見つかった。出していなければ、そのまま公開していた。

同じ理由で、出力には「要求したURL」と「実際に読んだURL」を別の列で持っている。foundOn(どのページのリンクから見つけたか)も同様だ。数字だけを渡すと、その数字が何について測られたのかが失われる。

空欄・0・未測定を、出力の上で分ける

ここまでの全部が、最終的には出力の設計の話になった。決めたのは3つ。

  1. 測らなかったものは「測っていない」と書く。JavaScript 実行後の内容、Core Web Vitals、画像のファイルサイズ、検索順位は、この方法では測れない。だから全部の行に「これは見ていない」という一覧を付ける。空欄が「問題なし」なのか「見ていない」なのかを、読む側に推測させない
  2. 点数を付けない。0〜100点の総合スコアは、誰かが決めた重み付けであって測定値ではない。同じページに2つの道具が別の点数を出すし、どちらの数字も検算できない。代わりに、根拠になったタグ・ヘッダ・ステータスコードをそのまま添える
  3. 読めなかったページも行として返す。読めなかったものを黙って落とす巡回は、「何も見つからなかった」巡回と区別がつかない

3つ目は、今回の測定でいちばん効いた。alt 属性が無い画像と alt=""(空の alt)を別の列にしたのも同じ理由だ。空の alt は装飾画像に対する正しい書き方で、欠陥ではない。混ぜて数えると「修正すべき箇所」が実際より多く見える。

この判定を入れて公開している道具

上の判定と出力の決まりをそのまま入れたものを公開している。Technical SEO Site Audit Crawler(Apify)は、1ページ1行で、そのページが検索エンジンに何を宣言しているか(noindexcanonicalX-Robots-Tag・リダイレクト連鎖)と、その根拠になった値を返す。総合スコアは出さない。robots.txt が禁じているURLは取得せず、「禁止されていたので取得しなかった」という行で返す。本文が空だったページには、この記事で書いた但し書きが付く。

関連して、同じ「正常応答なのに中身が足りない」形の壁は GitHub の検索APIが7,705件 一致と言いながら1,000件で打ち切る話 にも書いた。あちらは件数、こちらは本文で、対処の形は同じだった。

筆者: GRAMSHIFT — Android アプリと自動化ツールを個人開発。本文の数値は 2026-09-13 に自作クローラの抽出部分をそのまま呼び出して14サイトを1回ずつ取得した結果で、測定スクリプトの作成と本文の整理には Claude Code を使っている。1回の取得・1つの回線・1つの時刻の値なので、同じサイトでも別の日には違う数字が出る。

よくある質問