【サンプルコード有り】streamlit cloudでseleniumを使おうとして沼にハマった話

今回はstreamlit cloud(旧:streamlit sharing)でseleniumを使う方法を解説します

  • 2023年12月30日コード修正しています(ブログに記載しているコードは修正後のコードです)
    修正内容は以下のとおり

    ・ChromeTypeのインストールするためのコード修正

実際に私がstreamlit cloudでアプリをデプロイした際に

いろんなエラーに遭遇して泣きそうになったので

エラーの解決方法をここにまとめておきます

おへんじ
ローカルでは動くのにstreamlit cloud上ではエラーがでて泣きそうになってたよ

この記事を読むとわかること・・

  • streamlit cloudでseleniumを使う方法
  • streamlit cloudでパッケージをインストールするときに気をつけること
  • seleniumを使う際のポイント

この記事で解説しないこと・・・

  • selenium・streamlitの仕組みやできることについて
  • selenium・streamlitの基本的なコードの書き方
  • streamlit cloudでアプリのdeploy(公開)の仕方

この記事では上の点については詳しく解説しませんので

知っていることを前提に解説をしていきます

この記事で紹介しているサンプルコードはgithub上に公開していますので

参考にされてください

おへんじ
説明で使用するコードにはコメントしておくね

エラー解決後のコード

遭遇した数々のエラーを解説する前に

最初に全てのエラーを解決した後のコードを

公開しておきます

ちなみにこのプログラムはstreamlit cloud上でseleniumを使って

適当なwebページ(当サイト)のタイトル画像を取得するプログラムです

実用的なアプリではなく、seleniumを使うための

最小限のコードとなっています

フォルダ構成

root
 ├── packages.txt
 └── main_app
     └── requirements.txt
     └── app.py
  • packages.txt:apt-get によりインストールするパッケージを記載
  • requirements.txt:pipによりインストールするパッケージを記載
  • app.py:streamlit cloud上にdeploy(公開)するファイル
packages.txtは必ずrootディレクトリ直下に配置する必要があります(公式ドキュメントより)

packages.txt

chromium

packages.txtに記載したパッケージは

アプリを公開すると同時にインストールされます

今回はseleniumを使うためにwebブラウザ・webドライバーが必要なので

packages.txtでchromiumブラウザをインストールします

【豆知識】packages.txtに書いたパッケージはapt-getによりインストールされます
apt-get:Linux関係のパッケージの管理・操作するためのコマンド

詳細についてはstreamlit公式ドキュメントに記載してあります

公式にはファイル名によってpipやconda、pipenv、poetryおよびapt-getなど

どのコマンドでパッケージをインストールするか変わるよーって書いてあります

簡単にまとめると・・・

requirements.txt

selenium
webdriver-manager

app.pyで使用するpythonパッケージを記載します

pythonの標準パッケージは書く必要はありません(記載するとエラーの原因になります)

標準パッケージ一覧はこちらに記載してあります

webdriver-managerはseleniumで必要な

webドライバーをインストールしてくれるパッケージです

webドライバーとwebブラウザは

同じバージョンにしておく必要がありますが

webdriver-managerを使用することで

自動でwebブラウザと同じバージョン

webドライバーをインストールしてくれます

【豆知識】requirements.txtに書いたパッケージはpipによりインストールされます

requirements.txtとpackages.txtの超ざっくりとした使い分けは

  • requirements.txt:pythonで使用されるパッケージをインストールする場合
  • packages.txt:python以外でも使用されるソフトウェアをインストールする場合
おへんじ
pythonパッケージはcondaなどでもインストールできるから注意してね

app.py

# coding:utf-8

# 必要なパッケージのインポート
import streamlit as st
from selenium import webdriver
from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.chrome import service as fs
from selenium.webdriver import ChromeOptions
from webdriver_manager.core.os_manager import ChromeType
from selenium.webdriver.common.by import By

# タイトルを設定
st.title("seleniumテストアプリ")

# ボタンを作成(このボタンをアプリ上で押すと"if press_button:"より下の部分が実行される)
press_button = st.button("スクレイピング開始")

