はじめに
この記事は 株式会社システムアイのAdvent Calendar 2023 の 14日目の記事です。
日々の業務効率化のためにWebスクレイピングを活用しようと思い、本記事を執筆することにしました。
Webスクレイピングを知らないメンバーがいたのでチーム内の共有も兼ねて。
環境について
Windowsの方は必要に応じて読み替えてください。
- mac OS 14.1.1
- Node.js v18.18.0
- npm 10.2.5
説明
ウェブスクレイピング(英: Web scraping)とは、ウェブサイトから情報を抽出するコンピュータソフトウェア技術のこと。通常このようなソフトウェアプログラムは低レベルのHTTPを実装することで、もしくはウェブブラウザを埋め込むことによって、WWWのコンテンツを取得する。ウェブスクレイピングはユーザーが手動で行なうこともできるが、一般的にはボットやクローラ(英: Web crawler)を利用した自動化プロセスを指す。
今回はPuppeteerというGoogle製のライブラリを使用し、Webスクレイピングを行っていきたいと思います。
https://pptr.dev/
2023/12/14時点の最新版(21.6.0)を使用します。
# インストール
npm i puppeteer@21.6.0
ヘッドレスChromeを操作するためのライブラリなので、用途としては自動化やテストかと思いますが、スクレイピングにも利用できます。
使い方
基本
pupeteerは非同期で動作するので async/await が必要です。
以下でgoogleを開きます。
const puppeteer = require('puppeteer');
(async () => {
const url = 'https://google.co.jp';
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto(url);
})()
起動時にオプションを設定できます。
詳細は https://pptr.dev/api/puppeteer.puppeteernode.launch
const browser = await puppeteer.launch({
defaultViewport:{
width:375,
height:667
}
});
毎回以下の警告が出るのが邪魔なので
Puppeteer old Headless deprecation warning:
In the near future `headless: true` will default to the new Headless mode
for Chrome instead of the old Headless implementation. For more
information, please see https://developer.chrome.com/articles/new-headless/.
Consider opting in early by passing `headless: "new"` to `puppeteer.launch()`
If you encounter any bugs, please report them to https://github.com/puppeteer/puppeteer/issues/new/choose.
こうします。
const browser = await puppeteer.launch({headless: "new"});
{headless: false}
にすると実行時にブラウザが開きます。
ページの情報を取得する
今開いているURLを取得する。
console.log(page.url());
// https://www.google.co.jp/
全体のHTMLを取得する。
let content = await page.content();
console.log(content);
// <!DOCTYPE html><html itemscope="" itemtype="http://schema.org/WebPage" lang="ja"><head><meta charset="UTF-8">...
セレクタを使って要素の属性を取得する。
// document.querySelector('a') がページ内で実行される
let a = await page.$eval('a', el => el.innerHTML);
let href = await page.$eval('a', el => el.href);
// $$ は document.querySelectorAll('a') が実行される
let b = await page.$$eval('a', nodes => nodes.map(el => el.innerHTML));
console.log(a);
console.log(href);
console.log(b);
/*
Googleについて
https://about.google/?fg=1
[
'Googleについて',
'ストア',
'Gmail',
'画像', ...
*/
スクショを撮る
オプションは https://pptr.dev/api/puppeteer.screenshotoptions
デフォルトのviewportは800x600なので、解像度も800x600になります。
await page.screenshot({ path: 'ss.png' })
操作する
await Promise.all([
page.waitForNavigation(), // 読み込みが完了するまで待ちます
page.click('a'),
]);
console.log(page.url()); // 遷移後のURLを表示
// https://about.google/?fg=1
入力する
const elementHandle = await page.$('textarea');
await elementHandle.type('Yahoo');
await Promise.all([
page.waitForNavigation(), // 読み込みが完了するまで待ちます
elementHandle.press('Enter')
]);
console.log(page.url()); // 遷移後のURLを表示
// https://www.google.co.jp/search?q=Yahoo
実際に試してみる
リンク先のURLが存在するか確認する
const puppeteer = require('puppeteer');
(async () => {
const url = "https://google.co.jp";
const browser = await puppeteer.launch({headless: "new"});
const page = await browser.newPage();
await page.goto(url);
const href = await page.$eval('a', el => el.href);
const new_page = await browser.newPage(); // 新しくページを開く
const res = await new_page.goto(href);
console.log(`${res.status()} ${res.url()}`);
await new_page.close(); // 終わったら閉じる
})()
// 200 https://about.google/?fg=1
テーブルの情報をオブジェクトにして変換する
tabletojsonを使ってtableをオブジェクトに変換し、二つのサイトのテーブルを比較してみます。
Puppeteerなくてもできる
# インストール
npm i tabletojson
// 使い方
const { tabletojson } = require('tabletojson');
// URL から取得し変換する(今回は使わない)
tabletojson.convertUrl(url)
.then(function(tablesAsJson) {
let firstTable = tablesAsJson[0];
}
// htmlから変換する
let tablesAsJson = tabletojson.convertUrl(html);
let firstTable = tableAsJson[0];
比較するサイトは
const url_a = "https://npb.jp/scores/2023/1105/b-t-07/index.html";
const url_b = "https://baseball.yahoo.co.jp/npb/game/2021014380/top";
のスコアボードの点数が一致するかチェックします。
const puppeteer = require('puppeteer');
const { tabletojson } = require('tabletojson');
(async () => {
const browser = await puppeteer.launch({headless: "new"});
const page = await browser.newPage();
await page.goto(url_a);
const table_a = await page.$eval("#tablefix_ls",el => el.outerHTML);
await page.goto(url_b);
const table_b = await page.$eval("#ing_brd",el => el.outerHTML);
let data_a = tabletojson.convert(table_a,{useFirstRowForHeadings: true})[0];
let data_b = tabletojson.convert(table_b)[0];
npbの方のテーブルは先頭列に見出しが含まれているため、useFirstRowForHeadings: true
を設定し、他の列と同じように処理してあげます。
設定しないと↓みたいなのが返ってきます。
console.table(data_a)
直してあげるとこうなります
yahooの方のデータはこうなっています
チームの見出しがあっていることを確認した上で、点数が一致するか力技で順番に確認します。
一部列名の表記が漢字とアルファベットで異なるので、置き換えています。
const column_replace = {'H':'安','E':'失'} // 列名置換用
for (const a of data_a) {
// チーム名が前方一致する行で確認する
let b = data_b.find( b => a['0']?.startsWith(b['0']));
if(!b){
continue;
}
console.log(`${a['0']} ${b['0']}`)
for(const i in a){
if(i != 0){
let j = i;
// 列名が一致しないので変換する
if(i == 'H' || i == 'E'){
j = column_replace[i];
}
if(b[j] == a[i]){
console.log(`${i} ${b[j]} ${a[i]} 一致しました`)
}else{
console.log(`${i} ${b[j]} ${a[i]} 一致しません`)
}
}
}
}
})()
/*
阪神タイガース阪神 阪神
1 0 0 一致しました
2 0 0 一致しました
3 0 0 一致しました
4 3 3 一致しました
5 3 3 一致しました
6 0 0 一致しました
7 0 0 一致しました
8 0 0 一致しました
9 1 1 一致しました
計 7 7 一致しました
H 12 12 一致しました
E 0 0 一致しました
オリックス・バファローズオリックス オリックス
1 0 0 一致しました
2 0 0 一致しました
3 0 0 一致しました
4 0 0 一致しました
5 0 0 一致しました
6 0 0 一致しました
7 0 0 一致しました
8 0 0 一致しました
9 1 1 一致しました
計 1 1 一致しました
H 8 8 一致しました
E 1 1 一致しました
*/
今回のような数値の場合は簡単に比較ができますね。
最後に
簡単なものしか載せていませんが、役に立てれば幸いです。
最後までご覧いただきありがとうございました。