目次
この記事では、以下の内容についてご紹介します:
- 目次
- はじめに
- 仮想Webブラウザとは
- 仮想Webブラウザのメリット
- 仮想Webブラウザのデメリット
- 実装イメージ図
- 実装コード
- 実行コマンド
- 実装の工夫点
- デモ画像
- 今回利用した技術
- Streamlitサンプルコード
- Seleniumサンプルコード
- Pillowサンプルコード
- venvサンプルコマンド
- Gitサンプルコマンド
- 感想
はじめに
東京ガスiネットアジャイル推進ユニットに所属している髙橋です。
当社が参画する電力トレーディング案件にて、Streamlitを用いてデータベースの内容を画面表示する機能を実装しました。
その経験から、私が大学で研究していた仮想Webブラウザを、Streamlit上で簡易的に再現できると考え、今回プロトタイプを作成しました。
本記事では、仮想Webブラウザの説明、実装の工夫点、および今後の展望について紹介します。
仮想Webブラウザとは
仮想Webブラウザとは、利用者の端末からWeb閲覧機能を分離する方法の一つです。
仕組みとしては、別のコンピュータでWebブラウザを起動し、その表示画面のみを利用者の端末に転送します。そして、利用者による操作は仮想Webブラウザに送信され、遠隔で操作することができます。
参考: 仮想Webブラウザ
仮想Webブラウザのメリット
- セキュリティ
- リソースの効率化
- 仮想Webブラウザを使用することで、ローカルマシンのリソースを節約できます。
仮想Webブラウザのデメリット
- パフォーマンス
- 仮想環境のオーバーヘッドにより、ネイティブのブラウザと比べて動作が遅くなることがあります。
- インターネットの通信速度に依存するため、ネットワーク遅延が影響することがあります。
- 互換性
- ユーザビリティ
- 仮想Webブラウザの操作に慣れるまで時間がかかることがあります。
- ローカルマシンのブラウザよりもインターフェースが使いにくい場合があります。
実装イメージ図

