AI要約サービスで robots.txt を尊重する実装パターン

はじめに
生成 AI の普及により、Web サイトのコンテンツが AI の学習データとして無断で利用されることへの懸念が高まっています。多くのメディアサイトは robots.txt で AI クローラーをブロックしていますが、これは同時に AI を使ったサービスがそのコンテンツを処理してよいか の指標にもなります。
この記事では、read-it-later アプリ「Tuck」で実装した、robots.txt を活用した AI 処理の倫理的判断 について解説します。
TL;DR
- robots.txt でAIクローラーをブロック = 「AI処理お断り」の意思表示
- 既知ドメインリスト: 日経、NYTimes 等のメディアは事前にブロック判定(高速化)
- Cache API: robots.txt を1時間キャッシュして無駄なリクエストを削減
- 透明性: AI処理できない理由をユーザーに明示する
なぜ robots.txt を確認するのか
背景
Tuck には保存した記事を AI で要約する機能があります。しかし、すべての記事を無条件に AI 処理するのは問題があります:
| 懸念点 | 説明 |
|---|---|
| 著作権 | コンテンツ所有者の意図を無視した処理 |
| 利用規約 | サイトの ToS に違反する可能性 |
| エシカル | AI 利用を明示的に拒否しているサイトへの配慮 |
AI 学習禁止の流れ
最近では TEGAKI のような AI 学習を明確に禁止するプラットフォーム も登場しています。クリエイターの作品を AI の学習データとして無断利用することへの反発は強く、サービス開発者はこうした 意思を尊重する必要があります。
read-it-later アプリとしての立場
Tuck のような read-it-later アプリは、本質的に 元サイトへのトラフィックを促進する サービスです:
- ユーザーは記事を「あとで読む」ために保存する
- 実際に読む際は元サイトにアクセスする
- AI 要約はあくまで補助機能であり、元コンテンツの代替ではない
この立場を踏まえ、Tuck では コンテンツ所有者の意図を最大限尊重 しつつ、ユーザーの利便性を提供しています。
robots.txt が示す意図
サイトが robots.txt で AI クローラー(GPTBot, Claude-Web など)をブロックしている場合、それは 「AI にコンテンツを利用されたくない」 という明確な意思表示です。
実例: メディアサイトの robots.txt
実際に AI クローラーをブロックしているサイトの例を見てみましょう。
日経新聞(nikkei.com)
User-agent: GPTBot
Disallow: /
User-agent: ChatGPT-User
Disallow: /
User-agent: Google-Extended
Disallow: /
User-agent: CCBot
Disallow: /
New York Times(nytimes.com)
User-agent: GPTBot
Disallow: /
User-agent: ChatGPT-User
Disallow: /
User-agent: CCBot
Disallow: /
User-agent: Google-Extended
Disallow: /
User-agent: anthropic-ai
Disallow: /
このように、主要メディアは AI クローラーを明確にブロックしています。
AI クローラーの種類
主要な AI クローラーの User-Agent を以下にまとめます:
const AI_USER_AGENTS = [
// OpenAI
'GPTBot',
'ChatGPT-User',
// Anthropic
'anthropic-ai',
'Claude-Web',
// Common Crawl (多くのAIモデルのデータソース)
'CCBot',
// Google AI
'Google-Extended',
// ByteDance
'Bytespider',
// Amazon
'Amazonbot',
// Meta
'FacebookBot',
// Apple
'Applebot-Extended',
// Perplexity
'PerplexityBot',
// その他
'Diffbot',
'Omgilibot',
'Omgili',
'YouBot',
'Scrapy',
'PetalBot',
'img2dataset',
'Ai2Bot',
'cohere-ai',
]
実装: robots.txt の取得とパース
キャッシュ付き取得処理
毎回 robots.txt を取得するとパフォーマンスに影響します。Cloudflare Workers では Cache API を使って効率化できます:
// キャッシュの有効期限(1時間)
const CACHE_TTL = 60 * 60
async function fetchRobotsTxt(url: string): Promise<string | null> {
try {
const parsedUrl = new URL(url)
const robotsUrl = `${parsedUrl.protocol}//${parsedUrl.host}/robots.txt`
// Cache API でキャッシュを確認
const cache = caches.default
const cacheKey = new Request(robotsUrl)
const cachedResponse = await cache.match(cacheKey)
if (cachedResponse) {
return await cachedResponse.text()
}
// キャッシュがなければ取得
const response = await fetch(robotsUrl, {
headers: { 'User-Agent': 'TuckBot/1.0' },
})
if (!response.ok) {
// 404 などもキャッシュして無駄なリクエストを防ぐ
const emptyResponse = new Response('', {
headers: { 'Cache-Control': `max-age=${CACHE_TTL}` },
})
await cache.put(cacheKey, emptyResponse)
return null
}
const text = await response.text()
// レスポンスをキャッシュ
const cachedRes = new Response(text, {
headers: { 'Cache-Control': `max-age=${CACHE_TTL}` },
})
await cache.put(cacheKey, cachedRes)
return text
} catch (error) {
console.error(`Failed to fetch robots.txt for ${url}:`, error)
return null
}
}
シンプル版(キャッシュなし)
開発環境やシンプルな実装の場合:
async function fetchRobotsTxtSimple(url: string): Promise<string | null> {
try {
const parsedUrl = new URL(url)
const robotsUrl = `${parsedUrl.protocol}//${parsedUrl.host}/robots.txt`
const response = await fetch(robotsUrl, {
headers: { 'User-Agent': 'TuckBot/1.0' },
})
if (!response.ok) return null
return await response.text()
} catch {
return null
}
}
パース処理
function parseRobotsTxt(
content: string,
userAgents: string[],
): { disallowed: boolean; rules: string[] } {
const lines = content.split('\n')
let currentUserAgent = ''
let isRelevantSection = false
const disallowedPaths: string[] = []
for (const line of lines) {
const trimmed = line.trim().toLowerCase()
if (trimmed.startsWith('user-agent:')) {
currentUserAgent = trimmed.replace('user-agent:', '').trim()
// 該当するUser-Agentセクションか、または全体(*)か
isRelevantSection =
currentUserAgent === '*' ||
userAgents.some((ua) =>
currentUserAgent.includes(ua.toLowerCase())
)
} else if (isRelevantSection && trimmed.startsWith('disallow:')) {
const path = trimmed.replace('disallow:', '').trim()
if (path === '/' || path === '/*') {
return { disallowed: true, rules: [path] }
}
disallowedPaths.push(path)
}
}
return { disallowed: false, rules: disallowedPaths }
}
パフォーマンス最適化: 既知のブロックドメイン
毎回 robots.txt を取得すると遅延が発生します。よく知られた AI ブロックサイトはハードコードしておくことで高速化できます:
const KNOWN_AI_BLOCKED_DOMAINS = [
// 日本のメディア
'nikkei.com',
'asahi.com',
'yomiuri.co.jp',
'mainichi.jp',
'sankei.com',
'nhk.or.jp',
'toyokeizai.net',
'diamond.jp',
'president.jp',
'bunshun.jp',
// 国際メディア
'nytimes.com',
'washingtonpost.com',
'wsj.com',
'bloomberg.com',
'reuters.com',
'bbc.com',
'theguardian.com',
'cnn.com',
// テック系
'wired.com',
'wired.jp',
'techcrunch.com',
'theverge.com',
]
function isKnownBlockedDomain(url: string): boolean {
try {
const hostname = new URL(url).hostname.toLowerCase()
return KNOWN_AI_BLOCKED_DOMAINS.some(
(domain) => hostname === domain || hostname.endsWith(`.${domain}`)
)
} catch {
return false
}
}
判定フロー
AI 処理の可否判定は以下のフローで行います:
このフローにより、高速なチェック(既知ドメイン)を先に行い、必要な場合のみ robots.txt を取得します。
統合: AI 処理の可否判定
export async function canProcessWithAI(url: string): Promise<{
allowed: boolean
reason: string
}> {
// 1. 既知のブロックドメインをチェック(高速)
if (isKnownBlockedDomain(url)) {
return {
allowed: false,
reason: 'Known AI-blocked domain',
}
}
// 2. robots.txt を取得してチェック
const robotsTxt = await fetchRobotsTxt(url)
if (!robotsTxt) {
// robots.txt がない場合は許可とみなす
return { allowed: true, reason: 'No robots.txt found' }
}
const result = parseRobotsTxt(robotsTxt, AI_USER_AGENTS)
if (result.disallowed) {
return {
allowed: false,
reason: 'Disallowed by robots.txt',
}
}
return { allowed: true, reason: 'Allowed by robots.txt' }
}
ユーザーへのフィードバック
AI 処理ができない場合は、ユーザーに理由を説明することが重要です:
// API レスポンス例
app.post('/api/articles/:id/summarize', async (c) => {
const article = await getArticle(c.req.param('id'))
const { allowed, reason } = await canProcessWithAI(article.url)
if (!allowed) {
return c.json({
error: 'AI processing not available',
reason: reason,
message: 'このサイトはAIによるコンテンツ処理を許可していません',
}, 403)
}
// AI 処理を実行
const summary = await generateSummary(article)
return c.json({ summary })
})
フロントエンドでの表示例
React でユーザーに適切なフィードバックを表示する例:
function ArticleSummary({ article }: { article: Article }) {
const { mutate: summarize, isPending, error } = useMutation({
mutationFn: () => api.summarizeArticle(article.id),
})
// AI処理が許可されていない場合のエラー
if (error?.status === 403) {
return (
<div className="p-4 bg-yellow-50 rounded-lg">
<p className="text-sm text-yellow-800">
⚠️ このサイトはAI要約に対応していません
</p>
<p className="text-xs text-yellow-600 mt-1">
サイト運営者がAIによるコンテンツ処理を許可していないため、
要約機能はご利用いただけません。
</p>
</div>
)
}
return (
<button
onClick={() => summarize()}
disabled={isPending}
className="btn-primary"
>
{isPending ? '要約中...' : 'AIで要約する'}
</button>
)
}
まとめ
robots.txt を活用した AI 処理のエシカル対応についてまとめます。
| ポイント | 説明 |
|---|---|
| 意図の尊重 | robots.txt は AI 利用に対するサイト側の意思表示 |
| 既知ドメイン | よく知られたブロックサイトは事前にリスト化して高速化 |
| 透明性 | 処理できない理由をユーザーに明示する |
| 段階的チェック | 高速なチェック → 詳細なチェックの順で効率化 |
read-it-later アプリの価値
Tuck のような read-it-later アプリは、AI 要約機能を持ちながらも 元サイトへのトラフィックを促進する という本質的な価値を持っています。ユーザーが保存した記事は最終的に元サイトで読まれるため、コンテンツ所有者 にとってもメリットがあります。
AI サービスを開発する際は、技術的な実現可能性だけでなく、コンテンツ所有者の意図を尊重し、エコシステム全体にとってプラスになる設計 を心がけることが重要です。
参考リンク
関連記事
- 自分用に作ったRead-it-laterアプリをProduct Huntに出してみた【Tuck開発記】 - Tuck の開発背景と技術スタックについて
Discussion