if press_button:
    # スクレイピングするwebサイトのURL
    URL = "https://ohenziblog.com"

    # ドライバのオプション
    options = ChromeOptions()

    # option設定を追加(設定する理由はメモリの削減)
    options.add_argument("--headless")
    options.add_argument('--disable-gpu')
    options.add_argument('--no-sandbox')
    options.add_argument('--disable-dev-shm-usage')

    # webdriver_managerによりドライバーをインストール
   # chromiumを使用したいのでchrome_type引数でchromiumを指定しておく
    CHROMEDRIVER = ChromeDriverManager(chrome_type=ChromeType.CHROMIUM).install()
    service = fs.Service(CHROMEDRIVER)
    driver = webdriver.Chrome(
                              options=options,
                              service=service
                             )

    # URLで指定したwebページを開く
    driver.get(URL)

    # webページ上のタイトル画像を取得
    img = driver.find_element(By.TAG_NAME, 'img')
    src = img.get_attribute('src')

    # 取得した画像をカレントディレクトリに保存
    with open(f"tmp_img.png", "wb") as f:
        f.write(img.screenshot_as_png)

    # 保存した画像をstreamlitアプリ上に表示
    st.image("tmp_img.png")

    # webページを閉じる
    driver.close()

    # スクレピン完了したことをstreamlitアプリ上に表示する
    st.write("スクレイピング完了!!!")

今回のstreamlit cloud上に公開するプログラムです

内容としては・・・

『スクレイピング開始』ボタンを押すと

seleniumが起動して当サイトトップページを開き

タイトル画像を取得・表示するだけのシンプルなwebアプリです

streamlit cloudで実行するとこんな感じになります

ファイル構成とファイルの内容については以上になります

プログラムの中身の解説は下の方で説明します

実際に遭遇したエラーについて

次に実際に遭遇したエラーと

その解決方法を説明します

参考にしたページもあわせて載せておきます

生じたエラーは全部でこんな感じ・・・

  1. webブラウザがstreamlit cloud上にない

    →(解決策)packages.txtをrootディレクトリ直下に配置することで解決

  2. packages.txtではchrome・firefoxはインストールできない

    →(解決策)chromeはあきらめてchromiumに変更して解決

  3. メモリが上限だからアプリ動かない

    →(解決策)メモリファイルを変更する設定をドライバーのオプションに追加して解決

  4. webブラウザとwebドライバーのバージョンが違う

    →(解決策)ChromeDriverManagerのchrome_type引数にChromeType.CHROMIUMを追加して解決

1つずつ解決方法を説明していきます

エラーについて解説する前に

最初にデプロイした初期コードを載せておきます

最終コードと違う箇所に印をつけています

フォルダ構成

root
 └── main_app
     └── packages.txt # main_appの下に配置していた
     └── requirements.txt
     └── app.py

packages.txt

chrome # 初期はchromeブラウザを使用したかった

requirements.txt

selenium
webdriver-manager

app.py

# coding:utf-8
import streamlit as st
from selenium import webdriver
from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.chrome import service as fs
from selenium.webdriver import ChromeOptions

# タイトルを設定
st.title("seleniumテストアプリ")

# スクレイピングするwebサイトのURL
URL = "https://ohenziblog.com"

# ドライバのオプション
options = ChromeOptions()

# option設定を追加
options.add_argument("--headless")  # ブラウザを画面に表示せずに起動できる(streamlit cloudでは、この設定は必須)

# webdriver_managerによりドライバーをインストール
CHROMEDRIVER = ChromeDriverManager().install()
service = fs.Service(CHROMEDRIVER)
driver = webdriver.Chrome(
                          options=options,
                          service=service
                         )

# URLで指定したwebページを開く
driver.get(URL)

# webページを閉じる
driver.close()

# スクレピン完了したことをstreamlitアプリ上に表示する
st.write("selenium終了")

最初は、画像を取得・表示!なんてことはせず

ただ、seleniumを起動・終了するだけのコードにしてました

