目次

= selenium-webdriver LM: [2025-11-06 20:33:13]


1. 概要

2. インストール

2.1 ブラウザ環境

2.2 python からの利用 [2023-09-07] [2022-08-17]

# pip list | grep selenium # 入っているか確認
# pip install selenium  # うまく動作した.
# portmaster -D www/py-selenium # うまく動作してなかったかもしれない.

2.3 ruby binding 環境での構築 [2014年頃]

% sudo gem install selenium-webdriver
% sudo gem install watir-webdriver

2.4 tips

2.4.1 画像保存の方法

[2025-01-13]

selenium を使って,Web 上の画像を保存する方法は,大きく分けて,

  1. screenshot をとる
  2. 名前をつけて保存
  3. 画像の URL を取得し,request.get() する

の3つな気がする.1 番目は表示さえされてれば確実に画像が保存できるのだが,画像が縮小されていると画質が落ちてしまうし,取得した画像を jpeg や webp に変換していると結構な CPU リソースを食ってしまう.

2 番目の名前をつけて保存は,もし画像の URL とスクレイピング先のホストが同じ場合,キャッシュから保存されるので,1度の URL アクセスで画像が保存できるのでもっとも効率的になるが,違う場合,クロスサイト対策(?)の関係で,もう一度 URL アクセスが発生してしまう.

そのため,2 番目と 3 番目はアクセスという面で考えると同じになってしまう.

状況に応じて,適した方法を選ぶ必要がある.

2.4.2 名前をつけて保存

[2025-01-13]

ブラウザ上の画像を右クリックし,名前をつけて保存,と押すと,保存ダイアログが出てきて,保存先を選び,画像を保存することになる. この時出てくる保存ダイアログは,ブラウザの一部ではなく,OS(Windows)の管理下にあるので,selenium からコントロールすることができない. よって,名前をつけて保存の処理を自動で行ないたい場合は,selenium とは別の枠組で考える必要がある.

FreeBSD/Linux などの XWindow な環境の場合,xdotool を利用すると,簡単に XWindow 上での操作が行なえるので,selenium と組み合わせると便利かもしれない.他にも pyautogui などがあるようだが,僕の FreeBSD 環境では上手く動かせなかった.参考1参考2

僕の場合,ウィンドウマネージャを指定せずに,Xvfb に対して seleinum で操作している chromium を表示している状態である.

次に,selenium において,次のように javascript を利用してブラウザ上での右クリックを行なわせる.

driver.execute_script(f"""
   const downloadImage = document.createElement('a');
   document.body.appendChild(downloadImage);
   downloadImage.setAttribute('download', 'image');
   downloadImage.href = '{img_src}';
   downloadImage.click();
""")

実行すると,Xvfb 上では名前をつけて保存ダイアログにフォーカスがあたった状態で出てくる. あとは,保存先ディレクトリを指定し,保存ボタンを押す.xdotool ならば,

% xdotool key Home
% xdotool type /home/skk/Downloads/
% xdotool key Return 

となる.もしコマンドライン上で実際に動かす場合は,DISPLAY 環境変数も指定しなければならない. これらのコマンドを python 上でコマンド呼出せばよい.例えば,以下のような形.

xdotool = ["xdotool", "key", "Home"]
subprocess.run(xdotool)

2.4.3 selenium (bot) 検知への対応について

特定のサイトでは,selenium でのアクセスを禁止している.UA などのアクセス情報から selenium で動作していることを検知している.

2.4.3.1 selenium detection の解説サイト

2.4.3.2 undetected chromedriver [2023-09-07]

selenium であることの検知は,主に chromedriver (webdriver) と chrome の通信の中に痕跡が残り,アクセス時にその痕跡がサーバ側に通知されてしまうことで発生する模様.

これを解決する為,通常の chromedriver に少し手を加えるツールが存在している.[2023-09-07] 時点で試したのは,undetected-chromedriver

undetected-chromedriver は内部的に,以下のような動作になっている.

FreeBSD で動作させる為には,下記参照.

2.4.3.3 seleniumbase [2025-02-22]

undetected_chromedriver は,2025/02 現在,あまりメンテされておらず,Cloudflare などの anti-bot システムで検知されがちなツールになってきている模様.

ZenRow というクラウドサービスを使っても anti-bot の回避はできるようだが月額がまーまー高い.広告記事だと思うけど,この ZenRow の特集記事が良くできているので参考に読むと良い.

今は,SeleniumBase というツールまたは,nodriver というツールが最近ではアクティブな模様.nodriver は undetected_chromedriver の作者が作っている後継だけど,selenium の書きにくいところと決別したいらしく,結構独自の書き方にしないといけなくて,既存のコードがある場合には導入しにくい.SeleniumBase は undetected_chromedriver を fork して独自進化させてるっぽいので,undetected_chromedriver からの乗り換えにはとても便利.

