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スクレイピング・自動化ツールの開発についてはお気軽にご相談ください。