robots.txtのAllowとDisallowの優先順位は*も長さに数える

robots.txt で Disallow: /admin/ と Allow: /admin/help/ を両方書いたとき、/admin/help/ がクロールされるかどうかは「書いた順番」では決まらない。2022年9月に標準化された RFC 9309(Robots Exclusion Protocol)は、URL に一致したルールのうち最も長く一致したものを使い、Allow と Disallow が同じ長さなら Allow を使うよう定めている(2.2.2 節)。Google の検索セントラルの説明も同じで、ルールが衝突したら制限の緩い方、つまり Allow を使うと書いている。

この記事で確かめるのはその先の細かい点である。「長さ」に * や $ を含めるかどうかで、同じ robots.txt でも判定が変わる。また、Python 標準の urllib.robotparser はこの最長一致を実装していない。判定ロジックを自作するときや、手元のスクリプトでクロール可否を確かめるときに効いてくる差を、Node.js と Python で実測した。

ブラウザで robots.txt を組み立てて判定まで試すなら robots.txt 作成(ハシトシステム) がある。この記事の後半で、このツールの判定と Google の実装との違いにも触れる。

RFC 9309 と Google が決めていること

一次情報で確認できた規則を並べる。

項目 内容 出典
一致の仕方 パスの先頭からの前方一致。* は0文字以上の任意の並び、$ はパターンの終わりを表す RFC 9309 2.2.3
優先順位 最も具体的な一致(octet 数が最も多いもの)を使う。Allow と Disallow が同等なら Allow を使う(SHOULD) RFC 9309 2.2.2
大文字小文字 区別する(SHOULD) RFC 9309 2.2.2、Google
サイズ上限 パースの上限は最低 500KiB(RFC)。Google は 500KiB を超えた部分を無視する RFC 9309 2.5、Google
4xx RFC は全リソースへのアクセスを許可(MAY)。Google は 429 を除く 4xx を「robots.txt が無い」と同じに扱う RFC 9309 2.3.1.3、Google
5xx RFC は全面的な Disallow とみなす(MUST)。Google は最初の12時間クロールを止め、その後は最大30日キャッシュを使う RFC 9309 2.3.1.4、Google

「長さ」について、RFC は「octet 数が多い一致」としか書いていない。Google の説明は「ルールのパスの長さ」に基づくとしており、Google が公開している C++ パーサー google/robotstxt の robots.cc を読むと、LongestMatchRobotsMatchStrategy は一致したときにパターン文字列の長さ(pattern.length())を優先度として返している。* も $ も1文字として数えられる。同じ優先度なら Disallow が勝たない(disallow の優先度が厳密に大きいときだけ Disallow)ので、Allow が勝つ。

実際に試す

前提は Node.js(ここでは Windows 11 上の v24.11.1)。外部パッケージは使わない。次のファイルを robots-match.mjs として保存する。パターンを正規表現に変換し、長さの数え方だけを2通りに差し替えて判定を比べる。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// robots-match.mjs : robots.txt の Allow/Disallow を最長一致で判定し、長さの数え方の違いを比べる

// パターンを正規表現に変換する。先頭固定、* は任意の並び、末尾の $ だけが行末固定
function toRegExp(pattern) {
const anchored = pattern.endsWith('$');
const body = anchored ? pattern.slice(0, -1) : pattern;
const src = body.split('*')
.map(s => s.replace(/[.+?^${}()|[\]\\]/g, '\\$&'))
.join('.*');
return new RegExp('^' + src + (anchored ? '$' : ''));
}

// 長さの数え方を差し替えられる判定関数
function decide(rules, path, lengthOf) {
let allow = -1, disallow = -1;
for (const [type, pattern] of rules) {
if (!toRegExp(pattern).test(path)) continue;
const n = lengthOf(pattern);
if (type === 'allow') allow = Math.max(allow, n);
else disallow = Math.max(disallow, n);
}
return disallow > allow ? 'disallow' : 'allow'; // 同じ長さなら allow
}

const fullLength = p => p.length; // * と $ も1文字に数える
const literalLength = p => p.replace(/\*/g, '').replace(/\$$/, '').length; // 記号を除いて数える