また,multiprocessing 環境への対応を頑張った形跡が見られるのも嬉しい.undetected_chromedriver は,複数のプロセスを動かそうとすると,Text Busy と言われることがちょいちょいあった.これは,ChromeDriver を利用するたびに chromedriver をダウンロードしてきてパッチを当ててたので,複数プロセスで動かそうとすると,時々競合のような状態になっていたと想像している. SeleniumBase は,初回起動時に uc_driver という,パッチを当てまくった chromedriver を作成して,以降は必要がなければずっとそれを使い続けるので,複数のプロセスから ChromeDriver(=uc_driver) を利用しても問題が起きない.

さらに,undetected_chromedriver を FreeBSD で無理矢理動かしていた時は,zombie プロセスが大量に作成されてしまったので,定期的にプログラムを再起動してゾンビを殺していたが,終了処理などがきれいになっているのか,SeleniumBase だと zombie が発生しなかった.

ということで,今から利用する場合は,SeleniumBase の方が全然良い.(nodriver ももしかしたら良いのかもしれないけど,試してはいない)

2.4.3.4 UA について

2.4.4 /tmp に作成される cache の場所を変更したい.[2022-08-17] [2014年頃]

2.4.4.1 chrome 一般 [2022-08-17]

% export TMPDIR=/exp/tmp 

2.4.4.2 ruby の場合 [2014 年頃]

profile_dir = @model ? create_tmp_copy(@model) : Dir.mktmpdir("webdriver-profile")
 ↓
profile_dir = @model ? create_tmp_copy(@model) : Dir.mktmpdir("webdriver-profile", "/exp/tmp")

3. freebsd における tips

3.1 SeleniumBase の動作設定 [2025-02-22]

SeleniumBase もソースコードないでは Linux への分岐しか対応してない.ただ,undetected_chromedriver は Linux バイナリをダウンロードしてきていたので,Linux Emulation しなければならなかったが,SeleniumBase は chromedriver に対してなんらかの方法でパッチを当てているので,FreeBSD でインストールできる Chromium に附属している chromedriver をベースにして動作する.つまり,linux emulation しなくても大丈夫.

[2025-11-06] 8 ヶ月ぶりにバージョンアップしたが,すでにそこそこ変更点があった.特に,OS/プラットフォームの分岐のコードが増えていたので,その部分は要修正.というかこれ,FreeBSD で使いたい人いないのかな.ports にして,patch をみんなで管理したら便利だと思うんだけどな...

3.1.1 python ライブラリの準備

# pip install seleniumbase 

3.1.2 python ライブラリの FreeBSD 対応

/usr/local/lib/python3.11/site-packages/seleniumbase/undetected 以下に,下記のファイルが存在している.

__init__.py     cdp_driver      dprocess.py     patcher.py      webelement.py
__pycache__     cdp.py          options.py      reactor.py

patecher.py で以下.

15c16
< IS_POSIX = sys.platform.startswith(("darwin", "cygwin", "linux"))
---
> IS_POSIX = sys.platform.startswith(("darwin", "cygwin", "linux", "freebsd"))
30c31
<     if sys_plat.endswith("linux"):
---
>     if sys_plat.endswith("linux") or sys_plat.endswith("freebsd"):

/usr/local/lib/python3.11/site-packages/seleniumbase/fixtures/shared_utils.py というファイルでも OS 分岐を行なっていたので,ここでも Linux と同じ動作にしてしまう.

47 def is_linux():
48     return "linux" in sys.platform or "freebsd" in sys.platform

[2025-11-06] init.py は,以下の行数の部分で,FreeBSD の分岐を追加.

30 IS_POSIX = sys.platform.startswith(("darwin", "cygwin", "linux", "freebsd"))
481                     # (So that close() is always called)
482                     if "linux" in sys.platform or "freebsd" in sys.platform:
483                         self.close()
533                     if "linux" in sys.platform or "freebsd" in sys.platform:
534                         self.close()
535                     if self.service.is_connectable():
559             os.kill(self.browser_pid, 15)
560             if "linux" in sys.platform or "freebsd" in sys.platform:
561                 os.waitpid(self.browser_pid, 0)

[2025-11-06] core/detect_b_ver.py で,以下の行数で分岐を追加.517 - 526 については,丸っと追加している.

 34 class OSType(object):
 35     LINUX = "linux"
 36     FREEBSD = "freebsd"
 37     MAC = "mac"
 38     WIN = "win"
 39
 56 def os_name():
 57     if "linux" in sys.platform:
 58         return OSType.LINUX
 59     elif "freebsd" in sys.platform:
 60         return OSType.FREEBSD
 61     elif "darwin" in sys.platform:
 62         return OSType.MAC
