Knowledge Base

お知らせや身辺のことを綴っています。
目次
検索エンジンのBotについて考える

検索エンジンのBotについて考える

世の中は大スクレイピング時代。

検索エンジンのBotについて考える

近頃Botのクローリングが激しく、かなりの確率で503エラーになる現象が起こる気がする。今朝もウェブサイトが死んでいて、原因調査のためにアクセスログを見ていたら大層びっくりさせられた。とくに目を引くものはMJ12botという特定のクローラーで、このクローラによるアクセスを抽出すると、大量に意味のない文字列でクローリングをしているようだった。(これが昨日だけで588件ある)

これまで検索エンジンなどのBotのクローリングに関しては何も手を打ってこなかったが、まずは対策の第一歩として、どんなクローラーがこのサイトを訪れているのかについて調べることにした。

クローラーの種類と訪問回数の調査

6年間分のアクセスログはさすがに残していないので、別の手段を用いて調査を行う必要がある。そこで、今回は筆者が利用しているアクセス解析ツールのAWStatsのキャッシュを使うことができそうだったので、それらをスクレイピングすることで調べてみることにした。期間は2018年の1月1日から2024年4月19日までの6年弱で、ページ自体はシェルスクリプトでダウンロードし、HTMLのテーブルから情報を抜き出すために、手慣れているPHPで情報を抜き出して集計した。ソースコードは以下。

ソースコード
<?php

declare(strict_types=1);

class Robot
{
    public string $type;
    public int $count;
    public float $intensity;
    public string $lastAccessed;

    public function __construct(string $type, int $count, float $intensity, string $lastAccessed)
    {
        $this->type = $type;
        $this->count = $count;
        $this->intensity = $intensity;
        $this->lastAccessed = $lastAccessed;
    }
}

class RobotsMonth
{
    public int $year;
    public int $month;
    public RobotsTypeMonth $robots;

    public function __construct(string $markup, int $year, int $month)
    {
        $this->year = $year;
        $this->month = $month;
        $this->robots = new RobotsTypeMonth($markup);
    }
}


/**
 * A class that represents the variation of robots in a month
 */
class RobotsTypeMonth extends ArrayObject
{
    public function __construct(string $markup)
    {
        $this->aggragateRobots($this->processMarkup($markup));
    }

    public function processMarkup(string $markup): array
    {
        $xml = new SimpleXMLElement($markup, LIBXML_NOERROR | LIBXML_ERR_NONE | LIBXML_ERR_FATAL);
        $result = $xml->xpath('//table/tbody/tr');
        return $result;
    }

    public function unescapeString(string $str): string
    {
        $repl = [
            ['[\x20]', ' '],
            ['\-',     '-'],
            ['\;',     ';'],
            ['\/',     '/'],
            ['\.',     '.'],
        ];

        $str = trim($str, "/");
        foreach ($repl as list($s, $r)) {
            $str = str_replace_recursive($s, $r, $str);
        }
        return $str;
    }

    public function aggragateRobots(array $parent): void
    {
        foreach ($parent as $key => $node) {
            // skip header
            if (isset($node->th)) continue;
            $type = $this->unescapeString((string)$node->td[0]);
            $robot = new Robot($type, (int)$node->td[1], (float)$node->td[2], (string)$node->td[3],);
            $this[] = $robot;
        }
    }
}



$years = [
    2018,
    2019,
    2020,
    2021,
    2022,
    2023,
    2024,
];

$monthRange = [
    [1, 12],
    [1, 12],
    [1, 12],
    [1, 12],
    [1, 12],
    [1, 12],
    [1, 04],
];


function str_replace_recursive(string $search, string $replace, string $subject): string
{
    if (strpos($subject, $search) !== false) {
        $subject = str_replace($search, $replace, $subject);
        return str_replace_recursive($search, $replace, $subject);
    }
    return $subject;
}

function getRangeFormatted(array $monthRange, int $i): array
{
    $array = [];
    foreach (range(...$monthRange[$i]) as $m) {
        $array[] = sprintf("%02d", $m);
    }
    return $array;
}

$aggragated = [];
foreach ($years as $i => $y) {
    foreach (getRangeFormatted($monthRange, $i) as $m) {
        $raw = file_get_contents(__DIR__ . "/$y/$m.html");
        $content = mb_convert_encoding($raw, 'utf-8', 'euc-jp');

        $robots = new RobotsMonth($content, (int)$y, (int)$m);
        $aggragated[] = $robots;
    }
}

