目次12

robots.txt の User-agent: * に Disallow: /go/ を書いても、Googlebot には効いていなかった。/go/ はこのサイトの Amazon への転送リンクで、検索エンジンにも AI のクローラーにもたどってほしくない URL だ。10月6日に Disallow を入れて、止めたつもりでいた。

3週間後に Cloudflare のログを見ると、本物と確認できた Googlebot が3日で133回、ClaudeBot が5日で213回、この転送リンクを取っていました。原因は robots.txt の上のほうに並べていた User-agent: Googlebot や User-agent: ClaudeBot のグループです。名前を書いたグループがある bot は、* のグループを読まない。

時刻はすべて UTC で書いている。

名前を書いたグループがあると * は読まれない

bot は robots.txt の中から自分の名前に一致するグループを1つだけ選び、User-agent: * は一致するグループが無いときにしか使わない。* は全 bot に共通の土台ではなく、名前を書かなかった bot 向けの受け皿だ。

RFC 9309 の 2.2.1 節は、一致するグループが無いときは * のグループに従わなければならない(MUST)と書いている。一致するグループがあれば * は使われない。Google の robots.txt の仕様も「1つのクローラーに有効なグループは1つだけ」と明記していて、残りのグループは無視される。

直す前の robots.txt を短くするとこうなる。

User-agent: Googlebot
Allow: /

User-agent: ClaudeBot
Allow: /

User-agent: *
Allow: /
Disallow: /go/

AI 検索のクローラーを名前で許可しておきたくて、Googlebot・bingbot・Applebot・GPTBot・ClaudeBot など13のグループを並べていた。どれも Allow: / の1行だけ。/go/ の Disallow は後から足したもので、全 bot に効く場所のつもりで * の下に書いた。

flowchart TD
  Bot["bot が robots.txt を読む"] --> Q{"自分の名前のグループがある?"}
  Q -->|"ある(Googlebot / ClaudeBot など)"| G["そのグループだけを使う<br/>Allow: / なので /go/ も許可"]
  Q -->|"無い"| W["User-agent: * を使う<br/>Disallow: /go/ なので不許可"]

結果として /go/ で止まっていたのは名前を書かなかった bot だけで、止めたかった検索エンジンと AI のクローラーは素通りしていた。

実測 — 本物の Googlebot と ClaudeBot が転送リンクを取っていた

Cloudflare の GraphQL Analytics API(httpRequestsAdaptiveGroups)で、/go/ へのアクセスを UA・verifiedBotCategory・ステータス別に集計した。verifiedBotCategory は Cloudflare が IP と逆引きで確認した結果で、偽装の UA なら空になる(この分け方は前に書いた)。下の表の Googlebot・ClaudeBot・bingbot は、すべて検証済みのリクエストだ。

bot期間/go/ へのリクエスト応答
Googlebot10-25 10時〜10-27133(67種類の URL)302 が104、200 が29
ClaudeBot10-21〜10-25213すべて 302
bingbot10-272302

Googlebot の期間が短いのは、無料プランの Cloudflare では詳細なログが8日しか残らないからだ。AI クローラーの分は週1回取って保存しているが、Googlebot は対象に入れていなかった。10月25日より前も同じように取られていた可能性は高いものの、数字は無い。

転送リンクには rel="sponsored nofollow" を付け、応答には X-Robots-Tag: noindex, nofollow も返している。それでも取られていたのは、Google が 2020年3月1日から nofollow をクロールのヒントとして扱っているからだと思う。ヒントなので、たどるかどうかは Google が決める。X-Robots-Tag は取得した応答に付くヘッダーなので、取得そのものは止められない。

取得を止められるのは robots.txt だけで、その robots.txt が効いていなかった。

直し方 — 止めたいパスを全グループに書く

グループの間でルールは引き継がれないので、止めたいパスは名前を書いた全グループに書くしかない。10月27日 13:31 に、13のグループそれぞれへ2行ずつ足して push した。

# Named groups below override "User-agent: *" entirely, so each repeats the
# Disallow lines (affiliate redirector /go/ and the decorative /_code-lines.json).

User-agent: Googlebot
Allow: /
Disallow: /go/
Disallow: /_code-lines.json

User-agent: ClaudeBot
Allow: /
Disallow: /go/
Disallow: /_code-lines.json

Allow: / と Disallow: /go/ が同じグループに並んでいても問題はない。RFC 9309 の 2.2.2 節では、パスに一致するルールが複数あれば、一致した部分が最も長いルールを採る。/go/amazon/... には /(1文字)と /go/(4文字)の両方が一致し、長いほうの Disallow: /go/ が勝つ。

先頭のコメントは次に Disallow を足すときの自分向けに残した。* の下に1行足して終わり、という同じ間違いをもう一度やりそうなので。

直した後 — Googlebot は robots.txt を取り直してから0回になった

Googlebot が /go/ を取ったのは、10月27日の17時台が最後だった。そこから11月2日まで、転送リンクへのリクエストは0回。

日(UTC)Googlebot の /go/Googlebot の robots.txt 取得
10-25(10時〜)3220
10-264915
10-27(13:31 に修正を push)5218
10-28011
10-29〜11-02049

時間単位で見ると、直した直後はまだ取られていた。push の後、15時台に2回、17時台に2回。Googlebot は13時台にも robots.txt を取っているが、push と同じ時間帯なので新旧どちらを読んだかは分からない。15時台に取られているので、たぶん古いほうだ。次に robots.txt を取ったのが17時台で、それを最後に /go/ へは来ていない。Google は robots.txt を最大24時間キャッシュするので、直してから効くまでに時間差が出るのは仕様どおりだと思う。

