Node.jsとPuppeteerを使用してWebサイトをスクレイプする方法

提供:Dev Guides
移動先:案内検索

著者は、 Write for DOnations プログラムの一環として、 Free and Open SourceFundを選択して寄付を受け取りました。

序章

Webスクレイピングは、Webからのデータ収集を自動化するプロセスです。 このプロセスは通常、Webを自動的にサーフィンし、選択したページからデータを取得する「クローラー」をデプロイします。 データをスクレイピングする理由はたくさんあります。 主に、手動のデータ収集プロセスを排除することにより、データ収集をはるかに高速化します。 スクレイピングは、データ収集が必要または必要であるが、WebサイトがAPIを提供していない場合の解決策でもあります。

このチュートリアルでは、Node.jsPuppeteerを使用してWebスクレイピングアプリケーションを構築します。 進行するにつれて、アプリは複雑になります。 まず、 Chromium を開き、ウェブスクレイピングサンドボックスとして設計された特別なウェブサイトbooks.toscrape.comを読み込むようにアプリをコーディングします。 次の2つのステップでは、books.toscrapeの1ページにあるすべての本をスクレイプしてから、複数のページにまたがるすべての本をスクレイプします。 残りの手順では、本のカテゴリでスクレイピングをフィルタリングしてから、データをJSONファイルとして保存します。

警告:ウェブスクレイピングの倫理と合法性は非常に複雑で、常に進化しています。 また、場所、データの場所、および問題のWebサイトによっても異なります。 このチュートリアルでは、スクレーパーアプリケーションをテストするために特別に設計された特別なWebサイトbooks.toscrape.comをスクレイピングします。 他のドメインをスクレイピングすることは、このチュートリアルの範囲外です。


前提条件

ステップ1—Webスクレイパーを設定する

Node.jsをインストールすると、Webスクレイパーのセットアップを開始できます。 まず、プロジェクトのルートディレクトリを作成してから、必要な依存関係をインストールします。 このチュートリアルに必要な依存関係は1つだけで、Node.jsのデフォルトのパッケージマネージャーnpmを使用してインストールします。 npmにはNode.jsがプリインストールされているため、インストールする必要はありません。

このプロジェクトのフォルダーを作成してから、次の場所に移動します。

mkdir book-scraper
cd book-scraper

以降のすべてのコマンドは、このディレクトリから実行します。

npmまたはノードパッケージマネージャーを使用して1つのパッケージをインストールする必要があります。 プロジェクトの依存関係とメタデータを管理するpackages.jsonファイルを作成するために、最初にnpmを初期化します。

プロジェクトのnpmを初期化します。

npm init

npmは一連のプロンプトを表示します。 すべてのプロンプトでENTERを押すか、パーソナライズされた説明を追加できます。 必ずENTERを押し、entry point:およびtest command:の入力を求められたら、デフォルト値のままにしておきます。 または、yフラグをnpmnpm init -yに渡すと、すべてのデフォルト値が送信されます。

出力は次のようになります。

Output{
  "name": "sammy_scraper",
  "version": "1.0.0",
  "description": "a web scraper",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "sammy the shark",
  "license": "ISC"
}


Is this OK? (yes) yes

yesと入力し、ENTERを押します。 npmは、この出力をpackage.jsonファイルとして保存します。

次に、npmを使用してPuppeteerをインストールします。

npm install --save puppeteer

このコマンドは、Puppeteerと、PuppeteerチームがAPIで動作することがわかっているバージョンのChromiumの両方をインストールします。

Linuxマシンでは、Puppeteerにいくつかの追加の依存関係が必要になる場合があります。

Ubuntu 18.04を使用している場合は、Puppeteerのトラブルシューティングドキュメントの「ChromeヘッドレスがUNIXで起動しない」セクション内の「Debian依存関係」ドロップダウンを確認してください。 次のコマンドを使用して、不足している依存関係を見つけることができます。

ldd chrome | grep not

npm、Puppeteer、およびその他の依存関係がインストールされている場合、コーディングを開始する前に、package.jsonファイルに最後の構成が1つ必要です。 このチュートリアルでは、npm run startを使用してコマンドラインからアプリを起動します。 このstartスクリプトに関する情報をpackage.jsonに追加する必要があります。 具体的には、startコマンドに関して、scriptsディレクティブの下に1行追加する必要があります。

お好みのテキストエディタでファイルを開きます。

nano package.json

scripts:セクションを見つけて、次の構成を追加します。 testスクリプト行の最後にコンマを置くことを忘れないでください。そうしないと、ファイルが正しく解析されません。

Output{
  . . .
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node index.js"
  },
  . . .
  "dependencies": {
    "puppeteer": "^5.2.1"
  }
}