const cases = [
['/page', [['allow', '/p'], ['disallow', '/']]],
['/folder/page', [['allow', '/folder'], ['disallow', '/folder']]],
['/page.htm', [['allow', '/page'], ['disallow', '/*.htm']]],
['/page.php5', [['allow', '/page'], ['disallow', '/*.ph']]],
['/', [['allow', '/$'], ['disallow', '/']]],
['/page.htm', [['allow', '/$'], ['disallow', '/']]],
['/Admin/', [['disallow', '/admin/']]],
];

console.log('path'.padEnd(14), 'rules'.padEnd(30), 'full'.padEnd(9), 'literal');
for (const [path, rules] of cases) {
const r = rules.map(([t, p]) => `${t}:${p}`).join(' ');
console.log(path.padEnd(14), r.padEnd(30),
decide(rules, path, fullLength).padEnd(9), decide(rules, path, literalLength));
}
console.log('Node', process.version);
1
node robots-match.mjs

出力:

1
2
3
4
5
6
7
8
9
path           rules                          full      literal
/page allow:/p disallow:/ allow allow
/folder/page allow:/folder disallow:/folder allow allow
/page.htm allow:/page disallow:/*.htm disallow allow
/page.php5 allow:/page disallow:/*.ph allow allow
/ allow:/$ disallow:/ allow allow
/page.htm allow:/$ disallow:/ disallow disallow
/Admin/ disallow:/admin/ allow allow
Node v24.11.1

上の6行は Google の robots.txt 仕様ページにある優先順位の例と同じ URL・ルールである。full(パターン全体の長さ)の列は、6例すべてで Google のページに書かれた結論と一致した。

  • /page.htm に allow: /page(5文字)と disallow: /*.htm(6文字)。Google のページは「ルールのパスが長い」として disallow を適用すると書いている。* を数えない literal では /.htm の5文字になって Allow と同点になり、結論が逆転する。
  • /page.php5 に allow: /page(5文字)と disallow: /*.ph(5文字)。同点なので Allow。
  • allow: /$ は / そのものにしか一致しない。/page.htm には disallow: / だけが一致するので Disallow になる。
  • 最後の行は大文字小文字の区別である。/Admin/ は disallow: /admin/ に一致しないので、ブロックされない。

* を最初の一致で決め打ちすると $ と噛み合わない

自作の判定でもう1つ起きやすいのが、* の後ろの文字列を indexOf で最初に見つかった位置に決め打ちする実装である。$ で終わるパターンと組み合わせると、後ろにもう一度出てくる一致を見逃す。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// dollar.mjs : * を「最初に見つかった位置」で決め打ちすると $ と組み合わせたときに取りこぼす
function greedyFirst(pattern, path) {
const anchored = pattern.endsWith('$');
const parts = (anchored ? pattern.slice(0, -1) : pattern).split('*');
let pos = 0;
for (let i = 0; i < parts.length; i++) {
const p = parts[i];
if (p === '') continue;
const at = i === 0 ? (path.startsWith(p) ? 0 : -1) : path.indexOf(p, pos);
if (at < 0) return false;
pos = at + p.length;
}
return !(anchored && pos !== path.length && parts.at(-1) !== '');
}

function backtrack(pattern, path) {
const anchored = pattern.endsWith('$');
const src = (anchored ? pattern.slice(0, -1) : pattern).split('*')
.map(s => s.replace(/[.+?^${}()|[\]\\]/g, '\\$&')).join('.*');
return new RegExp('^' + src + (anchored ? '$' : '')).test(path);
}

for (const path of ['/a.pdf', '/a.pdf.pdf', '/pdf/b.pdf', '/a.pdf?dl=1']) {
console.log(path.padEnd(12), 'greedyFirst', String(greedyFirst('/*.pdf$', path)).padEnd(5),
'backtrack', backtrack('/*.pdf$', path));
}
1
2
3
4
/a.pdf       greedyFirst true  backtrack true
/a.pdf.pdf greedyFirst false backtrack true
/pdf/b.pdf greedyFirst true backtrack true
/a.pdf?dl=1 greedyFirst false backtrack false

/a.pdf.pdf は .pdf で終わるので Disallow: /*.pdf$ に一致するはずだが、greedyFirst は最初の .pdf の位置で止まり、その後ろに文字が残るので「一致しない」と判定する。正規表現の .* はバックトラックして後ろの .pdf を試すので一致する。google/robotstxt の Matches 関数は正規表現を使わず、パス上で一致し得る位置の集合を1文字ずつ更新していく方式で、同じく後ろの一致を取りこぼさない。/a.pdf?dl=1 のようにクエリが付いた URL は、$ の位置に ?dl=1 が残るのでどちらの実装でも一致しない。

Python の urllib.robotparser は書いた順で決まる

手元のスクリプトでクロール可否を見るとき、Python 標準ライブラリの urllib.robotparser を使うことがある。これは最長一致ではない。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# py_robotparser.py : Python 標準の urllib.robotparser で同じルールを判定する
import sys
from urllib.robotparser import RobotFileParser

def check(lines, path):
rp = RobotFileParser()
rp.parse(lines)
return 'allow' if rp.can_fetch('mybot', 'https://example.com' + path) else 'disallow'

cases = [
(['User-agent: *', 'Disallow: /', 'Allow: /p'], '/page'),
(['User-agent: *', 'Allow: /p', 'Disallow: /'], '/page'),
(['User-agent: *', 'Allow: /page', 'Disallow: /*.htm'], '/page.htm'),
(['User-agent: *', 'Disallow: /*.pdf$'], '/a.pdf'),
]
for lines, path in cases:
print(path.ljust(10), ' | '.join(lines[1:]).ljust(32), check(lines, path))
print('Python', sys.version.split()[0])
1
python py_robotparser.py
1
2
3
4
5
/page      Disallow: / | Allow: /p          disallow
/page Allow: /p | Disallow: / allow
/page.htm Allow: /page | Disallow: /*.htm allow
/a.pdf Disallow: /*.pdf$ allow
Python 3.11.9
  • 同じ2行でも、Disallow: / を先に書くと /page は disallow、Allow: /p を先に書くと allow になる。RFC 9309 と Google の規則ではどちらも allow である。
  • * と $ は特別扱いされず、Disallow: /*.pdf$ は /a.pdf に一致しない。

理由はソースを見ると分かる。Python 3.11.9 の Entry.allowance はルール行を書いた順に調べて最初に当たったものを返し、RuleLine.applies_to は filename.startswith(self.path) で前方一致を見るだけである(inspect.getsource で確認した)。Google のクローラーと同じ判定が欲しいなら、urllib.robotparser の結果をそのまま信じない方がよい。

ハシトシステムのツールの判定について

robots.txt 作成(ハシトシステム) の判定も最長一致で、同じ長さなら Allow が勝つ。ただし執筆時点のコードは、長さを * と末尾の $ を除いた文字数で比べている(上の表の literal と同じ)。そのため allow: /page と disallow: /*.htm の組み合わせでは、/page.htm が Google の結論とは逆に「クロールできます」と出る。* を含むルールを Allow と競わせるときは、上の full の数え方で確かめてほしい。* を含まない Disallow: /admin/ と Allow: /admin/help/ のような典型的な書き方では、どちらの数え方でも結論は同じになる。

まとめ

  • robots.txt は書いた順ではなく最長一致で判定する。同じ長さなら Allow が勝つ(RFC 9309 2.2.2、Google)。
  • Google の実装はパターン文字列そのものの長さで比べ、* と $ も1文字に数える。/page.htm に allow: /page と disallow: /*.htm なら Disallow になる。
  • * を最初の一致位置で決め打ちすると、/*.pdf$ が /a.pdf.pdf に一致しなくなる。バックトラックするか、一致し得る位置の集合を持つ。
  • パスの大文字小文字は区別される。/Admin/ は Disallow: /admin/ でブロックされない。
  • Python 3.11.9 の urllib.robotparser は先に書いた行が勝ち、* と $ を解釈しない。

robots.txt はクロールを止めるだけで、検索結果から消す手段ではない。サイト内の構造側の検査については 孤立ページを構造的に作らない:sitemapに書く前に一覧から辿れるかを検証する も書いている。

参照した一次情報:

ツール: robots.txt 作成(ハシトシステム)。User-agent・Disallow・Allow・Sitemap を入力して robots.txt を組み立て、指定したパスがクロールされるかの判定と、よくある書き間違いの指摘まで出す。処理はブラウザ内で完結する。


robots.txtのAllowとDisallowの優先順位は*も長さに数える
https://blog.hashito.biz/2026/10/22/robots-txt-allow-disallow-longest-match-wildcard-length-rfc9309/
著者
hashito
作成日
2026年10月22日
著作権