Power Query へそのゴマ　第１２章　Html.Table の CSS セレクター指定によるデータ抽出

`Html.Table` は、Power Query M 言語で提供される関数の一つで、HTML コンテンツからテーブル形式のデータを抽出するために使用します。この関数を利用すると、Web ページに含まれるHTML表（`<table>` タグ）を解析して、Power Query内で扱えるテーブルデータとして取り込むことができます。

# 12.1. `Html.Table` 関数の概要

`Html.Table` は、HTML データから指定したパターンに基づいてテーブル形式のデータを抽出するための Power Query 関数です。

```:構文
Html.Table(
    html as any, 
    columnNameSelectorPairs as list, 
    optional options as nullable record
) as table
```
- **`html`**: HTML コンテンツ（通常は `Web.Contents` で取得）。
- **`columnNameSelectorPairs`**: 列名と対応する CSS セレクターまたは列データを抽出する式のペアをリスト形式で指定。
- **`options`**: オプション設定で、`RowSelector` を使用して行単位で抽出条件を指定可能。

---

# 12.2. CSS セレクターの基本

CSS セレクターは、HTML 内の特定の要素や属性を指定するための方法です。以下は主要なセレクターです。

### 基本セレクター

|名称|説明|例|
|:--|:--|:--|
|全称セレクター|すべての要素|*|
|要素型セレクター|要素名を指定|input|
|クラスセレクター|class属性を指定|.classname|
|IDセレクター|id属性を指定|#idname|
|属性セレクター|属性を指定|[attr] [attr=value]|

### 属性セレクター

