0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Playwright Scraping: Retrieve Data Usage from Rakuten Web Portal

0
Last updated at Posted at 2025-12-25

楽天モバイルの直近データ使用量を取得するpythonコードを作成してみました。使用状況ページのデータ使用量を取得して実行結果に出力します。スクレイピングにはplaywrightを使います。

playwrightについて

・Microsoft製のWebアプリ用テスト自動化ツール(Node.jsのライブラリとしてスタートしている)
・ブラウザ操作を高速かつ安定的に自動化できる
・JavaScriptで動的に生成されるページも正しく取得できる
・seleniumの時のようにwebdriverの更新が不要(構築環境がシンプル) ←これが一番のメリットだと思う。Selenium のようにブラウザとドライバのバージョンを揃える作業は不要。

python 環境

Python 3.8 or higher. See System requirements of Playwright.

ライブラリのインストール

playwright:今回のブラウザ操作用メインライブラリ
python-dotenv:.envファイルから環境変数を読み込むために使用

pip install playwright python-dotenv 

ブラウザバイナリはchromiumだけインストール(他は使わないので)

python -m playwright install chromium

ファイル構成

project-root/
├── .env                     # RAKUTEN_USER, RAKUTEN_PASS を定義
├── rakuten_login.py         # ログイン処理
└── rakuten_data_usage.py    # 利用状況ページのデータ使用量を取得

Playwrightからは、Chrome互換のオープンソースブラウザであるChromiumを使ってスクレイピングを行います。
ヘッドレスモード(headless=True)で実行しますが、不安定な場合はFalseでお試しください。また、要素の取得については基本XPathで取得しています。(CSSセレクタでは取得の難しい箇所が多かった。)

python code for retrieving data usage

.env

RAKUTEN_USER=your_username
RAKUTEN_PASS=your_password

rakuten_login.py

# rakuten_login.py
import os
from dotenv import load_dotenv
from playwright.sync_api import sync_playwright

def rakuten_login():
    # --- 環境変数の読み込み ---
    load_dotenv()
    username = os.getenv("RAKUTEN_USER")
    password = os.getenv("RAKUTEN_PASS")

    # --- ユーザー名・パスワードの存在確認 ---
    if not username or not password:
        raise ValueError("RAKUTEN_USER または RAKUTEN_PASS が .env に設定されていません")

    # --- Playwright の起動とブラウザ準備 ---
    playwright = sync_playwright().start()
    browser = playwright.chromium.launch(headless=True)  # headless=True でブラウザを表示
    context = browser.new_context()
    page = context.new_page()

    # --- 楽天モバイルポータルにアクセス ---
    page.goto("https://portal.mobile.rakuten.co.jp/")

    # --- ログイン処理 ---
    # ログインボタンをクリック
    page.click('xpath=//*[@id="c_head_7-0"]')
    page.wait_for_url("**login.account.rakuten.com**")

    # ユーザーID入力
    page.wait_for_selector('xpath=//*[@id="user_id"]')
    page.fill('xpath=//*[@id="user_id"]', username)

    # 次へボタンをクリック
    page.click('xpath=/html/body/form/div/div[3]/div/div[2]/div/div[2]/div/div/div[2]/div[4]/div')

    # パスワード入力
    page.wait_for_selector('xpath=//*[@id="password_current"]')
    page.fill('xpath=//*[@id="password_current"]', password)

    # ログインボタンをクリック
    page.click('xpath=/html/body/form/div/div[3]/div/div[2]/div/div[2]/div/div/div[2]/div[5]/div')

    # 少し待機(ログイン完了待ち)
    page.wait_for_timeout(2000)

    # --- 呼び出し元に Playwright オブジェクトを返す ---
    return playwright, browser, context, page

rakuten_data_usage.py

# rakuten_usage.py
from rakuten_login import rakuten_login

