PR

スクレイピングをやめてWordPress REST APIでデータ取得する方法|/wp-json/wp/v2/posts でブログ記事一覧を抽出【後編】

Python

Pythonスクレイピング 後編

スポンサーリンク

はじめに:HTML解析をやめて「裏口」を使う

こんにちは、ニンジンです🥕

前編では、自作の記事収集ツールが自分のブログを相手に「取得件数:0」という結果に終わるまでをお届けしました。

この記事では、HTMLという「表口」ではなく、データが整理された「専用の裏口」——WordPress REST API——を使って、一瞬でデータを取得する方法と、自動化エンジニアとしての「最終的な着地点」を書きます。

この記事で分かること

  • WordPress REST API(/wp-json/wp/v2/posts)とは何か
  • APIから記事一覧をExcel化する、10数行のシンプルなコード
  • CustomTkinter + threading で作った、モード切替つきGUIツールの構造
  • 「ECサイトの価格調査」を最終的に断念した理由

スポンサーリンク

救世主「WordPress REST API」

多くのWordPressサイトには、外部システムとデータをやり取りするための専用窓口 REST API が標準で備わっています。サイトのURLの末尾に /wp-json/wp/v2/posts を付けてブラウザで開くと、記事のデータがJSON形式で表示されます。

https://(サイトのURL)/wp-json/wp/v2/posts
HTMLの表口とAPIの裏口を対比した図

HTML(飾り付けられた部屋)を解析する必要がなく、直接データベース(整理された棚)から、タイトルやURLを抜き出せます。


スポンサーリンク

APIを使うと、コードはここまで短くなる

import requests
import openpyxl
from datetime import datetime

# WordPressの公式APIエンドポイント
API_URL = "https://py-vbalab.com/wp-json/wp/v2/posts"

print("APIからデータを直接取得中...")

# 1. データを取ってくる
response = requests.get(API_URL)
if response.status_code == 200:
    posts = response.json()  # 文字の羅列をPythonの「辞書型」に変換
    
    # Excelの準備
    wb = openpyxl.Workbook()
    ws = wb.active
    ws.append(["投稿日", "タイトル", "URL"])

    for post in posts:
        # JSONの中から必要な項目を抜き出す
        title = post['title']['rendered']
        link = post['link']
        # 日付を読みやすく加工
        date_raw = post['date']
        date_formated = date_raw.split('T')[0] # 2026-04-06T... を 2026-04-06 に

        ws.append([date_formated, title, link])
        print(f"取得: {title}")

    # 保存
    filename = f"Quick_Report_{datetime.now().strftime('%Y%m%d')}.xlsx"
    wb.save(filename)
    print(f"\n✨ 完了!爆速で '{filename}' を作成しました。")
else:
    print("データが取得できませんでした。")

結果:10件の記事を取得成功

APIで10件の記事を取得できた実行結果

前編であれだけ苦労していたのが嘘のように、すぐに記事一覧を取得できました。

デフォルトでは10件返ります。?per_page=100 で最大100件、?page=2 でページ送りができます。

スポンサーリンク

自動レポートくん(NINJIN Edition):GUIツール化

自動レポートくんのGUI画面

コードを書くだけでは「ツール」とは呼べません。VBAユーザーにも馴染みのある CustomTkinter で、誰でも操作できるGUIを付けました。

このツールの目玉は、調査モードを画面から切り替えられる点です。

  • WordPress APIモード:相手がWordPressサイトならこれ。HTML解析が不要で、正確かつ短時間でデータを抽出できます。
  • Seleniumモード:APIが無い汎用サイト向けの最終手段。ブラウザを動かすので、JavaScriptで描画される動的サイトにも対応できます。

threading(マルチスレッド)でGUIを固まらせない

このツールは裏側で「スレッド」を使っています。VBAで重い処理を実行するとExcelが「応答なし」で固まった経験はありませんか。Pythonの threading を使えば、「裏で作業する担当」と「画面を動かし続ける担当」を分けられます。これにより、スクレイピング中でもログがリアルタイムに流れます。

