[Python] SeleniumでXPathによる動的ID, Class取得

作ったプログラムの備忘録

# はじめに
* 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を取得する方法について紹介<br>※Yahoo JapanでIDやClassが動的に変化するかは確認していない。あくまでサンプルとして使用

* ここでClass以外で変化しないであろう要素としては、`aria-label="主要ニュース"`が考えられ、そこからClassを取得し、最終的にニュースのタイトルとURLを取得することを試みる

```HTML:Yahoo Japan トップページ ニュース部抜粋
<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取得について検討
```Python:動的ID, Class取得の例1
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')
```
```:dynamic_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` が存在する場合は、<b>in</b>でエラーが出る

* また、長くなるが、内包表記を使えば1行で書ける

```Python:動的ID, Class取得の例2（内包表記）
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が取得可能となる
```Python:News URL取得
news_url = [x.get_attribute('href') for x in driver.find_elements(By.XPATH, f'//*[@class="{dynamic_class[0]}"]/ul/li/article/a')]
```
```:news_url 出力結果
['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の書き方は参考文献を確認してください

* 同様にテキストも取得可能
```Python:News テキスト取得
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')]
```
```:news_txt 出力結果
['日本海側 29日から雪強まる見込み', '中国が水際大幅緩和へ 医療は限界',
'日本語学校増 背景に中国受験競争', '松本元死刑囚の三女語る 宗教2世',
'埼玉3人殺害 数分間の犯行か', '元中日・平田良介 引退を発表',
'深夜アニメで町おこし なぜ15年も', 'GLAYのTAKURO 4人で終わりたい']
```
* また取得する要素が1つの場合は、`driver.find_elements`ではなく、`driver.find_element` を使って、以下のようにしてもOK<br>（ちなみに`driver.find_element` は該当する要素が複数存在する場合には最初の要素を返す）
```Python:News テキスト取得
news_txt = driver.find_element(By.XPATH, f'//*[@class="{dynamic_class[0]}"]/ul/li/article/a/div/div/h1/span').text
```
```:news_txt 出力結果
日本海側 29日から雪強まる見込み
```
## サンプル
一連の流れをまとめた場合のソースコード
```Python:sample.py
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])
```
```:sample.py 出力結果
日本海側 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のサイトでもほぼすべての要素をソースコードの変更なしで取得できるはず。。。

```Python:一般化
# 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
```



# 参考文献
* <a href="https://qiita.com/rllllho/items/cb1187cec0fb17fc650a">クローラ作成に必須！XPATHの記法まとめ</a>