🤖

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 サービスを開発する際は、技術的な実現可能性だけでなく、コンテンツ所有者の意図を尊重し、エコシステム全体にとってプラスになる設計 を心がけることが重要です。


参考リンク

関連記事

Discussion