また、ファイルの終わり近くのdependenciesの下にpuppeteerが表示されることにも気付くでしょう。 package.jsonファイルにこれ以上のリビジョンは必要ありません。 変更を保存して、エディターを閉じます。

これで、スクレーパーのコーディングを開始する準備が整いました。 次のステップでは、ブラウザインスタンスを設定し、スクレーパーの基本機能をテストします。

ステップ2—ブラウザインスタンスの設定

従来のブラウザを開くと、ボタンをクリックしたり、マウスでナビゲートしたり、入力したり、開発ツールを開いたりすることができます。 Chromiumのようなヘッドレスブラウザを使用すると、これらと同じことができますが、プログラムによって、ユーザーインターフェイスなしで実行できます。 このステップでは、スクレーパーのブラウザーインスタンスを設定します。 アプリケーションを起動すると、Chromiumが自動的に開き、books.toscrape.comに移動します。 これらの最初のアクションは、プログラムの基礎を形成します。

Webスクレイパーには、browser.jsindex,jspageController.js、およびpageScraper.jsの4つの.jsファイルが必要です。 このステップでは、4つのファイルすべてを作成し、プログラムが高度化するにつれてそれらを継続的に更新します。 browser.jsから始めます。 このファイルには、ブラウザを起動するスクリプトが含まれています。

プロジェクトのルートディレクトリから、テキストエディタでbrowser.jsを作成して開きます。

nano browser.js

まず、require Puppeteerを作成してから、startBrowser()というasync関数を作成します。 この関数はブラウザを起動し、そのインスタンスを返します。 次のコードを追加します。

./book-scraper/browser.js

const puppeteer = require('puppeteer');

async function startBrowser(){
    let browser;
    try {
        console.log("Opening the browser......");
        browser = await puppeteer.launch({
            headless: false,
            args: ["--disable-setuid-sandbox"],
            'ignoreHTTPSErrors': true
        });
    } catch (err) {
        console.log("Could not create a browser instance => : ", err);
    }
    return browser;
}

module.exports = {
    startBrowser
};

Puppeteerには、ブラウザのインスタンスを起動する.launch()メソッドがあります。 このメソッドはPromiseを返すため、.thenまたはawaitブロックを使用してPromiseが解決されることを確認する必要があります。

awaitを使用して、Promiseが解決されることを確認し、このインスタンスを try-catchコードブロックでラップしてから、ブラウザーのインスタンスを返します。

.launch()メソッドは、いくつかの値を持つJSONパラメーターを受け取ることに注意してください。

  • ヘッドレス-falseは、ブラウザがインターフェイスで実行されるため、スクリプトの実行を監視できることを意味し、trueは、ブラウザがヘッドレスモードで実行されることを意味します。 ただし、スクレーパーをクラウドにデプロイする場合は、headlesstrueに戻すことに注意してください。 ほとんどの仮想マシンはヘッドレスであり、ユーザーインターフェイスが含まれていないため、ブラウザはヘッドレスモードでのみ実行できます。 Puppeteerにはheadfulモードも含まれていますが、これはテスト目的でのみ使用する必要があります。
  • ignoreHTTPSErrors -trueを使用すると、安全なHTTPSプロトコルでホストされていないWebサイトにアクセスし、HTTPS関連のエラーを無視できます。

ファイルを保存して閉じます。

次に、2番目の.jsファイルindex.jsを作成します。

nano index.js

ここでは、requirebrowser.jspageController.jsになります。 次に、startBrowser()関数を呼び出し、作成されたブラウザインスタンスをページコントローラに渡します。ページコントローラはそのアクションを指示します。 次のコードを追加します。

./book-scraper/index.js

const browserObject = require('./browser');
const scraperController = require('./pageController');

//Start the browser and create a browser instance
let browserInstance = browserObject.startBrowser();

// Pass the browser instance to the scraper controller
scraperController(browserInstance)

ファイルを保存して閉じます。

3番目の.jsファイルpageController.jsを作成します。

nano pageController.js

pageController.jsは、スクレイピングプロセスを制御します。 ブラウザインスタンスを使用して、すべてのスクレイピングスクリプトが実行されるpageScraper.jsファイルを制御します。 最終的には、これを使用して、スクレイプする本のカテゴリを指定します。 ただし、今のところ、Chromiumを開いてWebページに移動できることを確認する必要があります。

./book-scraper/pageController.js

const pageScraper = require('./pageScraper');
async function scrapeAll(browserInstance){
    let browser;
    try{
        browser = await browserInstance;
        await pageScraper.scraper(browser);  
        
    }
    catch(err){
        console.log("Could not resolve the browser instance => ", err);
    }
}