🧊 GUIが固まる理由 PythonのGUIが固まる理由とThreadの使い方|Tkinterでモーター制御中もボタンを効かせる なぜ重い処理でGUIが固まるのか、Threadでどう解決するのかの詳しい解説。 → 🎨 CustomTkinter入門 CustomTkinterでPythonツールをGUIアプリ化する方法|filedialogでファイル選択・クラスで設計 CTkLabel / CTkButton / CTkEntry の基本と、クラスで画面を設計する方法。 →

Pythonコード(全文)

import customtkinter as ctk
import threading
import requests
import openpyxl
from datetime import datetime
import time
import os  # ファイル保存確認用
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.service import Service

class NinjinScraperApp(ctk.CTk):
    def __init__(self):
        super().__init__()
        self.title("自動レポートくん - NINJIN Edition")
        self.geometry("600x450")
        
        # --- レイアウト設定 ---
        self.label = ctk.CTkLabel(self, text="調査モードを選択して、URLを入力してください")
        self.label.pack(pady=10)

        # モード選択(セグメントボタン)
        self.mode_var = ctk.StringVar(value="Selenium")
        self.mode_switch = ctk.CTkSegmentedButton(self, 
                                                 values=["Selenium", "WordPress API"],
                                                 variable=self.mode_var)
        self.mode_switch.pack(pady=10)

        self.url_entry = ctk.CTkEntry(self, placeholder_text="https://...", width=400)
        self.url_entry.pack(pady=10)

        self.btn_run = ctk.CTkButton(self, text="レポート作成開始", 
                                     fg_color="#FF8C00", # 人参オレンジ
                                     command=self.start_task)
        self.btn_run.pack(pady=20)

        self.log_box = ctk.CTkTextbox(self, width=500, height=150)
        self.log_box.pack(pady=10)

    def start_task(self):
        # メインスレッドが止まらないように別スレッドで実行
        thread = threading.Thread(target=self.run_scraping)
        thread.start()

    def run_scraping(self):
        mode = self.mode_var.get()
        url = self.url_entry.get()  #self.url_entry(GUIで入力されたURL)からURLを取得
        
        if not url.startswith("http"):
            self.update_log("⚠️ 有効なURLを入力してください。")
            return

        self.update_log(f"🚀 【{mode}】で調査を開始します...")
        
        if mode == "WordPress API":
            self.run_api_logic(url)  # ← WordPress API版を呼び出す urlを引数として渡す
        else:
            self.run_selenium_logic(url) # ← ここでSelenium版を呼び出す urlを引数として渡す
            
        self.update_log("✨ すべてのタスクが終了しました。")
    
    def run_api_logic(self, target_url):
        """
        WordPress REST APIを使用してデータを抜くロジック
        """
        # 末尾に /wp-json/wp/v2/posts を付与(入力URLの整形)
        base_url = target_url.rstrip('/')
        api_endpoint = f"{base_url}/wp-json/wp/v2/posts"
        
        try:
            self.update_log(f"🔗 接続中: {api_endpoint}")
            response = requests.get(api_endpoint, timeout=10)
            
            if response.status_code == 200:
                posts = response.json()
                
                # Excelの準備
                wb = openpyxl.Workbook()
                ws = wb.active
                ws.append(["投稿日", "タイトル", "URL"])

                for post in posts:
                    title = post['title']['rendered']
                    link = post['link']
                    date_raw = post['date']
                    date_formated = date_raw.split('T')[0]

                    ws.append([date_formated, title, link])
                    self.update_log(f"📖 取得成功: {title[:20]}...") # 長いので省略表示

                # 保存処理
                filename = f"API_Report_{datetime.now().strftime('%Y%m%d_%H%M')}.xlsx"
                wb.save(filename)
                
                self.update_log(f"✅ 保存完了: {filename}")
                self.show_finish_message(filename)
            else:
                self.update_log(f"❌ エラー: ステータスコード {response.status_code}")
                self.update_log("※WordPressサイトではないか、APIが閉じられている可能性があります。")

        except Exception as e:
            self.update_log(f"⚠️ 想定外のエラー: {str(e)}")

    def update_log(self, message):
        """ログボックスに文字を出す補助関数"""
        self.log_box.insert("end", f"{message}\n")
        self.log_box.see("end") # 常に最新の行を表示

    def show_finish_message(self, filename):
        """完了通知(人参くんらしいメッセージ)"""
        from tkinter import messagebox
        messagebox.showinfo("調査完了", f"レポートの作成が終わりました!\nファイル名: {filename}")
    
    def run_selenium_logic(self, target_url):
        """
        Seleniumを使用して汎用的にデータを抜くロジック
        """
        options = Options()
        # ユーザーに動きを見せたい場合は headless を False に、
        # 裏でこっそり動かしたい場合は True にします
        options.add_argument('--headless') 
        
        driver = None
        try:
            self.update_log("🌐 ブラウザを起動しています...")
            driver = webdriver.Chrome(options=options)
            
            self.update_log(f"🚚 サイトへアクセス中: {target_url}")
            driver.get(target_url)
            
            # ページ読み込みと遅延読み込み対策
            time.sleep(5)
            driver.execute_script("window.scrollTo(0, 800);")
            time.sleep(2)

            self.update_log("🔍 記事リンクを探索中...")
            links = driver.find_elements(By.TAG_NAME, "a")
            
            wb = openpyxl.Workbook()
            ws = wb.active
            ws.append(["取得日", "タイトル", "URL"])

            memo_urls = set()
            count = 0

            # ドメイン名を取得してフィルタリングに使用
            from urllib.parse import urlparse
            domain = urlparse(target_url).netloc

            for link in links:
                url = link.get_attribute("href")
                text = link.text.strip()
                
                # 自分のサイト内のリンクで、かつテキストがある程度長いものを抽出
                if url and text and domain in url and url not in memo_urls:
                    if len(text) > 10: # 短すぎるリンク(「ホーム」など)を除外
                        clean_title = text.split('\n')[0]
                        ws.append([datetime.now().strftime('%Y-%m-%d'), clean_title, url])
                        memo_urls.add(url)
                        count += 1
                        
                        if count % 5 == 0: # 5件ごとにログを更新
                            self.update_log(f"📥 {count}件目のデータを保持...")

            # 保存
            filename = f"Sel_Report_{datetime.now().strftime('%Y%m%d_%H%M')}.xlsx"
            wb.save(filename)
            
            self.update_log(f"✅ 調査完了!合計 {count} 件保存しました。")
            self.show_finish_message(filename)

        except Exception as e:
            self.update_log(f"❌ Seleniumエラー: {str(e)}")
        
        finally:
            if driver:
                driver.quit()
                self.update_log("🔌 ブラウザを正常に終了しました。")

