Closed7

スクリーンショットの自動取得(RPA)をAIに作ってもらう

うすいうすい

やんどころなき理由により、とあるWebサイトの画像を取得したい。

今回は、古いサイトのように簡単にクローリングかけたり、データがダウンロードできたりする形式ではない。
ただ、取得するのは画像で、かつ、画面遷移も外部からブラウザの動きをトレースできれば取得できる。
そして、取得したい画像は必ずしも完全に同一である必要がないという要件のため、スクリーンショットで対応をしようと思う。

E2Eテストで使われるようなChromeドライバーと、スクショ取得をpythonで作るイメージ。

今回もAIに質問して対応する。
単体で解決する処理は、基本AIにやってもらうのが良いと思っている。

うすいうすい

相談相手:perplexity (内部ではClaude3.5 Sonnet)

python で URLを入力したら、その画面の画像のある部分をスクリーンショットを取得するプログラムを書いてほしいです。以下の仕様を満たすプログラムを書いてください

1.スクリーンショットを取って欲しい画像の場所は以下を参考にしてください
(こちらはスクリーンショットを取得してほしい場所のURL)
2.左ボタンを押して次の画面になったら再度スクリーンショットを取り、それを繰り返してください
3.左ボタンで画像が変わらなくなったらそこで終了です。

うすいうすい

以下が回答。ぱっと見かなり良さげ。
考えた通りにChromeDriverを使って、作業をトレースするような仕組みで動いてくれている。
↓↓↓↓↓↓
はい、pythonとseleniumを使って指定された仕様を満たすプログラムを書いてみました。

from selenium import webdriver
from selenium.webdriver.common.by import By
from PIL import Image
import time
import io

# ChromeDriverのパスを指定
driver_path = '/path/to/chromedriver'

# ChromeDriverを起動
driver = webdriver.Chrome(driver_path)

# 指定したURLを開く
url = 'https://comic-walker.com/detail/KC_000021_S?episodeType=comics'
driver.get(url)

# 最初の画像をスクリーンショット
img_element = driver.find_element(By.CSS_SELECTOR, '.comicViewer__image')
img_bytes = img_element.screenshot_as_png
img = Image.open(io.BytesIO(img_bytes))
img.save('screenshot1.png')

count = 2
while True:
    # 左ボタンをクリック
    left_button = driver.find_element(By.CSS_SELECTOR, '.comicViewer__button--left')
    left_button.click()
    time.sleep(1)  # 画像の読み込みを待つ
    
    # 新しい画像をスクリーンショット
    new_img_element = driver.find_element(By.CSS_SELECTOR, '.comicViewer__image')
    new_img_bytes = new_img_element.screenshot_as_png
    new_img = Image.open(io.BytesIO(new_img_bytes))
    
    # 前の画像と比較して同じなら終了
    if img_bytes == new_img_bytes:
        break
    
    # 新しい画像を保存
    new_img.save(f'screenshot{count}.png')
    img_bytes = new_img_bytes
    count += 1

# ChromeDriverを終了
driver.quit()

プログラムの説明:
seleniumとPillowライブラリをインポートします。
ChromeDriverのパスを指定し、ChromeDriverを起動します。
指定したURLを開きます。
CSSセレクタを使って最初の画像要素を見つけ、スクリーンショットを取得します。
取得した画像をPillowを使って開き、'screenshot1.png'として保存します。
whileループを使って以下を繰り返します:
左ボタンをクリックして次の画面に進みます。
新しい画像要素を見つけ、スクリーンショットを取得します。
前の画像と新しい画像を比較し、同じならループを終了します。
新しい画像を'screenshot{count}.png'として保存します。
最後にChromeDriverを終了します。
このプログラムを実行すると、指定したURLの画像をスクリーンショットで順番に取得し、画像が変わらなくなるまで繰り返します。取得した画像は'screenshot1.png', 'screenshot2.png'のように連番で保存されます。
ご要望の仕様を満たすプログラムになっていると思います。ご確認ください。

うすいうすい

さすがに一回で成功とはいかずエラーになる。
質問:実行したら以下のエラーになったのでエラーの原因を教えてもらえますか?

