| @ブログ

普段は Vim などのテキストエディターで文章を書いていて、ブログの投稿画面にはできあがった内容をコピペするだけだったので、投稿画面の使いやすさやは気にしたことがなかった。画像のドラッグ・アンド・ドロップ・アップロードや、オン・ザ・フライでプレビューをできるようにしたが、テキスト自体の書きやすさを改善しようとはしてこなかった。

大筋はテキストエディターで書いたとしても、最後に推敲したり、推敲していて気が付いたおかしなところの修正は投稿画面で行うことが多い。やっぱり投稿画面の書きやすさは重要だ。特に長大な内容を編集しているときにテキストエリアが狭いととても使いづらい。ある程度大きさがあり、画面内に大量の文字が表示されるテキストエリアが好みだ。

Lokka が開発されていた 10 年以上前は解像度の小さいディスプレイが主流で、 Lokka の管理画面は小さいサイズのウィンドウで閲覧することを想定したテキストエリアのサイズになっている。今日の高解像度ディスプレイで見ると不便なので画面一杯にテキストエリアが広がるようにした。

投稿画面のレイアウトを改善

iPad からも投稿しやすいように投稿画面のレイアウトも修正した。本文のテキストエリアが広がったためスクロールしないと「スラッグ」以降の入力欄にアクセスできなくなったので、ある程度横幅のあるウィンドウのときにはこれらを右側に配置するようにした。

加えて、フォームを書きかけで保存したかどうかがはっきりせず、未保存の内容があるのに保存せずページから離脱してしまうことがあったので、変更点があるときは背景色を変えてわかるようにした。これにより記入内容を保存せずページを離脱してしまい、内容が失われるという悲劇を回避できるようになった。やり方は適当に検索して出てきた Stack Overflow を参考に、ページを読み込んだ時点で JavaScript でフィールド内の要素を JSON.stringify して data attribute に格納し、その後各フィールドの input イベントを監視して変更があるかどうかをチェックしている。

class FormObserver {
  constructor() {
    this.initializeFields();
    this.observeFieldsChange();
  }

  initializeFields() {
    const fields = Array.from(document.querySelectorAll('div.field'));
    for (const field of fields) {
      const inputElement = field.querySelector('input[type="text"], textarea, input[type="datetime-local"], select option:checked');
      if (inputElement === null) {
        continue;
      }
      field.dataset.serialize = JSON.stringify(inputElement.value);
    }
  }

  observeFieldsChange() {
    const fields = Array.from(document.querySelectorAll('div.field'));
    for (const field of fields) {
      const inputElement = field.querySelector('input[type="text"], textarea, input[type="datetime-local"], select');
      if (inputElement === null) {
        continue;
      }
      inputElement.addEventListener('input', () => {
        if (field.dataset.serialize != JSON.stringify(inputElement.value)) {
          inputElement.dataset.changed = 'true';
          field.classList.add('edited');
        } else {
          inputElement.dataset.changed = 'false';
          field.classList.remove('edited');
        }
      })
    }
  }
}

input イベントを監視すると動作がもっさりするのではないかと心配したが、そんなことはないようだ。普通に使えている。

いまのところ管理画面の HTML レンダリングはサーバーサイドで Ruby で行っているが、これ以上凝ったことをやるなら React などを使って JavaScript で HTML を組み立てる方式にしていく方が効率が良さそうだ。

| @技術/プログラミング

偶発的に puma のバージョンを上げたところ Encoding::CompatibilityError: incompatible character encodings: UTF-8 and ISO-8859-1 が多発して厳しい感じになった。

このブログでは puma は v4 系を使っていたが、調べると最近 v6 もリリースされたようで v5 系に上げてみることにした。すると忘れていたのだが puma は v5 系から daemonize する機能が削除され、デーモン化は systemd を使うべしということになっていた。プロセスのデーモン化は puma にやってもらわないと capistrano で deploy するときに面倒なので以前は v5 に上げるのを諦めて v4 を維持していたのだった。

capistrano3-puma が systemd に対応していたのでえいやっと puma を v5 に上げて deploy してみたところ、冒頭の Encoding::CompatibilityError: incompatible character encodings: UTF-8 and ISO-8859-1 が多発してページが全く表示されなくなってしまった。

一方で管理画面やアーカイブページは表示に問題がなかった。どうもファイルの読み込みが発生するページ(このブログではキャッシュを多用していて、ファイルに書き出したキャッシュを読み込んでいる)でエラーが発生しているようだった。