理由としてはseleniumがstreamlit cloud上で使えることが

確認できればいい!と思っていたので

こんなクソみたいなコードになっています笑

【エラーその1】webブラウザがインストールされていない

上記の初期コードでデプロイしたときに

次のようなエラーが発生しました

Traceback (most recent call last):
  File "/home/appuser/venv/lib/python3.9/site-packages/streamlit/runtime/scriptrunner/script_runner.py", line 563, in _run_script
    exec(code, module.__dict__)
  File "/app/root/main_app/blog_app.py", line 27, in <module>
    driver = webdriver.Chrome(
  File "/home/appuser/venv/lib/python3.9/site-packages/selenium/webdriver/chrome/webdriver.py", line 69, in __init__
    super().__init__(DesiredCapabilities.CHROME['browserName'], "goog",
  File "/home/appuser/venv/lib/python3.9/site-packages/selenium/webdriver/chromium/webdriver.py", line 89, in __init__
    self.service.start()
  File "/home/appuser/venv/lib/python3.9/site-packages/selenium/webdriver/common/service.py", line 98, in start
    self.assert_process_still_running()
  File "/home/appuser/venv/lib/python3.9/site-packages/selenium/webdriver/common/service.py", line 110, in assert_process_still_running
    raise WebDriverException(
selenium.common.exceptions.WebDriverException: Message: Service /home/appuser/.wdm/drivers/chromedriver/linux64/107.0.5304/chromedriver unexpectedly exited. Status code was: 127

上記のエラーについていろいろ調べてみると

chromedriver周りでエラーが出たときに上がるエラーコードらしくて

考えられる直接的な原因がなかなかわかりませんでした

ここでseleniumはwebドライバーとwebブラウザの両方がないと

起動することができないので、そのどちらかがうまくインストールできてない

可能性があるかなと考えてstreamlit cloud上のログを確認してみると

webドライバーは以下のログがあったのでインストールできてそうな感じでした

2022-10-28 14:53:07.427 ====== WebDriver manager ======
2022-10-28 14:53:07.448 Get LATEST chromedriver version for google-chrome None
2022-10-28 14:53:07.502 There is no [linux64] chromedriver for browser None in cache
2022-10-28 14:53:07.502 About to download new driver from https://chromedriver.storage.googleapis.com/107.0.5304.62/chromedriver_linux64.zip
[WDM] - Downloading: 100%|██████████| 7.26M/7.26M [00:00<00:00, 194MB/s]-28 14:53:07.572878] 
2022-10-28 14:53:07.728 Driver has been saved in cache [/home/appuser/.wdm/drivers/chromedriver/linux64/107.0.5304]

ちなみにstreamlit coludのログはwebページ上の

画像下の赤枠の部分をクリックすると表示されます

アプリを公開した人しか『manage app』のログを開くことができません

webドライバーはインストールできているので

webブラウザがうまくインストールできていないのかなーと考え

streamlitの公式ドキュメントを確認してみると

以下の記載がありました

packages.txtがリポジトリのルートディレクトリに存在する場合、自動的に検出、解析し、リストされたパッケージをインストールします。

公式ドキュメントはこちら

要はpackages.txtはrootディレクトリの直下に配置しろよーーってことです

私はrootディレクトリの直下ではなく

main_appディレクトリ配下にpackages.txtを

配置していたのでchromeブラウザを

インストールできていませんでした

見習くん
いつも英語だから読まないけど公式ドキュメントって大切なんですねー

【エラーその2】chrome・firefoxはパッケージリポジトリにない

packages.txtをrootディレクトリ直下に配置して

streamlit cloudログを見てみると・・・

Apt dependencies were installed from /app/streamlit_temple/packages.txt using apt-get.

どうやらpackages.txtをみつけたから

書かれているパッケージをapt-getで

インストールするよーって書かれています

やったー!と思った次の瞬間・・・

こんなエラーがでました泣

E: Unable to locate package chrome

chromeってパッケージみつからないよーってことらしいです

どういうことやねん!と思いながら、調べてみると

githubでstreamlit cloudでseleniumが使えるサンプルコードを

公開している海外の方がおられまして(githubサイトはこちら)

そのページを見てみるとpackages.txtに書かれたパッケージは

Debian Buster apt package repositoriesという

リポジトリからインストールされるらしく

そのデフォルトリポジトリに

chromeは存在していないらしいです

デフォルトリポジトリにあるのは

chromiumだよーってことも書いてありました

てことで、chromeはあきらめてchromiumブラウザを

使うことにしました

ちなみにfirefoxもchrome同様インストールできませんでした

【豆知識】firefoxを使用したい場合は、『firefox-esr』は
     デフォルトリポジトリに存在するためインストールできます
chromium・firefox:ブラウザの一種

packages.txtを以下のとおり修正

chromium

【エラーその3】メモリ不足によりseleniumが強制終了

packages.txtを修正して更新してみると

次はこんなエラーが発生しました

selenium.common.exceptions.WebDriverException: Message: unknown error: session deleted because of page crash

どうやらページがクラッシュしたらしいです

クラッシュ:衝突する、潰れる、砕ける

なんや!クラッシュってなんや!勘弁してくれ!

と思いながら、折れそうな心にムチ打って調べてみると

メモリが不足していることによって

ページがクラッシュしていることがわかりました

webドライバーにはオプションで様々な設定を

追加することができますが

『–disable-dev-shm-usage』オプションがあったので追加してみました

chromiumの場合『–disable-dev-shm-usage』オプションを追加すると

メモリファイルが『/dev/shm』→『/tmp』ディレクトリに変更され

メモリ不足によるクラッシュを防ぐことができるそうです

『/dev/shm』は容量が64MBしかないためすぐクラッシュするらしいです

結論を言うと『–disable-dev-shm-usage』オプションだけで

メモリ不足のエラーは解消できましたが

そのほかにも必要そうな『–disable-gpu』『–no-sandbox』オプションを追加しました

以下のとおりオプションを追加しました

# ドライバのオプション
options = ChromeOptions()

# option設定を追加
options.add_argument("--headless")
# option設定を追加(設定する理由はメモリの削減)
options.add_argument('--disable-gpu')
options.add_argument('--no-sandbox')
options.add_argument('--disable-dev-shm-usage')

オプションの簡単は説明は以下のとおり

  • –headless:ブラウザを非表示で起動する(streamlit cloudではこのオプションは必須)
  • –disable-gpu:GPUハードウェアアクセラレーションを無効にする→『–headless』とセットで必要
  • –no-sandbox:sandbox(仮想環境によるセキュリティ向上)モードを解除→権限不足によるエラー防止
  • –disable-dev-shm-usage:メモリファイルを『/dev/shm』(64MB)→『/tmp』ディレクトリに変更しメモリ不足回避
GPUハードウェアアクセラレーション:CPU処理の一部をGPU側にお願いする機能

【エラーその4】ブラウザとドライバーのバージョンが違う

上のとおり修正をして今度こそデプロイしてみたところ・・・

またエラーがでました泣

2022-10-25 13:14:39.062 ====== WebDriver manager ======
2022-10-25 13:14:39.098 Get LATEST chromedriver version for google-chrome None
2022-10-25 13:14:39.166 Driver [/home/appuser/.wdm/drivers/chromedriver/linux64/107.0.5304/chromedriver] found in cache
2022-10-25 13:14:39.799 Uncaught app exception
--(略)--
selenium.common.exceptions.SessionNotCreatedException: Message: session not created: This version of ChromeDriver only supports Chrome version 107
Current browser version is 106.0.5249.119 with binary path /usr/bin/chromium

どうやらドライバーとブラウザのバージョンが違うらしい

今回の場合・・・

  • webドライバー version:107.0.5304
  • webブラウザ version:106.0.5249

バージョンが一致すればエラーは解消しそうだけど

そもそもドライバーはwebdriver_managerでインストールしているので

自動でブラウザど同じバージョンのドライバーを

インストールしてくれるはずなのにー、おかしいなーという

壁にぶち当たりました

いろいろ考えたところ・・・

現在のドライバーを起動するコードはこの3行

# webdriver_managerによりドライバーをインストール
CHROMEDRIVER = ChromeDriverManager().install()
service = fs.Service(CHROMEDRIVER)
driver = webdriver.Chrome(options=options, service=service)

現在packages.txtによりインストールしているのは

chromiumブラウザです

chromeブラウザを使用するときも

同じコードを使用しているため

chromiumを使用する場合は、引数とかに何かしらを

記載する必要があるかなーと思って公式ドキュメント読んでみると

ChromeDriverManager()の引数にchrome_type

指定する必要がありました

てことで以下のようにドライバー部分を修正

# CromeTypeクラスを新たにインポート
from webdriver_manager.core.os_manager import ChromeType

# webdriver_managerによりドライバーをインストール
CHROMEDRIVER = ChromeDriverManager(chrome_type=ChromeType.CHROMIUM).install()
service = fs.Service(CHROMEDRIVER)
driver = webdriver.Chrome(options=options, service=service)

これで四度目の正直のデプロイをしたところ

ついに・・・

seleniumが正常に起動しました

めでたしめでたし

これでstreamlit cloud上でseleniumが起動することが確認できたので

スクレイピングでwebページのタイトル画像を取得・表示と

スクレイピング開始ボタンを追加して最終版のコードが完成

# coding:utf-8

# 必要なパッケージのインポート
import streamlit as st
from selenium import webdriver
from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.chrome import service as fs
from selenium.webdriver import ChromeOptions
from webdriver_manager.core.os_manager import ChromeType
from selenium.webdriver.common.by import By

# タイトルを設定
st.title("seleniumテストアプリ")

# ボタンを作成(このボタンをアプリ上で押すと"if press_button:"より下の部分が実行される)
press_button = st.button("スクレイピング開始")

if press_button:
    # スクレイピングするwebサイトのURL
    URL = "https://ohenziblog.com"

    # ドライバのオプション
    options = ChromeOptions()

    # option設定を追加(設定する理由はメモリの削減)
    options.add_argument("--headless")
    options.add_argument('--disable-gpu')
    options.add_argument('--no-sandbox')
    options.add_argument('--disable-dev-shm-usage')
    options.add_argument('--remote-debugging-port=9222')

    # webdriver_managerによりドライバーをインストール
    CHROMEDRIVER = ChromeDriverManager(chrome_type=ChromeType.CHROMIUM).install()  # chromiumを使用したいので引数でchromiumを指定しておく
    service = fs.Service(CHROMEDRIVER)
    driver = webdriver.Chrome(
                              options=options,
                              service=service
                             )

    # URLで指定したwebページを開く
    driver.get(URL)

    # webページ上のタイトル画像を取得
    img = driver.find_element(By.TAG_NAME, 'img')
    src = img.get_attribute('src')

    # 取得した画像をカレントディレクトリに保存
    with open(f"tmp_img.png", "wb") as f:
        f.write(img.screenshot_as_png)

    # 保存した画像をstreamlitアプリ上に表示
    st.image("tmp_img.png")

    # webページを閉じる
    driver.close()

    # スクレピン完了したことをstreamlitアプリ上に表示する
    st.write("スクレイピング完了!!!")

デプロイして動作確認をしたことろ

ちゃんと取得した画像が表示されました

今回のデバック作業で公式ドキュメントを読むことの大切さがわかりました笑

ちなみに今後はstreamli cloudを使って

競馬予想アプリを公開する予定ですので

そちらもまた完成次第公開しますので

よろしければぜひ参考にしてみてください


まとめ

streamlit cloudでseleniumを使用する時のポイント

  • packages.txtはrootディレクトリ直下に配置する
  • packages.txtではchrome・firefoxブラウザはインストールできない
  • ドライバにオプションを設定してメモリ不足を避ける
  • chromiumブラウザを使用する場合は
    ChromeDriverManagerの引数にChromeType.CHROMIUMを追加する
最新情報をチェックしよう!