module.exports = (browserInstance) => scrapeAll(browserInstance)

このコードは、ブラウザインスタンスを取り込んで、それをscrapeAll()という関数に渡す関数をエクスポートします。 次に、この関数は、このインスタンスを引数としてpageScraper.scraper()に渡し、これを使用してページをスクレイプします。

ファイルを保存して閉じます。

最後に、最後の.jsファイルpageScraper.jsを作成します。

nano pageScraper.js

ここでは、urlプロパティとscraper()メソッドを使用してオブジェクトリテラルを作成します。 urlはスクレイピングするWebページのWebURLであり、scraper()メソッドには実際のスクレイピングを実行するコードが含まれていますが、この段階ではURLに移動するだけです。 次のコードを追加します。

./book-scraper/pageScraper.js

const scraperObject = {
    url: 'http://books.toscrape.com',
    async scraper(browser){
        let page = await browser.newPage();
        console.log(`Navigating to ${this.url}...`);
        await page.goto(this.url);
        
    }
}

module.exports = scraperObject;

Puppeteerには、ブラウザに新しいページインスタンスを作成するnewPage()メソッドがあり、これらのページインスタンスはかなりの数のことを実行できます。 scraper()メソッドでは、ページインスタンスを作成し、 page.goto()メソッドを使用してbooks.toscrape.comホームページに移動しました。

ファイルを保存して閉じます。

これで、プログラムのファイル構造が完成しました。 プロジェクトのディレクトリツリーの最初のレベルは次のようになります。

Output.
├── browser.js
├── index.js
├── node_modules
├── package-lock.json
├── package.json
├── pageController.js
└── pageScraper.js

次に、コマンドnpm run startを実行し、スクレーパーアプリケーションが実行されるのを確認します。

npm run start

Chromiumブラウザインスタンスが自動的に開き、ブラウザで新しいページが開き、books.toscrape.comに移動します。

このステップでは、Chromiumを開き、ダミーのオンライン書店books.toscrape.comのホームページをロードするPuppeteerアプリケーションを作成しました。 次のステップでは、そのホームページ上のすべての本のデータをスクレイピングします。

ステップ3—単一のページからデータをスクレイピングする

スクレーパーアプリケーションに機能を追加する前に、お好みのWebブラウザーを開き、ブックに手動で移動してホームページをスクレイプします。 サイトを閲覧して、データがどのように構造化されているかを理解してください。

左側にカテゴリセクションがあり、右側に本が表示されています。 書籍をクリックすると、ブラウザはその特定の書籍に関連する情報を表示する新しいURLに移動します。

このステップでは、この動作を複製しますが、コードを使用します。 あなたはウェブサイトをナビゲートしてそのデータを消費するビジネスを自動化します。

まず、ブラウザ内のDev Toolsを使用してホームページのソースコードを調べると、ページにsectionタグの下に各書籍のデータがリストされていることがわかります。 sectionタグ内では、すべての本がlistli)タグの下にあり、ここに本の専用ページ、価格、および在庫あり。

これらの本のURLをスクレイピングし、在庫のある本をフィルタリングし、個々の本のページに移動して、その本のデータをスクレイピングします。

pageScraper.jsファイルを再度開きます。

nano pageScraper.js

次の強調表示されたコンテンツを追加します。 await page.goto(this.url);内に別のawaitブロックをネストします。

./book-scraper/pageScraper.js

const scraperObject = {
    url: 'http://books.toscrape.com',
    async scraper(browser){
        let page = await browser.newPage();
        console.log(`Navigating to ${this.url}...`);
        // Navigate to the selected page
        await page.goto(this.url);
        // Wait for the required DOM to be rendered
        await page.waitForSelector('.page_inner');
        // Get the link to all the required books
        let urls = await page.$$eval('section ol > li', links => {
            // Make sure the book to be scraped is in stock
            links = links.filter(link => link.querySelector('.instock.availability > i').textContent !== "In stock")
            // Extract the links from the data
            links = links.map(el => el.querySelector('h3 > a').href)
            return links;
        });
        console.log(urls);
    }
}

module.exports = scraperObject;

このコードブロックでは、 page.waitForSelector()メソッドを呼び出しました。 これは、すべての本関連情報を含むdivがDOMにレンダリングされるのを待ってから、ページを呼び出しました。$$ eval()メソッド。 このメソッドは、セレクターsection ol liを使用してURL要素を取得します(page.$eval()およびpage.$$eval()メソッドからは常に文字列または数値のみを返すようにしてください)。

