= [[selenium-webdriver]] LM: [2025-02-22 16:05:39]
~~NOCACHE~~
----
====== - 概要 ======
* selnium は,web のテストフレームワーク.
* ブラウザを外部から叩きつつ,テスト仕様通りに動作しているかを確認できる.
* chrome の場合,webdriver が daemon として動作しつつ,ブラウザとは独立したプログラムである devtool を叩くことで chrome を動作させている模様.
* 詳細は https://chromedriver.chromium.org/home ここら辺を読むときっといっぱい書いてあるに違いない.W3C でも標準化されてるっぽい.
* ブラウザを起動しているので,XmlHttpRequest(AJAX) などで動的に追加された要素など,HTMLのパースだけでは取得できないデータの取得が可能.ただし,グラフなどの canvas に描画されてしまったものは,多分取得できない.
* Xvfb と併用することで,サーバにおいてブラウザを立ち上げて,スクレイピングしたりスクリーンショットをとったりしやすい. 2018 年くらいには -headless モードが実装されており,Xvfb と併用しなくてサーバ内で動作が完結するようになっていた模様.
====== - インストール ======
===== - ブラウザ環境 =====
* firefox または Chrome を ports で入れておく.
===== - python からの利用 [2023-09-07] [2022-08-17] =====
* 特に特別なライブラリを入れずとも動作する.
* pip で selenium が入っているか確認する.もし入ってなかったら,
# pip list | grep selenium # 入っているか確認
# pip install selenium # うまく動作した.
# portmaster -D www/py-selenium # うまく動作してなかったかもしれない.
* [2024-06-27] chrome 126 を headless で動かそうとしたが上手く動作せず.もしかしたら,下記 URL のように,FreeBSD だと上手く動かないのかもしれない.X forward/デスクトップ環境以外で,headless のように動かしたいならば,Xvfb の方が安定すると思われる.
* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=278413
===== - ruby binding 環境での構築 [2014年頃] =====
* 本当は,rvm 環境とかを使う方が良いのだけど,FreeBSD での利用方法を良く知らないので,system wide に入れちゃう.
* devel/ruby-gems を ports で入れておく.
* 以下の gem を入れる.
% sudo gem install selenium-webdriver
% sudo gem install watir-webdriver
* 以下参照しながら,適当にサンプルを.
* ruby bindings 専用記事: http://code.google.com/p/selenium/wiki/RubyBindings
* API のちょっと詳細な使い方: http://seleniumhq.org/docs/03_webdriver.jsp#selenium-webdriver-api-commands-and-operations
* 本ページと似たような説明文章(日本語): http://yakinikunotare.boo.jp/orebase2/ruby/web_browser_control
===== - tips =====
==== - 画像保存の方法 ====
[2025-01-13]
selenium を使って,Web 上の画像を保存する方法は,大きく分けて,
- screenshot をとる
- 名前をつけて保存
- 画像の URL を取得し,request.get() する
の3つな気がする.1 番目は表示さえされてれば確実に画像が保存できるのだが,画像が縮小されていると画質が落ちてしまうし,取得した画像を jpeg や webp に変換していると結構な CPU リソースを食ってしまう.
2 番目の名前をつけて保存は,もし画像の URL とスクレイピング先のホストが同じ場合,キャッシュから保存されるので,1度の URL アクセスで画像が保存できるのでもっとも効率的になるが,違う場合,クロスサイト対策(?)の関係で,もう一度 URL アクセスが発生してしまう.
そのため,2 番目と 3 番目はアクセスという面で考えると同じになってしまう.
状況に応じて,適した方法を選ぶ必要がある.
==== - 名前をつけて保存 ====
[2025-01-13]
ブラウザ上の画像を右クリックし,名前をつけて保存,と押すと,保存ダイアログが出てきて,保存先を選び,画像を保存することになる.
この時出てくる保存ダイアログは,ブラウザの一部ではなく,OS(Windows)の管理下にあるので,selenium からコントロールすることができない.
よって,名前をつけて保存の処理を自動で行ないたい場合は,selenium とは別の枠組で考える必要がある.
FreeBSD/Linux などの XWindow な環境の場合,xdotool を利用すると,簡単に XWindow 上での操作が行なえるので,selenium と組み合わせると便利かもしれない.他にも pyautogui などがあるようだが,僕の FreeBSD 環境では上手く動かせなかった.[[http://takeno.iee.niit.ac.jp/~shige/FreeBSD/fbsd-omemo.html#o-20220127|参考1]][[https://stackoverflow.com/questions/58432057/how-do-i-click-enter-when-a-save-as-window-is-open-with-selenium|参考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)
==== - selenium (bot) 検知への対応について ====
特定のサイトでは,selenium でのアクセスを禁止している.UA などのアクセス情報から selenium で動作していることを検知している.
=== - selenium detection の解説サイト ===
* https://stackoverflow.com/questions/67551031/how-to-pass-desired-capabilities-to-undetected-chromedriver-with-selenium-python
* https://stackoverflow.com/questions/72406597/how-to-avoid-bot-detection-on-websites-using-selenium-python
* [2025-02-22] https://www.zenrows.com/blog/selenium-cloudflare-bypass
=== - undetected chromedriver [2023-09-07] ===
selenium であることの検知は,主に chromedriver (webdriver) と chrome の通信の中に痕跡が残り,アクセス時にその痕跡がサーバ側に通知されてしまうことで発生する模様.
これを解決する為,通常の chromedriver に少し手を加えるツールが存在している.[2023-09-07] 時点で試したのは,[[https://github.com/ultrafunkamsterdam/undetected-chromedriver|undetected-chromedriver]].
undetected-chromedriver は内部的に,以下のような動作になっている.
* chrome のバージョンが指定されてなかったら,chrome のバージョンを取得する
* そのバージョンと同じ chromedriver をダウンロードしてくる.Windows, Mac, Linux のバイナリに対応している.
* chromedriver そのものにパッチをあてる.
* python で webdriver.Chrome() として chromedriver->chrome と呼び出すところを,undetected_chromedriver.Chrome() でラップし,UA 書き換えなどを行なった状態で,chromedriver->chrome と呼び出すようにする
FreeBSD で動作させる為には,[[https://www.sakaki.works/doku/doku.php?id=selenium-webdriver#undetected-chromedriver_%E3%81%AE%E5%8B%95%E4%BD%9C%E8%A8%AD%E5%AE%9A_2023-09-07|下記]]参照.
=== - 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 ももしかしたら良いのかもしれないけど,試してはいない)
=== - UA について ===
* headless モードで動作させると,UA に headless であることが記載されてしまう.
* 不都合が生じる場合は,chrome の起動オプションで UA の変更ができるので,headless ではない記載をすると良い.UA のリストはググルといっぱい出てくる.
==== - /tmp に作成される cache の場所を変更したい.[2022-08-17] [2014年頃] ====
=== - chrome 一般 [2022-08-17] ===
* chrome が screenshot をとる際,デフォルトでは /tmp を利用する.
* 環境変数 TMPDIR を指定すれば screenshot の一時保存先の変更が可能.
% export TMPDIR=/exp/tmp
* [2022-08-17] headless 環境では,--disable-gpu をつけることで,動作が安定した.もしかしたら,--no-sandbox,--disable-dev-shm-usage も効いているかもしれないが,詳細は実験していない.
=== - ruby の場合 [2014 年頃] ===
* webdriver が利用する cache が Dir.mktmpdir で作成されるため,/tmp に必ず作成される.大量のページをスクレイプする場合には,/tmp を使いきってしまう可能性がある.
* /usr/local/lib/ruby/gems/1.8/gems/selenium-webdriver-2.27.2/lib/selenium/webdriver/firefox/profile.rb の Dir.mktmpdir を以下のように書き換えて,とりあえずしのぐ.
profile_dir = @model ? create_tmp_copy(@model) : Dir.mktmpdir("webdriver-profile")
↓
profile_dir = @model ? create_tmp_copy(@model) : Dir.mktmpdir("webdriver-profile", "/exp/tmp")
* 参照元: http://stackoverflow.com/questions/13638228/change-the-default-folder-where-temporary-profile-is-created-by-firefox
====== - freebsd における tips ======
===== - SeleniumBase の動作設定 [2025-02-22] =====
SeleniumBase もソースコードないでは Linux への分岐しか対応してない.ただ,undetected_chromedriver は Linux バイナリをダウンロードしてきていたので,Linux Emulation しなければならなかったが,SeleniumBase は chromedriver に対してなんらかの方法でパッチを当てているので,FreeBSD でインストールできる Chromium に附属している chromedriver をベースにして動作する.つまり,linux emulation しなくても大丈夫.
==== - python ライブラリの準備 ====
# pip install seleniumbase
==== - 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
これで少なくとも,僕の環境では SeleniumBase が FreeBSD で動作している.
===== - undetected-chromedriver の動作設定 [2023-09-07] =====
undetected-chromedriver がダウンロードできるバイナリは上述の通りで,FreeBSD で動作させる為には,以下のどちらかの対応かと考えた.
* Linux emulation で chromedriver を動かす
* FreeBSD のバイナリにパッチを当てられるようにする.
[[https://www.slideshare.net/hirofumitouhei/selenium-webdriverfreebsd-102google-chrome|こちら]]を見ると,chromedriver のみ linux emulation で動かし,chrome を FreeBSD バイナリというのは成功している模様だったので,1 を試したところ成功したので,FreeBSD バイナリへのパッチあては試していない.
==== - linux emulation 設定 ====
* [[https://www.sakaki.works/doku/doku.php?id=freebsd#linux_emulation_2023-09-07|FreeBSD 側の設定を参照]]
==== - python ライブラリの準備 ====
# pip install undetected-chromedriver
[2023-09-07] 時点での最新版は 3.5.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() 呼出しの際に,[[https://www.selenium.dev/selenium/docs/api/py/webdriver_chromium/selenium.webdriver.chromium.service.html|API Document]] を見ると env を渡せると書いてあったが,うまく動作させられなかった.今後の TODO.
==== - headless への対応 ====
undetected_chromedriver の場合,headless での動作がうまくいかない気がする.調べ切れていないが,headless で動作させると,bot detection に引っかかる可能性が高くなるっぽい.
安定動作の為には,Xvfb を利用して,Window を立ち上げてしまう方が無難.
==== - 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")
===== - firefox? chrome? [2022-08-17] =====
* 2022 年 8 月時点では、ports において Firefox 向けの webdriver の更新が止まっているので、動作しない。
* Chrome は、chromium のソースコード内に webdriver が存在しているので、バージョンミスマッチを気にせず利用可能.
===== - firefox のバグ? [2022-08-17] [2014年頃] =====
* 一定以上の高さ(20000pxとか)のあるページをキャプチャしようとすると,save_screenshot が JS 部分で落ちる.
* 実装的には,firefox の js エンジンで,ページを canvas に書き出し,PNG として保存している.
* ./firefox/extension/webdriver.xpi の中にある driver_component.js.
* 多分,firefox のメモリ管理の問題で,一定以上の大きさの画像を扱えないと思われる.
* workaround としては,以下のように javascript を実行して window の高さを事前に取得し,一定以上なら,js 経由ではなく import コマンド経由で取得する.
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
* [2022-08-17] 2022 年時点の chrome では,高さを 2 万 px などにしても問題はなかった.マシンは 2014 年時点と同じマシンなので,スペックの問題ではないと思われる.
* [2023-09-07] window.scrollTo() などをうまく利用して,数万 px の高さの Window は作らない方が無難と思われる.どこにバグが潜んでいるか怪しい.
===== - patch [2014年頃] =====
* 起動のタイミングの関係で、ネットワークコネクションが立ち上がるまで sleep を入れる必要があるみたい。(かな?)
* https://github.com/rdeguzman/selenium-webdriver/blob/master/lib/selenium/webdriver/common/socket_poller.rb
----
このページへのアクセス
今日: {{counter|today}} / 昨日: {{counter|yesterday}}
総計: {{counter|total}}