LoginSignup
74
22

今更Webスクレイピングしてみる(Node.js + Puppeteer)

Last updated at Posted at 2023-12-13

はじめに

この記事は 株式会社システムアイの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を開きます。

index.js
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' })

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が存在するか確認する

index.js
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)
image.png
直してあげるとこうなります
image.png
yahooの方のデータはこうなっています
image.png

チームの見出しがあっていることを確認した上で、点数が一致するか力技で順番に確認します。
一部列名の表記が漢字とアルファベットで異なるので、置き換えています。

    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 一致しました
*/

今回のような数値の場合は簡単に比較ができますね。

最後に

簡単なものしか載せていませんが、役に立てれば幸いです。
最後までご覧いただきありがとうございました。

74
22
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
74
22