【子育て】CODMON データ自動抽出【保育園】


## はじめに

私の子どもが通う保育園では、連絡帳アプリとして **CODMON** というサービスが利用されている。

https://www.codmon.com/

このサービスにより、子どもたちの日々の様子の共有や活動記録の確認、写真の閲覧・購入、保育士の方々からの諸連絡を確認することができる、大変安心・便利なものである。

貴重な子どもたちの記録を自分の管理下にも置いておきたいとふと思い、データを何らかの形でダウンロードすることを考えた。あいにくエクスポート機能などは備わっていないが、本サービスは、ネイティブアプリおよび web アプリに対応しているため、「web アプリのスクレイピングでハードコピー取っちゃえ☆」ということで、その時のメモとして残しておきたいと思う。

## リポジトリ

https://github.com/yasuki0707/codmon-extract

## 前提・環境

- プログラム実行環境: `MacOS Sonoma 14.4 / Apple M1 Pro`
- Google Chrome バージョン: `126.0.6478.127（Official Build） （arm64）`
- Python バージョン: `3.11.9`
- CODMON にアカウント登録していること

## 作業

### インストール

```sh
pip install -r requirements.txt
```

以下の依存モジュールをインストールする。

```txt:requirements.txt
attrs==23.2.0
certifi==2024.2.2
chromedriver-binary==126.0.6478.126
h11==0.14.0
idna==3.7
outcome==1.3.0.post0
PySocks==1.7.1
selenium==4.19.0
sniffio==1.3.1
sortedcontainers==2.4.0
trio==0.25.0
trio-websocket==0.11.1
typing_extensions==4.11.0
urllib3==2.2.1
wsproto==1.2.0
```

