やってみる

アウトプットすべく己を導くためのブログ。その試行錯誤すらたれ流す。

はてなフォトライフの全画像を自動取得したいができなかった(reCAPTCHA)

 原因はreCAPTUCHA。こいつのせいでログインできず公開範囲が「自分のみ」のフォルダにある画像一覧をRSSから取得できない。

はじめに

 かつて私は表題の件を解決していた。

 しかし時と共に、はてなサイトの仕様が変更された。当時のコードではもはやログイン処理が失敗してしまう。その原因について調査したところ、reCAPTUCHAの仕業だと思われることが判明した。

そもそもAPIがクソ💩

 はてなフォトライフFeedAPIがある。これで取得できればよかったのだが、できない。こいつで取得できるのはトップフォルダのみ。たとえば「Hatena Blog」など任意フォルダに配置された画像は取得できない。

しかたなくRSS

 なので、仕方なくはてなフォトライフのサイトにあるRSSリンクからRSSデータを取得することで一覧を取得する。

 だが、フォルダの公開範囲が「トップフォルダと同じ」でなければならない。もし「自分のみ」などであった場合、RSSを参照するならログインしている必要がある。昔はログインできたが、今はreCAPTCHAに阻まれてログインできない。よって自分のみが参照できる画像のリンクを取得することができない。これが今回の本題である。

 どうでもいいけど、私にはFeedAPIの存在意義がわからなかった。RSSの下位互換じゃん。

調査

ログインページ

 まずははてなサイトのログインページをリクエストする。

HTMLコード

 HTMLコードを見る。このうちログイン処理をするフォーム部分のみ抜粋する。

      <form name="login" action="/login" method="post">
        <div>
<div class="error-message">
  <p>時間を空けて再度お試しください。または<a href="https://hatena.zendesk.com/hc/ja">サポート窓口</a>にお問い合わせください。</p>
</div>
          <div class="input-item">
            <div class="input-item-inner">
              <input placeholder="はてなID または メールアドレス" value="*******************" pattern=".{3,}" id="login-name" type="text" class="text" required="required" autofocus="autofocus" name="name">
            </div>
          </div>
          <div class="input-item">
            <div class="input-item-inner">
              <input class="password" type="password" name="password" required="required" placeholder="パスワード" value="*************">
            </div>
          </div>
          <div id="option" class="config-button">              <div class="auto-login">
                <a href="#" class="checkbox-tab"><label for="auto_login"></label></a>
                <div class="checkbox-item">
                  <input type="checkbox" class="checkbox" checked="checked" name="persistent" value="1" id="auto_login" />
                  <label for="auto_login" class="checkbox-text">
                    <span>次回から自動的にログイン</span>
                  </label>
                </div>
              </div>                        <input value="" name="recaptcha_token" type="hidden" />
            <button type="button" id="login-button" class="submit-button">送信する</button>
          </div>
        </div>
      </form>

