作ったプログラムの備忘録
はじめに
- Seleniumでスクレイピングをする際にIDやClassが動的に変わるサイトに遭遇
- 直接ID, Class指定ができないので、他の要素からIDを取得する方法を検討した
- 他のサイトにも有効な手段かどうかは不明だが、メモとして記載
動作テスト環境
OS: Windows 10 Pro 64bit
言語: Python 3.9.13
ライブラリ:Selenium 4.7.2
動的ID, Class取得方法
テスト用WEBサイト(Yahoo Japan)
-
例としてYahoo Japanのトップにあるニュース部分で、Classが動的に変化して直接指定出来ないと仮定して、変化しない他の要素からClassを取得する方法について紹介
※Yahoo JapanでIDやClassが動的に変化するかは確認していない。あくまでサンプルとして使用 -
ここでClass以外で変化しないであろう要素としては、
aria-label="主要ニュース"
が考えられ、そこからClassを取得し、最終的にニュースのタイトルとURLを取得することを試みる
<section id="tabpanelTopics1" role="tabpanel" aria-labelledby="tabTopics1" aria-hidden="false">
<header class="_5swsXRdQMtHOvhS2bwHid _3v9VCsGKsktTB7eaN70w-n">
<h1>主要 ニュース</h1>
</header>
<div class="_1XAfHUWtx6tfYZuWDVjNxZ">
<div class="_2jjSS8r_I9Zd6O9NFJtDN-" aria-label="主要 ニュース">
<p class="_2vq5UBfOFpYcyqNRy51gDf">
<span class="fQMqQTGJTbIMxjQwZA2zk _2VDw54wcDejORZkozpOysW">12/28(水) 9:17更新</span>
</p>
<ul>
<li class="_2j0udhv5jERZtYzddeDwcv">
<article class="QLtbNZwO-lssuRUcWewbd" data-ual-view-type="list" data-ual="id_type:shannon_article;content_id:f5af286b551b5ebb7a21cefac4f9be8e9dc5d0d0;element_id:first-news-topics-text">
<a class="yMWCYupQNdgppL-NV6sMi _3sAlKGsIBCxTUbNi86oSjt" href="https://news.yahoo.co.jp/pickup/6449014" data-cl-params="_cl_vmodule:tpto;_cl_link:title;_cl_position:1;tpid:6449014;imgsize:s;cmt_num:650" data-ual-gotocontent="true" data-cl_cl_index="1">
<div class="_2cXD1uC4eaOih4-zkRgqjU">
<div class="TRuzXRRZHRqbqgLUCCco9">
<h1 class="_3cl937Zpn1ce8mDKd5kp7u">
<span class="fQMqQTGJTbIMxjQwZA2zk _1alzSpTqJzvSVUWqpx82d4">埼玉3人殺害 家族が過去6回被害届</span>
</h1>
<span class="_2obRU_TgAxzHaYqOXrZYlv">
<span class="h4yLXygiSc5wwNlJOQEdz _1dr5aVDbNPF63JCS2bJhij _2M3AyDfFaeJl3Uo7lUPMAp" style="width:30px;height:12px">NEW</span>
</span>
<span class="_2dkkFdF6iwwsvj59371Trg">
<span class="_2Uq6Pw5lfFfxr_OD36xHp6 _1dr5aVDbNPF63JCS2bJhij _1I_5hzC8N7rC_DyCqrkXsj" style="width:10px;height:10px"></span>
<span class="fQMqQTGJTbIMxjQwZA2zk _2VDw54wcDejORZkozpOysW">
<span class="ASQOQKzZhjBC7R0_pxI3g">650</span>
</span>
</span>
</div>
</div>
</a>
</article>
</li>
<li class="_2j0udhv5jERZtYzddeDwcv">(省略)</li>
<li class="_2j0udhv5jERZtYzddeDwcv">(省略)</li>
<li class="_2j0udhv5jERZtYzddeDwcv">(省略)</li>
<li class="_2j0udhv5jERZtYzddeDwcv">(省略)</li>
<li class="_2j0udhv5jERZtYzddeDwcv">(省略)</li>
<li class="_2j0udhv5jERZtYzddeDwcv">(省略)</li>
<li class="_2j0udhv5jERZtYzddeDwcv">(省略)</li>
</ul>
</div>
</div>
(省略)
動的ID, Class取得
- まず
<div class="_2jjSS8r_I9Zd6O9NFJtDN-" aria-label="主要 ニュース">
部分のClass取得について検討
tag_list = driver.find_elements(By.XPATH, '//section/div/div')
for x in tag_list:
if x.get_attribute('aria-label')=='主要 ニュース':
dynamic_class = x.get_attribute('class')
['_2jjSS8r_I9Zd6O9NFJtDN-']
-
driver.find_elements
で指定するタグ構造を全てリストに格納(今回はサイト内で<section><div><div>
となるタグ部分をすべて取得) -
次にリストのなかで
'aria-label'
要素の値が'主要ニュース'
になっているタグを検索して、そのタグのClass
要素を取得する -
if x.get_attribute('aria-label')=='主要 ニュース'
は状況によってif '主要 ニュース' in x.get_attribute('aria-label')
としてもよいが、他の要素にaria-label
がない場合や要素にNone
が存在する場合は、inでエラーが出る -
また、長くなるが、内包表記を使えば1行で書ける
dynamic_class = [x.get_attribute('class') for x in driver.find_elements(By.XPATH, '//section/div/div') if x.get_attribute('aria-label')=='主要 ニュース']
URL, ニュースタイトル取得
- Classが一意に定まったので、そのClassを用いてURLが取得可能となる
news_url = [x.get_attribute('href') for x in driver.find_elements(By.XPATH, f'//*[@class="{dynamic_class[0]}"]/ul/li/article/a')]
['https://news.yahoo.co.jp/pickup/6449032', 'https://news.yahoo.co.jp/pickup/6449017',
'https://news.yahoo.co.jp/pickup/6449027', 'https://news.yahoo.co.jp/pickup/6449024',
'https://news.yahoo.co.jp/pickup/6449035', 'https://news.yahoo.co.jp/pickup/6449034',
'https://news.yahoo.co.jp/pickup/6449038', 'https://news.yahoo.co.jp/pickup/6449041']
-
XPathの書き方は参考文献を確認してください
-
同様にテキストも取得可能
news_txt = [x.text for x in driver.find_elements(By.XPATH, f'//*[@class="{dynamic_class[0]}"]/ul/li/article/a/div/div/h1/span')]
['日本海側 29日から雪強まる見込み', '中国が水際大幅緩和へ 医療は限界',
'日本語学校増 背景に中国受験競争', '松本元死刑囚の三女語る 宗教2世',
'埼玉3人殺害 数分間の犯行か', '元中日・平田良介 引退を発表',
'深夜アニメで町おこし なぜ15年も', 'GLAYのTAKURO 4人で終わりたい']
- また取得する要素が1つの場合は、
driver.find_elements
ではなく、driver.find_element
を使って、以下のようにしてもOK
(ちなみにdriver.find_element
は該当する要素が複数存在する場合には最初の要素を返す)
news_txt = driver.find_element(By.XPATH, f'//*[@class="{dynamic_class[0]}"]/ul/li/article/a/div/div/h1/span').text
日本海側 29日から雪強まる見込み
サンプル
一連の流れをまとめた場合のソースコード
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.edge.service import Service
driver_path = 'edgedriver_win64/msedgedriver.exe'
edge_service = Service(executable_path=driver_path)
driver = webdriver.Edge(service=edge_service)
driver.get("https://www.yahoo.co.jp/")
dynamic_class = [x.get_attribute('class') for x in driver.find_elements(By.XPATH, '//section/div/div') if x.get_attribute('aria-label')=='主要 ニュース']
news_url = [x.get_attribute('href') for x in driver.find_elements(By.XPATH, f'//*[@class="{dynamic_class[0]}"]/ul/li/article/a')]
news_txt = [x.text for x in driver.find_elements(By.XPATH, f'//*[@class="{dynamic_class[0]}"]/ul/li/article/a/div/div/h1/span')]
driver.close()
driver.quit()
for i in range(len(news_url)):
print(news_txt[i], news_url[i])
日本海側 29日から雪強まる見込み https://news.yahoo.co.jp/pickup/6449032
中国が水際大幅緩和へ 医療は限界 https://news.yahoo.co.jp/pickup/6449017
国立競技場 管理に公費年10億円も https://news.yahoo.co.jp/pickup/6449046
茅ヶ崎の刺殺 業者を装い呼び出し https://news.yahoo.co.jp/pickup/6449044
香港便停止の要請 沖縄観光に衝撃 https://news.yahoo.co.jp/pickup/6449040
元中日・平田良介 引退を発表 https://news.yahoo.co.jp/pickup/6449034
深夜アニメで町おこし なぜ15年も https://news.yahoo.co.jp/pickup/6449038
GLAYのTAKURO 4人で終わりたい https://news.yahoo.co.jp/pickup/6449041
まとめ
-
動的ID, Classを取得するには、
driver.find_elements
で広くタグを取得して、そのあとにID, Classを特定可能な別の要素で絞り込んでID, Classを取得する -
その後、取得したID, Classを使って目的のデータを再度取得する
-
この2段階のステップを踏めば、毎回変わる動的なID, Classのサイトでもほぼすべての要素をソースコードの変更なしで取得できるはず。。。
# ID取得
get_id = [x.get_attribute('id') for x in driver.find_elements(By.XPATH, '//TAG') if '検索語' in x.get_attribute('要素')]
# 取得要素が複数の場合(リスト取得)
get_list = [x.text for x in driver.find_elements(By.XPATH, f'//*[@id="{get_id[0]}"]/')]
# 取得要素が1つの場合
get_elem = driver.find_element(By.XPATH, f'//*[@id="{get_id[0]}"]/').text