自分で fork した sinatra-cache.gem でファイル読み込みする部分で encoding オプションを指定してみたりしたが問題が直らない。 Haml や Sinatra のバージョンも古いのでこれらも上げてみようかと試みたが、そうするとより盛大にエラーが出てしまう( Haml を v6 にすると html_safe している出力もさらにエスケープされて HTML がぶっ壊れる)。

気になるのはローカル環境( Mac )ではこのエラーが発生しないこと。「これは環境起因では?」と思い至ってガチャガチャやってみたところ修正することができた。

Lokka では Encoding.default_external を参照しつつ String#force_encoding しているところがある。「ひょっとして Encoding.default_external の値がローカルとサーバーで異なるのでは?」試してみたところ、ローカルでは #<Encoding:UTF-8> となる Encoding.default_external の結果が、サーバーでは #<Encoding:ISO-8859-1> となっていた。

以下のブログを参考に、環境変数 RUBYOPT でエンコーディングを指定して puma を動かすことでエラーを回避できた。

systemd 経由で puma を動かすときに環境変数を設定するのは結構難しい。最初は puma が RACK_ENV=production で動かず困ったが、 systemd 用の設定ファイルで EnvironmentFile のパスを指定し、環境変数用のファイルの中で各種環境変数を定義してやる必要があった。こんな感じ。

systemd の設定ファイル

[Unit]
Description=Puma HTTP Server for portalshit (production)
After=network.target

[Service]
Type=simple

WorkingDirectory=/var/www/deploys/portalshit/current
# Support older bundler versions where file descriptors weren't kept
# See https://github.com/rubygems/rubygems/issues/3254
EnvironmentFile=/var/www/app/.config/systemd/user/portalshit_env
ExecStart=/var/www/app/.rbenv/bin/rbenv exec bundle exec --keep-file-descriptors puma -C /var/www/app/portalshit/config/puma.rb
ExecReload=/bin/kill -USR1 $MAINPID
StandardOutput=append:/var/www/deploys/portalshit/shared/log/puma_access.log
StandardError=append:/var/www/deploys/portalshit/shared/log/puma_error.log

Restart=always
RestartSec=1

SyslogIdentifier=puma

[Install]
WantedBy=default.target

環境変数の定義ファイル

RACK_ENV=production
RUBYOPT=-EUTF-8

puma v5 に移行しようとしている方の参考になれば幸いです。

| @技術/プログラミング

MySQL だけでお手軽に全文検索ができるということを知らなかった。 MySQL 5.6 から入っていたようだった。 Tantivy および Tantiny を使ったやり方を以前記事に書いてサイトで実装しているが、 MeCab によるトークナイズでは二文字の熟語がセットになって四文字になっているようなパターンを取り逃すことがあった(「関連記事」は「関連」と「記事」に分割され、「関連」や「記事」というキーワードで検索したときにはヒットするが「関連記事」で検索するとヒットしない)し、記事追加時の検索インデックス更新処理が不要( MySQL にレコードが追加されたときに勝手に更新される)なので試してみることにした。

やり方は以下の記事を参考にした。

最初にデータベースに全文検索用のインデックスを作成した。

ALTER TABLE `entries` ADD FULLTEXT INDEX index_entry_fulltext(title, body) WITH PARSER ngram;

その後、検索部分のコードを書き換えて以下のようにした。

class Entry < ActiveRecord::Base
  scope :search,
        ->(words) {
          return all if words.blank?
          where('MATCH (entries.title, entries.body) AGAINST (? in BOOLEAN MODE)', words)
        }
end

めっちゃ簡単。

このブログは記事数が 1500 記事くらいなのでぶっちゃけ LIKE 検索でも実用的な速度( 100msec 以内)で結果を取得できるが、 FULLTEXT インデックスを使うと 10msec 程度で結果を取得できる。

ただし Tantivy と比べて劣る点もあって以下は注意が必要。

  1. なぜかわからないが Vim で検索すると何もヒットしない。また Rails で検索すると Rails について触れていない記事もヒットする。 ngram によるインデックスというのはこんなものなのかもしれない。検索ワードが日本語のときはいい感じに結果が表示される。
  2. 複数のテーブルにまたがるデータを一個の検索インデックスにまとめることができない。例えば Tantivy のインデックスは記事のタイトル、本文、カテゴリー、タグをインデックス対象としているが、 MySQL の FULLTEXT インデックスだとテーブルごとにしかインデックスを作れないので(当たり前)、複数のテーブルにまたがる検索をするときにはテーブルを JOIN するしかない。 OR マッパーを使っている場合には利用しづらい。