if __name__ == "__main__":
    app = NinjinScraperApp()
    app.mainloop()

ロジックの心臓部

入力されたURLは、選ばれたモードに応じてそれぞれのロジックへリレーされます。

# モードに応じて呼び出す関数をバトンタッチ
if mode == "WordPress API":
    self.run_api_logic(url)
else:
    self.run_selenium_logic(url)
  • API版:requests でJSONを取得し、辞書型データからタイトルやURLを抜き出します。
  • Selenium版:driver.find_elements でページ内のリンク(aタグ)を全走査し、ドメイン一致などでフィルタします。

どちらのモードでも、最終的には使い慣れたExcelファイル(.xlsx)として保存されます。


スポンサーリンク

なぜ「ECサイトの価格調査」を断念したのか

当初の目的だった「ECサイトの価格調査」の実装は、最終的に見送りました。理由はシンプルで、「やろうと思えばできるが、やるべきではない」と判断したためです。

  • メンテナンスの限界:大手サイトは構造変更が激しく、昨日のコードが今日動かなくなる「いたちごっこ」になります。個人での運用は現実的ではありません。
  • 規約と負荷:無理なスクレイピングで負荷をかけるより、公式API(AmazonならPA-API、楽天なら楽天ウェブサービスなど)を申請するのが、長期的には最も安全で確実です。

スポンサーリンク

結び:まずは「体験」してみよう

この開発記でたどり着いた答えは、次の3つです。

  • 正面突破(スクレイピング)がダメなら、裏口(API)を探す
  • 相手のサイトを尊重し、最適な技術を選ぶ
  • 「できること」と「やるべきこと」を切り分ける

まずは自分のブログや気になるサイトで、/wp-json/wp/v2/posts を試してみてください。「スクレイピングしなくても取れる世界」を体験できるはずです。