def rakuten_usage_message():
    # ============================================================
    # ① ログイン処理(Playwright コンテキスト取得)
    # ============================================================
    playwright, browser, context, page = rakuten_login()

    # ============================================================
    # ② 利用状況ページへ移動
    # ============================================================
    page.click('xpath=/html/body/div[1]/div/div[2]/div/div[1]/div/div/div[2]/div[3]/div/button')
    page.click('xpath=/html/body/div[1]/div/div[2]/div/div[1]/div/div/div[2]/div[4]/div/div[1]/div/div/div[2]/ul[1]/li[3]/div/a/p')

    # ============================================================
    # ③ ダッシュボードのロード完了を待機
    # ============================================================
    page.wait_for_selector("div.rktn-usage-statistics-top", timeout=20000)
    page.wait_for_selector("rktn-loader-usage-statistics-totals", state="detached", timeout=20000)

    # ============================================================
    # ④ データ有無の判定と出力
    # ============================================================
    empty_el = page.query_selector("div.totals__empty.ng-star-inserted")

    if empty_el:
        # --- データなし ---
        line_el = page.query_selector("div.ng-tns-c139-8:nth-child(2)")
        line_label = line_el.text_content().strip() if line_el else "回線不明"
        print(f"{line_label} はデータの利用がありません")

    else:
        # --- データあり(対象回線ラベルを取得) ---
        line_el = page.query_selector("div.rktn-dropdown-list__value-template__label")
        line_label = line_el.text_content().strip() if line_el else "回線不明"

        names = page.query_selector_all("div.rktn-usage-statistics-totals__main-name")
        values = page.query_selector_all("div.rktn-usage-statistics-totals__main-value")

        if names and values:
            for name, value in zip(names, values):
                print(f"{line_label}{name.text_content().strip()} : {value.text_content().strip()}")
        else:
            print(f"{line_label} : 利用統計の項目が見つかりませんでした")

    # ============================================================
    # ⑤ ブラウザ終了処理
    # ============================================================
    browser.close()
    playwright.stop()


# ============================================================
# ⑥ エントリーポイント
# ============================================================
if __name__ == "__main__":
    rakuten_usage_message()

rakuten_data_usage.pyの実行結果(サンプル)

070-1234-5678  データ利用量 : 10.32 GB

プロセスは終了コード 0 で終了しました

直近の請求書(pdf)の取得

次に楽天モバイルの請求書PDFをダウンロードして指定フォルダに保存するpythonコードを作成します。今月分が未確定でダウンロードできない場合は先月分を取得します。すでに同一名ファイルが保存されている場合は上書きするかを確認します。

先程の.envにPDF保存先ディレクトリを指定するために下記を追加します。

SAVE_DIR=/path/to/target directory

python code for retrieving invoice pdf

rakuten_mobile_invoice.py

import os
import re
from datetime import datetime
from dotenv import load_dotenv
from rakuten_login import rakuten_login
from playwright.sync_api import TimeoutError, Error

# ============================================================
# 1. ファイル名生成・日付処理など
# ============================================================

def extract_year_month_prefix(period_text: str) -> str:
    """請求期間から「翌月分」の年月を抽出する"""
    if not period_text:
        return ""
    date_strs = re.findall(r"\d{4}/\d{1,2}/\d{1,2}", period_text)
    if not date_strs:
        return ""
    end_date = datetime.strptime(date_strs[-1], "%Y/%m/%d")
    year, next_month = end_date.year, end_date.month + 1
    if next_month == 13:
        year += 1
        next_month = 1
    return f"{year}{next_month}月分"

def sanitize_filename(name: str) -> str:
    """ファイル名に使えない文字を安全な文字に置換"""
    return re.sub(r'[\\/:*?"<>|]+', '_', name)

def save_pdf_with_confirmation(download, save_dir, pdf_filename):
    """PDF を保存(既存ファイルがある場合は上書き確認)"""
    pdf_path = os.path.join(save_dir, pdf_filename)
    if os.path.exists(pdf_path):
        ans = input(f"ファイル {pdf_filename} は既に存在します。上書きしますか? (y/n): ").strip().lower()
        if ans != "y":
            return None
    download.save_as(pdf_path)
    print(f"PDF保存完了: {pdf_path}")
    return pdf_path


# ============================================================
# 2. 今月分 PDF ダウンロード処理
# ============================================================