109     if os_name() != OSType.LINUX or os_name() != OSType.FREEBSD:
110         return ""
111     paths = [
112         "/bin/google-chrome",
434         ChromeType.GOOGLE: {
435             OSType.LINUX: chrome_on_linux_path(chromium_ok, browser_type),
436             OSType.FREEBSD: chrome_on_linux_path(chromium_ok, browser_type),
437             OSType.MAC: r"/Applications/Google Chrome.app"
438                         r"/Contents/MacOS/Google Chrome",
439             OSType.WIN: chrome_on_windows_path(browser_type),
440         },     
517             OSType.FREEBSD: linux_browser_apps_to_cmd(
518                 "google-chrome",
519                 "google-chrome-stable",
520                 "chrome",
521                 "chromium",
522                 "chromium-browser",
523                 "google-chrome-beta",
524                 "google-chrome-dev",
525                 "google-chrome-unstable",
526             ),

これで少なくとも,僕の環境では SeleniumBase が FreeBSD で動作している.

3.2 undetected-chromedriver の動作設定 [2023-09-07]

undetected-chromedriver がダウンロードできるバイナリは上述の通りで,FreeBSD で動作させる為には,以下のどちらかの対応かと考えた.

こちらを見ると,chromedriver のみ linux emulation で動かし,chrome を FreeBSD バイナリというのは成功している模様だったので,1 を試したところ成功したので,FreeBSD バイナリへのパッチあては試していない.

3.2.1 linux emulation 設定

3.2.2 python ライブラリの準備

# pip install undetected-chromedriver 

[2023-09-07] 時点での最新版は 3.5.3.

3.2.3 python ライブラリの FreeBSD 対応

(もっときれいなやりかたをした方が良いと思う)

/usr/local/lib/python3.9/site-packages/undetected_chromedriver 以下に,下記のファイルが存在している.

__init__.py     devtool.py      options.py      reactor.py
cdp.py          dprocess.py     patcher.py      webelement.py

patcher.py が OS の分岐,ダウンロードバイナリの選定,パッチあて,などを行なっている.OS の分岐の部分で,Linux に振っているところに FreeBSD を入れる.また,unzip したあとに,executable にならないことがあった為,chmod コマンドで実行権をつけた.以下にパッチを示す.

< import subprocess
25c24
< IS_POSIX = sys.platform.startswith(("darwin", "cygwin", "linux", "linux2","freebsd"))
---
> IS_POSIX = sys.platform.startswith(("darwin", "cygwin", "linux", "linux2"))
37c36
<     elif platform.startswith(("linux", "linux2", "freebsd")):
---
>     elif platform.startswith(("linux", "linux2")):
112c111
<         if self.platform.endswith(("linux", "linux2")) or self.platform.startswith(("freebsd")):
---
>         if self.platform.endswith(("linux", "linux2")):
121d119
<         # self.platform_name = "linux64"
180,181c178
<         fname = self.unzip_package(self.fetch_package())
<         subprocess.run(["chmod", "+x", fname])
---
>         self.unzip_package(self.fetch_package())

dprocess.py が chromedriver プロセスを立ち上げている.ここの中で,start_detacher() という関数が使われているようで,multiprocessing.Process の前の行に,以下を追加.

os.environ['LD_LIBRARY_PATH'] = "/compat/ubuntu/usr/lib64"

Chrome() 呼出しの際に,API Document を見ると env を渡せると書いてあったが,うまく動作させられなかった.今後の TODO.

3.2.4 headless への対応

undetected_chromedriver の場合,headless での動作がうまくいかない気がする.調べ切れていないが,headless で動作させると,bot detection に引っかかる可能性が高くなるっぽい.

安定動作の為には,Xvfb を利用して,Window を立ち上げてしまう方が無難.

3.2.5 undetected_driver.Chrome() の使い方の注意

スクリプトを書いている中で,エラーが頻発した際などへの対応として,Chrome の再起動を行なう場合がある.

その時,Chrome に渡すオプションを扱う為の Opiotns() というクラスがあるが,これを再利用してはいけない模様.__init__.py の中に,以下のような記載があった.

        try:
            if hasattr(options, "_session") and options._session is not None:
                #  prevent reuse of options,
                #  as it just appends arguments, not replace them
                #  you'll get conflicts starting chrome
                raise RuntimeError("you cannot reuse the ChromeOptions object")

3.3 firefox? chrome? [2022-08-17]

3.4 firefox のバグ? [2022-08-17] [2014年頃]

window_height = @browser.driver.execute_script("return document.body.clientHeight;")
if (window_height < 15000) # within 15000px, capture whole page. otherwise, capture only the displayed window
 @browser.screenshot.save("file.png")
else 
 # hoge 
end

3.5 patch [2014年頃]


このページへのアクセス 今日: 4 / 昨日: 1 総計: 1709