1 の問題に関しては、 MySQL 5.7 からインデックス生成時の PARSER に MeCab などを指定できるようになったのでそうすると回避できるかもしれない。ただし MeCab のインストールや設定を行う必要があるので要注意。

2 の問題に関しては全文検索システムを入れた方が良さげ。 Tantivy であれば非常に簡単に導入できる。

現状、このサイトでは右上の検索窓から検索したときのインクリメンタルサーチとアーカイブページでの絞り込みは Tantivy を、インクリメンタルサーチの結果で必要な情報が得られなかったときの「全文検索する」と 404 Not Found ページの検索は MySQL の全文検索を使うようにしている。

二つの検索

| @ブログ

404 ページ、昔はそもそもなくて 404 Not Found ステータスを返すだけだったり、あっても「見つかりません」というだけのものが多かったけど、最近はサイトマップ的なコンテンツや代替となるコンテンツを表示するサイトも見かける。というわけでこのサイトでもやってみることにした。

このブログの URL は /YYYY/MM/DD/slug という形式になっている。パスの /YYYY/MM/DD の部分はお飾りで、実際は slug がユニークになっているので slug で表示すべき記事を判定している。

よくあるのが記事を公開後、 slug 部分にタイポを見つけて変更するというケース。しかしすでにその時点で記事が Twitter などでバズってたりすると、 Twitter で共有されている記事を見てやってきた人が 404 Not Found ページを見ることになる(この前の「不便になるインターネット」がまさにそうだった)。それはまずいので slug のタイポを修正すると同時に Nginx の設定ファイルをいじってタイポ修正前の URL から修正後の URL へリダイレクトするようにしていた。しかしリダイレクトごときでサーバーの設定ファイルを修正して root 権限でリロードするというのはめんどい。 SSH でログインもしなければならない。大げさすぎる。

というわけで思いついたのがこの機能で、 Ruby でクラス名やメソッド名をタイポしたときに正しい候補を表示する did_you_mean.gem を利用した。存在しない slug で URL を開くと以下のように候補が表示される。

404 Not Found

コードはこんな感じ。

# Helper
def not_found_candidates
  @not_found_candidates ||=
    begin
      slugs = Entry.published.where.not('slug REGEXP ?', '^[0-9]+$').pluck(:slug)
      spell_checker = DidYouMean::SpellChecker.new(dictionary: slugs)
      current_slug = request.path_info.split('/').last
      slug_candidate = spell_checker.correct(current_slug)
      Entry.published.where(slug: slug_candidate)
    end
end

# View
- if not_found_candidates.any?
    %p Did you mean?
    - not_found_candidates.each do |candidate|
      = link_to candidate.title, candidate.link

データベースから slug 一覧を取り出して辞書とし、 DidYouMean::SpellChecker に食わせて似たページの候補を取得して表示する。タイポありのページを訪れた人はワンクリックしなければならないという手間が増えるが、これでタイポを修正したときに面倒なリダイレクトの設定をする必要がなくなった。

なお 404 ページには検索窓や最近の記事、カテゴリー一覧も表示して回遊性を高めている。

404 Not Found ページ

| @技術/プログラミング

ブログのアクセス数を集計してランキング(人気記事一覧)を表示している。

シェルスクリプトでログを集計して頑張っているが、ボットからのアクセスを除外など結構やることが複雑化してきた。また最近は主にロシア方面からのスパマーによるアクセスが多く、全然いま読まれる要素がない記事がランキング上位に入ったりしてた。スパマーは以下の 2 記事が好きなようだ。

Google Analytics でアクセス数を見るとこれらの記事は上位に入ってこないので、 Google はちゃんとスパマーからのアクセスを除外しているのだろう。

というわけで Google Analytics の API からアクセス数を取得してみることにした。

しかし調べてみた感じ、あまり情報がない。 Google の公式ドキュメントは Java とPython と Go と PHP と JavaScript のサンプルしかない。

Google が公開している Ruby のライブラリはあるが、ドキュメントがえらく貧弱で勘で使うしかない。

使い方を紹介しているブログもあるにはあるが、この Ruby 製のライブラリはアルファ版とベータ版しかなくてころころ仕様が変わるようだ。先人の情報通りに動かしてみたら全然動かなかった。