:::note info
`chromedriver-binary` は (ブラウザの) Chrome バージョンより小さいもののち最新のものを [ここ](https://googlechromelabs.github.io/chrome-for-testing/) から選択する。
:::

### スクレイピング

以下のコードを丸々貼り付ける。
`your_mail_address` と `your_password` の部分を自分のアカウントのものを指定するだけで、基本的には動くはず。。


```python:main.py
from selenium import webdriver
from selenium.webdriver.common.by import By
import chromedriver_binary
import time

# ドライバ設定とURL取得
driver = webdriver.Chrome()
driver.get("https://parents.codmon.com/menu")
time.sleep(10)

# ブラウザの操作
# 「すでにアカウントをお持ちの方」画面
elm = driver.find_element(by=By.CSS_SELECTOR, value="body > div > div:nth-child(1) > ons-page > ons-page > div.page__content > ons-navigator > ons-page > div.page__content > section > div.menu__loginLink")
elm.click()
time.sleep(3)

# ログイン画面
elm = driver.find_element(by=By.CSS_SELECTOR, value="body > div > div:nth-child(1) > ons-page > ons-page > div.page__content > ons-navigator > ons-page > div.page__content > div.loginPage--parent > section > input")
elm.send_keys(<your_mail_address>)
elm = driver.find_element(by=By.CSS_SELECTOR, value="body > div > div:nth-child(1) > ons-page > ons-page > div.page__content > ons-navigator > ons-page > div.page__content > div.loginPage--parent > section > div:nth-child(4) > input")
elm.send_keys(<your_password>)
elm = driver.find_element(by=By.CSS_SELECTOR, value="body > div > div:nth-child(1) > ons-page > ons-page > div.page__content > ons-navigator > ons-page > div.page__content > div.loginPage--parent > section > ons-button")
elm.click()
time.sleep(3)

# ログイン後、「連絡帳」でフィルター
## フィルター表示
elm = driver.find_element(by=By.CSS_SELECTOR, value="#timeline_page > div.page__content > ons-navigator > ons-page > div.page__content > ons-splitter > ons-splitter-content > ons-page > div.content.page__content > div.timelineHeader__wrapper > div > div.timelineHeader__filter")
elm.click()
time.sleep(3)

## 「連絡帳」チェック
elm = driver.find_element(by=By.CSS_SELECTOR, value="#timeline_page > div.page__content > ons-navigator > ons-page > div.page__content > div > div > div.timelineFilter__container > div:nth-child(4) > label:nth-child(2)")
elm.click()

## 決定ボタンクリック
elm = driver.find_element(by=By.CSS_SELECTOR, value="#timeline_page > div.page__content > ons-navigator > ons-page > div.page__content > div > div > div.timelineFilter__footer > div.timelineFilter__searchBtn")
elm.click()

time.sleep(3)

hasNext = True
while hasNext:
    posts = driver.find_elements(by=By.CSS_SELECTOR, value="#timeline_page > div.page__content > ons-navigator > ons-page > div.page__content > ons-splitter > ons-splitter-content > ons-page > div.content.page__content > div.timeline_content > div")
    for i in range(1, min(20, len(posts)) + 1):
        post = posts[i]

        # 詳細ページに入る
        post.click()
        time.sleep(2)
        # 
        content = driver.find_element(by=By.CSS_SELECTOR, value="#timeline_page > div.page__content > ons-navigator > ons-page.selectable-container.page > div.page__content > div.block__white--padding.block__white--noborder")
        date = content.text.split("\n")[-1].split("日")[0] + "日"
        driver.get_screenshot_as_file(date + ".png")
        # 戻るボタンクリック
        back = driver.find_element(by=By.CSS_SELECTOR, value="#timeline_page > div.page__content > ons-navigator > ons-page.selectable-container.page > ons-toolbar > div.left.toolbar__left > ons-back-button > span.back-button__label")
        back.click()
        time.sleep(1)
        
    # 次のページがあるかどうか
    next = driver.find_element(by=By.CSS_SELECTOR, value="#timeline_page > div.page__content > ons-navigator > ons-page > div.page__content > ons-splitter > ons-splitter-content > ons-page > div.content.page__content > div.timeline_content > section > div:nth-child(2) > ons-button")
    hasNext = next.get_attribute("disabled") == None
    if hasNext:
        next.click()
        time.sleep(2)
        

# WebDriverの終了
driver.close()
driver.quit()
```

やっていることをざっくり解説すると、
1. 起動直後は、以下のようなページに遷移するため、「すでにアカウントをお持ちの方」を選択する
    <img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/671630/9c89e4fd-0e49-1077-bb27-48f1d099d0f9.png width=400>

2. 指定した credentials(メールアドレス & パスワード) でログインする
3. ホーム画面に遷移後、フィルターで「連絡帳」に設定する
    <img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/671630/41515cba-7a9b-7be8-e5a2-6b318173a972.png width=400>

4. フィルター結果が表示されるため、一日ずつページに入ってスクショを撮影・保存し、一覧に戻って、、を繰り返す
    こんな感じの画像となる:
    <img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/671630/0ed6340f-9c50-255a-3699-ac34e38f8f62.png width=400>
5. 「次の 20 件」がある限り、次のページに進んで 4 を繰り返す

### 実行

```sh
python main.py
```

### 頻発するランタイムエラー

- google-driver バージョン不備
	- → [ここ](#インストール) を参照して、バージョン指定が正しいか確認する
- element が見つからない問題(`selenium.common.exceptions.NoSuchElementException: Message: no such element: Unable to locate element`)
	- → `time.sleep()` に指定する秒数を増やすと解決しがち

## 終わりに

きょうびの保育園では、保育園との連絡ツールとしてスマホアプリを使用するケースが多いらしく、時代も変わったものである。CODMON は web にも対応しているため今回のようなスクレイピングが可能だったが、アプリのみのサービスだとこうはいかない。
また、**API を公開してくれたりすると非常に嬉しい。**（有料サービスでも課金しちゃうかも）

このほかにもいくつか保育シーンで使用されているアプリはあり、そのようなレビューや hack 的な記事をどなたか書いてくれたら是非見てみたい。