株式会社WR

株式会社WR

WEB TOTAL CONSULTING

PuppeteerのXPathセレクタ活用——CSSセレクタでは難しい要素を取得する
ブログ一覧へ
技術ブログ

PuppeteerのXPathセレクタ活用——CSSセレクタでは難しい要素を取得する

PuppeteerでCSSセレクタが使えない複雑なHTML構造や、テキスト内容で要素を絞り込む場合はXPathが有効です。よく使うXPath式と実装パターンを紹介します。

CSSセレクタの限界とXPathの強み

Puppeteerでは通常 page.$('.class')page.$('#id') などのCSSセレクタを使いますが、以下のような場面でCSSセレクタでは対応が難しくなります。

  • 「テキストが〜を含む要素」を選択する
  • 親要素・兄弟要素を辿る
  • 要素のインデックスで指定する(nth系が使えない場合)
  • 名前空間を含むXML/HTMLを扱う

XPath(XML Path Language)はこれらを柔軟に扱えます。


XPathの基本構文

/ ← ルートから絶対パスで指定
// ← どの位置からでも検索
. ← 現在のノード
.. ← 親ノード
@ ← 属性
//div[@class="product-name"]       ← class="product-name" のdiv
//a[contains(@href, "/products/")]  ← hrefに"/products/"を含むa
//h2[text()="特集商品"]             ← テキストが一致するh2
//li[position()=1]                  ← 最初のli
//div[@id="main"]//p               ← id="main"のdiv内の全p

PuppeteerでXPathを使う

const puppeteer = require('puppeteer');

async function scrapeWithXPath(url) {
    const browser = await puppeteer.launch({ headless: true });
    const page    = await browser.newPage();

    await page.setUserAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36');
    await page.goto(url, { waitUntil: 'networkidle2' });

    // XPathで要素を取得
    // 方法1:page.$x()(配列で返る)
    const titleElements = await page.$x('//h1[@class="product-title"]');
    const title = await page.evaluate(el => el.textContent.trim(), titleElements[0]);
    console.log('タイトル:', title);

    // 方法2:page.evaluate() 内で document.evaluate() を使う
    const prices = await page.evaluate(() => {
        const xpath    = '//span[contains(@class, "price") and not(contains(@class, "tax"))]';
        const result   = document.evaluate(
            xpath,
            document,
            null,
            XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
            null
        );

        const items = [];
        for (let i = 0; i < result.snapshotLength; i++) {
            items.push(result.snapshotItem(i).textContent.trim());
        }
        return items;
    });

    console.log('価格一覧:', prices);
    await browser.close();
}

テキストで要素を特定する

「カゴに入れる」ボタンをテキストで特定する実例:

// CSSセレクタ:ボタンのテキストでは選択できない
// page.$('button') ← 全ボタンが対象になってしまう

// XPath:ボタンのテキストで絞り込む
async function clickButtonByText(page, text) {
    const buttons = await page.$x(`//button[contains(text(), "${text}")]`);

    if (buttons.length === 0) {
        throw new Error(`ボタン「${text}」が見つかりません`);
    }

    await buttons[0].click();
    await page.waitForNavigation({ waitUntil: 'networkidle2' });
}

// 使い方
await clickButtonByText(page, 'カゴに入れる');
await clickButtonByText(page, 'レジに進む');

親要素・兄弟要素を取得する

// 「価格」というラベルの次の兄弟要素の値を取得
const prices = await page.evaluate(() => {
    const labelXPath = '//th[text()="価格"]/following-sibling::td';
    const result = document.evaluate(labelXPath, document, null,
        XPathResult.FIRST_ORDERED_NODE_TYPE, null);
    return result.singleNodeValue?.textContent.trim();
});

// 特定のdiv内の最初と最後のリスト項目
const firstAndLast = await page.evaluate(() => {
    const firstXPath = '//div[@id="menu"]//li[1]';
    const lastXPath  = '//div[@id="menu"]//li[last()]';

    const first = document.evaluate(firstXPath, document, null,
        XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
    const last  = document.evaluate(lastXPath,  document, null,
        XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;

    return {
        first: first?.textContent.trim(),
        last:  last?.textContent.trim(),
    };
});

動的に変わるクラス名への対応

SPAのフレームワーク(Next.js・Vue.jsなど)ではビルドごとにクラス名が変わることがあります。XPathのテキストや属性の一部一致で安定した選択ができます。

// NG:動的クラス(ビルドごとに変わる可能性)
page.$('.ProductPrice__price--abcd1234')

// OK:contains() で部分一致
page.$x('//span[contains(@class, "ProductPrice__price")]')

// さらに安定した方法:aria-labelで選択
page.$x('//*[@aria-label="商品価格"]')
page.$x('//input[@name="quantity"]')

XPathヘルパー関数

/**
 * XPathで単一要素のテキストを取得
 */
async function getTextByXPath(page, xpath, defaultValue = '') {
    const elements = await page.$x(xpath);
    if (elements.length === 0) return defaultValue;

    return page.evaluate(el => el.textContent.trim(), elements[0]);
}

/**
 * XPathで複数要素のテキストを配列で取得
 */
async function getAllTextByXPath(page, xpath) {
    const elements = await page.$x(xpath);
    return Promise.all(
        elements.map(el => page.evaluate(e => e.textContent.trim(), el))
    );
}

// 使い方
const productName   = await getTextByXPath(page, '//h1[@class="product-name"]');
const reviewTexts   = await getAllTextByXPath(page, '//div[@class="review-text"]');

まとめ

XPathはCSSセレクタでは難しい「テキストによる検索」「親・兄弟要素の取得」「動的クラス名への対応」を可能にします。Puppeteerでの本格的なスクレイピングにはXPathの知識が不可欠です。弊社では複雑な構造のECサイトやSPAのスクレイピングにXPathを活用しています。

Webスクレイピング・自動化ツールの開発についてはお気軽にご相談ください。

Category 技術ブログ

Related Posts

関連記事

開発・技術のご相談はお気軽に

お見積りは無料です。まずはお気軽にご相談ください。

お問い合わせ →