def download_this_month(page, save_dir):
    try:
        page.click(
            'xpath=//*[@id="angc_DashboardPortlet_INSTANCE_snuh"]/div[2]/div/'
            'rktn-billing/div[2]/rktn-billing-bill-summary/div[1]/div/rktn-button/button'
        )
        page.wait_for_timeout(800)

        period_text = page.text_content("p.rktn-invoice-payment__block-value.ta_param_value") or ""
        prefix = extract_year_month_prefix(period_text)

        with page.expect_download() as info:
            page.click(
                'xpath=//*[@id="angc_DashboardPortlet_INSTANCE_snuh"]/div[2]/div/'
                'rktn-invoice/div[2]/div[3]/rktn-invoice-documents/div[1]/'
                'rktn-button[1]/a/span[2]'
            )
        download = info.value

        suggested = sanitize_filename(download.suggested_filename)
        filename = f"{prefix}_{suggested}" if prefix else suggested
        return save_pdf_with_confirmation(download, save_dir, filename)

    except (TimeoutError, Error):
        return None


# ============================================================
# 3. 先月分 PDF ダウンロード処理
# ============================================================

def download_last_month(page, save_dir):
    """先月分の PDF をダウンロード(invoice-documents → billing-summary の順)"""
    try:
        # swiper で先月へ移動
        page.wait_for_selector('div.swiper-button-prev', timeout=10000)
        prev = page.query_selector('div.swiper-button-prev')
        if not (prev and prev.is_enabled()):
            return None

        prev.click()
        page.wait_for_timeout(3000)

        # ① invoice-documents を探す
        pdf_xpath = 'xpath=//*[@id="angc_DashboardPortlet_INSTANCE_snuh"]//rktn-invoice-documents//rktn-button[1]/a/span[2]'
        btn = page.query_selector(pdf_xpath)

        if btn and btn.is_enabled():
            btn.click()
            page.wait_for_timeout(800)

            period_text = page.text_content("p.rktn-invoice-payment__block-value.ta_param_value") or ""
            prefix = extract_year_month_prefix(period_text)

            with page.expect_download() as info:
                page.click(pdf_xpath)
            download = info.value

            suggested = sanitize_filename(download.suggested_filename)
            filename = f"{prefix}_{suggested}" if prefix else suggested
            return save_pdf_with_confirmation(download, save_dir, filename)

        # ② invoice-documents が無い場合 → billing-summary
        summary_xpath = 'xpath=//*[@id="angc_DashboardPortlet_INSTANCE_snuh"]//rktn-billing-bill-summary//rktn-button/button'
        summary_btn = page.query_selector(summary_xpath)

        if summary_btn and summary_btn.is_enabled():
            summary_btn.click()
            page.wait_for_timeout(800)

            period_text = page.text_content("p.rktn-invoice-payment__block-value.ta_param_value") or ""
            prefix = extract_year_month_prefix(period_text)

            with page.expect_download() as info:
                page.click(pdf_xpath)
            download = info.value

            suggested = sanitize_filename(download.suggested_filename)
            filename = f"{prefix}_{suggested}" if prefix else suggested
            return save_pdf_with_confirmation(download, save_dir, filename)

    except (TimeoutError, Error):
        return None

    return None


# ============================================================
# 4. メイン処理(今月 → 先月 fallback)
# ============================================================

def rakuten_get_bill():
    load_dotenv()
    save_dir = os.getenv("SAVE_DIR")

    playwright, browser, context, page = rakuten_login()
    page.set_default_timeout(60000)

    # 請求ページへ遷移
    page.click('xpath=/html/body/div[1]/div/div[2]/div/div[1]/div/div/div[2]/div[3]/div/button')
    page.click('xpath=/html/body/div[1]/div/div[2]/div/div[1]/div/div/div[2]/div[4]/div/div[1]/div/div/div[2]/ul[1]/li[4]/div/a/p')

    # 今月分を試す
    downloaded = download_this_month(page, save_dir)

    # 今月分がダメなら先月分
    if not downloaded:
        download_last_month(page, save_dir)

    browser.close()
    playwright.stop()


# ============================================================
# 5. エントリーポイント
# ============================================================

if __name__ == "__main__":
    rakuten_get_bill()

rakuten_mobile_invoice.pyの実行結果(サンプル)

ファイル 2025年12月分_利用明細.pdf は既に存在します上書きしますか (y/n): y
PDF保存完了: /path/to/2025年12月分_利用明細.pdf

プロセスは終了コード 0 で終了しました

データ使用量がどれくらいなのか自動取得できるのは良い点だと思います。請求書のダウンロード機能は定期的にlaunchdやcronなどで実行すれば便利かと。この楽天ポータルはDOM構造が複雑なので要素取得に苦労しました。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?