|構文|説明|例|
|:--|:--|:--|
|[attr]|その属性を持つ要素|[name]|
|[attr=value]|指定された属性の値が完全一致<br> [class="a b c"]は<br>class="a b c"と一致|[class="bold-01"]|
|[attr~=value]|[class="b"]は<br>class="a b c"と一致|[class~="red-02"]|
|[attr&#124;=value]|[id&#124;="pre"]は<br>preとpre-viewと一致|[class&#124;="red"]|
|[attr^=value]|要素の最初の値に一致|[class^="bold-02"]|
|[attr$=value]|要素の最後の値に一致|[class$="red-02"]|
|[attr*=value]|[id*="re"]はpreとrenと一致|[class*="old"]|
|[attr operator value i]|値の大文字と小文字を区別しません|[class$="LISTITEM1" i]|
|[attr operator value s]|値の大文字と小文字を区別します|[id="ListItem1" s]

### 組み合わせセレクター

| セレクター | 説明 | 例 |
|------------|------|----|
| `tag.class` | 特定のタグとクラス | `div.content` |
| `tag#id` | 特定のタグと ID | `table#sales` |
| `tag[attr=value]` | 属性が特定の値の要素 | `a[href="example.com"]` |

### 階層セレクター

|名称|構文|説明|例|
|:--:|:--:|:--|:--|
|子孫結合子|A B|子孫にあたるノード|#MainContent p|
|子結合子|A > B|直接の子のノード|#MainContent > div > p|
|一般兄弟結合子|A ~ B|Aの後にあるBの要素|p ~ p|
|隣接兄弟結合子|A + B|Aの直後のBの要素|span + p|
|複数選択|A, B|AまたはBの要素|ul, dl|

### 子要素フィルター

|構文|説明|例|
|:--|:--|:--|
|:first-child|最初の子要素|#Table-1 > tbody > tr > :first-child|
|:first-of-type|セレクタで指定された<br>最初の子要素|#Table-1 > tbody   td:first-of-type|
|:last-child|最後の子要素|#Table-1 > tbody > tr > :last-child
|:last-of-type|セレクタで指定された<br>最後の子要素|#Table-1 > tbody   td:first-of-type|
|:only-child|子要素が<br>１つだけのときに取得|
|:only-of-type|セレクタで指定された要素が<br>１つだけのときに取得|
|:nth-child()|親要素内にあるセレクタで<br>引数で指定した要素|#FirstLetter > span:nth-child(2)|
|:nth-of-type()|セレクタと引数で指定された要素|#FirstLetter > p:nth-of-type(2)|
|:nth-last-child()|nth-childを最後から数える|#FirstLetter > :nth-last-child(2)|
|:nth-last-of-type()|nth-of-typeを最後から数える|#FirstLetter > span:nth-last-of-type(2)

# 12.3. `RowSelector` を使用した行単位のデータ抽出

`RowSelector` は、複数行のデータ抽出を可能にするオプションです。

## 12.3.1 単純な `table` 要素の抽出

以下の例では、CSS セレクターと `RowSelector` を使用してウェブページからデータを抽出します。

<table id=""products"">
    <thead>
        <tr>
            <th>Product</th>
            <th>Price</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>Widget</td>
            <td>$50</td>
        </tr>
        <tr>
            <td>Gadget</td>
            <td>$30</td>
        </tr>
    </tbody>
</table>

上記のテーブルは、`tr`で行が区切られているため、以下の様になります。

```
let
    // Htmlコード
    Source = "
<table id=""products"">
    <thead>
        <tr>
            <th>Product</th>
            <th>Price</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>Widget</td>
            <td>$50</td>
        </tr>
        <tr>
            <td>Gadget</td>
            <td>$30</td>
        </tr>
    </tbody>
</table>",

    // テーブル作成
    ExtractedTable = Html.Table(
        Source,
        {
            {"Product", "td:nth-child(1)"},
            {"Price", "td:nth-child(2)"}
        },
        [RowSelector = "table#products tbody tr"]
    )
in
    ExtractedTable
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/aa181a97-ddf7-ff32-d7f3-ff8a154b387f.png)

## 12.3.2 複数の`table`要素からの抽出

以下は、２つの`table`要素に分かれているデータを１つのテーブルにします。


<section id=""electronics"">
  <h3>Electronics</h3>
  <table>
    <tr><td>Phone</td><td>$600</td></tr>
    <tr><td>Laptop</td><td>$1200</td></tr>
  </table>
</section>
<section id=""appliances"">
  <h3>Appliances</h3>
  <table>
    <tr><td>Microwave</td><td>$200</td></tr>
    <tr><td>Fridge</td><td>$800</td></tr>
  </table>
</section>

```
let
    Source = "
<section id=""electronics"">
  <h2>Electronics</h2>
  <table>
    <tr><td>Phone</td><td>$600</td></tr>
    <tr><td>Laptop</td><td>$1200</td></tr>
  </table>
</section>
<section id=""appliances"">
  <h2>Appliances</h2>
  <table>
    <tr><td>Microwave</td><td>$200</td></tr>
    <tr><td>Fridge</td><td>$800</td></tr>
  </table>
</section>",

    Electronics = Html.Table(
        Source,
        {
            {"Product", "td:nth-child(1)"},
            {"Price", "td:nth-child(2)"}
        },
        [RowSelector = "section#electronics table tr"]
    ),

    Appliances = Html.Table(
        Source,
        {
            {"Product", "td:nth-child(1)"},
            {"Price", "td:nth-child(2)"}
        },
        [RowSelector = "section#appliances table tr"]
    ),

    Result = Table.Combine({Electronics, Appliances})
in
    Result
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/1f4f7a37-6ef5-745d-9ea5-20f4e8ffceab.png)

## 12.3.4 `dl` や `ul` のリスト要素からの抽出