bingbot も同じで、push の後の20時台に1回取ったのを最後に止まった。

ClaudeBot は数字が合わない。最後に /go/ を取ったのは10月25日の18時台で、直す2日前に止まっている。その後も記事は取りに来ているので、サイトから離れたわけではない。直したから止まったとは言えず、たまたまこの期間に転送リンクを回らなかっただけかもしれない。

名前を書いていない GoogleOther も来ていた

* のグループに従うはずの bot も /go/ を取っていた。GoogleOther は Google の汎用クローラーで、robots.txt に名前を書いていない。それでも10月6日〜9日に7回、10月20日〜24日に9回、転送リンクを取っている。どれも検証済みのリクエストだ。

理由は分からない。Googlebot のグループを使っているなら説明はつくが、Google のクローラー一覧にそうした記述は無い。直した後は来ていないものの、直す前の10月24日から止まっているので確かめようがない。件数も少ないので、ここは分からないまま置いておく。

Python 標準の robotparser では確かめられない

直した robots.txt が効くかを手元で確かめようとして、もう1つ詰まった。Python 標準の urllib.robotparser で判定すると、直した後の robots.txt でも全 UA が allow になる。

from urllib.robotparser import RobotFileParser

AGENTS = ["Googlebot", "bingbot", "ClaudeBot", "GPTBot", "GoogleOther", "SomeNewBot"]
PATH = "/go/amazon/B0CL6H35DY"

rp = RobotFileParser()
rp.parse(open("robots_after.txt", encoding="utf-8").read().splitlines())
for ua in AGENTS:
    print(ua, "allow" if rp.can_fetch(ua, PATH) else "DISALLOW")

標準の実装はグループ内のルールを上から読み、最初に一致した行で決める。Allow: / を先に書いているので、/go/ の Disallow まで届かない。RFC 9309 と Google の「一致が最も長いルール」とは別の判定だ。

Scrapy が使っている Protego は最長一致で判定するので、こちらで直す前と後を比べた(Protego 0.7.0)。

from protego import Protego

URL = "https://aulvem.com/go/amazon/B0CL6H35DY"

for f in ["robots_before.txt", "robots_after.txt"]:
    rp = Protego.parse(open(f, encoding="utf-8").read())
    print(f)
    for ua in AGENTS:
        print(" ", ua, "allow" if rp.can_fetch(URL, ua) else "DISALLOW")
UA直す前直した後標準の robotparser(直した後)
GooglebotallowDISALLOWallow
bingbotallowDISALLOWallow
ClaudeBotallowDISALLOWallow
GPTBotallowDISALLOWallow
GoogleOtherDISALLOWDISALLOWallow
SomeNewBot(架空)DISALLOWDISALLOWallow

直す前の列は、ログで起きていたことと合っている。名前を書いた bot だけが通り、名前の無い bot は止まる。GoogleOther は判定上は止まる側で、だから実際に来ていた理由が分からない。

名前を書いたグループが多い robots.txt なら、全 UA の判定を一度回しておくと、書き忘れたグループが allow の行としてすぐ見つかる。robots.txt を変えるたびに CI で回してもいいと思う。

まとめ

User-agent: * に書いたルールが全 bot に効くのは、名前を書いたグループが1つも無いときだけです。AI のクローラーを名前で許可している robots.txt は多いので、後から * に Disallow を足したサイトは同じ状態になっているかもしれない。

確かめ方は2つある。1つは robots.txt を最長一致のパーサーにかけて、名前を書いた UA ごとに判定を出すこと。もう1つはエッジのログで、検証済みの bot が止めたはずのパスに来ていないかを見ること。今回は後者で気づいて、前者で直ったことを確かめた。

偽装の UA を見分ける方法はAIクローラーのUAは名乗るだけに、WAF で弾いた後の話は偽装UAをWAFで弾いた1か月に書いた。

よくある質問

User-agent: * に書いた Disallow は、Googlebot にも効く?

robots.txt に User-agent: Googlebot のグループが別にあれば効きません。RFC 9309 では、bot は自分の名前に一致するグループに従い、* のグループは一致するものが無いときだけ使います。Google のドキュメントも、1つのクローラーに有効なグループは1つだけだと書いています。名前を書いたグループに Allow: / しか無ければ、そのクローラーにとってはサイト全体が許可です。

名前を書いたグループと * の両方に同じ Disallow を書く必要がある?

あります。グループの間でルールは引き継がれないので、止めたいパスは名前を書いた全グループに書きます。このサイトでは Googlebot・bingbot・ClaudeBot など13のグループそれぞれに Disallow: /go/ を足しました。グループが増えるほど書き忘れが起きやすいので、パーサーで全 UA を判定するチェックを用意しておくと安全です。

rel=“nofollow” を付けたリンクなら、Googlebot はたどらない?

たどることがあります。Google は 2020年3月1日から、nofollow をクロールとインデックスについてもヒントとして扱うと発表しています。このサイトの転送リンクには rel="sponsored nofollow" が付いていましたが、Googlebot は67種類の転送 URL を133回取りに来ていました。クロールを止めたいなら robots.txt で止めます。

robots.txt の判定を Python の urllib.robotparser で確かめていい?

Allow と Disallow が両方ある robots.txt では向きません。標準ライブラリの robotparser はグループ内のルールを上から読んで最初に一致した行を採るので、Allow: / を先に書いていると、直した後の robots.txt でも全 UA が allow になりました。RFC 9309 と Google は一致したパスが最も長いルールを採ります。Scrapy が使っている Protego はこの方式で判定し、直す前と後の違いが正しく出ました。