今回の教訓:正式な窓口(API)があるならそれを使う。なければSeleniumで丁寧に。技術の使い分けこそが自動化の鍵。

前の記事

🕸️ 前編 Pythonスクレイピングで取得件数が0件になる原因と対策|robots.txt・待機時間の作法とRequests/Selenium RequestsとSeleniumで挑んで、どちらも0件だった理由とスクレイピングの作法。 →

スポンサーリンク

よくある質問(FAQ)

Q/wp-json/wp/v2/posts では何件取得できますか?

Aデフォルトは10件です。?per_page=100 を付けると最大100件、?page=2 で次の100件、というようにページ送りで取得します。

QどのサイトでもREST APIが使えますか?

AWordPressサイトなら基本的に有効です。ただし、セキュリティプラグインなどでAPIを無効化・制限しているサイトもあります。403や404が返る場合は閉じられています。

QAPIとスクレイピングでは、どちらが「行儀がよい」ですか?

AAPIです。整形済みのデータを、想定された窓口から取得するので、サーバーへの負荷も、サイト構造変更の影響も小さくなります。

Qタイトルが {'rendered': '...'} という形で返るのはなぜですか?

AWordPressはタイトルを「編集用の生データ」と「表示用にHTMLエスケープ済みのデータ」に分けて返します。画面表示に使うのは rendered の方です。

QAmazonや楽天の価格もAPIで取得できますか?

A公式のPA-API(Amazon)や楽天ウェブサービスを申請すれば取得できます。規約の範囲で使え、構造変更にも強いので、無理にスクレイピングするより確実です。

QGUIのログが処理中に固まらないのはなぜですか?

Athreading で、時間のかかるスクレイピング処理を別スレッドに逃がしているためです。メインスレッド(画面の描画)は止まらないので、ログがリアルタイムに流れます。


ニンジン🥕アイコン

Follow me on X

本ブログの中の人「ニンジン🥕」はXやってます。新着記事はいち早くポストでお知らせしているので、見逃したくない方は @ninjin_py_vba をフォローしてみてください🥕

@ninjin_py_vba をフォロー

この記事が役に立ったら、シェアしてもらえると嬉しいです🥕

Xでポスト はてブ LINE Pocket

PROJECT LOG — SERIES INDEX
スポンサーリンク

シリーズ記事一覧

電子工作・自動化ツールの製作記録をシリーズごとにまとめています。気になるテーマからお読みください。


GPSロガーのCSVを地図に表示する方法|Pico 2 WでGT-502MGG-NとATGM336Hを車に積んで走行・トンネル比較【Python・folium】
GPSロガーで記録したCSVを、Pythonのfoliumと地理院タイルで地図に表示する方法。GT-502MGG-NとATGM336Hを車に積んで4回走り、再起動の原因やトンネルでの違いも比べました。
今週の注目ガジェット〜10/19週〜|電子工作・自作向け新製品まとめ(Romu/W1700K SDR ほか)
大手が拾わないニッチな新製品を毎週チェック。今週はe-Paper開発キット、超小型USBボード、自作SDRなど5本立て。
GT-502MGG-NとATGM336Hの使い方|Raspberry Pi Pico 2 WとMicroPythonでGPSを受信して2台を比較
GT-502MGG-NとATGM336Hの使い方を、Pico 2 W×MicroPythonで解説。北斗で衛星数が多いATGM336H、信号が強いGT-502MGG-N。単精度floatで緯度経度が約1.4m刻みになる落とし穴も紹介します。
今週の注目ガジェット〜10/12週〜|電子工作・自作向け新製品まとめ(Q8botOne/Sipeed SLogic32U3 ほか)
大手が拾わないニッチな新製品を毎週チェック。今週は四足歩行ロボット、ロジックアナライザー、広告ブロッカーなど5本立て。
MOSFETはPicoの3.3Vで足りる?DCソレノイド駆動でゲート電圧不足を実測【Pico 2 W】
Pico 2 WとMOSFET(IRLU3410)で12VのDCソレノイドを駆動。ゲート3.3Vでは発振し、オン抵抗も約2.4倍に。レベル変換で5V駆動にした比較と、電流が8割止まりだった本当の原因を実測で紹介します。
まとめハブ
Python
スポンサーリンク

コメント