概要
seleniumで、指定した要素をスクロールしてスクリーンショットを撮るためのメモ。
背景と目的
seleniumを使って画面キャプチャをしていたところ、テーブルのすべての値をキャプチャするのにスクロールが必要だった。そこで、その方法をメモした。
なお、ブラウザのウインドウサイズを大きくすればすべて表示される作りになっている画面は対象外とする。ブラウザサイズを変えればよいだけなので。
詳細
0. 環境
1. スクリーンショットの対象サンプル
以下は、縦、横ともスクロールバーが発生するテーブルのサンプル。表示領域が1000×200pxに限定されていて、いくつかの行、列が隠れている。
<!DOCTYPE html> <html> <head> <style> .table-wrap { overflow: scroll; width: 1000px; height: 200px; } .table { border-collapse: collapse; white-space: nowrap; } .table th, .table td { border: 2px solid #eee; padding: 4px 8px; } </style> </head> <body> <div class="table-wrap" id="scroll_area"> <table class="table"> <tr> <th>サンプルテキスト00</th> <td>これはサンプルテキストです。00</td> <td>これはサンプルテキストです。01</td> : 中略 <td>これはサンプルテキストです。09</td> </tr> : 中略 <tr> <th>サンプルテキスト90</th> <td>これはサンプルテキストです。90</td> <td>これはサンプルテキストです。91</td> : 中略 <td>これはサンプルテキストです。99</td> </tr> </table> </div> </body> </html>
参考: https://webukatu.com/wordpress/blog/7058/#overflow
2. seleniumコード
2.0 キャプチャ対象要素を選択
q = driver.find_element(By.ID, "scroll_area")
2.1 対象要素をスクロール
特定の要素をスクロールするには、WebDriverのexecute_scriptを使い、javascript側のDOM操作をする。第一引数には実行したいスクリプト(javascript)、第2引数以降はスクリプトに渡す引数。スクリプト側では、
arguments[n]
として、第n+2引数を参照できる。seleniumのElementを渡せば、DOMとして受け取られるので、スクリプト側でDOMの各種操作が実行可能。
以下に、スクロールに関連するDOMのプロパティを操作する例を列挙。※javascript側の仕様なのでそちらを調べた方がよいが。
# スクロール位置を指定したピクセル数にする driver.execute_script("arguments[0].scrollTo(10,10);", q) # scrollToの縦だけ driver.execute_script("arguments[0].scrollTop = 10;", q) # scrollToの横だけ driver.execute_script("arguments[0].scrollLeft= 10;", q) # 現在位置から相対的にスクロール driver.execute_script("arguments[0].scrollBy(10,10);", q) # 横スクロール範囲のピクセル数 driver.execute_script("arguments[0].scrollWidth;", q) # 縦スクロール範囲のピクセル数 driver.execute_script("arguments[0].scrollHeight;", q) # 対象要素の表示領域の大きさ q.size
2.2 指定した子要素が表示されるように親をスクロール
javascript側でscrollIntoViewというメソッドがある。それも呼び出して使える。
サンプルとして、最終行の7列目セルを表示ターゲットとする。
: 中略 <tr> <th>サンプルテキスト90</th> <td>これはサンプルテキストです。90</td> <td>これはサンプルテキストです。91</td> : 中略 <td id="target">スクロールターゲット</td> : 中略 <td>これはサンプルテキストです。99</td> </tr>
以下のように、当該セル.scrollIntoViewを呼び出せば、親(#scroll_area)がスクロールされる。
q = driver.find_element(By.ID, "scroll_area") # スクロールターゲット driver.execute_script("arguments[0].scrollIntoView();", q)
3. すべてのスクリーンショットを撮る
安直な方法として、上下左右に少しずつスクロールして複数枚のスクリーンショットを撮ることにする。scrollIntoViewは使っていない。
ちょっと注意が必要なのは、
- 表示領域には、スクロールバーの幅が考慮されないので、sizeで取得した値よりも少し狭く考える
- スクロールをする前に、Elementを再度取得する。しないとエラーが出るため。
selenium.common.exceptions.StaleElementReferenceException: Message: stale element reference: stale element not found
コードは以下。
# 表示領域を考慮する height = q.size["height"] - 30 # スクロールバーの幅分を考慮する width = q.size["width"] - 30 # スクロール幅 scrollWidth = driver.execute_script("return arguments[0].scrollWidth;", q) scrollHeight = driver.execute_script("return arguments[0].scrollHeight;", q) # スクロール回数 horizontalScrollCount = int(scrollWidth / width) + 1 verticalScrollCount = int(scrollHeight / height) + 1 # 一回のスクロール量 horizontalScrollAmount = int(scrollWidth / horizontalScrollCount) verticalScrollAmount = int(scrollHeight / verticalScrollCount) # はじめに表示位置をリセット driver.execute_script("arguments[0].scrollTo(0, 0);", q) # 順次、スクロール、スクリーンショットを繰り返す for i in range(verticalScrollCount): # 縦スクロール for j in range(horizontalScrollCount): # Elementを再度取得 # しないとstale element not foundというエラーが出る q = driver.find_element(By.ID, "scroll_area") # 横スクロール driver.execute_script( f"arguments[0].scrollTo({j * horizontalScrollAmount}, {i * verticalScrollAmount});", q, ) # スクリーンショットを保存 png = q.screenshot_as_png with open(f"./img_{i}_{j}.png", "wb") as f: f.write(png)
撮れたスクリーンショットは以下。
上段
中段
下段
まとめと今後の課題
seleniumで、指定した要素をスクロールして、要素内すべてのスクリーンショットを撮るスクリプトを作ることができた。