API の仕様や上述のライブラリのコードを読みつつ以下のようなコードを書いたところいい感じに使えるようになった。 Ruby で Google Analytics の API にアクセスしたいと思っている人には参考になるんじゃないかと思う。

↑のコードでは metrics は screenPageViewstotalUsers を取得している。 dimension は pagePathpageTitle だ。ほかのが必要であれば変えてあげればよい。これを Rake タスクから呼び出して必要な情報を得るようにしている。

API 呼び出しについては Google が提供している Query Explorer で確認するとよい。

また Analytics API は利用開始前に設定が必要。 Quickstart ページで API を有効化し、 GCP に IAM を作成して credential をダウンロードして Google Analytics 側でこの IAM への API アクセスを許可する必要がある。コード書く前にこの辺でくじけそうになるだろうけど頑張ってほしい。

| @技術/プログラミング

Rust 製の全文検索システム Tantivy を Ruby から使える Tantiny を導入したことを書いた。

結構手軽に使えるのだがやはり日本語のトークナイズ(形態素解析)ができないのでいまいちなところがあった。 Tantivy には lindera-tantiby というものがあって、 Lindera は kuromoji のポートなので、これを使うと日本語や中国語、韓国語の形態素解析ができる。 Tantiny に導入できないか試してみたが、自分の Rust 力では到底無理だった。

ちなみに関連記事の表示でも日本語の形態素解析は行っている。

MeCab に neologd/mecab-ipadic-neologd を組み合わせてナウな日本語に対応させつつ形態素解析している。

この仕組みを作ってトークナイズは Ruby で自前で行い、 Tantiny および Tantivy にはトークナイズ済みの配列を食わせるだけにした( Tantiny はトークナイズ済みのテキストを受け付けることもできる)。トークナイズを自前で行うことで辞書ファイルで拾いきれないような固有名詞もカバーできる。例えば 山と道 なんかは MeCab と mecab-ipadic-neologd にトークナイズさせると に分割されてしまう。自前のトークナイザーで単語として認識させていている。おかげで「山と道」をちゃんと検索できるようになっている

なお、自前のトークナイザーはこんなコードになっている。

class Tokenizer
  attr_reader :text

  class << self
    def run(text)
      self.new(text).tokenize
    end
  end

  def initialize(text)
    @text = text
  end

  def cleansed_text
    @cleansed_ ||= text.
      gsub(/<.+?>/, '').
      gsub(/!?\[(.+)?\].+?\)/, '\1').
      gsub(%r{(?:```|<code>)(.+?)(?:```|</code>)}m, '\1')
  end

  def words_to_ignore
    @words_to_ignore ||= %w[
      これ こと とき よう そう やつ とこ ところ 用 もの はず みたい たち いま 後 確か 中 気 方
      頃 上 先 点 前 一 内 lt gt ここ なか どこ まま わけ ため 的 それ あと
    ]
  end

  def preserved_words
    @preserved_words ||= %w[
      山と道 ハイキング 縦走 散歩 プログラミング はてブ 鐘撞山 散財 はてなブックマーク はてな
    ]
  end

  def nm
    require 'natto'
    @nm ||= Natto::MeCab.new
  end

  def words
    @words ||= []
  end

  def tokenize
    preserved_words.each do |word|
      words << word if cleansed_text.match?(word)
    end

    nm.parse(cleansed_text) do |n|
      next unless n.feature.match?(/名詞/)
      next if n.feature.match?(/(サ変接続|数)/)
      next if n.surface.match?(/\A([a-z][0-9]|\p{hiragana}|\p{katakana})\Z/i)
      next if words_to_ignore.include?(n.surface)
      words << n.surface
    end

    words
  end
end

preserved_words が手製の辞書だ。 はてなはてブ も辞書登録しておかないと MeCab だとバラバラに分割されてしまって検索できなかった。

難点としては記事更新後に自動でインデックスの更新が行われず、 cron によるバッチ処理でインデックス更新を行っている1。なので検索インデックスにデータが反映されるまでにタイムラグがある。 Tantiny でやれれば記事作成・更新時のコールバックとして処理できるのでリアルタイムに変更を検索インデックスに反映させることができるが、個人の日記なのでタイムラグありでも大きな問題にはならない。