Traceback (most recent call last):
File "D:\testscreenshot\screenshot.py", line 11, in <module>
driver = webdriver.Chrome(driver_path)
File "C:\Users\KazukiUsui\AppData\Local\Programs\Python\Python310\lib\site-packages\selenium\webdriver\chrome\webdriver.py", line 45, in init
super().init(
File "C:\Users\KazukiUsui\AppData\Local\Programs\Python\Python310\lib\site-packages\selenium\webdriver\chromium\webdriver.py", line 50, in init
if finder.get_browser_path():
File "C:\Users\KazukiUsui\AppData\Local\Programs\Python\Python310\lib\site-packages\selenium\webdriver\common\driver_finder.py", line 47, in get_browser_path
return self._binary_paths()["browser_path"]
File "C:\Users\KazukiUsui\AppData\Local\Programs\Python\Python310\lib\site-packages\selenium\webdriver\common\driver_finder.py", line 56, in _binary_paths
browser = self._options.capabilities["browserName"]
AttributeError: 'str' object has no attribute 'capabilities'

AIさんの回答:
エラーメッセージを見ると、ChromeDriverのパスを指定する部分でエラーが発生しているようです。

driver_path = '/path/to/chromedriver'
driver = webdriver.Chrome(driver_path)

エラーメッセージ: AttributeError: 'str' object has no attribute 'capabilities'
このエラーは、webdriver.Chrome()に渡された引数が正しくないことを示しています。driver_pathは文字列として指定されていますが、webdriver.Chrome()はChromeOptionsオブジェクトを期待しています。
エラーを修正するには、以下のようにdriver_pathをexecutable_path引数に渡してください。

driver_path = '/path/to/chromedriver'
driver = webdriver.Chrome(executable_path=driver_path)
``
また、driver_pathには実際のChromeDriverのパスを指定する必要があります。例えば、WindowsでChromeDriverがC:\chromedriver\chromedriver.exeにある場合は、以下のようにパスを指定します。
```python
driver_path = 'C:\chromedriver\chromedriver.exe'
driver = webdriver.Chrome(executable_path=driver_path)

これでエラーが解決し、プログラムが正常に実行されるはずです。ChromeDriverのパスが正しく指定されていることを確認してください。

どうやら原因はWindowsで、\を重ねないといけないパスがあることだったようだ。解決。

うすいうすい

又エラー
質問:別のエラーになりました。
Traceback (most recent call last):
File "D:\testscreenshot\screenshot.py", line 11, in <module>
webdriver.Chrome(executable_path=driver_path)
TypeError: WebDriver.init() got an unexpected keyword argument 'executable_path'
↓↓↓↓↓↓
AIさんの回答:
はい、エラーメッセージを見ると、executable_path引数が予期しないキーワード引数だと言われています。これは、Selenium 4以降でexecutable_path引数が非推奨になったためです。
検索結果とで説明されているように、Selenium 4以降ではServiceクラスを使ってChromeDriverのパスを指定する必要があります。以下のようにコードを修正してみてください。

from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from PIL import Image
import time
import io

# ChromeDriverのパスを指定
driver_path = '/path/to/chromedriver'

# Serviceオブジェクトを作成
service = Service(executable_path=driver_path)

# ChromeDriverを起動
driver = webdriver.Chrome(service=service)

# 以下は変更なし
# 指定したURLを開く
url = 'https://comic-walker.com/detail/KC_000021_S?episodeType=comics'
driver.get(url)

# 最初の画像をスクリーンショット
img_element = driver.find_element(By.CSS_SELECTOR, '.comicViewer__image')
img_bytes = img_element.screenshot_as_png
img = Image.open(io.BytesIO(img_bytes))
img.save('screenshot1.png')

count = 2
while True:
    # 左ボタンをクリック
    left_button = driver.find_element(By.CSS_SELECTOR, '.comicViewer__button--left')
    left_button.click()
    time.sleep(1)  # 画像の読み込みを待つ
    
    # 新しい画像をスクリーンショット
    new_img_element = driver.find_element(By.CSS_SELECTOR, '.comicViewer__image')
    new_img_bytes = new_img_element.screenshot_as_png
    new_img = Image.open(io.BytesIO(new_img_bytes))
    
    # 前の画像と比較して同じなら終了
    if img_bytes == new_img_bytes:
        break
    
    # 新しい画像を保存
    new_img.save(f'screenshot{count}.png')
    img_bytes = new_img_bytes
    count += 1

# ChromeDriverを終了
driver.quit()

変更点は以下の2つです:
selenium.webdriver.chrome.serviceからServiceクラスをインポート
Serviceオブジェクトを作成し、executable_path引数でChromeDriverのパスを指定
webdriver.Chrome()の引数としてserviceを渡す
これで、executable_path引数に関するエラーが解消されるはずです。再度実行してみてください。
↑↑↑↑↑↑

なるほど、最初から非推奨のモジュールははじいてくれんかなと思うが、そうもいかないのだろう。
たまに、非推奨だから最新を使えと言われ、使ってみたら、最新だと動かない、みたいな状態でデッドロックな回答を出すこともある。
この点では、AIは支援はしてくれるが、チェックはしてあげないとダメだなと改めて思う。完全に分からない状態の開発でAIを使うのは難しそうだ。

うすいうすい

その他、以下のようなエラーにあった
File "D:\testscreenshot\screenshot.py", line 11, in <module>
webdriver.Chrome(executable_path=driver_path)
TypeError: WebDriver.init() got an unexpected keyword argument 'executable_path'

回答:エラーメッセージを見ると、executable_path引数が予期しないキーワード引数だと言われています。これは、Selenium 4以降でexecutable_path引数が非推奨になったためです。
検索結果とで説明されているように、Selenium 4以降ではServiceクラスを使ってChromeDriverのパスを指定する必要があります。以下のようにコードを修正してみてください。

後付けで言うか?
perplexity AIはネットで情報を検索するので最新の情報もある程度検知できるので、プログラムのようなものは間違えてもすぐに修正できる一方で、最初から検索する時に古い情報(例えば、ある技術において、検索され、一番読まれた記事だが、内容はやや古くて最新バージョンではエラーとなってしまうなど)となるようなものを検索して回答してくるので、ひょっとしたら最適とまではいかないのかな、と思う。

それと、Webの画面から画像の位置を確認しようとしたが、エラーとなってしまう。
仕方がないので相対パスで画面の位置をある程度固定してスクリーンショットを取るようにした。

うすいうすい

結果的に、スクリーンショットを取って画像を保存するプログラムは以下のような形になった。
この処理は、バッチ処理で、CSVファイルを引数として渡す。CSVは取得したい画面のURLが入っている。
事前にChromeのバージョンに一致したChromeドライバーをPCに入れておかないといけない(自動選択するようなモジュールもあったが動作が非常に遅かったので止めた)

取得したい画面を左クリックしながら特定の場所のスクリーンショットを取り、最終的に左ボタンを押しても取得できる画像が同じ(つまり最後の画面まで行きついた)場合には処理は完了となり、次のURLを検索しに行く、という動作を取るバッチができて、終了だ。
そこそこ引っかかったが、8-10時間で終わった気がする。
作成したバッチのサンプルはこちら

python
import sys
import csv
import os
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from PIL import Image
import time
import io
from urllib.parse import urlparse

# ChromeDriverのパスを指定
driver_path = '/path/to/chromedriver'

# Serviceクラスにドライバーのパスを設定
service = Service(executable_path=driver_path)

# ChromeDriverを起動
options = webdriver.ChromeOptions()
options.add_argument('--ignore-certificate-errors')
options.add_argument('--ignore-ssl-errors')
driver = webdriver.Chrome(service=service, options=options)

# 引数からCSVファイル名を取得
csv_file = sys.argv[1]

# CSVファイルからURLを読み込む
with open(csv_file, 'r') as file:
    reader = csv.reader(file)
    urls = [row[0] for row in reader]

for url in urls:
    # URLを開く
    driver.get(url)

    # ウィンドウサイズを取得
    window_size = driver.get_window_size()
    window_width = window_size['width']
    window_height = window_size['height']

    # スクリーンショットの位置と大きさを計算
    screenshot_width = 300
    screenshot_height = 200
    left = (window_width - screenshot_width) // 2
    top = (window_height - screenshot_height) // 2
    right = left + screenshot_width
    bottom = top + screenshot_height

    # 3秒待機
    time.sleep(3)

    # URLからページ名のみを取得
    parsed_url = urlparse(url)
    page_name = parsed_url.path.split('/')[-1]

    # ページ名のフォルダを作成(存在しない場合)
    if not os.path.exists(page_name):
        os.makedirs(page_name)

    # 最初の画像をスクリーンショット
    driver.execute_script("document.body.style.zoom='100%'")
    img_bytes = driver.get_screenshot_as_png()
    img = Image.open(io.BytesIO(img_bytes))
    img = img.crop((left, top, right, bottom))
    img.save(f'{page_name}/screenshot1.png')

    count = 2
    while True:
        # 左矢印キーを押して次の画面に移動
        body = driver.find_element(By.TAG_NAME, 'body')
        body.send_keys(Keys.ARROW_LEFT)
        
        # 3秒待機
        time.sleep(3)
        
        # 新しい画像をスクリーンショット
        new_img_bytes = driver.get_screenshot_as_png()
        new_img = Image.open(io.BytesIO(new_img_bytes))
        new_img = new_img.crop((left, top, right, bottom))
        
        # 前の画像と比較して同じなら終了
        if img_bytes == new_img_bytes:
            break
        
        # 新しい画像を保存
        new_img.save(f'{page_name}/screenshot{count}.png')
        img_bytes = new_img_bytes
        count += 1

# ChromeDriverを終了
driver.quit()
このスクラップは2ヶ月前にクローズされました