selenium とは何ですか?#
Selenium は、Web ブラウザの自動化をサポートする一連のツールとライブラリの統合プロジェクトです。
ユーザーがブラウザと対話するのを模倣するための拡張機能を提供し、ブラウザが割り当てた配信サーバーを拡張し、すべての主要な Web ブラウザ用に相互運用可能なコードを書くことを可能にするW3C WebDriver仕様を実装するための基盤を提供します。
Python の selenium ライブラリは selenium のインターフェースであり、ブラウザが人間のようにページを操作し、Web ページの情報を取得することができます。
この特性に基づいて、selenium は特定の Web サイトの情報をクロールする際にコードのロジックがよりシンプルで、JS 暗号化コードを逆向きにする必要がありません。
しかし、模倣操作であるため、クロール効率は他のクローラーには及びません。
selenium の強力さを示すために、例を挙げます:
bilibili の個人ページからファンの名前とファンの数をクロールします。
注意: データをクロールする際は、Web サイトの robots.txt 内の規定に注意し、あまり高いクロール頻度を持たないようにして、Web サイトに負担をかけないようにしてください。本記事でクロールしたファンの名前とファンの数は公開情報に属します。
インストール#
$ pip install selenium
サイト分析#
個人スペースのファンページでは、ファン情報は<ul>
内の<li>
要素にあります。
しかし、<li>
内にはファンの数に関する情報はなく、サーバー上にあり、ローカルには保存されていません。これは、マウスをファンのアイコンまたは名前に移動させることで、JS がローカルに転送されるトリガーとなります。この操作は AJAX 技術によって実現されています。
AJAX(非同期 JavaScript および XML) は、ページ全体を再読み込みすることなく、部分的に Web ページを更新できる技術です。
マウスがアイコンの上に移動すると、<body>
の末尾に<div id="id-card">
が生成されます:
ファンの数は<div id="id-card">
内の<span class="idc-meta-item">
内にあります:
マッチング方法#
selenium には要素をマッチングするための多くの方法があります:
- xpath(最も一般的)
- by id
- by name/tag name/class name
- by link
- by css selector
xpath が便利なのは、相対パスでマッチングでき、構文がシンプルだからです。
例えば、ファンのアイコンをマッチングするには次のように書けます:
//div[@id="id-card"]
XML ではその要素の位置は次のようになります:
<html>
...
<body>
...
<div id = "id-card">
...
</div>
</body>
</html>
もちろん、css selector も時には非常に便利です:
XML:
<html>
<body>
<p class="content">サイトのコンテンツがここに表示されます。</p>
</body>
<html>
css selector:
p.content
クローラーを書く#
初期化:
def initDriver(url):
#ヘッドレスブラウザを設定
options = webdriver.ChromeOptions()
options.add_argument('headless')
options.add_experimental_option('excludeSwitches', ['enable-logging'])
#初期化
driver = webdriver.Chrome(options=options)
actions = ActionChains(driver)
#リンクを開く
driver.get(url)
driver.implicitly_wait(10)
return driver, actions
ページ番号を取得:
def getPageNum(driver):
#xpathを使用してページ下部の翻ページ要素の位置をマッチングし、ページ番号を取得
text = driver.find_element("xpath", '//ul[@class="be-pager"]/span[@class="be-pager-total"]')
.get_attribute("textContent")
.split(' ')
return text[1]
すべてのページを巡回:
def spawnCards(page, driver, actions):
#すべてのページを巡回
for i in range(1,int(page) + 1):
print(f"ページ {i} のデータを取得\n")
#ajaxをトリガーしてカードを生成
spawn(driver, actions)
if (i != int(page)):
#翻ページ
goNextPage(driver, actions)
time.sleep(6)
カードを生成:
def spawn(driver, actions):
#カードリストを取得
ulList = driver.find_elements("xpath", '//ul[@class="relation-list"]/li')
#カードを生成
for li in ulList:
getCard(li, actions)
time.sleep(2)
def getCard(li, actions):
cover = li.find_element("xpath", './/a[@class="cover"]')
actions.move_to_element(cover)
actions.perform()
actions.reset_actions()
データを取得して保存:
def writeData(driver):
#カードリストを取得
cardList = driver.find_elements("xpath", '//div[@id="id-card"]')
for card in cardList:
up_name = card.find_element("xpath", './/img[@class="idc-avatar"]').get_attribute("alt")
up_fansNum = card.find_elements('css selector','span.idc-meta-item')[1].get_attribute("textContent")
print(f'name:{up_name}, {up_fansNum}')
#csvファイルに書き込む
with open('.\\date.csv', mode='a', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
writer.writerow([up_name, up_fansNum])
完全なコード:
from selenium import webdriver
from selenium.webdriver.common.action_chains import ActionChains
import time
import csv
def initDriver(url):
options = webdriver.ChromeOptions()
options.add_argument('headless')
options.add_experimental_option('excludeSwitches', ['enable-logging'])
driver = webdriver.Chrome(options=options)
actions = ActionChains(driver)
driver.get(url)
driver.get(url)
driver.implicitly_wait(10)
return driver, actions
def getPageNum(driver):
text = driver.find_element("xpath", '//ul[@class="be-pager"]/span[@class="be-pager-total"]').get_attribute("textContent").split(' ')
return text[1]
def goNextPage(driver, actions):
bottom = driver.find_element("xpath", '//li[@class="be-pager-next"]/a')
actions.click(bottom)
actions.perform()
actions.reset_actions()
def getCard(li, actions):
cover = li.find_element("xpath", './/a[@class="cover"]')
actions.move_to_element(cover)
actions.perform()
actions.reset_actions()
def writeData(driver):
#カードリストを取得
cardList = driver.find_elements("xpath", '//div[@id="id-card"]')
for card in cardList:
up_name = card.find_element("xpath", './/img[@class="idc-avatar"]').get_attribute("alt")
up_fansNum = card.find_elements('css selector','span.idc-meta-item')[1].get_attribute("textContent")
print(f'name:{up_name}, {up_fansNum}')
#情報をcsvファイルに書き込む
with open('.\\date.csv', mode='a', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
writer.writerow([up_name, up_fansNum])
def spawn(driver, actions):
#カードリストを取得
ulList = driver.find_elements("xpath", '//ul[@class="relation-list"]/li')
#カードを生成
for li in ulList:
getCard(li, actions)
time.sleep(2)
def spawnCards(page, driver, actions):
for i in range(1,int(page) + 1):
print(f"ページ {i} のデータを取得\n")
spawn(driver, actions)
if (i != int(page)):
goNextPage(driver, actions)
time.sleep(6)
def main():
#ドライバーを初期化
uid = input("bilibili uid:")
url = "https://space.bilibili.com/" + uid + "/fans/fans"
driver, actions = initDriver(url)
page = getPageNum(driver)
#カード情報を生成(ajax)
spawnCards(page, driver, actions)
writeData(driver)
driver.quit()
if __name__ == "__main__":
main()
結果#
反省#
改善できる点:
- AJAX の非同期読み込みのため、ページが完全に読み込まれてから要素の位置を特定する必要があります。
time.sleep()
メソッドを使用するのは効率的ではなく優雅ではありません。WebDriverWait()
メソッドを使用すると解決できます。これはページの状態をポーリングし、ページが読み込まれたらtrue
を返します。 - 繰り返しのパスを含む xpath 式を何度も使用しており、マッチングに多くのメモリを消費しています。
- データを完全に並行して抽出することができ、結果をより早く取得できます。しかし、サーバーの負荷を考慮して、単一スレッドのバージョンのみを記述しました。
参考文献#
selenium doc
推奨読書:
Ajax
Xpath