2022-01-20

Selenium 4 で Firefox に拡張機能をインストールする

Selenium で拡張機能がインストールされたブラウザを操作する方法を調べた。

拡張機能の開発について詳しくは アドオン - Mozilla | MDN を参照。

環境

  • Intel Mac
  • Python 3.9.10

準備

Selenium

Selenium をインストールする。今回はバージョン 4.1.0 を使う。

Firefox Browser

Mozilla によって署名されていないアドオン(拡張機能はアドオンに含まれる)はインストールできない。ただし、Firefox の中でも一部のバージョンでは設定を変更することでインストールが可能である。

Firefox 延長サポート版 (ESR)、Firefox Developer Edition および Nightly バージョンでは、Firefox の設定エディター (about:config ページ) で xpinstall.signatures.required 設定の値を false に変更することで、アドオン署名の強制を無効にできます。
[

Firefox のアドオン署名 | Firefox ヘルプ ](https://support.mozilla.org/ja/kb/add-on-signing-in-firefox)

個人的に利用するだけであれば署名なしで利用できるバージョンを選んだほうが良い。今回はFirefox Developer Edition を brew でインストールした。バージョンは 97.0 である。

余談:Waterfox Classic ではうまくいかなかったので Firefox Developer Edition に変更したという経緯がある。ブラウザのバージョンが古いと動かすのが大変かもしれない。

geckodriver

GitHub - mozilla/geckodriver: WebDriver for Firefox からダウンロードする。今回は 0.30.0 を使う。

拡張機能を作る

例として https://www.google.com/ に設定した文字列を表示するだけの拡張機能を作る。拡張機能の作り方は 初めての拡張機能 - Mozilla | MDN が参考になる。

src ディレクトリ以下に、設定ファイルと実行するスクリプトを置く。

manifest.json
{
    "manifest_version": 2,
    "name": "Sample Extension",
    "version": "1.0",
    "content_scripts": [
        {
            "matches": [
                "*://www.google.com/"
            ],
            "js": [
                "main.js"
            ]
        }
    ]
}
main.js
(function () {
    const div = document.getElementById('SIvCob')
    div.innerText = "拡張機能がインストールされました。"
})();

拡張機能の設定が間違っていると当然読み込めない。Selenium でインストールに失敗したときのエラーメッセージでは詳細が全くわからないので、ブラウザからインストールして正しく読み込めるか試すと良い。

Firefox で "about:debugging" を開き、"一時的なアドオンを読み込む" をクリックし、自分で作成した manifest.json ファイルを選択してください。拡張機能のアイコンが Firefox のツールバーに表示されているはずです。
https://developer.mozilla.org/ja/docs/Mozilla/Add-ons/WebExtensions/Your_second_WebExtension

Selenium で動かす

以下のように書ける。

test_extension.py
import unittest
from pathlib import Path
from time import sleep

from selenium import webdriver
from selenium.webdriver.firefox.options import Options
from selenium.webdriver.firefox.service import Service


class TestExtension(unittest.TestCase):

    def setUp(self) -> None:
        service = Service(executable_path='./geckodriver')
        options = Options()
        options.binary = "/usr/local/Caskroom/firefox-developer-edition/latest/Firefox Developer Edition.app/Contents/MacOS/firefox-bin"
        d = webdriver.Firefox(
            service=service,
            options=options,
        )
        d.install_addon(str(Path('./src').resolve()), temporary=True)
        self.driver = d
    
    def test_delete_logo(self) -> None:
        self.driver.get("https://www.google.com")
        sleep(10)
        self.driver.close()

ディレクトリ構造はこのようになっている。

.
├── geckodriver
├── src
│   ├── main.js
│   └── manifest.json
└── test_extension.py

Firefox の特定のバージョンを利用している場合は options.binary にバイナリのパスを与える必要がある。参考:firefoxOptions - WebDriver | MDN

geckodriver は上記の書き方以外にも環境変数で設定しておくこともできる。

install_addon に与えるパスは絶対パスで manifest.json が入っているディレクトリを示す必要がある。ブラウザからだと manifest.json を選択するように指示されるが、ここではディレクトリを示す。temporaryTrue でないとインストールできない。

Selenium の実行方法を調べるとサンプルコードがたくさん出てくるが、現在のバージョンでは非推奨になっている部分もあった。Profile()を使っていたり webdriver.Firefox(executable_path="/path/to/geckodriver") にしていると警告がたくさん出てくるので使わないようにした。

警告を止めようと思ってドキュメントを読んでもよくわからなかったのでコードを読んで調べた。間違った書き方をしている可能性は十分にある。

拡張機能がインストールされました
拡張機能がインストールされました

##temporary=False の場合

このように書ける。

test_extension.py
import unittest
from pathlib import Path
from time import sleep
from zipfile import ZIP_DEFLATED, ZipFile

from selenium import webdriver
from selenium.webdriver.firefox.options import Options
from selenium.webdriver.firefox.service import Service


class TestExtension(unittest.TestCase):

    def setUp(self) -> None:
        service = Service(executable_path='./geckodriver')
        options = Options()
        options.set_preference("xpinstall.signatures.required", False)
        options.binary = "/usr/local/Caskroom/firefox-developer-edition/latest/Firefox Developer Edition.app/Contents/MacOS/firefox-bin"
        d = webdriver.Firefox( service=service, options=options,)
        xpi = str(Path("./extension.xpi").resolve())
        with ZipFile(xpi, compression=ZIP_DEFLATED, mode="w") as zip:
            for file in Path('src').glob("*"):
                print(file, file.name)
                with zip.open(file.name, "w") as zip_file:
                    with open(file, "br") as f:
                        zip_file.write(f.read())
        d.install_addon(xpi, temporary=False)
        self.driver = d
    
    def test_delete_logo(self) -> None:
        self.driver.get("https://www.google.com")
        sleep(10)
        self.driver.close()

manifest.json に id を追加

id がなくても一時的なアドオンとして読み込めるが、一時的なアドオンではない場合は id が必要になる。id がない場合、インストール時に ERROR_CORRUPT_FILE: The file appears to be corrupt. というエラーが出る。

参考

時折、あなたの拡張機能用に ID を指定する必要があります。アドオンの ID が必要なとき、manifest.json 内に applications キーを入れて gecko.id プロパティをセットします:

"applications": {
  "gecko": {
    "id": "borderify@example.com"
  }
}

初めての拡張機能 - Mozilla | MDN

###署名がない拡張機能のインストールを許可する

options.set_preference("xpinstall.signatures.required", False)

を追加する。about:configに出てくる項目はすべてこれで設定できると思う(未確認)。

xpi ファイルに圧縮する

zipfile --- ZIP アーカイブの処理 — Python 3.10.4 ドキュメント を使えばよい。compression=ZIP_DEFLATEDでなければいけないという情報を見た気がするが、デフォルトの compression=ZIP_STORED でも動いた。

zip コマンドが使えるなら以下のようなコマンドで圧縮できる。

zip -r -FS ../my-extension.zip * --exclude '*.git*'

Package your extension | Firefox Extension Workshop

install_addon する

xpi ファイルへの絶対パスを与える。

感想

  • 拡張機能に関するドキュメントはたくさんある
  • ドキュメント読んでもわからないときはコードを眺めるとなんとかなることもある
    • Selenium の話