実装コード
app.py
import io import os import time from datetime import datetime import psutil from PIL import Image from PIL.PngImagePlugin import PngImageFile from selenium import webdriver from selenium.webdriver.common.action_chains import ActionChains from selenium.webdriver.common.keys import Keys from selenium.webdriver.firefox.options import Options from selenium.webdriver.firefox.service import Service from streamlit_image_coordinates import streamlit_image_coordinates import const import streamlit as st def set_selenium_driver(): """ドライバ設定 """ options = Options() options.add_argument("--headless") service = Service(const.DRIVER_PATH) st.session_state.driver = webdriver.Firefox(service=service, options=options) st.session_state.actions = ActionChains(st.session_state.driver) print("Firefoxドライバ起動時刻:", datetime.now().strftime(const.TIME_FORMAT)) def remove_screenshot_file(): """スクリーンショットの画像ファイルを削除する """ if os.path.isfile(const.SCREENSHOT_PNG_PATH): os.remove(const.SCREENSHOT_PNG_PATH) print("PNG形式の画像ファイル削除時刻:", datetime.now().strftime(const.TIME_FORMAT)) if os.path.isfile(const.SCREENSHOT_WEBP_PATH): os.remove(const.SCREENSHOT_WEBP_PATH) print("WebP形式の画像ファイル削除時刻:", datetime.now().strftime(const.TIME_FORMAT)) def close_firefox(): """Firefoxを閉じる """ for proc in psutil.process_iter(["pid", "name"]): if proc.info["name"] == const.PROCESS_NAME: proc.terminate() try: proc.wait(timeout=3) except psutil.NoSuchProcess: pass print("Firefox閉じ時刻:", datetime.now().strftime(const.TIME_FORMAT)) def take_screenshot_url(target_url: str) -> bytes: """対象URLのスクリーンショットをとる Args: target_url (str): 対象URL Returns: bytes: png形式の画像データ """ st.session_state.driver.get(target_url) print("対象URLへのアクセス時刻:", datetime.now().strftime(const.TIME_FORMAT)) st.session_state.actions.send_keys(Keys.END).perform() st.session_state.actions.send_keys(Keys.HOME).perform() time.sleep(3) screenshot_png = st.session_state.driver.get_full_page_screenshot_as_png() print("スクリーンショット撮影時刻:", datetime.now().strftime(const.TIME_FORMAT)) return screenshot_png def take_screenshot_click(value: dict): """クリックされた場所のスクリーンショットをとる Args: value (str): 画像に対する座標などのクリック情報 """ image_size = {"width": value["width"], "height": value["height"]} window_size = st.session_state.driver.get_window_size() x = round(value["x"] * window_size["width"] / image_size["width"]) scroll_height = window_size["height"] * (value["y"] // window_size["height"]) y = value["y"] - scroll_height st.session_state.driver.execute_script(f"window.scrollBy(0, {scroll_height});") st.session_state.actions.move_by_offset(x, y).click().perform() print("Firefoxドライバでのクリック時刻:", datetime.now().strftime(const.TIME_FORMAT)) time.sleep(3) st.session_state.actions.send_keys(Keys.END).perform() st.session_state.actions.send_keys(Keys.HOME).perform() time.sleep(3) screenshot_png = st.session_state.driver.get_full_page_screenshot_as_png() print("スクリーンショット撮影時刻:", datetime.now().strftime(const.TIME_FORMAT)) with Image.open(io.BytesIO(screenshot_png)) as png_data: if st.session_state.screenshot_path == const.SCREENSHOT_PNG_PATH: save_png(png_data) elif st.session_state.screenshot_path == const.SCREENSHOT_WEBP_PATH: save_webp(png_data) def save_png(png_data: PngImageFile): """PNG形式で画像ファイルを保存する Args: png_data (PngImageFile): スクリーンショットの画像データ """ png_data.save(const.SCREENSHOT_PNG_PATH, format=const.IMAGE_FORMAT_PNG) print("PNG形式の画像ファイル保存時刻:", datetime.now().strftime(const.TIME_FORMAT)) st.session_state.screenshot_path = const.SCREENSHOT_PNG_PATH st.rerun() def save_webp(png_data: PngImageFile): """WebP形式で画像ファイルを保存する Args: png_data (PngImageFile): スクリーンショットの画像データ """ png_data = png_data.resize((png_data.width // const.IMAGE_RESIZE_LEVEL, png_data.height // const.IMAGE_RESIZE_LEVEL)) png_data.convert("RGB").save(const.SCREENSHOT_WEBP_PATH, format=const.IMAGE_FORMAT_WEBP, quality=const.IMAGE_QUALITY) print("WebP形式の画像ファイル保存時刻:", datetime.now().strftime(const.TIME_FORMAT)) st.session_state.screenshot_path = const.SCREENSHOT_WEBP_PATH st.rerun() @st.dialog("仮想Webブラウザ") def virtual_web_browser_dialog(): """ダイアログ """ print("ダイアログ表示時刻:", datetime.now().strftime(const.TIME_FORMAT)) url = st.text_input( label="対象URL", value=const.DEFAULT_URL, placeholder=const.PLACEHOLDER_URL) if st.button("実行"): if url: screenshot_png = take_screenshot_url(url) with Image.open(io.BytesIO(screenshot_png)) as png_data: save_png(png_data) elif st.button("実行(画像圧縮)"): if url: screenshot_png = take_screenshot_url(url) with Image.open(io.BytesIO(screenshot_png)) as png_data: save_webp(png_data) def app(): """メイン """ st.set_page_config(**const.SET_PAGE_CONFIG) st.markdown(const.ST_STYLE, unsafe_allow_html=True) if "screenshot_path" not in st.session_state: virtual_web_browser_dialog() else: value = streamlit_image_coordinates(st.session_state.screenshot_path, use_column_width=True) if value: print("ユーザの画面クリック時刻:", datetime.now().strftime(const.TIME_FORMAT)) take_screenshot_click(value) if __name__ == "__main__": if "driver" not in st.session_state: print("========================= start =========================") remove_screenshot_file() close_firefox() set_selenium_driver() app()
const.py
DRIVER_PATH = r"driver\geckodriver.exe" PLACEHOLDER_URL = "https://example.com/" DEFAULT_URL = "https://www.google.co.jp/" SCREENSHOT_PNG_PATH = r"src\screenshot\screenshot.png" SCREENSHOT_WEBP_PATH = r"src\screenshot\screenshot.webp" PROCESS_NAME = "firefox.exe" IMAGE_FORMAT_PNG = "PNG" IMAGE_FORMAT_WEBP = "WebP" IMAGE_QUALITY = 30 IMAGE_RESIZE_LEVEL = 2 TIME_FORMAT = "%Y/%m/%d %H:%M:%S" SET_PAGE_CONFIG = { "page_title": "My App", "layout": "wide", "initial_sidebar_state": "collapsed", } ST_STYLE = """ <style> header[data-testid="stHeader"] { visibility: hidden; height: 0%; } div[data-testid="stToolbar"] { visibility: hidden; height: 0%; } div[data-testid="stDecoration"] { visibility: hidden; height: 0%; } div[data-testid="stVerticalBlock"] { gap: 0; } div[data-testid="stButton"] { padding-top: 1rem; } div[data-testid="stMainBlockContainer"] { padding: 0; } div[data-testid="stElementToolbar"]{ visibility: hidden; } </style> """
実行コマンド
streamlit run app.py
実装の工夫点
- 通常パターンと画像圧縮パターンの2パターンで動作するようにしたこと
- ドライバで上下にスクロールする処理を追加して、動的なサイトになるべく対応したこと
- ダイアログを利用して、実際に対象のWebページにアクセスしたかのように見せかけたこと
デモ画像
初期起動画面
GoogleのURLを指定して、実行(画像圧縮)ボタンをクリック後
表示された画像の左上部の「Googleについて」の部分をクリック後
表示された画像のタグの「日本におけるGoogle」の部分をクリック後

今回利用した技術
- Streamlit (Webアプリケーションフレームワーク)
- Selenium (スクレイピング)
- Pillow(PIL) (画像処理)
- venv (仮想実行環境)
- GitHub (バージョン管理プラットフォーム)
Streamlitサンプルコード
表示系
# テキスト st.write("タイトル") # 注釈 st.caption("注釈") # 画像 st.image("https://picsum.photos/100") df = pd.DataFrame({"カラム1": [1, 2, 3], "カラム2": [10, 20, 30]}) # テーブル st.table(df) # チャート st.line_chart(df)

入力系
# テキスト入力 name = st.text_input("名前") st.write(name) # 数値入力 age = st.number_input("年齢", step=1) st.write(age) # ボタン if st.button("ボタン"): st.write("ボタンを押しました") # プルダウン select = st.selectbox("好きな果物", options=["りんご", "ばなな", "いちご"]) st.write(select) # プルダウン(複数選択) multi_select = st.multiselect("好きな色", options=["赤", "青", "黄"]) st.write(multi_select) # チェックボックス check = st.checkbox("OK") st.write(f"チェック:{check}") # ラジオボタン radio = st.radio("選択", ["猫", "犬"]) st.write(f"選択結果: {radio}") # ファイルアップロード uploaded_file = st.file_uploader("アップロード", type=["csv"]) if uploaded_file: df = pd.read_csv(uploaded_file) st.table(df)

レイアウト系
# ワイドレイアウト st.set_page_config(layout="wide") # 横並び cols = st.columns(2) with cols[0]: st.write("列1") with cols[1]: st.write("列2") # タブ tabs = st.tabs(["タブ1", "タブ2"]) with tabs[0]: st.write("タブ1") with tabs[1]: st.write("タブ2") # アコーディオン with st.expander("開きます"): st.write("開きました") # サイドバー with st.sidebar: st.write("サイドバー")

Seleniumサンプルコード
from selenium import webdriver from selenium.webdriver.firefox.options import Options from selenium.webdriver.firefox.service import Service # Firefoxのオプションを設定 options = Options() # ヘッドレスモードで実行(ブラウザのUIを表示しない) options.add_argument("--headless") # GeckoDriverのパスを指定 service = Service(executable_path=r"driver\geckodriver.exe") # Firefoxドライバを起動 driver = webdriver.Firefox(service=service, options=options) # GoogleのWebページを開く driver.get("https://www.google.com") # スクリーンショットを保存 driver.save_screenshot(r"screenshot\screenshot.png") # ドライバを終了 driver.quit()
Pillowサンプルコード
from PIL import Image # PNG形式の画像ファイルを開く input_image = Image.open("input_image.png") # JPEG形式で保存する input_image.convert("RGB").save("output_image.jpg", "JPEG")
venvサンプルコマンド
# 新しい仮想環境を作成(ここでは.venv) python -m venv .venv # Windowsでの仮想環境の有効化 source .venv/Scripts/activate # 仮想環境に必要なライブラリをpipでインストール pip install streamlit selenium pillow ... etc. # requirements.txtにインストールしたライブラリ情報を保存 # 2回目からは「pip install -r requirements.txt」で必要なライブラリをインストールできる pip freeze > requirements.txt ... pythonコマンドを実行(仮想環境で実行される) # 仮想環境の無効化 deactivate
Gitサンプルコマンド
# リモートリポジトリをローカルにクローン
git clone {リモートリポジトリのURL}
# リモート(mainブランチ)の最新情報をローカルに反映
git pull origin main
# ローカルブランチの一覧を表示(チェックアウト中のブランチに「*」が付く)
git branch
# リモートのブランチから指定したブランチ名でブランチを切る
git switch -c {ブランチ名} origin/{リモートのブランチ名}
# 変更したファイルの一覧を表示
git status
# 変更した全ファイルをコミット対象に追加
git add --all
# コミットメッセージを付けてコミット
git commit -m '{コミットメッセージ}'
# ブランチをリモートにプッシュ
git push origin {ブランチ名}
# ブランチを削除
git branch -D {ブランチ名}
# メッセージ付きで変更内容をスタッシュ(レビュー時に便利)
git stash save -u {メッセージ}
# スタッシュの一覧を表示(レビュー時に便利)
git stash list
# スタッシュを復元(レビュー時に便利)
git stash pop stash@{対象の数字}
# リモートのブランチを対象に一時的にブランチを切る(レビュー時に便利)
git switch -d origin/{リモートのブランチ名}
感想
Streamlitは数行のPythonコードを書くだけですぐにWebページを作成できるため、直観的で実装していて楽しかったです。
また、業務で使っている技術や今まで扱ったことのない技術を利用したことでとても勉強になりました。
しかし、今回の実装では画像クリックの座標を完全には扱えなかったため、今後修正していきたいと思います。
Streamlitを調査していてst.data_editor()というメソッドを見つけて、
Excelライクな表を画面に表示できたので電力トレーディング案件で実施しているExcelからPythonへの移行に使えるのではないかと考えています。