[サンプルページ](https://fukuyori.github.io/RPALT20220117/)の中で、CSSの修飾でテーブルのように見えているけど、実はリスト要素で作成されているデータも抽出できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/71e3f493-699c-df4b-45ef-d41dbc2d614f.png)

```html:HTMLコード
<div id="route">
<h4>id=rsltlst</h4>
<ul id="rsltlst" class="routeList">
  <li>
    <dl>
      <dt>ルート1</dt>
      <dd>
        <ul>
          <li class="time">19:03→<span class="mark">19:38</span><span class="small">35分</span></li>
          <li><span class="mark">726円</span></li>
          <li>乗換：<span class="mark">0回</span></li>
        </ul>
      </dd>
    </dl>
  </li>
  <li>
    <dl>
      <dt>ルート2</dt>
      <dd>
        <ul>
          <li class="time">19:00→<span class="mark">19:38</span><span class="small">38分</span></li>
          <li class="fare"><span class="mark">726円</span></li>
          <li class="transfer">乗換：1回</li>
        </ul>
      </dd>
    </dl>
  </li>
  <li>
    <dl>
      <dt>ルート3</dt>
      <dd>
        <ul>
          <li class="time">18:58→<span class="mark">19:38</span><span class="small">40分</span></li>
          <li class="fare"><span class="mark">726円</span></li>
          <li class="transfer">乗換：1回</li>
        </ul>
      </dd>
    </dl>
  </li>
</ul>
</div>
```

```
let
    Source = Web.Contents("https://fukuyori.github.io/RPALT20220117/"),
    MakeTable =
        Html.Table(
            Source,
            {
                {"ルート","dl > dt"},
                {"修正前時刻", "dl > dd > ul > li.time"},
                {"所要時間", "dl > dd > ul > li.time > span.small"},
                {"金額", "dl > dd > ul > li:nth-child(2)"},
                {"乗換", "dl > dd > ul > li:nth-child(3)"}
            },
            [RowSelector = "ul#rsltlst > li > dl"]
        ),

        // 時刻項目の修正
        ItemModification = 
            Table.AddColumn(
                MakeTable,
                "時刻",
                each Text.Replace([修正前時刻], [所要時間], "")
            )
in
    ItemModification
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/8bf6ea98-51de-99a6-589d-90dcc1489036.png)

最後のステップで、「修正前時刻」に入り込んでいる「所要時間」を消す作業を行っています。

## 12.3.4 特定の属性を抽出する方法

HTML 要素の属性値を抽出するには、CSS セレクターを指定した後、`[Attributes][属性名]` を付加します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/147e3bad-cd80-fc56-38ce-445f04bfac94.png)

```html:HTMLコード
<div id="SideBarLink" class="quicklinks" name="quicklinks">
<div>
  <h3>id=SideBarLink</h3>
  <h4>id=List-1</h4>
  <ul id="List-1" class="list">
    <li id="ListItem1" class="ListItemClass"><a href="http://www.google.com/">google</a></li>
    <li id="ListItem2" class="ListItemClass"><a href="http://www.yahoo.co.jp/">yahoo</a></li>
    <li id="ListItem3" class="ListItemClass"><a href="https://www.bing.com/">bing</a></li>
  </ul>
</div>
```

```
let
    Source = Web.Contents("https://fukuyori.github.io/RPALT20220117/"),
    Table =
        Html.Table(
            Source,
            {
                {"Name","li > a"},
                {"URL", "li > a", each [Attributes][href]?}
            },
            [RowSelector = "ul#List-1 > li"]
        )
in
    Table
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/90cd5309-c8d2-ff66-a6a8-049d45c8243a.png)

`{"URL", "li > a",each [Attributes][href]?}`の行では、３番目の部分で全ての属性から `href` を選び出しています。該当の値が存在しない場合、このステップがエラーとなってしまい、クエリが止まってしまいます。`?` をつけておくと、該当の値が存在しない場合には `null` が返され、エラーを回避できます。

# 12.4 ベストプラクティス

### 1. 行選択の精度を上げる

   - `RowSelector` を使用することで、ターゲットとなる行を正確に指定します。これにより、不要なデータを抽出するリスクを軽減できます。

### 2. セレクターの構造を確認
ブラウザの「要素を検証」機能を活用して、ページの DOM 構造に基づいて適切なセレクターを特定します。

CSSセレクターを適切に指定するためには、以下のツールを活用すると効果的です。

- **CSSセレクターのコピー**: HTML構造を確認し、要素を特定するのに役立ちます。
  - Google Chrome: 要素を右クリック → 検証 → 要素のコードを右クリック → Copy → Copy selector
  ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/a33a0d00-05fb-0001-8b35-8d98b6e6975c.png)

  - Microsoft Edge: 要素を右クリック → 開発者ツールで調査する → 要素のコードを右クリック → コピー → selectorをコピー
  ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/9625f28b-61f2-6d01-de64-6cd81e6da8d2.png)

- **セレクターテストツール**: CSSセレクターをテストするツールを使用して、正確なセレクターを確認します。
    - Google Chrome: 
        - Ctrl + Shift + i (Option + Command + i) を押してデベロッパー ツール起動。
        - Consoleを選択。
        - cssセレクターの実験は $$(css_selector);
        ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/a66eaec8-f6b3-90ea-7b50-260420ab4a0d.png)
    - Microsoft Edge:
        - Ctrl + Shift + i (Option + Command + i) を押して開発者ツール起動。
        - コンソールを選択。
        - cssセレクターの実験は $$(css_selector);
        ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/f16b7a42-a7cb-b52d-7175-2a390d4eaf7a.png)

### 3. 動的ページに注意

   - JavaScript によって生成されたコンテンツがある場合、`Web.BrowserContents` を使用する必要がある場合があります。