すべての本には2つのステータスがあります。 本はIn StockまたはOut of stockのいずれかです。 In Stockの本だけをこすりたい。 page.$$eval()は一致するすべての要素の配列を返すため、この配列をフィルター処理して、在庫のある本のみを操作するようにしました。 これを行うには、クラス.instock.availabilityを検索して評価します。 次に、本のリンクのhrefプロパティをマップし、メソッドから返しました。

ファイルを保存して閉じます。

アプリケーションを再実行します。

npm run start

ブラウザが開き、Webページに移動し、タスクが完了すると閉じます。 次に、コンソールを確認します。 削り取られたすべてのURLが含まれます。

Output> [email protected] start /Users/sammy/book-scraper
> node index.js

Opening the browser......
Navigating to http://books.toscrape.com...
[
  'http://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html',
  'http://books.toscrape.com/catalogue/tipping-the-velvet_999/index.html',
  'http://books.toscrape.com/catalogue/soumission_998/index.html',
  'http://books.toscrape.com/catalogue/sharp-objects_997/index.html',
  'http://books.toscrape.com/catalogue/sapiens-a-brief-history-of-humankind_996/index.html',
  'http://books.toscrape.com/catalogue/the-requiem-red_995/index.html',
  'http://books.toscrape.com/catalogue/the-dirty-little-secrets-of-getting-your-dream-job_994/index.html',
  'http://books.toscrape.com/catalogue/the-coming-woman-a-novel-based-on-the-life-of-the-infamous-feminist-victoria-woodhull_993/index.html',
  'http://books.toscrape.com/catalogue/the-boys-in-the-boat-nine-americans-and-their-epic-quest-for-gold-at-the-1936-berlin-olympics_992/index.html',
  'http://books.toscrape.com/catalogue/the-black-maria_991/index.html',
  'http://books.toscrape.com/catalogue/starving-hearts-triangular-trade-trilogy-1_990/index.html',
  'http://books.toscrape.com/catalogue/shakespeares-sonnets_989/index.html',
  'http://books.toscrape.com/catalogue/set-me-free_988/index.html',
  'http://books.toscrape.com/catalogue/scott-pilgrims-precious-little-life-scott-pilgrim-1_987/index.html',
  'http://books.toscrape.com/catalogue/rip-it-up-and-start-again_986/index.html',
  'http://books.toscrape.com/catalogue/our-band-could-be-your-life-scenes-from-the-american-indie-underground-1981-1991_985/index.html',
  'http://books.toscrape.com/catalogue/olio_984/index.html',
  'http://books.toscrape.com/catalogue/mesaerion-the-best-science-fiction-stories-1800-1849_983/index.html',
  'http://books.toscrape.com/catalogue/libertarianism-for-beginners_982/index.html',
  'http://books.toscrape.com/catalogue/its-only-the-himalayas_981/index.html'
]

これは素晴らしいスタートですが、URLだけでなく、特定の本に関連するすべてのデータを取得する必要があります。 次に、これらのURLを使用して各ページを開き、書籍のタイトル、著者、価格、在庫状況、UPC、説明、画像のURLを取得します。

pageScraper.jsを再度開きます:

nano pageScraper.js

次のコードを追加します。このコードは、スクレイプされた各リンクをループし、新しいページインスタンスを開いてから、関連するデータを取得します。

./book-scraper/pageScraper.js

const scraperObject = {
    url: 'http://books.toscrape.com',
    async scraper(browser){
        let page = await browser.newPage();
        console.log(`Navigating to ${this.url}...`);
        // Navigate to the selected page
        await page.goto(this.url);
        // Wait for the required DOM to be rendered
        await page.waitForSelector('.page_inner');
        // Get the link to all the required books
        let urls = await page.$$eval('section ol > li', links => {
            // Make sure the book to be scraped is in stock
            links = links.filter(link => link.querySelector('.instock.availability > i').textContent !== "In stock")
            // Extract the links from the data
            links = links.map(el => el.querySelector('h3 > a').href)
            return links;
        });


        // Loop through each of those links, open a new page instance and get the relevant data from them
        let pagePromise = (link) => new Promise(async(resolve, reject) => {
            let dataObj = {};
            let newPage = await browser.newPage();
            await newPage.goto(link);
            dataObj['bookTitle'] = await newPage.$eval('.product_main > h1', text => text.textContent);
            dataObj['bookPrice'] = await newPage.$eval('.price_color', text => text.textContent);
            dataObj['noAvailable'] = await newPage.$eval('.instock.availability', text => {
                // Strip new line and tab spaces
                text = text.textContent.replace(/(\r\n\t|\n|\r|\t)/gm, "");
                // Get the number of stock available
                let regexp = /^.*\((.*)\).*$/i;
                let stockAvailable = regexp.exec(text)[1].split(' ')[0];
                return stockAvailable;
            });
            dataObj['imageUrl'] = await newPage.$eval('#product_gallery img', img => img.src);
            dataObj['bookDescription'] = await newPage.$eval('#product_description', div => div.nextSibling.nextSibling.textContent);
            dataObj['upc'] = await newPage.$eval('.table.table-striped > tbody > tr > td', table => table.textContent);
            resolve(dataObj);
            await newPage.close();
        });

        for(link in urls){
            let currentPageData = await pagePromise(urls[link]);
            // scrapedData.push(currentPageData);
            console.log(currentPageData);
        }

    }
}

