# Mesurer les visites des robots d'IA dans les logs nginx et Apache - grep, awk et GoAccess

> Compter les visites de GPTBot, ClaudeBot, PerplexityBot et autres robots d'IA dans les logs nginx ou Apache, par robot et par page, avec un script complet.

Source : https://iabig.com/robots-ia/mesurer-visites-robots-ia-logs-nginx-apache/ — Guides CC BY 4.0, extraits de code MIT

Les outils d'audience fondés sur un script JavaScript ne voient que les visiteurs qui exécutent ce script. Les robots d'IA qui ne l'exécutent pas, comme CCBot selon Common Crawl, n'y apparaissent jamais. Pour savoir qui lit réellement votre site, quelles pages et à quel rythme, la source fiable reste le journal d'accès du serveur web. Ce guide part du format de log standard, donne les commandes grep, awk et GoAccess utiles, puis un script complet qui produit un résumé par robot.

## Le format de log à connaître

nginx utilise par défaut le format « combined », qu'Apache fournit aussi sous le même nom. Une ligne ressemble à ceci :

```text
20.171.206.5 - - [24/Oct/2026:10:00:01 +0200] "GET /guide/ HTTP/1.1" 200 5120 "-" "Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko); compatible; GPTBot/1.4; +https://openai.com/gptbot"
```

Dans nginx, ce format est prédéfini sous le nom `combined` et c'est celui qu'utilise `access_log` quand aucun format n'est précisé :

```nginx
# Définition intégrée à nginx (inutile de la recopier)
log_format combined '$remote_addr - $remote_user [$time_local] '
                    '"$request" $status $body_bytes_sent '
                    '"$http_referer" "$http_user_agent"';
```

Dans Apache, le format porte le même nom ; vérifiez que votre `CustomLog` l'utilise bien, et non `common` qui n'enregistre pas le User-Agent :

```apache
LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-agent}i\"" combined
CustomLog ${APACHE_LOG_DIR}/access.log combined
```

Emplacements habituels : `/var/log/nginx/access.log` pour nginx, `/var/log/apache2/access.log` sur Debian et Ubuntu, `/var/log/httpd/access_log` sur les distributions dérivées de Red Hat. Les journaux plus anciens sont souvent compressés (`access.log.2.gz`) : `zcat -f` lit indifféremment les fichiers compressés et non compressés.

Le découpage sur le guillemet est la clé des commandes awk qui suivent. Avec `awk -F'"'`, le champ `$2` contient la requête (`GET /guide/ HTTP/1.1`), `$3` le code HTTP et la taille, `$6` le User-Agent.

## Les robots à rechercher

Les jetons ci-dessous sont ceux que publient les éditeurs. Ils apparaissent dans l'en-tête User-Agent des requêtes, à l'exception de `Google-Extended` et `Applebot-Extended`, qui ne sont que des jetons robots.txt : vous ne les verrez jamais dans un journal.

| Famille | Jetons à chercher |
|---|---|
| Entraînement | `GPTBot`, `ClaudeBot`, `CCBot`, `meta-externalagent`, `MistralAI-Training`, `Amazonbot`, `Bytespider` |
| Recherche et indexation | `OAI-SearchBot`, `Claude-SearchBot`, `PerplexityBot`, `Applebot`, `Amzn-SearchBot`, `meta-webindexer`, `MistralAI-Index` |
| À la demande d'un utilisateur | `ChatGPT-User`, `Claude-User`, `Perplexity-User`, `MistralAI-User`, `meta-externalfetcher`, `Amzn-User`, `Google-Agent` |