本当は Tantiny で lindera-tantiby を使えるようにして Pull Request がカッチョイイのだが、とりあえずは自分は目的が達成できたので満足してしまった。 5 年くらい前から Rust 勉強したいと思っているが、いつまでも経っても Rust を書けるようにはならない。


  1. mecab-ipadic-neologd を VPS 上でインストールできず(めっちゃメモリを使う)、手元の Mac で Docker コンテナ化して Docker Hub 経由でコンテナイメージを Pull して VPS 上で Docker 経由で動かしている(その辺について書いてる記事: ブログのコンテナ化を試みたけどやめた) 

| @WWW

以前、以下の記事でこのウェブサイトへのアクセス元 User Agent について書いた。

そのとき Hatena::Russia::Crawler というのが謎だということを書いた。最近のアクセスログを見ても相変わらずこの User Agent からのアクセスが多い。またアクセス頻度も高く、同一の URL に対して何度もアクセスしている。

これはやはりはてなの名を騙った怪しいクローラーなのではないかと思い調べてみた。

まず Hatena::Russia::Crawler という User Agent からのアクセスの IP アドレスを調べてみたところ以下だった。

cat log/access.log | grep 'useragent:Hatena::Russia::Crawler/0.01' | cut -f2 | sort | uniq -c | sort -nr
    434 remote_addr:52.68.0.227
    419 remote_addr:54.249.85.140
    417 remote_addr:54.92.97.59
    379 remote_addr:54.250.227.185

whois してみると AWS で運用されているものであることがわかるが、はてなのものかは断定できない。

もしこの IP からはてなブックマークやはてなアンテナなどの User Agent でのアクセスもあれば Hatena::Russia::Crawler ははてなのクローラーであると断定できるだろう。ということで調べてみたところこんな感じだった。

zcat -f log/access.log* | grep -E 'remote_addr:(52\.68\.0\.227|54\.249\.85\.140|54\.92\.97\.59|54\.250\.227\.185)' | cut -f13,2 | sort | uniq -c | sort -nr
  16687 remote_addr:54.250.227.185      useragent:Hatena::Russia::Crawler/0.01
  16448 remote_addr:54.92.97.59 useragent:Hatena::Russia::Crawler/0.01
  16370 remote_addr:54.249.85.140       useragent:Hatena::Russia::Crawler/0.01
  16272 remote_addr:52.68.0.227 useragent:Hatena::Russia::Crawler/0.01
     73 remote_addr:54.249.85.140       useragent:HatenaBookmark/4.0 (Hatena::Bookmark; Scissors)
     60 remote_addr:54.250.227.185      useragent:HatenaBookmark/4.0 (Hatena::Bookmark; Scissors)
     56 remote_addr:52.68.0.227 useragent:HatenaBookmark/4.0 (Hatena::Bookmark; Scissors)
     50 remote_addr:54.92.97.59 useragent:HatenaBookmark/4.0 (Hatena::Bookmark; Scissors)
     31 remote_addr:54.92.97.59 useragent:Hatena::Fetcher/0.01 (master) Furl/3.13
     31 remote_addr:54.250.227.185      useragent:Hatena::Fetcher/0.01 (master) Furl/3.13
     31 remote_addr:54.249.85.140       useragent:Hatena::Fetcher/0.01 (master) Furl/3.13
     26 remote_addr:52.68.0.227 useragent:Hatena::Fetcher/0.01 (master) Furl/3.13
     19 remote_addr:54.92.97.59 useragent:Hatena::Scissors/0.01
     19 remote_addr:54.250.227.185      useragent:Hatena::Scissors/0.01
     16 remote_addr:52.68.0.227 useragent:Hatena::Scissors/0.01
      9 remote_addr:54.249.85.140       useragent:Hatena::Scissors/0.01

なんと、 IP アドレスで検索してはてなのその他のクローラーもヒットしてしまった。つまり Hatena::Russia::Crawler ははてなのクローラーということだ。

ただしググっても一切情報が出てこない。 Hatena::Russia::Crawler で検索してトップヒットするのは自分のブログだ。

改めて Hatena::Russia::Crawler による直近 30 日間のアクセス状況を調べてみるとこんな感じだ。

