블로그를 돌아다니다 보면 글 아래에 이런 게 붙어있는 걸 가끔 본다.
GPTBot 1,204회, ClaudeBot 883회, PerplexityBot 47회.
사람이 아니라 봇이 다녀간 횟수를 세어 보여주는 카운터이다. 처음 봤을 때는 뭔가 대단한 걸 붙여놨나 싶었는데 알고 보니 별 것 없었다.
이 AI 크롤러 카운터가 어떤 원리인지 그리고 내 블로그에는 어떻게 다는지 알아보도록 하자.
구글 애널리틱스로는 셀 수 없다
구글 애널리틱스로 세면 되지 않나 싶은데 안 된다.
GA는 브라우저에서 자바스크립트가 실행되어야 집계가 되는데 AI 크롤러는 대개 HTML만 받아가고 스크립트는 실행하지 않는다고 한다.
Googlebot처럼 렌더링까지 하는 봇도 있긴 하다. 그런데 GA가 알려진 봇 트래픽을 걸러내기 때문에 어느 쪽이든 대시보드에는 잡히지 않는다고 한다.
그러니 요청이 도착하는 서버에서 바로 세야한다.
다행히 봇은 자기 이름을 스스로 밝힌다고 한다. HTTP 요청 헤더의 User-Agent에 이렇게 적어서 보낸다.
Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko); compatible; GPTBot/1.1; +https://openai.com/gptbot
그래서 카운터는 문자열 검색 한줄로 만들 수 있다.
서버 로그가 있는 경우
nginx 접근 로그가 있다면 쉽게 끝낼 수 있다.
grep -ohiE 'GPTBot|ClaudeBot|PerplexityBot|Googlebot|bingbot|Amazonbot|Bytespider|CCBot|meta-externalagent' /var/log/nginx/access.log | tr 'A-Z' 'a-z' | sort | uniq -c | sort -rn
출력이 곧 카운터이기 때문이다.
1204 gptbot
883 claudebot
47 perplexitybot
이제 이 숫자를 페이지에 띄우면 된다. 실시간일 필요는 없으니 하루 한 번 JSON으로 떨궈두면 충분할 것 같다.
위 명령 뒤에 awk를 하나 붙여서 스크립트 파일로 저장하면 된다.
#!/bin/sh
grep -ohiE 'GPTBot|ClaudeBot|PerplexityBot|Googlebot|bingbot|Amazonbot|Bytespider|CCBot|meta-externalagent' /var/log/nginx/access.log \
| tr 'A-Z' 'a-z' | sort | uniq -c | sort -rn \
| awk 'BEGIN{print "{"} {printf "%s \"%s\": %d", sep, $2, $1; sep=",\n"} END{print "\n}"}' \
> /var/www/html/bots.json
이걸 count-bots.sh로 저장하고 cron에는 스크립트 경로만 적는다.
crontab 안에서는 %가 줄바꿈으로 해석되기 때문에 awk 포맷 문자열을 크론 줄에 직접 쓰면 명령이 통째로 깨진다고 한다.
0 4 * * * /usr/local/bin/count-bots.sh
페이지에는 이것만 붙이면 된다.
<ul id="bots"></ul>
<script>
fetch('/bots.json').then(r => r.json()).then(d =>
document.getElementById('bots').innerHTML = Object.entries(d)
.sort((a, b) => b[1] - a[1])
.map(([name, n]) => `<li>${name} — ${n.toLocaleString()}회</li>`).join('')
);
</script>
서버 로그가 없는 경우
GitHub Pages나 Netlify 같은 정적 호스팅에는 들여다볼 접근 로그가 없다. 이 경우 요청을 가로챌 자리를 하나 만들어야한다.
Cloudflare를 쓰고 있다면 Worker 하나로 만들 수 있다.
export default {
async fetch(req, env) {
const ua = req.headers.get('user-agent') || '';
const bot = ua.match(/GPTBot|ClaudeBot|PerplexityBot|Googlebot|bingbot|CCBot/i);
if (bot) {
const key = bot[0].toLowerCase();
const n = Number(await env.BOTS.get(key)) || 0;
await env.BOTS.put(key, n + 1);
}
return fetch(req);
}
};
한 가지 알아둘 점. KV를 읽고 다시 쓰는 사이에 다른 요청이 끼어들면 숫자를 하나씩 놓친다고 한다.
정확한 값을 원하면 D1이나 Durable Object를 써야한다는데 그러면 너무 복잡해진다. 봇이 대충 몇 번 다녀갔는지 보려는 것이니 몇 개 새는건 넘어가자.
이 숫자를 믿어도 되는가
User-Agent는 그냥 자기소개라서 위조를 막을 방법이 없다.
진짜 Googlebot인지 확인하려면 접속 IP를 역방향 DNS 조회(reverse DNS lookup)해서 googlebot.com으로 풀리는지 보고 그 도메인을 다시 정방향으로 조회해 원래 IP와 일치하는지까지 봐야한다고 한다. OpenAI나 Anthropic처럼 크롤러 IP 대역을 JSON으로 공개하는 곳도 있어서 그걸 대조하는 방법도 있다.
그런데 이걸 붙이는 순간 더 이상 간단한 카운터가 아니게 된다. 위조해서 남이 얻을 게 딱히 없는 숫자이니 그냥 자칭을 믿기로 하자.
목록에 없는 봇이 안 잡히는 문제도 있다. 크롤러 이름은 계속 늘어나기 마련이라 한 번씩은 정규식 없이 넓게 훑어봐야한다.
grep -ohiE '[a-z0-9_-]*(bot|crawler|spider)[a-z0-9_/.-]*' /var/log/nginx/access.log | tr 'A-Z' 'a-z' | sort | uniq -c | sort -rn | head -30
robots.txt 요청만 세는 최소 버전
정말 최소한만 하고 싶다면 이런 방법도 있다.
예의 있는 크롤러는 사이트를 긁기 전에 robots.txt를 먼저 읽는다고 한다. 그러니 robots.txt 요청만 세도 누가 다녀갔는지 그림은 나온다.
필터 한 줄이 늘어나는 대신 처리할 로그 양이 확 줄어든다.
grep 'GET /robots.txt' /var/log/nginx/access.log | grep -ohiE 'GPTBot|ClaudeBot|PerplexityBot|Googlebot|bingbot|CCBot' | sort | uniq -c | sort -rn
결론
나도 하나 달아봐야겠다.
티스토리에 다는 법부터 또 알아봐야지.