Les numéros de version changent (`GPTBot/1.4` aujourd'hui) : cherchez toujours le jeton, jamais la chaîne complète.

## Commandes rapides

```bash
LOG=/var/log/nginx/access.log
BOTS='GPTBot|OAI-SearchBot|ChatGPT-User|ClaudeBot|Claude-SearchBot|Claude-User|PerplexityBot|Perplexity-User|CCBot|Bytespider|meta-externalagent|meta-externalfetcher|meta-webindexer|Applebot|Amazonbot|Amzn-SearchBot|Amzn-User|MistralAI-User|MistralAI-Index|MistralAI-Training|Google-Agent'

# Nombre total de requêtes de robots d'IA
grep -ciE "$BOTS" "$LOG"

# Requêtes par robot, journaux archivés compris
# (grep -o compterait aussi les URL de documentation contenues dans le User-Agent :
#  on ne garde que la première occurrence dans le champ User-Agent ; GNU awk requis)
zcat -f /var/log/nginx/access.log* \
  | awk -F'"' -v bots="$BOTS" 'BEGIN { IGNORECASE = 1 } match($6, bots) { print substr($6, RSTART, RLENGTH) }' \
  | sort | uniq -c | sort -rn

# Les 20 pages les plus lues par GPTBot (paramètres d'URL retirés)
awk -F'"' 'tolower($6) ~ /gptbot/ { split($2, r, " "); sub(/\?.*/, "", r[2]); print r[2] }' "$LOG" \
  | sort | uniq -c | sort -rn | head -20

# Codes HTTP renvoyés à ClaudeBot (repérer les 403, 404 et 5xx)
awk -F'"' '$6 ~ /ClaudeBot/ { split($3, s, " "); print s[1] }' "$LOG" | sort | uniq -c | sort -rn

# Requêtes par jour pour OAI-SearchBot
awk -F'"' '$6 ~ /OAI-SearchBot/ { print substr($1, index($1, "[") + 1, 11) }' "$LOG" | uniq -c

# Qui lit robots.txt, et avec quel code de réponse
awk -F'"' '$2 ~ /^GET \/robots\.txt/ { split($3, s, " "); print s[1], $6 }' "$LOG" \
  | grep -iE "$BOTS" | sort | uniq -c | sort -rn

# Volume transféré (octets) par robot
awk -F'"' -v bots="$BOTS" 'BEGIN { IGNORECASE = 1 }
  match($6, bots) { split($3, s, " "); o[substr($6, RSTART, RLENGTH)] += s[2] }
  END { for (b in o) printf "%12d  %s\n", o[b], b }' "$LOG" | sort -rn
```

Les commandes qui utilisent `IGNORECASE` exigent GNU awk (`gawk`). Avec un autre awk, retirez le bloc `BEGIN` et respectez la casse des jetons.

La commande sur robots.txt est utile pour contrôler vos réglages : un robot qui reçoit une erreur 5xx sur ce fichier doit, selon la RFC 9309, considérer tout le site comme interdit.

## Isoler les robots d'IA dans un journal dédié

Plutôt que de filtrer a posteriori, vous pouvez demander au serveur d'écrire ces requêtes dans un fichier séparé.

Avec nginx, dans le bloc `http` :

```nginx
map $http_user_agent $robot_ia {
    default 0;
    "~*(GPTBot|OAI-SearchBot|ChatGPT-User|ClaudeBot|Claude-SearchBot|Claude-User|PerplexityBot|Perplexity-User|CCBot|Bytespider|meta-external|meta-webindexer|Amazonbot|Amzn-|MistralAI-|Google-Agent|Applebot)" 1;
}
```

Puis dans le bloc `server` :

```nginx
# access_log dans server remplace les directives héritées : on garde le journal principal.
access_log /var/log/nginx/access.log combined;
access_log /var/log/nginx/robots-ia.log combined if=$robot_ia;
```

Avec Apache (modules `mod_setenvif` et `mod_log_config`) :

```apache
SetEnvIfNoCase User-Agent "(GPTBot|OAI-SearchBot|ChatGPT-User|ClaudeBot|Claude-SearchBot|Claude-User|PerplexityBot|Perplexity-User|CCBot|Bytespider|meta-external|meta-webindexer|Amazonbot|Amzn-|MistralAI-|Google-Agent|Applebot)" robot_ia
CustomLog ${APACHE_LOG_DIR}/robots-ia.log combined env=robot_ia
```

Pensez à ajouter ce fichier à la configuration de `logrotate`, comme les autres journaux du serveur.

## GoAccess

GoAccess analyse les journaux au format combined et produit un rapport HTML. Depuis la version 1.12 (septembre 2026), il range les robots d'IA dans une catégorie « AI Crawlers » distincte des moteurs de recherche classiques.

```bash
# Rapport complet
goaccess /var/log/nginx/access.log --log-format=COMBINED -o rapport.html

# Rapport limité aux robots d'IA, journaux compressés compris
zcat -f /var/log/nginx/access.log* \
  | grep -iE 'GPTBot|OAI-SearchBot|ChatGPT-User|ClaudeBot|Claude-SearchBot|Claude-User|PerplexityBot|Perplexity-User|CCBot|Bytespider|meta-external' \
  | goaccess - --log-format=COMBINED -o robots-ia.html
```

Avec une version plus ancienne, déclarez vous-même les jetons dans une liste fournie via `--browsers-file` (un mot, une ou plusieurs tabulations, puis la catégorie) :

```bash
printf '%s\t\tCrawlers\n' GPTBot OAI-SearchBot ChatGPT-User ClaudeBot Claude-SearchBot Claude-User PerplexityBot Perplexity-User > ia.list
goaccess /var/log/nginx/access.log --log-format=COMBINED --browsers-file=ia.list -o rapport.html
```

Si votre serveur écrit l'hôte virtuel en tête de ligne, utilisez `VCOMBINED` au lieu de `COMBINED`.

## Script prêt à l'emploi

Ce script lit un ou plusieurs journaux au format combined, compressés ou non, et affiche le nombre de requêtes par robot, les codes HTTP, le nombre d'adresses IP distinctes, l'activité par jour et les pages les plus lues.

```bash
#!/usr/bin/env bash
# robots-ia-logs.sh - résumé des visites de robots d'IA (format « combined »)
# Usage : ./robots-ia-logs.sh /var/log/nginx/access.log*   (fichiers .gz acceptés)
set -euo pipefail
[ "$#" -ge 1 ] || { echo "usage : $0 fichier_log [fichier_log…]" >&2; exit 1; }

# Jetons recherchés dans le User-Agent (insensible à la casse)
BOTS="GPTBot OAI-SearchBot ChatGPT-User ClaudeBot Claude-SearchBot Claude-User PerplexityBot Perplexity-User Google-Agent Applebot Amazonbot Amzn-SearchBot Amzn-User meta-externalagent meta-externalfetcher meta-webindexer CCBot Bytespider MistralAI-User MistralAI-Index MistralAI-Training"

zcat -f -- "$@" | awk -F'"' -v bots="$BOTS" '
BEGIN { n = split(bots, B, " "); for (i = 1; i <= n; i++) L[i] = tolower(B[i]) }
{
  ua = tolower($6); bot = ""
  for (i = 1; i <= n; i++) if (index(ua, L[i])) { bot = B[i]; break }
  if (bot == "") next
  split($2, req, " "); url = req[2]; sub(/\?.*/, "", url)      # chemin sans paramètres
  split($3, st, " "); code = st[1]
  split($1, f, "["); jour = substr(f[2], 1, 11)                  # 25/Oct/2026
  split($1, h, " "); ip = h[1]
  hits[bot]++; codes[bot " " code]++; pages[bot "\t" url]++
  jours[jour " " bot]++; ips[bot " " ip] = 1
}
END {
  print "== Requêtes par robot"
  for (b in hits) printf "%8d  %s\n", hits[b], b | "sort -rn"; close("sort -rn")
  print "\n== Codes HTTP par robot"
  for (k in codes) printf "%8d  %s\n", codes[k], k | "sort -k2,2 -k1,1rn"; close("sort -k2,2 -k1,1rn")
  print "\n== Adresses IP distinctes par robot"
  for (k in ips) { split(k, a, " "); nip[a[1]]++ }
  for (b in nip) printf "%8d  %s\n", nip[b], b | "sort -rn"; close("sort -rn")
  print "\n== Requêtes par jour"
  for (k in jours) printf "%8d  %s\n", jours[k], k | "sort -k2,2 -k1,1rn"; close("sort -k2,2 -k1,1rn")
  print "\n== 20 pages les plus lues (tous robots d IA)"
  for (k in pages) { split(k, a, "\t"); tot[a[2]] += pages[k] }
  for (u in tot) printf "%8d  %s\n", tot[u], u | "sort -rn | head -20"; close("sort -rn | head -20")
}'
```

```bash
chmod +x robots-ia-logs.sh
./robots-ia-logs.sh /var/log/nginx/access.log /var/log/nginx/access.log.*.gz
sudo ./robots-ia-logs.sh /var/log/apache2/access.log*
```

Exemple de sortie sur un petit journal de test :

```text
== Requêtes par robot
       3  GPTBot
       1  PerplexityBot
       1  ClaudeBot
       1  Claude-SearchBot

== Codes HTTP par robot
       1  Claude-SearchBot 200
       1  ClaudeBot 200
       1  GPTBot 200
       1  GPTBot 403
       1  GPTBot 404
       1  PerplexityBot 200
```

Le script compte ce que les requêtes **déclarent**. Les dates sont triées comme du texte, ce qui convient pour un mois donné ; pour une période plus longue, exportez les données vers un tableur ou une base.

## Séparer les résultats robot par robot

Le résumé global mélange des robots aux rôles très différents. Pour comparer ce que lit chaque robot, une boucle suffit : elle produit un classement des pages par robot, ce qui permet de voir si un robot de recherche atteint bien vos contenus importants, ou s'il s'épuise sur des pages de filtres, de pagination ou d'archives.

```bash
for bot in GPTBot OAI-SearchBot ClaudeBot Claude-SearchBot PerplexityBot; do
  echo "== $bot"
  zcat -f /var/log/nginx/access.log* \
    | awk -F'"' -v b="$bot" 'index(tolower($6), tolower(b)) { split($2, r, " "); sub(/\?.*/, "", r[2]); print r[2] }' \
    | sort | uniq -c | sort -rn | head -10
done
```

## Suivre l'évolution dans le temps

Les journaux sont souvent effacés après quelques semaines par `logrotate`. Pour garder une trace, enregistrez chaque nuit un compteur par robot dans un fichier CSV, à partir du journal de la veille (ici `access.log.1`, nom habituel après rotation quotidienne) :

```bash
# /etc/cron.d/robots-ia : chaque jour à 00 h 30
30 0 * * * root awk -F'"' 'BEGIN { IGNORECASE = 1; d = strftime("\%Y-\%m-\%d", systime() - 86400) } match($6, /GPTBot|OAI-SearchBot|ChatGPT-User|ClaudeBot|Claude-SearchBot|Claude-User|PerplexityBot|Perplexity-User|CCBot/) { n[substr($6, RSTART, RLENGTH)]++ } END { for (b in n) print d ";" b ";" n[b] }' /var/log/nginx/access.log.1 >> /var/log/robots-ia.csv
```

Dans un fichier cron, le caractère `%` doit être échappé, d'où les `\%`. Les fonctions `strftime` et `systime` sont, elles aussi, propres à GNU awk. Le fichier obtenu s'ouvre dans n'importe quel tableur : une colonne date, une colonne robot, une colonne nombre de requêtes.

Que regarder en priorité ? Trois signaux suffisent le plus souvent. Les robots de recherche (`OAI-SearchBot`, `Claude-SearchBot`, `PerplexityBot`) doivent apparaître régulièrement si vous les autorisez ; leur absence durable mérite de vérifier robots.txt et le pare-feu. Les agents à la demande (`ChatGPT-User`, `Claude-User`, `Perplexity-User`) montrent quelles pages sont consultées au moment où un utilisateur pose une question. Enfin, un robot que vous avez interdit dans robots.txt et qui continue de lire d'autres pages que `/robots.txt` doit être vérifié : soit il s'agit d'un imitateur, soit la règle ne le vise pas correctement.

## Interpréter les chiffres avec prudence

- **Usurpations** : un User-Agent se falsifie facilement. Une IP qui se dit GPTBot mais n'appartient pas aux plages d'OpenAI n'est pas GPTBot. Croisez la liste des adresses avec les fichiers JSON officiels, comme expliqué dans le guide [Vérifier qu'un robot GPTBot, ClaudeBot ou PerplexityBot est authentique](/robots-ia/verifier-authenticite-gptbot-claudebot-perplexitybot/).
- **CDN et proxy** : derrière un CDN, la première colonne contient l'adresse du proxy. Activez le module `realip` de nginx ou `mod_remoteip` d'Apache pour journaliser l'IP du client, ou utilisez les journaux du CDN.
- **Explorations mutualisées** : OpenAI indique qu'une même exploration peut servir à la fois à `GPTBot` et à `OAI-SearchBot` si les deux sont autorisés. L'absence de l'un ne signifie donc pas que vos pages sont ignorées.
- **Visites à la demande** : une requête `ChatGPT-User`, `Claude-User` ou `Perplexity-User` correspond en général à la question d'un utilisateur. C'est l'indicateur le plus proche d'une lecture réelle de votre contenu par un assistant, mais il ne dit rien du nombre de personnes qui verront la réponse.
- **Codes d'erreur** : un taux élevé de 403 ou 429 pour un robot de recherche signale souvent un pare-feu ou une limite de débit qui le bloque à votre insu.