zcat -f log/access.log* | grep 'useragent:Hatena::Russia::Crawler/0.01' | cut -f5 | sort | uniq -c | sort -nr
  15697 request_uri:/index.atom
   9913 request_uri:/2022/04/20/integrate-charts-category-with-select-boxs
   9373 request_uri:/2022/05/04/reputation-and-interpretation
   8422 request_uri:/2022/05/11/fly-to-kamikochi-from-fukuoka
   7716 request_uri:/2022/05/16/using-tantivy-over-tantiny
   5906 request_uri:/2022/04/17/quit-using-hey
   5139 request_uri:/2021/12/29/thoughts-on-manga-subscription
   1308 request_uri:/2021/12/13/keep-a-stack-books-whether-reading-them-or-not
    787 request_uri:/2022/06/23/each-entry-title-should-be-marked-up-with-h1
    741 request_uri:/2021/12/13/keep-stack-books-whether-reading-them-or-not
    456 request_uri:/2022/06/24/if-you-feel-apple-musics-recommendation-is-awful
    100 request_uri:/2022/06/14/thoughts-on-hatena-bookmark
     46 request_uri:/2015/12/07/thoughts-on-rural-life
     46 request_uri:/2015/12/02/thoughts-on-t-on-t
     46 request_uri:/2015/12/02/thoughts-on-christmas-song
     45 request_uri:/2019/12/02/stop-drinking-outside-frequently
     16 request_uri:/2020/11/08/where-i-went-in-2019
     11 request_uri:/2015/12/05/omm-writer-music-is-nice-to-listen-to-while-writing
      5 request_uri:/2022/05/04/the-golden-maintenance-week
      2 request_uri:/2022/06/24/
      2 request_uri:/2022/06/24

index.atom はフィードの URL なので除外するとして、特定の記事に対して数千回もアクセスがある。 30 日間で 9000 回ということは一日あたり 300 回だ。 1 時間あたり 12.5 回である。何のためにこんなに高頻度でクローリングしているのだろうか。

とここまで調べたところほかの Bot 系アクセスはどうなのかと改めて User Agent 毎のアクセス数を調べてみたらこんな感じだった。

zcat -f log/access.log* | cut -f13 | sort | uniq -c | sort -nr | head -10
 124894 useragent:Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)
  65794 useragent:Hatena::Russia::Crawler/0.01
  58274 useragent:Ruby
  31493 useragent:Mozilla/5.0 (iPhone; CPU iPhone OS 15_4_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1
  29454 useragent:Mozilla/5.0 (compatible; AhrefsBot/7.0; +http://ahrefs.com/robot/)
  21492 useragent:Tiny Tiny RSS/21.11-7cfc30a (https://tt-rss.org/)
  20351 useragent:Slackbot 1.0 (+https://api.slack.com/robots)
  18765 useragent:Mozilla/5.0 (compatible; SemrushBot/7~bl; +http://www.semrush.com/bot.html)
  18395 useragent:Mozilla/5.0 (compatible; MJ12bot/v1.4.8; http://mj12bot.com/)
  17942 useragent:Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)

bingbot からのアクセスの方が Hatena::Russia::Crawler からのアクセスの 2 倍近くあった。ただし bingbot は検索エンジンのクローラーらしく、サイト全体をまんべんなくクローリングするような挙動で、特定の URL に一ヶ月間で数千回アクセスするような感じではない。

zcat -f log/access.log* | grep 'useragent:Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)' | cut -f5 | sort | uniq -c | sort -nr | head -25
    571 request_uri:/robots.txt
    352 request_uri:/
    205 request_uri:/category/misc
    160 request_uri:/2007/01/13/732
    156 request_uri:/2005/10/28/129
    150 request_uri:/2009/03/23/1010
    148 request_uri:/category/music
    146 request_uri:/archives
    144 request_uri:/category/www
    143 request_uri:/2016/07/
    143 request_uri:/2009/08/31/1074
    139 request_uri:/2010/07/05/1140
    138 request_uri:/category/photo
    138 request_uri:/2009/02/
    137 request_uri:/2006/09/09/658
    136 request_uri:/tags/netatmo
    136 request_uri:/2010/07/17/1145
    136 request_uri:/2006/07/23/611
    135 request_uri:/2014/03/
    134 request_uri:/?page=32
    134 request_uri:/2007/02/09/747
    133 request_uri:/category/shopping
    133 request_uri:/2021/07/26/how-to-get-to-kamikochi-from-fukuoka
    133 request_uri:/2011/11/03/finally-got-hhkpro2
    132 request_uri:/2006/01/

Hatena::Russia::Crawler は同一 URL に数千回もアクセスして何をしているのだろう? 謎は深まるばかりだ。