module.exports = scraperObject; 

すべてのURLの配列があります。 この配列をループして、新しいページでURLを開き、そのページのデータを取得し、そのページを閉じて、配列内の次のURLの新しいページを開きます。 このコードをPromiseでラップしていることに注意してください。 これは、ループ内の各アクションが完了するのを待つことができるようにするためです。 したがって、各Promiseは新しいURLを開き、プログラムがURL上のすべてのデータをスクレイプし、そのページインスタンスが閉じるまで解決されません。

警告:for-inループを使用してPromiseを待機したことに注意してください。 他のループでも十分ですが、forEachなどの配列反復法、またはコールバック関数を使用する他の方法を使用してURL配列を反復処理することは避けてください。 これは、コールバック関数が最初にコールバックキューとイベントループを通過する必要があるためです。したがって、複数のページインスタンスが一度に開きます。 これはあなたの記憶にはるかに大きな負担をかけます。


pagePromise関数を詳しく見てください。 スクレーパーは最初にURLごとに新しいページを作成し、次にpage.$eval()関数を使用して、新しいページでスクレイプしたい関連する詳細のセレクターをターゲットにしました。 一部のテキストには、正規表現を使用して削除した空白、タブ、改行、およびその他の英数字以外の文字が含まれています。 次に、このページでスクレイピングされたすべてのデータの値をオブジェクトに追加し、そのオブジェクトを解決しました。

ファイルを保存して閉じます。

スクリプトを再度実行します。

npm run start

ブラウザはホームページを開き、次に各本のページを開き、それらの各ページからスクレイピングされたデータをログに記録します。 この出力はコンソールに出力されます。

OutputOpening the browser......
Navigating to http://books.toscrape.com...
{
  bookTitle: 'A Light in the Attic',
  bookPrice: '£51.77',
  noAvailable: '22',
  imageUrl: 'http://books.toscrape.com/media/cache/fe/72/fe72f0532301ec28892ae79a629a293c.jpg',
  bookDescription: "It's hard to imagine a world without A Light in the Attic. [...]',
  upc: 'a897fe39b1053632'
}
{
  bookTitle: 'Tipping the Velvet',
  bookPrice: '£53.74',
  noAvailable: '20',
  imageUrl: 'http://books.toscrape.com/media/cache/08/e9/08e94f3731d7d6b760dfbfbc02ca5c62.jpg',
  bookDescription: `"Erotic and absorbing...Written with starling power."--"The New York Times Book Review " Nan King, an oyster girl, is captivated by the music hall phenomenon Kitty Butler [...]`,
  upc: '90fa61229261140a'
}
{
  bookTitle: 'Soumission',
  bookPrice: '£50.10',
  noAvailable: '20',
  imageUrl: 'http://books.toscrape.com/media/cache/ee/cf/eecfe998905e455df12064dba399c075.jpg',
  bookDescription: 'Dans une France assez proche de la nôtre, [...]',
  upc: '6957f44c3847a760'
}
...

このステップでは、 books.toscrape.com のホームページにあるすべての本の関連データをスクレイプしましたが、さらに多くの機能を追加することができます。 たとえば、本の各ページはページ付けされています。 これらの他のページからどのように本を入手しますか? また、ウェブサイトの左側には、本のカテゴリがあります。 すべての本が必要ではなく、特定のジャンルの本だけが必要な場合はどうなりますか? 次に、これらの機能を追加します。

ステップ4—複数のページからデータをスクレイピングする

ページが表示されているbooks.toscrape.comのページには、コンテンツの下にnextボタンがありますが、ページが表示されていないページにはありません。

このボタンの存在を使用して、ページがページ分割されているかどうかを判断します。 各ページのデータは同じ構造であり、同じマークアップを持っているため、すべての可能なページにスクレーパーを作成することはありません。 むしろ、再帰のプラクティスを使用します。

まず、コードの構造を少し変更して、複数のページに再帰的に移動できるようにする必要があります。

pagescraper.jsを再度開きます:

nano pagescraper.js