フォーム解析

  • HTTP通信方法:POST
  • データ(<input name>
    • name: はてなID または メールアドレス
    • password: パスワード
    • persistent: 次回から自動的にログインするか否か
    • recaptcha_token: 「私はボットではありません」チェックの進化版。ユーザが操作せずとも自動的にBOTチェックする。そのためにJavaScriptで実行することが必要である。

 これらのデータを送信せねばならない。そのうちrecaptcha_tokenというのが問題。

 なにこれ? どうしたらいいの? とりあえず調査してみる。

reCAPTCHA

 reCAPTUCHAとはBOTを防ぐための技術。自動ツールによる投稿を防ぐのが目的。

 たぶんこいつのせい。

 reCAPTUCHAのせいでログインするときJavaScriptの実行が必要になってしまった。今までのようにCookieの実装だけではダメ。

はてなフォトライフAPI

 ふつうならやりたい処理にマッチしたWebAPIを使いたいところ。だが今回のはてなフォトライフAPIでは、公開範囲が「自分のみ」のフォルダを取得する術がない。トップフォルダの取得しかできない。APIがしょぼい。

 なので仕方なくサイトからログインしてRSSを取得することでゲットしようと考えたわけだ。HTTP通信するプログラムを書いたが、ログインでreCAPTUCHAに阻まれてしまった。reCAPTUCHAの処理を動作させるためにJavaScript実行させるような処理が必要だ。

突破する方法

2Captcha

 2Captchaという有料ツールによってrecaptchaを突破できるらしい。有料なのは嫌だし、ロシアというのも嫌だ。北方領土から出ていけ侵略者め。

Selenium

予めブラウザでログインをしておいて、そのプロファイルを読み込めば良い。

 これなら半自動化はできそうだ。おそらくreCAPTUCHA対策はこれくらいしかないのだろう。

fp      = webdriver.FirefoxProfile("/home/[ユーザー名]/.mozilla/firefox/[プロファイル名].default")
browser = webdriver.Firefox(fp)

既存プロフィールで運用する場合には悪意ある運用を避けるためのセーブ機構の為に、すでにSelenium以外から立ち上げられた同プロフィールのブラウザがある場合にエラーが吐かれるのでご注意を。

環境構築できず

 ラズパイ4で環境構築に失敗した。seleniumchromeで使うためにchromedriver-binaryを入れたい。だが、arm32用バイナリがないと怒られた。

pip3 install selenium
pip3 install chromedriver-binary

エラーログ

Looking in indexes: https://pypi.org/simple, https://www.piwheels.org/simple
Collecting chromedriver-binary
  Downloading https://files.pythonhosted.org/packages/8b/c5/77a9822034f3e7c244dab99f92a7a4923ac4372623eca9bf683801c17003/chromedriver-binary-95.0.4638.17.0.tar.gz
Building wheels for collected packages: chromedriver-binary
  Running setup.py bdist_wheel for chromedriver-binary ... error
  Complete output from command /usr/bin/python3 -u -c "import setuptools, tokenize;__file__='/tmp/pip-install-dgv6d01o/chromedriver-binary/setup.py';f=getattr(tokenize, 'open', open)(__file__);code=f.read().replace('\r\n', '\n');f.close();exec(compile(code, __file__, 'exec'))" bdist_wheel -d /tmp/pip-wheel-yhdfg6ec --python-tag cp37:
  running bdist_wheel
  running build
  running build_py
  
  Downloading Chromedriver...
  
  Traceback (most recent call last):
    File "<string>", line 1, in <module>
    File "/tmp/pip-install-dgv6d01o/chromedriver-binary/setup.py", line 84, in <module>
      cmdclass={'build_py': DownloadChromedriver}
    File "/usr/lib/python3/dist-packages/setuptools/__init__.py", line 145, in setup
      return distutils.core.setup(**attrs)
    File "/usr/lib/python3.7/distutils/core.py", line 148, in setup
      dist.run_commands()
    File "/usr/lib/python3.7/distutils/dist.py", line 966, in run_commands
      self.run_command(cmd)
    File "/usr/lib/python3.7/distutils/dist.py", line 985, in run_command
      cmd_obj.run()
    File "/usr/lib/python3/dist-packages/wheel/bdist_wheel.py", line 188, in run
      self.run_command('build')
    File "/usr/lib/python3.7/distutils/cmd.py", line 313, in run_command
      self.distribution.run_command(command)
    File "/usr/lib/python3.7/distutils/dist.py", line 985, in run_command
      cmd_obj.run()
    File "/usr/lib/python3.7/distutils/command/build.py", line 135, in run
      self.run_command(cmd_name)
    File "/usr/lib/python3.7/distutils/cmd.py", line 313, in run_command
      self.distribution.run_command(command)
    File "/usr/lib/python3.7/distutils/dist.py", line 985, in run_command
      cmd_obj.run()
    File "/tmp/pip-install-dgv6d01o/chromedriver-binary/setup.py", line 42, in run
      url = get_chromedriver_url(version=chromedriver_version)
    File "/tmp/pip-install-dgv6d01o/chromedriver-binary/chromedriver_binary/utils.py", line 57, in get_chromedriver_url
      raise RuntimeError('Could not determine chromedriver download URL for this platform.')
  RuntimeError: Could not determine chromedriver download URL for this platform.
  
  ----------------------------------------
  Failed building wheel for chromedriver-binary
  Running setup.py clean for chromedriver-binary
Failed to build chromedriver-binary
Installing collected packages: chromedriver-binary
  Running setup.py install for chromedriver-binary ... error
    Complete output from command /usr/bin/python3 -u -c "import setuptools, tokenize;__file__='/tmp/pip-install-dgv6d01o/chromedriver-binary/setup.py';f=getattr(tokenize, 'open', open)(__file__);code=f.read().replace('\r\n', '\n');f.close();exec(compile(code, __file__, 'exec'))" install --record /tmp/pip-record-0w4awj1b/install-record.txt --single-version-externally-managed --compile --user --prefix=:
    running install
    running build
    running build_py
    
    Downloading Chromedriver...
    
    Traceback (most recent call last):
      File "<string>", line 1, in <module>
      File "/tmp/pip-install-dgv6d01o/chromedriver-binary/setup.py", line 84, in <module>
        cmdclass={'build_py': DownloadChromedriver}
      File "/usr/lib/python3/dist-packages/setuptools/__init__.py", line 145, in setup
        return distutils.core.setup(**attrs)
      File "/usr/lib/python3.7/distutils/core.py", line 148, in setup
        dist.run_commands()
      File "/usr/lib/python3.7/distutils/dist.py", line 966, in run_commands
        self.run_command(cmd)
      File "/usr/lib/python3.7/distutils/dist.py", line 985, in run_command
        cmd_obj.run()
      File "/usr/lib/python3/dist-packages/setuptools/command/install.py", line 61, in run
        return orig.install.run(self)
      File "/usr/lib/python3.7/distutils/command/install.py", line 589, in run
        self.run_command('build')
      File "/usr/lib/python3.7/distutils/cmd.py", line 313, in run_command
        self.distribution.run_command(command)
      File "/usr/lib/python3.7/distutils/dist.py", line 985, in run_command
        cmd_obj.run()
      File "/usr/lib/python3.7/distutils/command/build.py", line 135, in run
        self.run_command(cmd_name)
      File "/usr/lib/python3.7/distutils/cmd.py", line 313, in run_command
        self.distribution.run_command(command)
      File "/usr/lib/python3.7/distutils/dist.py", line 985, in run_command
        cmd_obj.run()
      File "/tmp/pip-install-dgv6d01o/chromedriver-binary/setup.py", line 42, in run
        url = get_chromedriver_url(version=chromedriver_version)
      File "/tmp/pip-install-dgv6d01o/chromedriver-binary/chromedriver_binary/utils.py", line 57, in get_chromedriver_url
        raise RuntimeError('Could not determine chromedriver download URL for this platform.')
    RuntimeError: Could not determine chromedriver download URL for this platform.
    
    ----------------------------------------
Command "/usr/bin/python3 -u -c "import setuptools, tokenize;__file__='/tmp/pip-install-dgv6d01o/chromedriver-binary/setup.py';f=getattr(tokenize, 'open', open)(__file__);code=f.read().replace('\r\n', '\n');f.close();exec(compile(code, __file__, 'exec'))" install --record /tmp/pip-record-0w4awj1b/install-record.txt --single-version-externally-managed --compile --user --prefix=" failed with error code 1 in /tmp/pip-install-dgv6d01o/chromedriver-binary/

 エラーログのうち以下がポイントと思われる。

  RuntimeError: Could not determine chromedriver download URL for this platform.
  
  ----------------------------------------
  Failed building wheel for chromedriver-binary

 私の環境はRaspberry PI 4B。たぶんARMv7用バイナリがないのだろう。

 うーん、バージョンは次々とあがっていくし、きっとその都度バイナリやインストール方法も変わるし時間もかかるだろう。きっとセキュリティ関係で最新バージョンにしないと動作しないとか、そういったことになりそうな予感。

 面倒くさすぎる。これならブラウザのUI操作を自動化するテスト用ツールを使ったほうがいいかもしれない。でも今度はそれを調査する必要があるわけで。ああ面倒だ。

 一旦ここで諦めよう。

妥協案

 はてなフォトライフのフォルダの公開範囲を「トップと同じ」にすれば誰でもアクセスできるようになる。ログイン処理が不要になる。それで自動化できるようになる。本当は公開範囲を「自分のみ」のままにしておきたかったが、仕方ない。すべてはAPIの貧弱さが悪い。

所感

 セキュリティは大事だが、自動化できないとかPC使う意味ない。reCAPTCHAで妨害するなら、せめてAPIを整えて欲しかった。まっとうなユーザが自分の情報をAPIで取得できない。あまりに残念すぎる現状。はてなさん、なんとかして。

対象環境

$ uname -a
Linux raspberrypi 5.10.52-v7l+ #1441 SMP Tue Aug 3 18:11:56 BST 2021 armv7l GNU/Linux