$counter = [];

foreach ($aggragated as $m) {
    assert($m instanceof RobotsMonth);
    foreach ($m->robots as $robot) {
        if (!array_key_exists("$robot->type", $counter)) {
            $counter["$robot->type"] = 0;
        }
        $counter["$robot->type"] += $robot->count;
    }
}

unset($counter['no_user_agent']);
unset($counter['wordpress']);

printf("%-9s\0%s\0%s\n", "COUNT", "PERCENTAGE", "USER-AGENT",);

$sum = array_sum(array_values($counter));
foreach ($counter as $k => $v) {
    printf("%-9d\0%-6.1f\0 %s\n", $v, $v / $sum * 100 , $k,);
}

分析

このソースコードで数え上げたBotの訪問回数のうち、上位20位を見てみるとこのような構成になっている。1列目はヒット数、2列目は全体の中の割合、そして3列目はBotの名前を示している。

$ php CrawlerInspector.php | sort -t '\0' -k 1 -n | tail -n 20
3664     0.6    (firefox/)([0-9].|[0-1][0].)
3747     0.7    Googlebot-Image
4660     0.8    Sogou web spider
4720     0.8    feedburner
4875     0.9    Applebot
5478     1.0    YandexBot
7112     1.2    BLEXBot
7795     1.4    curl
7934     1.4    bingpreview
8436     1.5    SemrushBot
9484     1.7    spider
9752     1.7    DotBot
10698    1.9    baidu
11323    2.0    twitterbot
12491    2.2    crawl
21293    3.7    AhrefsBot
21739    3.8    bot[\s_+:,.;/-]
52567    9.2    Googlebot
64180    11.2   bingbot
240410   42.1   MJ12bot

この結果を見ていると、実に様々なBotがこのサイトのクローリングを行っていることがわかる。もちろん、すべてのBotが悪いというわけではなく、GooglebotBingbotなど、インターネット利用者全員にとって、検索機能という利潤をもたらすサービスのBotも見受けられる。

もちろんすべてがそのような利益をもたらすBotではない。お金を払っている顧客に、分析とデータを提供するための情報ソースの収集目的として、第三者の企業によって運用されているBotも存在する。

中でも、このうち4割にも及ぶ非常に激しいクローリングを行っているのが、冒頭にもあったMJ12botというBotで、他の追随を許さぬ240,410回ものアクセス数を誇っている。単純計算では一日に100回ほどのアクセスがあるようだが、実際には冒頭のような激しいクローリングを集中的に行う日があり、それがどうやら503を頻発させる要因になっていると推察できた。

MJ12botの対策をする(Todo)

このMJ12Botというクローラーに関しては、あちらこちらで長年の議論が行われているようで、ここでどんなBotかという言及はしない。しかし、6年間という長い時間の中で、24万回というアクセス数に相当するサーバーのリソースを持っていかれたとすると、ちょっと許しがたいところがあるし、もうサーバーを落とされたくない。記事執筆時点ではまだ対策は行っていないが、慎重にユーザーエージェント単位でのブロックを検討することにする。

追記: ブロックした。

前の記事

デジタル金庫環境の整備とPowerShell

コメント

0

コメントはありません。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

関連投稿

日記
「デジタル金庫環境の整備とPowerShell」のサムネイル画像

デジタル金庫環境の整備とPowerShell

PowerShellを書いて上手に暗号化ドライブを管理するソリューションを開発しました。 続きを読む

日記
「4月10日(水) スパムコメントで学ぶ英語のイディオム」のサムネイル画像

4月10日(水) スパムコメントで学ぶ英語のイディオム

out of thin airという英語のイディオムについて。 続きを読む

日記
「4月9日(火) シェルスクリプトであそぼう」のサムネイル画像

4月9日(火) シェルスクリプトであそぼう

今日の雨は土砂降りというレベルですごかった。AtCoder Problemsでの難易度が150~30… 続きを読む

日記
「4月8日(月) 日記」のサムネイル画像

4月8日(月) 日記

関心が変わった 最近着手しているプロジェクトもなく、最近なんとなく手持ち無沙汰に感じていたところがあ… 続きを読む