scrapeCurrentPage()という新しい関数をscraper()メソッドに追加します。 この関数には、特定のページからデータを取得し、存在する場合は[次へ]ボタンをクリックするすべてのコードが含まれます。 次の強調表示されたコードを追加します。

./book-scraper/pageScraper.js scarer()

const scraperObject = {
    url: 'http://books.toscrape.com',
    async scraper(browser){
        let page = await browser.newPage();
        console.log(`Navigating to ${this.url}...`);
        // Navigate to the selected page
        await page.goto(this.url);
        let scrapedData = [];
        // Wait for the required DOM to be rendered
        async function scrapeCurrentPage(){
            await page.waitForSelector('.page_inner');
            // Get the link to all the required books
            let urls = await page.$$eval('section ol > li', links => {
                // Make sure the book to be scraped is in stock
                links = links.filter(link => link.querySelector('.instock.availability > i').textContent !== "In stock")
                // Extract the links from the data
                links = links.map(el => el.querySelector('h3 > a').href)
                return links;
            });
            // Loop through each of those links, open a new page instance and get the relevant data from them
            let pagePromise = (link) => new Promise(async(resolve, reject) => {
                let dataObj = {};
                let newPage = await browser.newPage();
                await newPage.goto(link);
                dataObj['bookTitle'] = await newPage.$eval('.product_main > h1', text => text.textContent);
                dataObj['bookPrice'] = await newPage.$eval('.price_color', text => text.textContent);
                dataObj['noAvailable'] = await newPage.$eval('.instock.availability', text => {
                    // Strip new line and tab spaces
                    text = text.textContent.replace(/(\r\n\t|\n|\r|\t)/gm, "");
                    // Get the number of stock available
                    let regexp = /^.*\((.*)\).*$/i;
                    let stockAvailable = regexp.exec(text)[1].split(' ')[0];
                    return stockAvailable;
                });
                dataObj['imageUrl'] = await newPage.$eval('#product_gallery img', img => img.src);
                dataObj['bookDescription'] = await newPage.$eval('#product_description', div => div.nextSibling.nextSibling.textContent);
                dataObj['upc'] = await newPage.$eval('.table.table-striped > tbody > tr > td', table => table.textContent);
                resolve(dataObj);
                await newPage.close();
            });

            for(link in urls){
                let currentPageData = await pagePromise(urls[link]);
                scrapedData.push(currentPageData);
                // console.log(currentPageData);
            }
            // When all the data on this page is done, click the next button and start the scraping of the next page
            // You are going to check if this button exist first, so you know if there really is a next page.
            let nextButtonExist = false;
            try{
                const nextButton = await page.$eval('.next > a', a => a.textContent);
                nextButtonExist = true;
            }
            catch(err){
                nextButtonExist = false;
            }
            if(nextButtonExist){
                await page.click('.next > a');    
                return scrapeCurrentPage(); // Call this function recursively
            }
            await page.close();
            return scrapedData;
        }
        let data = await scrapeCurrentPage();
        console.log(data);
        return data;
    }
}

module.exports = scraperObject;

最初にnextButtonExist変数をfalseに設定してから、ボタンが存在するかどうかを確認します。 nextボタンがある場合は、nextButtonExiststrueに設定し、nextボタンをクリックして、この関数を再帰的に呼び出します。

nextButtonExistsがfalseの場合、通常どおりscrapedData配列を返します。

ファイルを保存して閉じます。

スクリプトを再度実行します。

npm run start

これは完了するまでに時間がかかる場合があります。 結局のところ、アプリケーションは800冊を超える本からデータをスクレイピングしています。 ブラウザを閉じるか、CTRL + Cを押してプロセスをキャンセルしてください。

これでスクレーパーの機能が最大化されましたが、その過程で新しい問題が発生しました。 ここで問題となるのは、データが少なすぎることではなく、データが多すぎることです。 次のステップでは、アプリケーションを微調整して、本のカテゴリでスクレイピングをフィルタリングします。

ステップ5—カテゴリ別のデータのスクレイピング

カテゴリ別にデータをスクレイピングするには、pageScraper.jsファイルとpageController.jsファイルの両方を変更する必要があります。

テキストエディタでpageController.jsを開きます。

nano pageController.js

旅行の本だけをこすり取るようにスクレーパーを呼び出します。 次のコードを追加します。

./book-scraper/pageController.js

const pageScraper = require('./pageScraper');
async function scrapeAll(browserInstance){
    let browser;
    try{
        browser = await browserInstance;
        let scrapedData = {};
        // Call the scraper for different set of books to be scraped
        scrapedData['Travel'] = await pageScraper.scraper(browser, 'Travel');
        await browser.close();
        console.log(scrapedData)
    }
    catch(err){
        console.log("Could not resolve the browser instance => ", err);
    }
}
module.exports = (browserInstance) => scrapeAll(browserInstance)

