Nginx 로그에서 AI 크롤러 요청 세기
GPTBot·ClaudeBot·PerplexityBot 같은 AI 봇이 얼마나 왔는지 User-Agent 로 세고, robots.txt 응답과 공식 IP 목록으로 진짜인지 대조하는 명령입니다.
접속 로그의 마지막 따옴표 칸이 User-Agent 이고, AI 봇은 거기에 이름을 남깁니다. 아래 출력은 2026-10-17 하루치로 직접 만든 combined 로그(197줄, 클라이언트는 문서용 주소 203.0.113.x·198.51.100.x·192.0.2.x)에 돌린 결과입니다. 경로는 access.log 로 줄였고, 운영 서버에서는 보통 /var/log/nginx/access.log 입니다. 상태 코드·경로를 awk 로 세는 기본 방법은 이 글과 같습니다.
[1] 먼저 봇 이름을 정합니다. 공식 문서에 나온 User-Agent 이름은 아래와 같습니다.
| 회사 | 이름 | 용도(공식 설명) |
|---|---|---|
| OpenAI | GPTBot | 생성형 AI 모델 학습에 쓸 수 있는 콘텐츠 수집 |
| OpenAI | OAI-SearchBot | ChatGPT 검색 결과에 사이트 노출 |
| OpenAI | ChatGPT-User | 사용자가 시킨 동작(페이지 열기 등) · 자동 크롤링 아님 |
| Anthropic | ClaudeBot | 모델 개발용 웹 콘텐츠 수집 |
| Anthropic | Claude-User | 사용자 질문에 답하려고 페이지를 가져옴 |
| Anthropic | Claude-SearchBot | 검색 결과 품질 개선 |
| Perplexity | PerplexityBot · Perplexity-User | 각각 검색 색인 · 사용자 요청 |
[2] User-Agent 칸에서 이름만 뽑아 셉니다. -F'"' 로 나누면 6번째 칸이 User-Agent 입니다.
$ awk -F'"' '{print $6}' access.log | grep -oE '(GPTBot|OAI-SearchBot|ChatGPT-User|ClaudeBot|Claude-User|Claude-SearchBot|PerplexityBot|Perplexity-User)' | sort | uniq -c | sort -rn 79 GPTBot 18 ClaudeBot 11 OAI-SearchBot 9 PerplexityBot 7 Claude-SearchBot 5 ChatGPT-User 4 Perplexity-User 4 Claude-User[3] 그 봇이 무엇을 가져갔는지 상태 코드와 경로로 봅니다. 요청 줄은 2번째 칸, 상태 코드는 3번째 칸의 첫 단어입니다.
$ awk -F'"' -v bots='GPTBot|OAI-SearchBot|ChatGPT-User|ClaudeBot|Claude-User|Claude-SearchBot|PerplexityBot|Perplexity-User' '$6 ~ bots {split($2,r," "); split($3,s," "); print s[1], r[2]}' access.log | sort | uniq -c | sort -rn | head 82 200 /posts/a/ 30 200 /posts/b/ 11 200 /robots.txt 9 200 /posts/c/ 3 404 /posts/b/ 2 404 /llms.txt/llms.txt 에 404 가 두 번 보입니다. 그 파일을 만들지 않았다는 뜻일 뿐, 봇이 그 파일을 따른다는 증거는 아닙니다. 이 글에서는 효과를 판단하지 않습니다.
[4] /robots.txt 요청만 따로 셉니다. 봇은 보통 글을 가져오기 전에 이 파일을 먼저 읽으므로, 200 이 나오는지 확인합니다.
$ awk -F'"' -v bots='GPTBot|OAI-SearchBot|ChatGPT-User|ClaudeBot|Claude-User|Claude-SearchBot|PerplexityBot|Perplexity-User' '$6 ~ bots {split($2,r," "); if (r[2]=="/robots.txt") {split($3,s," "); print s[1]}}' access.log | sort | uniq -c 11 200[5] 같은 이름이 어느 IP 에서 오는지 셉니다.
$ awk -F'"' -v bots='GPTBot|OAI-SearchBot|ChatGPT-User|ClaudeBot|Claude-User|Claude-SearchBot|PerplexityBot|Perplexity-User' '$6 ~ bots {split($1,a," "); print a[1]}' access.log | sort | uniq -c | sort -rn 54 203.0.113.10 25 198.51.100.77 18 203.0.113.13 11 203.0.113.11 9 203.0.113.14 7 203.0.113.16 5 203.0.113.12 4 203.0.113.17 4 203.0.113.15GPTBot 이 보낸 79건이 IP 두 개로 나뉩니다(54 + 25). User-Agent 는 누구나 적을 수 있는 문자열이라, 이름만으로는 진짜 봇인지 알 수 없습니다.
[6] 공식 IP 목록과 대조합니다. OpenAI 는 봇마다 JSON 을 공개하고(https://openai.com/gptbot.json · searchbot.json · chatgpt-user.json), Anthropic 은 https://claude.com/crawling/bots.json, Perplexity 는 https://www.perplexity.com/perplexitybot.json 과 perplexity-user.json 을 공개합니다. 내려받은 gptbot.json 은 prefixes 배열에 ipv4Prefix 가 들어 있는 모양이었습니다.
$ curl -sL https://openai.com/gptbot.json -o gptbot.json$ jq -r '.prefixes[]|.ipv4Prefix // .ipv6Prefix' gptbot.json > gptbot.cidr$ wc -l gptbot.cidr18 gptbot.cidrCIDR 포함 여부는 awk 로 하기 번거로워 Python 표준 라이브러리로 확인합니다. 아래 chk.py 는 첫 인자로 CIDR 파일을, 표준입력으로 uniq -c 출력(「개수 IP」 두 칸)을 받습니다.
import sys, ipaddressnets=[ipaddress.ip_network(l.strip()) for l in open(sys.argv[1]) if l.strip()]for line in sys.stdin: count, ip = line.split() ok=any(ipaddress.ip_address(ip) in n for n in nets) print(count, ip, 'listed' if ok else 'NOT listed')$ awk -F'"' '$6 ~ /GPTBot/ {split($1,a," "); print a[1]}' access.log | sort | uniq -c | sort -rn | python3 chk.py gptbot.cidr54 203.0.113.10 NOT listed25 198.51.100.77 NOT listed1 20.171.206.5 listedNginx 로그를 브라우저에서 바로 분석 ↗ — 아무것도 올라가지 않습니다.