Pour vérifier votre site, utilisez le [scanner IABIG](/).

## Sources officielles

- nginx, module de journalisation : <https://nginx.org/en/docs/http/ngx_http_log_module.html>
- nginx, module map : <https://nginx.org/en/docs/http/ngx_http_map_module.html>
- Apache, mod_log_config : <https://httpd.apache.org/docs/2.4/mod/mod_log_config.html>
- Apache, mod_setenvif : <https://httpd.apache.org/docs/2.4/mod/mod_setenvif.html>
- GoAccess, manuel et notes de version 1.12 : <https://goaccess.io/man> et <https://github.com/allinurl/goaccess/blob/master/ChangeLog>
- OpenAI, Overview of OpenAI Crawlers : <https://developers.openai.com/api/docs/bots>
- Anthropic, Does Anthropic crawl data from the web : <https://support.claude.com/en/articles/8896518>
- Perplexity, Perplexity Crawlers : <https://docs.perplexity.ai/docs/resources/perplexity-crawlers>
- Common Crawl, FAQ (exécution de JavaScript) : <https://commoncrawl.org/faq>
- Google, Google-Extended : <https://developers.google.com/crawling/docs/crawlers-fetchers/google-common-crawlers>
- Google, fetchers déclenchés par l'utilisateur (Google-Agent) : <https://developers.google.com/crawling/docs/crawlers-fetchers/google-user-triggered-fetchers>
- Apple, About Applebot : <https://support.apple.com/en-us/119829>