ここで、2つのパラメーターをpageScraper.scraper()メソッドに渡します。2番目のパラメーターは、スクレイプする本のカテゴリー(この例ではTravel)です。 ただし、pageScraper.jsファイルはまだこのパラメーターを認識していません。 このファイルも調整する必要があります。

ファイルを保存して閉じます。

pageScraper.jsを開きます:

nano pageScraper.js

次のコードを追加します。これにより、カテゴリパラメータが追加され、そのカテゴリページに移動して、ページ付けされた結果のスクレイピングを開始します。

./book-scraper/pageScraper.js

const scraperObject = {
    url: 'http://books.toscrape.com',
    async scraper(browser, category){
        let page = await browser.newPage();
        console.log(`Navigating to ${this.url}...`);
        // Navigate to the selected page
        await page.goto(this.url);
        // Select the category of book to be displayed
        let selectedCategory = await page.$$eval('.side_categories > ul > li > ul > li > a', (links, _category) => {

            // Search for the element that has the matching text
            links = links.map(a => a.textContent.replace(/(\r\n\t|\n|\r|\t|^\s|\s$|\B\s|\s\B)/gm, "") === _category ? a : null);
            let link = links.filter(tx => tx !== null)[0];
            return link.href;
        }, category);
        // Navigate to the selected category
        await page.goto(selectedCategory);
        let scrapedData = [];
        // Wait for the required DOM to be rendered
        async function scrapeCurrentPage(){
            await page.waitForSelector('.page_inner');
            // Get the link to all the required books
            let urls = await page.$$eval('section ol > li', links => {
                // Make sure the book to be scraped is in stock
                links = links.filter(link => link.querySelector('.instock.availability > i').textContent !== "In stock")
                // Extract the links from the data
                links = links.map(el => el.querySelector('h3 > a').href)
                return links;
            });
            // Loop through each of those links, open a new page instance and get the relevant data from them
            let pagePromise = (link) => new Promise(async(resolve, reject) => {
                let dataObj = {};
                let newPage = await browser.newPage();
                await newPage.goto(link);
                dataObj['bookTitle'] = await newPage.$eval('.product_main > h1', text => text.textContent);
                dataObj['bookPrice'] = await newPage.$eval('.price_color', text => text.textContent);
                dataObj['noAvailable'] = await newPage.$eval('.instock.availability', text => {
                    // Strip new line and tab spaces
                    text = text.textContent.replace(/(\r\n\t|\n|\r|\t)/gm, "");
                    // Get the number of stock available
                    let regexp = /^.*\((.*)\).*$/i;
                    let stockAvailable = regexp.exec(text)[1].split(' ')[0];
                    return stockAvailable;
                });
                dataObj['imageUrl'] = await newPage.$eval('#product_gallery img', img => img.src);
                dataObj['bookDescription'] = await newPage.$eval('#product_description', div => div.nextSibling.nextSibling.textContent);
                dataObj['upc'] = await newPage.$eval('.table.table-striped > tbody > tr > td', table => table.textContent);
                resolve(dataObj);
                await newPage.close();
            });

            for(link in urls){
                let currentPageData = await pagePromise(urls[link]);
                scrapedData.push(currentPageData);
                // console.log(currentPageData);
            }
            // When all the data on this page is done, click the next button and start the scraping of the next page
            // You are going to check if this button exist first, so you know if there really is a next page.
            let nextButtonExist = false;
            try{
                const nextButton = await page.$eval('.next > a', a => a.textContent);
                nextButtonExist = true;
            }
            catch(err){
                nextButtonExist = false;
            }
            if(nextButtonExist){
                await page.click('.next > a');   
                return scrapeCurrentPage(); // Call this function recursively
            }
            await page.close();
            return scrapedData;
        }
        let data = await scrapeCurrentPage();
        console.log(data);
        return data;
    }
}

module.exports = scraperObject;

このコードブロックは、渡されたカテゴリを使用して、そのカテゴリの書籍が存在するURLを取得します。

page.$$eval()は、引数を3番目のパラメーターとして$$eval()メソッドに渡し、コールバックの3番目のパラメーターとして次のように定義することで引数を取り込むことができます。

サンプルページ。$$eval()関数

page.$$eval('selector', function(elem, args){
    // .......
}, args)

これはあなたがあなたのコードでしたことでした。 削りたい本のカテゴリを渡し、すべてのカテゴリをマッピングして、どれが一致するかを確認してから、このカテゴリのURLを返しました。

次に、このURLを使用して、page.goto(selectedCategory)メソッドを使用してスクレイプする書籍のカテゴリを表示するページに移動します。

