【Python】GitHubActionsでスクレイピングを定期実行してみる

## はじめに

GitHub Actionsを使用してスケジュールトリガーで複数のWebサイトをスクレイピングし、データを取得するプログラムを作成します。

## スクレイピング

**取得したいデータ**
各地域の血液センターのウェブサイトでは、以下のような献血状況が公表されています。献血方法には、400mL、200mL、そして成分献血の3種類があり、血液型ごと（A型、O型、B型、AB型）に対して、「安心です」「心配です」「困っています」「非常に困っています」という4段階で献血状況が報告されています。これらの情報をスクレイピングして取得します。

![target.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3828517/58c7ba57-d383-a275-b603-69125313c650.png)

`BeautifulSoup`と`requests`を使ってスクレイピングをして、CSVファイルに書き込む処理を行います。
1. ライブラリのインポート
    ```python
    import csv

    import requests
    from bs4 import BeautifulSoup
    ```
2. WebサイトのURLをリストに格納する
    北海道、東北、関東甲信越、東海北陸、近畿、中四国、九州ブロックの7ブロックの献血センターに対して、それぞれのサイトのURLを表す`url`と地域コードを表す`code`を割り当てます。また、開発者ツールなどを用いて抽出したい`<p>`タグのクラス名を特定します。
    
    ![target_class.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3828517/751a346e-b0d8-3ece-25b0-08284323da01.png)
    ```python
    sites = [
        {'url':'https://www.bs.jrc.or.jp/hkd/hokkaido/index.html', 'code': 1, 'class': 'center-main-today-types-state'},
        {'url':'https://www.bs.jrc.or.jp/th/bbc/index.html', 'code': 2, 'class': 'block-main-today-types-state'},
        {'url':'https://www.bs.jrc.or.jp/ktks/bbc/index.html', 'code': 3, 'class': 'block-main-today-types-state'},
        {'url':'https://www.bs.jrc.or.jp/tkhr/bbc/index.html', 'code': 4, 'class': 'block-main-today-types-state'},
        {'url':'https://www.bs.jrc.or.jp/kk/bbc/index.html', 'code': 5, 'class': 'block-main-today-types-state'},
        {'url':'https://www.bs.jrc.or.jp/csk/bbc/index.html', 'code': 6, 'class': 'block-main-today-types-state'}, 
        {'url':'https://www.bs.jrc.or.jp/bc9/bbc/index.html', 'code': 7, 'class': 'block-main-today-types-state'}
    ]
    ```
    
3. スクレイピング処理
    `response.txet`では文字化けしてしまったので`response.content`としました。
    ```python
    for site in sites:
        response = requests.get(site['url'])
        soup = BeautifulSoup(response.content, "html.parser")
        
        p_tags = soup.find_all('p')

        target_p_tags = [tag for tag in p_tags if site['class'] in tag.get('class', [])]
        target_elements = [tag.text for tag in target_p_tags if tag.text.strip()]
    ```
    
4. CSVファイルに書き込む
    1~3の手順にCSVファイルにスクレイピング結果を書き込む処理を追加します。ここでは`BloodStock.csv`というファイルを用意しました。
    ```python
    # ヘッダー
    header = ['block_code', '400-a', '400-o', '400-b', '400-ab', '200-a', '200-o', '200-b', '200-ab', 'com-a', 'com-o', 'com-b', 'com-ab']
    
    # 'w'で上書き
    with open('BloodStock.csv', 'w', newline='', encoding='utf-8') as csvfile:
         writer = csv.writer(csvfile)
         writer.writerow(header)

    # 'a'で追加書き込み
    def write_to_csv(code, data, filename):
        with open(filename, 'a', newline='', encoding='utf-8') as csvfile:
             writer = csv.writer(csvfile)
             writer.writerow([code] + data)
    
    for site in sites:
        response = requests.get(site['url'])
        soup = BeautifulSoup(response.content, "html.parser")

        p_tags = soup.find_all('p')

        target_p_tags = [tag for tag in p_tags if site['class'] in tag.get('class', [])]
        target_elements = [tag.text for tag in target_p_tags if tag.text.strip()]

        write_to_csv(site['code'], target_elements, 'BloodStock.csv')
    ```
    これでCSVファイルにスクレイピング結果を保存することができました。

