Seleniumでスクレイピングしていたのですが、解析するページ数が増すにつれて、遅さを無視できなくなりました。速くする方法は、言うまでものなく「Seleniumへのアクセスを減らせば速くなる」です。
しかし、どのようなアクセスがどれだけ時間をつぶしているのか気になるかと、実数値が欲しくなると思います。そこで、調べました。
速度の計測方法
ローカルサーバに下のようなHTMLを置いて、PyCharmのデバッガで計測しています。ブラウザはChromeで、ヘッドレスモードなしです。
<html> <head></head> <body> <div id="sfc" class="titles"><p><span><span id="x-title" class="title">ゼビウス</span></span></p></div> <table id="foo"> <tr><th>00</th><th>A</th><th>B</th><th>C</th><th>D</th></tr> <tr><td>01</td><td>A</td><td>B</td><td>C</td><td>D</td></tr> <tr><td>02</td><td>A</td><td>B</td><td>C</td><td>D</td></tr> <tr><td>03</td><td>A</td><td>B</td><td>C</td><td>D</td></tr> <tr><td>04</td><td>A</td><td>B</td><td>C</td><td>D</td></tr> <tr><td>05</td><td>A</td><td>B</td><td>C</td><td>D</td></tr> <tr><td>06</td><td>A</td><td>B</td><td>C</td><td>D</td></tr> <tr><td>07</td><td>A</td><td>B</td><td>C</td><td>D</td></tr> </table> </body>
このHTMLを1000回x10計測して、平均を取ってます。
計測は、下のようなPythonスクリプトを用います。speed_test_sub00()とspeed_test_sub01()がSeleniumにアクセスする関数ですが、関数内で1000回同じ処理を繰り返しています。その前後で経過時間をtime.perf_counter()を使って計測するのですが、この方法ではPCの状況によって数値化が変化してしまうのです。
ということで、speed_test_sub00()とspeed_test_sub01()を交互に計測することで、その影響を少なくしています。
driver = webdriver.Chrome() url = 'http://localhost/speed_test.html' page = driver.get(url) time.sleep(3) cal_list: List[float] = [] cal_list2: List[float] = [] for _ in range(10): crt_counter = time.perf_counter() speed_test_sub00(driver) #< Selniumにアクセス cal_list.append(time.perf_counter() - crt_counter) crt_counter = time.perf_counter() speed_test_sub01(driver) #< Selniumにアクセス cal_list2.append(time.perf_counter() - crt_counter) print(cal_list) print(cal_list2)
要素検索方法の違いによる速度変化
要素をIDで検索したら速いのか、CSSセレクターは遅いのか、例外キャッチによる条件分岐は遅くないのか、気になるところです。ちゃんと、調べました。
前で説明したように、PCの影響排除のため、常にA vs. B と二つの方法での速度の違いを見ています。あくまでも相対速度で意味ある値ですので、倍率のところを見てみてください。
要素に対してID検索 vs. クラス名検索
IDだとそのページに一つしかないため、速く見つかるような気がしますが、"find_element_~"は、そんなことはありませんでした。同じです。
def speed_test_sub00(driver) -> None: for _ in range(1000): tag = driver.find_element_by_id('sfc') def speed_test_sub01(driver) -> None: for _ in range(1000): tag = driver.find_element_by_class_name('titles')
項目 | 速度[s] | 左記標準偏差[s] |
---|---|---|
ID検索 | 5.22 | 0.01 |
クラス検索 | 5.21 | 0.02 |
倍率 | 1.00 | - |
CSSセレクターによる要素特定は遅いのか
IDによる検索とCSSセレクターによる検索を比較しました。結果は同じです。
def speed_test_sub00(driver) -> None: for _ in range(1000): tag = driver.find_element_by_id('x-title') def speed_test_sub01(driver) -> None: for _ in range(1000): tag = driver.find_element_by_css_selector('#x-title')
項目 | 速度[s] | 左記標準偏差[s] |
---|---|---|
ID検索 | 5.14 | 0.05 |
CSSセレクター検索 | 5.11 | 0.04 |
倍率 | 1.01 |
CSSセレクターへの設定量は、検索時間に影響するのか
CSSセレクターの条件が複雑になれば、比例して遅くなるような気がしますが、結果は影響なしでした。
def speed_test_sub00(driver) -> None: for _ in range(1000): tag = driver.find_element_by_css_selector('#x-title') def speed_test_sub01(driver) -> None: for _ in range(1000): tag = driver.find_element_by_css_selector('div#sfc > p > span > span.title')
項目 | 速度[s] | 左記標準偏差[s] |
---|---|---|
単純CSSセレクター | 5.24 | 0.01 |
複雑CSSセレクター | 5.25 | 0.01 |
倍率 | 1.00 |
要素検索の速度の違いが見られない原因
Seleniumのソースを見れば、計測するまでもなく、速度は同じになることが分かります。ID検索だろうが、クラス名検索だろうが、CSSセクターだろうが、同じです。何故なら、どれもXPath形式に変換して、検索しているからです。つまり、同じ事を何度も計測していたのです。
要素検索のロジックによる速度の違い
結果的に同じ動きでも、作り方によっては速度に悪影響のあるものがあります。これを知らないと、ヤバいかもしれません。
CSSセレクターに頼らない手動検索は、とんでもなく遅い
CSSセレクターの設定方法が分からないと、find_elements_~()でタグの一覧を取得して、その下の要素を見つけたくなりますが、これは遅くなります。Seleniumに対するアクセスが、そもそも遅くなる原因だからです。
def speed_test_sub00(driver) -> None: for _ in range(1000): tag = driver.find_element_by_css_selector('div#sfc > p > span > span.title') def speed_test_sub01(driver) -> None: for _ in range(1000): span_tags = driver.find_elements_by_css_selector('div#sfc > p > span') for span_tag in span_tags: span_tag.find_element_by_css_selector('span.title')
項目 | 速度[s] | 左記標準偏差[s] |
---|---|---|
一発検索 | 5.25 | 0.04 |
手動検索 | 10.89 | 0.09 |
倍率 | 0.48 |
speed_test_sub01()は中で、2回Seleniumにアクセスしているため、処理時間が倍になってしまいました。
要素の存在確認に例外キャッチをすると、若干遅くなる
要素の有無の確認で、発生した例外をキャッチする方法が取られることがあります。異なる方法として、find_elements_()で要素の一覧を取得して「要素数で0だったらなし」という判定方法が取れることがあります。例外キャッチの方が6%遅いようですが、暇だったら改めるレベルでした。
def speed_test_sub00(driver) -> None: for _ in range(1000): try: tag = driver.find_element_by_css_selector('.zip') except selenium.common.exceptions.NoSuchElementException as e: pass def speed_test_sub01(driver) -> None: for _ in range(1000): tags = driver.find_elements_by_css_selector('.zip') if 0 == len(tags): pass
項目 | 速度[s] | 左記標準偏差[s] |
---|---|---|
例外キャッチ | 5.31 | 0.02 |
検索要素数のチェック | 5.03 | 0.03 |
倍率 | 1.06 |
表の値取得でループを使ったら、どれほど影響するのか
これまでの数値で分かりますが、遅い原因は、tableから値を取り出すとき、find_elements_()して、取り出した要素の配列をforで総なめしていたからです。対象となる表は5列x8行です。その1個1個をSeleniumで取得していたのですから、途方もない遅さになったのです。
下のように作ってました。
def speed_test_sub00(driver) -> None: out_list = [] for _ in range(1000): table = driver.find_element_by_id('foo') ths = table.find_elements_by_tag_name("th") out_list.append([th.text for th in ths]) trs = table.find_elements_by_tag_name("tr") for tr in trs: tds = tr.find_elements_by_tag_name('td') if 0 == len(tds): continue out_list.append([td.text for td in tds])
まだ、改めていないのですが、直したら、きっとこのようにします。
def speed_test_sub01(driver) -> None: for _ in range(1000): tag = driver.find_element_by_id('foo') dfs = pd.read_html(tag.get_attribute('outerHTML'))
項目 | 速度[s] | 左記標準偏差[s] |
---|---|---|
手動取得 | 292.60 | 9.79 |
pandas利用 | 15.86 | 0.30 |
倍率 | 18.45 |
つまり、table表を取得する場合、Pandasを利用するだけで、18倍強の速さになります。
コメント