ファイルを保存して閉じます。

アプリケーションを再度実行します。 Travelカテゴリに移動し、そのカテゴリの本をページごとに再帰的に開き、結果をログに記録します。

npm run start

このステップでは、複数のページにまたがってデータをスクレイピングしてから、1つの特定のカテゴリから複数のページにまたがってデータをスクレイピングしました。 最後のステップでは、スクリプトを変更して複数のカテゴリにまたがるデータをスクレイピングし、このスクレイピングされたデータを文字列化されたJSONファイルに保存します。

ステップ6—複数のカテゴリからデータをスクレイピングし、データをJSONとして保存する

この最後のステップでは、スクリプトで必要な数のカテゴリからデータを取得し、出力の方法を変更します。 結果をログに記録するのではなく、data.jsonという構造化ファイルに保存します。

スクレイプにカテゴリをすばやく追加できます。 これを行うには、ジャンルごとに1行追加するだけで済みます。

pageController.jsを開きます:

nano pageController.js

追加のカテゴリを含めるようにコードを調整します。 次の例では、HistoricalFictionMysteryを既存のTravelカテゴリに追加します。

./book-scraper/pageController.js

const pageScraper = require('./pageScraper');
async function scrapeAll(browserInstance){
    let browser;
    try{
        browser = await browserInstance;
        let scrapedData = {};
        // Call the scraper for different set of books to be scraped
        scrapedData['Travel'] = await pageScraper.scraper(browser, 'Travel');
        scrapedData['HistoricalFiction'] = await pageScraper.scraper(browser, 'Historical Fiction');
        scrapedData['Mystery'] = await pageScraper.scraper(browser, 'Mystery');
        await browser.close();
        console.log(scrapedData)
    }
    catch(err){
        console.log("Could not resolve the browser instance => ", err);
    }
}

module.exports = (browserInstance) => scrapeAll(browserInstance)

ファイルを保存して閉じます。

スクリプトを再度実行し、3つのカテゴリすべてのデータを取得するのを確認します。

npm run start

スクレーパーが完全に機能するようになったら、最後のステップとして、データをより便利な形式で保存します。 ここで、Node.jsfsモジュールを使用してJSONファイルに保存します。

まず、pageController.jsを再度開きます。

nano pageController.js

次の強調表示されたコードを追加します。

./book-scraper/pageController.js

const pageScraper = require('./pageScraper');
const fs = require('fs');
async function scrapeAll(browserInstance){
    let browser;
    try{
        browser = await browserInstance;
        let scrapedData = {};
        // Call the scraper for different set of books to be scraped
        scrapedData['Travel'] = await pageScraper.scraper(browser, 'Travel');
        scrapedData['HistoricalFiction'] = await pageScraper.scraper(browser, 'Historical Fiction');
        scrapedData['Mystery'] = await pageScraper.scraper(browser, 'Mystery');
        await browser.close();
        fs.writeFile("data.json", JSON.stringify(scrapedData), 'utf8', function(err) {
            if(err) {
                return console.log(err);
            }
            console.log("The data has been scraped and saved successfully! View it at './data.json'");
        });
    }
    catch(err){
        console.log("Could not resolve the browser instance => ", err);
    }
}

module.exports = (browserInstance) => scrapeAll(browserInstance)

まず、pageController.jsにNode、jsのfsモジュールが必要です。 これにより、データをJSONファイルとして保存できるようになります。 次に、スクレイピングが完了してブラウザが閉じたときに、プログラムがdata.jsonという名前の新しいファイルを作成するようにコードを追加します。 data.jsonの内容は文字列化されたJSONであることに注意してください。 したがって、data.jsonのコンテンツを読み取るときは、データを再利用する前に、常にそれをJSONとして解析してください。

ファイルを保存して閉じます。

これで、複数のカテゴリにわたって本をスクレイピングし、スクレイピングしたデータをJSONファイルに保存するWebスクレイピングアプリケーションを構築しました。 アプリケーションの複雑さが増すにつれて、このスクレイピングされたデータをデータベースに保存するか、APIを介して提供することをお勧めします。 このデータがどのように消費されるかは、本当にあなた次第です。

結論

このチュートリアルでは、複数のページにまたがるデータを再帰的にスクレイピングするWebクローラーを作成し、それをJSONファイルに保存しました。 つまり、Webサイトからのデータ収集を自動化する新しい方法を学びました。

Puppeteerには、このチュートリアルの範囲外の機能がたくさんあります。 詳細については、ヘッドレスクロームを簡単に制御するためのPuppeteerの使用をご覧ください。 Puppeteerの公式ドキュメントにもアクセスできます。