## GitHub Actionsで自動化

GitHub Actionsのワークフローを定義するymlファイルは、リポジトリのルートディレクトリに`.github`ディレクトリを作成し、その中に`workflows`というファイルを作成します。

### ymlファイル

1. name：アクションの名前を入力。リポジトリ内で一意の名前を付けてあげます。
    ```yaml
    name: Scrape
    ```  
2. on：アクションのトリガーを設定

    ```yaml
    on:
      workflow_dispatch:
      schedule:
        - cron: "0 15 * * 2,5"
    ```
    
    `workflow_dispatch`は手動でワークフローを起動するためのトリガーです。
    リポジトリのActionsから「Run workflow」をクリックすると実行できます。
    
    ![Untitled 3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3828517/5b11e021-1ce3-3319-6dcd-4b299493a70c.png)
    
    `schedule`は`cron`式を使用してスケジュールトリガーで実行を行います。`cron`式は以下のような構造になっています。
    | ＊ | ＊ | ＊ | ＊ | ＊ |
    | --- | --- | --- | --- | --- |
    | 分 | 時 | 日 | 月 | 曜日 |
    | 0 - 59 | 0 - 23(UTC時間) | 1 - 31 | 1 - 12 | 0 - 7(0,7が日曜) |
    
    注意したいのが、時間はUTC時間であることです。UTC時間(協定世界時)`UTC = JST - 9時間`で表現することができます。現在はUTCに置き換わりつつありますが、GMTと同じ時間を示します。ここでは、毎週水曜日と金曜日の午前0時に実行したいので`”0 15 * * 2,5”`としています。

3. jobs:ジョブを設定していきます
    
    ```yaml
    jobs:
    	Scrape:
    		runs-on: ubuntu-latest
    ```
    `runs-on`: ジョブを動作させる仮想環境をUbuntu最新版に指定しています。
    
4. steps:
    
    ```yaml
    steps:
            # GitHubリポジトリをチェックアウトする
          - name: Checkout repository
            uses: actions/checkout@v2

            # バージョン3.9でセットアップする
          - name: Set up Python
            uses: actions/setup-python@v2
            with:
              python-version: "3.9"

            # requirements.txtに書いたライブラリをインストールする
          - name: Install dependencies
            run: |
              pip install -r requirements.txt

            # robot.pyを実行する
          - name: Run scraping script
            run: |
              python robot.py
            
            # 更新されたデータをdevelopブランチにプッシュする
          - name: commit files
            run: |
              git config --global user.name "${{ github.actor }}"
              git config --global user.email "${{ github.actor }}@users.noreply.github.com"
              git add BloodStock.csv
              git commit -m 'update BloodStock.csv'
              git push origin develop
    ```
    `"${{ github.actor }}"`,`"${{ github.actor }}@users.noreply.github.com"`とすることで実行者のGitHubアカウントでコミットを行います。
    

### 403エラー

```
fatal: unable to access 'https://github.com/******/*****/': The requested URL returned error: 403
Error: Process completed with exit code 128.
```

コードを実行しようとしたところ、アクセスできないと言われ、403エラーが出てしまいました。調べてみると、GitHub Actionsでの書き込み権限がないようなので書き込み権限を与えます。
`Settings`タブから`Actions`の`General`を選択します。`Workflow permissions`を`Read and write permissions`に変更し、Saveすればエラーは解消されました。

![change_permissions.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3828517/80c55c07-fea6-d113-732e-82bb90afe488.png)


定期実行されCSVファイルが自動で更新されるようになりました。
![result.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3828517/021358dd-4bcc-785f-3c24-64658e5a1654.png)


## おわりに

GitHub Actionsで簡単にスクレイピングを自動実行できました。スクレイピングで一度に大量のデータを取得するとサーバーに負担をかけてしまう可能性があるので気を付けるひつようがあります。
今回作成したプログラムは以下のリポジトリで公開しています。

https://github.com/5522079/kenketsu-navi/tree/main/module

## 参考にしたサイト等

https://zenn.dev/hukurouo/articles/github-actions-ruby-scrape#mechanize

https://zenn.dev/kmukmu/articles/475b0542d05fa3