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

序章

Webスクレイピングは、Webクロールとも呼ばれ、ボットを使用してWebサイトからコンテンツとデータを抽出、解析、およびダウンロードします。

1台のマシンを使用して数十のWebページからデータを取得できますが、数百または数千のWebページからデータを取得する必要がある場合は、ワークロードの分散を検討することをお勧めします。

このチュートリアルでは、Puppeteerを使用してbooks.toscrapeをスクレイピングします。これは、初心者がWebスクレイピングを学び、開発者がスクレイピング技術を検証するための安全な場所として機能する架空の書店です。 これを書いている時点で、books.toscrapeには1000冊の本があり、したがって、1000のWebページをこすり取ることができます。 ただし、このチュートリアルでは、最初の400のみをスクレイプします。 これらすべてのWebページを短時間でスクレイプするには、 ExpressWebフレームワークとPuppeteerブラウザーコントローラーを含むスケーラブルなアプリをビルドしてKubernetesクラスターにデプロイします。 スクレーパーを操作するには、PromiseベースのHTTPクライアントである axios と、Node.js用の小さなJSONデータベースであるlowdbを含むアプリを作成します。

このチュートリアルを完了すると、複数のページから同時にデータを抽出できるスケーラブルなスクレーパーができあがります。 たとえば、デフォルト設定と3ノードのクラスターを使用すると、books.toscrapeで400ページをスクレイプするのに2分もかかりません。 クラスタをスケーリングした後、約30秒かかります。

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

前提条件

このチュートリアルに従うには、次のマシンが必要です。

ステップ1—ターゲットWebサイトの分析

コードを書く前に、Webブラウザでbooks.toscrapeに移動します。 データがどのように構造化されているか、および同時スクレイピングが最適なソリューションである理由を調べます。

books.toscrape homepage header

このウェブサイトには1,000冊の本がありますが、各ページには20冊しか表示されていないことに注意してください。

ページの一番下までスクロールします。

books.toscrape homepage footer

このウェブサイトのコンテンツはページ付けされており、合計50ページあります。 各ページには20冊の本が表示され、最初の400冊だけをスクレイプしたいので、最初の20ページに表示されるすべての本のタイトル、価格、評価、およびURLのみを取得します。

全体のプロセスは1分未満かかるはずです。

ブラウザの開発ツールを開き、ページの最初の本を調べます。 次のコンテンツが表示されます。

books.toscrape homepage with dev tools

すべての本は<section>タグ内にあり、各本は独自の<li>タグの下にリストされています。 各<li>タグ内には、product_podと等しいclass属性を持つ<article>タグがあります。 これが私たちが削りたい要素です。

最初の20ページのすべての本のメタデータを取得して保存すると、400冊の本を含むローカルデータベースが作成されます。 ただし、本に関するより詳細な情報は独自のページにあるため、各本のメタデータ内のURLを使用して400の追加ページをナビゲートする必要があります。 次に、必要な不足している本の詳細を取得し、このデータをローカルデータベースに追加します。 取得しようとしている不足しているデータは、説明、UPC(Universal Book Code)、レビューの数、および書籍の入手可能性です。 1台のマシンを使用して400ページを通過するには、7分以上かかる場合があります。そのため、Kubernetesを使用して作業を複数のマシンに分割する必要があります。

次に、ホームページの最初の本のリンクをクリックすると、その本の詳細ページが開きます。 ブラウザの開発ツールをもう一度開き、ページを調べます。

books.toscrape book page with dev tools

抽出したい不足している情報は、product_pageに等しいclass属性を持つ<article>タグ内にあります。

クラスター内のスクレーパーとやり取りするには、HTTPリクエストをKubernetesクラスターに送信できるクライアントアプリケーションを作成する必要があります。 最初にサーバー側をコーディングし、次にこのプロジェクトのクライアント側をコーディングします。

このセクションでは、スクレーパーが取得する情報と、このスクレーパーをKubernetesクラスターにデプロイする必要がある理由を確認しました。 次のセクションでは、クライアントアプリケーションとサーバーアプリケーションのディレクトリを作成します。

ステップ2—プロジェクトルートディレクトリを作成する

このステップでは、プロジェクトのディレクトリ構造を作成します。 次に、クライアントおよびサーバーアプリケーション用にNode.jsプロジェクトを初期化します。

ターミナルウィンドウを開き、concurrent-webscraperという名前の新しいディレクトリを作成します。

  1. mkdir concurrent-webscraper

ディレクトリに移動します。

  1. cd ./concurrent-webscraper

次に、serverclient、およびk8sという名前の3つのサブディレクトリを作成します。

  1. mkdir server client k8s

serverディレクトリに移動します。

  1. cd ./server

新しいNode.jsプロジェクトを作成します。 npmのinitコマンドを実行すると、package.jsonファイルが作成され、依存関係とメタデータの管理に役立ちます。

初期化コマンドを実行します。

  1. npm init

デフォルト値を受け入れるには、ENTERを押してすべてのプロンプトを表示します。 または、応答をパーソナライズすることもできます。 npmの初期化設定の詳細については、チュートリアルのステップ1、npmおよびpackage.jsonでNode.jsモジュールを使用する方法を参照してください。

package.jsonファイルを開き、編集します。

  1. nano package.json

mainプロパティを変更し、scriptsディレクティブに情報を追加してから、dependenciesディレクティブを作成する必要があります。

ファイル内の内容を強調表示されたコードに置き換えます。

./server/package.json
{
  "name": "server",
  "version": "1.0.0",
  "description": "",
  "main": "server.js",
  "scripts": {
    "start": "node server.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
  "body-parser": "^1.19.0",
  "express": "^4.17.1",
  "puppeteer": "^3.0.0"
  }
}

ここでは、mainおよびscriptsプロパティを変更し、dependenciesプロパティも編集しました。 サーバーアプリケーションはDockerコンテナ内で実行されるため、npm installコマンドを実行する必要はありません。このコマンドは通常、初期化に続き、各依存関係をpackage.jsonに自動的に追加します。

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

clientディレクトリに移動します。

  1. cd ../client

別のNode.jsプロジェクトを作成します。

  1. npm init

同じ手順に従って、デフォルト設定を受け入れるか、応答をカスタマイズします。

package.jsonファイルを開き、編集します。

  1. nano package.json

ファイル内の内容を強調表示されたコードに置き換えます。

./client/package.json
{
  "name": "client",
  "version": "1.0.0",
  "description": "",
  "main": "main.js",
  "scripts": {
    "start": "node main.js"
  },
  "author": "",
  "license": "ISC"
}

ここでは、mainおよびscriptsプロパティを変更しました。

今回は、npmを使用して必要な依存関係をインストールします。

  1. npm install axios lowdb --save

このコードブロックには、axioslowdbがインストールされています。 axiosは、ブラウザーとNode.js用のPromiseベースのHTTPクライアントです。 このモジュールを使用して、非同期HTTPリクエストをスクレーパー内のRESTエンドポイントに送信して対話します。 lowdbは、Node.jsとブラウザー用の小さなJSONデータベースであり、スクレイピングされたデータを保存するために使用します。

このステップでは、プロジェクトディレクトリを作成し、スクレーパーを含むアプリケーションサーバーのNode.jsプロジェクトを初期化しました。 次に、アプリケーションサーバーと対話するクライアントアプリケーションに対して同じことを行いました。 また、Kubernetes構成ファイル用のディレクトリも作成しました。 次のステップでは、アプリケーションサーバーの構築を開始します。

ステップ3—最初のスクレーパーファイルを作成する

このステップとステップ4では、サーバー側にスクレーパーを作成します。 このアプリケーションは、puppeteerManager.jsserver.jsの2つのファイルで構成されます。 puppeteerManager.jsファイルはブラウザセッションを作成および管理し、server.jsファイルは1つまたは複数のWebページをスクレイプする要求を受け取ります。 次に、これらのリクエストはpuppeteerManager.js内のメソッドを呼び出し、特定のWebページをスクレイピングし、スクレイピングされたデータを返します。 このステップでは、puppeteerManager.jsファイルを作成します。 手順4では、server.jsファイルを作成します。

まず、サーバーディレクトリに戻り、puppeteerManager.jsというファイルを作成します。

serverフォルダーに移動します。

  1. cd ../server

お好みのテキストエディタを使用して、puppeteerManager.jsファイルを作成して開きます。

  1. nano puppeteerManager.js

puppeteerManager.jsファイルにはPuppeteerManagerというクラスが含まれ、このクラスはPuppeteerブラウザインスタンスを作成および管理します。 最初にこのクラスを作成してから、コンストラクターを追加します。

次のコードをpuppeteerManager.jsファイルに追加します。

puppeteerManager.js
class PuppeteerManager {
    constructor(args) {
        this.url = args.url
        this.existingCommands = args.commands
        this.nrOfPages = args.nrOfPages
        this.allBooks = [];
        this.booksDetails = {}
    }
}
module.exports = { PuppeteerManager }

この最初のコードブロックでは、PuppeteerManagerクラスを作成し、それにコンストラクターを追加しました。 コンストラクターは、次のプロパティを含むオブジェクトを受け取ることを期待しています。

  • url:このプロパティは文字列を保持します。これは、スクレイプするページのアドレスになります。
  • commands:このプロパティは、ブラウザに指示を提供する配列を保持します。 たとえば、ボタンをクリックするか、特定のDOM要素を解析するようにブラウザに指示します。 各commandには、descriptionlocatorCss、およびtypeのプロパティがあります。 descriptionは、commandの機能を示し、locatorCssは、DOM内の適切な要素を見つけ、typeは特定のアクションを選択します。
  • nrOfPages:このプロパティは整数を保持します。これは、アプリケーションがcommandsを繰り返す回数を決定するために使用します。 たとえば、 books.toscrape.com は1ページあたり20冊の本しか表示しないため、20ページすべてで400冊すべてを取得するには、このプロパティを使用して既存のcommandsを20回繰り返します。 。

このコードブロックでは、受け取ったオブジェクトのプロパティをコンストラクター変数urlexistingCommands、およびnrOfPagesにも割り当てました。 次に、allBooksbooksDetailsの2つの追加変数を作成しました。 変数allBooksを使用して、取得したすべての本のメタデータを保存し、変数booksDetailsを使用して、特定の個々の本の不足している本の詳細を保存します。

これで、PuppeteerManagerクラスにいくつかのメソッドを追加する準備が整いました。 このクラスには、runPuppeteer()executeCommand()sleep()getAllBooks()、およびgetBooksDetails()のメソッドがあります。 これらのメソッドはスクレーパーアプリケーションのコアを形成するため、1つずつ調べる価値があります。

runPuppeteer()メソッドのコーディング

PuppeteerManagerクラス内の最初のメソッドはrunPuppeteer()です。 これには、Puppeteerモジュールが必要であり、ブラウザインスタンスを起動します。

PuppeteerManagerクラスの下部に、次のコードを追加します。

puppeteerManager.js
. . .
    async runPuppeteer() {
        const puppeteer = require('puppeteer')
        let commands = []
        if (this.nrOfPages > 1) {
            for (let i = 0; i < this.nrOfPages; i++) {
                if (i < this.nrOfPages - 1) {
                    commands.push(...this.existingCommands)
                } else {
                    commands.push(this.existingCommands[0])
                }
            }
        } else {
            commands = this.existingCommands
        }
        console.log('commands length', commands.length)
    }

このコードブロックでは、runPuppeteer()メソッドを作成しました。 まず、puppeteerモジュールが必要で、次にcommandsという空の配列で始まる変数を作成しました。 条件付きロジックを使用して、スクレイプするページ数が1より大きい場合、コードはnrOfPagesをループし、各ページのexistingCommands[に追加する必要があると述べました。 X186X]アレイ。 ただし、最後のページに到達すると、existingCommands配列の最後のcommandcommands配列に追加されません。これは、最後のcommandが原因です。 次のページボタンをクリックします。

次のステップは、ブラウザインスタンスを作成することです。

作成したrunPuppeteer()メソッドの下部に、次のコードを追加します。

puppeteerManager.js
. . .
    async runPuppeteer() {
        . . .

        const browser = await puppeteer.launch({
            headless: true,
            args: [
                "--no-sandbox",
                "--disable-gpu",
            ]
        });
        let page = await browser.newPage()

        . . .
    }

このコードブロックでは、組み込みのpuppeteer.launch()メソッドを使用してbrowserインスタンスを作成しました。 インスタンスがheadlessモードで実行されるように指定しています。 これはデフォルトのオプションであり、Kubernetesでアプリケーションを実行しているため、このプロジェクトに必要です。 次の2つの引数は、グラフィカルユーザーインターフェイスなしでブラウザを作成する場合の標準です。 最後に、 Puppeteerのbrowser.newPage()メソッドを使用して、新しいpageオブジェクトを作成しました。 .launch()メソッドは、 Promise を返します。これには、awaitキーワードが必要です。

これで、URLをナビゲートする方法など、新しいpageオブジェクトに動作を追加する準備が整いました。

runPuppeteer()メソッドの下部に、次のコードを追加します。

puppeteerManager.js
. . .
    async runPuppeteer() {
        . . .

        await page.setRequestInterception(true);
        page.on('request', (request) => {
            if (['image'].indexOf(request.resourceType()) !== -1) {
                request.abort();
            } else {
                request.continue();
            }
        });

        await page.on('console', msg => {
            for (let i = 0; i < msg._args.length; ++i) {
                msg._args[i].jsonValue().then(result => {
                    console.log(result);
                })
            }
        });

        await page.goto(this.url);

        . . .
    }

このコードブロックでは、pageオブジェクトがPuppeteerのpage.setRequestInterception()メソッドを使用してすべてのリクエストをインターセプトし、リクエストがimageをロードする場合、画像の読み込みが不要になるため、ウェブページの読み込みに必要な時間が短縮されます。 次に、pageオブジェクトは、 Puppeteerのpage.on(’ console ‘)event を使用して、ブラウザーコンテキストでメッセージを表示しようとする試みをインターセプトします。 次に、pageは、 page.goto()メソッドを使用して、指定されたurlに移動します。

次に、pageオブジェクトにいくつかの動作を追加して、DOM内の要素を検索し、それらに対してコマンドを実行する方法を制御します。

runPuppeteer()メソッドの下部に、次のコードを追加します。

puppeteerManager.js
. . .
    async runPuppeteer() {
        . . .

        let timeout = 6000
        let commandIndex = 0
        while (commandIndex < commands.length) {
            try {
                console.log(`command ${(commandIndex + 1)}/${commands.length}`)
                let frames = page.frames()
                await frames[0].waitForSelector(commands[commandIndex].locatorCss, { timeout: timeout })
                await this.executeCommand(frames[0], commands[commandIndex])
                await this.sleep(1000)
            } catch (error) {
                console.log(error)
                break
            }
            commandIndex++
        }
        console.log('done')
        await browser.close()
    }

このコードブロックでは、timeoutcommandIndexの2つの変数を作成しました。 最初の変数は、コードがWebページ上の要素を待機する時間を制限し、2番目の変数は、commands配列をループする方法を制御します。

whileループ内では、コードはcommands配列内のすべてのcommandを通過します。 まず、 page.frames()メソッドを使用して、ページにアタッチされたすべてのフレームの配列を作成します。 frame.waitForSelector()メソッドlocatorCssプロパティを使用して、pageframeオブジェクト内のDOM要素を検索します。 要素が見つかった場合は、executeCommand()メソッドを呼び出し、frameおよびcommandオブジェクトをパラメーターとして渡します。 executeCommandが戻った後、sleep()メソッドを呼び出します。これにより、コードは次のcommandを実行する前に1秒間待機します。 最後に、コマンドがなくなると、browserインスタンスが閉じます。

これで、runPuppeteer()メソッドが完了しました。 この時点で、puppeteerManager.jsファイルは次のようになります。

puppeteerManager.js
class PuppeteerManager {
    constructor(args) {
        this.url = args.url
        this.existingCommands = args.commands
        this.nrOfPages = args.nrOfPages
        this.allBooks = [];
        this.booksDetails = {}
    }

    async runPuppeteer() {
        const puppeteer = require('puppeteer')
        let commands = []
        if (this.nrOfPages > 1) {
            for (let i = 0; i < this.nrOfPages; i++) {
                if (i < this.nrOfPages - 1) {
                    commands.push(...this.existingCommands)
                } else {
                    commands.push(this.existingCommands[0])
                }
            }
        } else {
            commands = this.existingCommands
        }
        console.log('commands length', commands.length)

        const browser = await puppeteer.launch({
            headless: true,
            args: [
                "--no-sandbox",
                "--disable-gpu",
            ]
        });

        let page = await browser.newPage()
        await page.setRequestInterception(true);
        page.on('request', (request) => {
            if (['image'].indexOf(request.resourceType()) !== -1) {
                request.abort();
            } else {
                request.continue();
            }
        });

        await page.on('console', msg => {
            for (let i = 0; i < msg._args.length; ++i) {
                msg._args[i].jsonValue().then(result => {
                    console.log(result);
                })

            }
        });

        await page.goto(this.url);

        let timeout = 6000
        let commandIndex = 0
        while (commandIndex < commands.length) {
            try {
         
                console.log(`command ${(commandIndex + 1)}/${commands.length}`)
                let frames = page.frames()
                await frames[0].waitForSelector(commands[commandIndex].locatorCss, { timeout: timeout })
                await this.executeCommand(frames[0], commands[commandIndex])
                await this.sleep(1000)
            } catch (error) {
                console.log(error)
                break
            }
            commandIndex++
        }
        console.log('done')
        await browser.close();
    }
}

これで、puppeteerManager.jsの2番目のメソッドexecuteCommand()をコーディングする準備が整いました。

executeCommand()メソッドのコーディング

runPuppeteer()メソッドを作成したら、executeCommand()メソッドを作成します。 このメソッドは、ボタンをクリックしたり、1つまたは複数のDOM要素を解析したりするなど、Puppeteerが実行するアクションを決定する役割を果たします。

PuppeteerManagerクラスの下部に、次のコードを追加します。

puppeteerManager.js
. . .
    async executeCommand(frame, command) {
        await console.log(command.type, command.locatorCss)
        switch (command.type) {
            case "click":
                break;
            case "getItems":
                break;
            case "getItemDetails":
                break;
        }
    }

このコードブロックでは、executeCommand()メソッドを作成しました。 このメソッドは、ページ要素を含むframeオブジェクトと、コマンドを含むcommandオブジェクトの2つの引数を想定しています。 このメソッドは、switchステートメントで構成され、clickgetItems、およびgetItemDetailsの場合があります。

clickの場合を定義します。

case "click":の下にあるbreak;を次のコードに置き換えます。

puppeteerManager.js
    async executeCommand(frame, command) {
        . . .
            case "click":
                try {
                    await frame.$eval(command.locatorCss, element => element.click());
                    return true
                } catch (error) {
                    console.log("error", error)
                    return false
                }
        . . .        
    }

command.typeclickと等しい場合、コードはclickのケースをトリガーします。 このコードブロックは、次へボタンをクリックして、ページ化された書籍のリストを移動する役割を果たします。

次に、次のcaseステートメントをプログラムします。

case "getItems":の下にあるbreak;を次のコードに置き換えます。

puppeteerManager.js
    async executeCommand(frame, command) {
        . . .
            case "getItems":
                try {
                    let books = await frame.evaluate((command) => {
                        function wordToNumber(word) {
                            let number = 0
                            let words = ["zero","one","two","three","four","five"]
                            for(let n=0;n<words.length;words++){
                                if(word == words[n]){
                                    number = n
                                    break
                                }
                            }
                            return number
                        }

                        try {
                            let parsedItems = [];
                            let items = document.querySelectorAll(command.locatorCss);
                            items.forEach((item) => {
                                let link = 'http://books.toscrape.com/catalogue/' + item.querySelector('div.image_container a').getAttribute('href').replace('catalogue/', '')<^>
                                let starRating = item.querySelector('p.star-rating').getAttribute('class').replace('star-rating ', '').toLowerCase().trim()
                                let title = item.querySelector('h3 a').getAttribute('title')
                                let price = item.querySelector('p.price_color').innerText.replace('£', '').trim()
                                let book = {
                                    title: title,
                                    price: parseInt(price),
                                    rating: wordToNumber(starRating),
                                    url: link
                                }
                                parsedItems.push(book)
                            })
                            return parsedItems;
                        } catch (error) {
                            console.log(error)
                        }
                    }, command).then(result => {
                        this.allBooks.push.apply(this.allBooks, result)
                        console.log('allBooks length ', this.allBooks.length)
                    })
                    return true
                } catch (error) {
                    console.log("error", error)
                    return false
                }
        . . .
    }

getItemsの場合は、command.typegetItemsと等しい場合にトリガーされます。 frame.evaluate()メソッドを使用してブラウザーのコンテキストを切り替え、wordToNumber()という関数を作成しています。 この関数は、本のstarRatingを文字列から整数に変換します。 次に、コードは document.querySelectorAll()メソッドを使用して、DOMを解析および照合し、Webページの特定のframeに表示される書籍のメタデータを取得します。 。 メタデータが取得されると、コードはそれをallBooks配列に追加します。

これで、最後のcaseステートメントを定義できます。

case "getItemDetails"の下にあるbreak;を次のコードに置き換えます。

puppeteerManager.js
    async executeCommand(frame, command) {
        . . .
            case "getItemDetails":
                try {
                    this.booksDetails = JSON.parse(JSON.stringify(await frame.evaluate((command) => {
                        try {
                            let item = document.querySelector(command.locatorCss);
                            let description = item.querySelector('.product_page > p:nth-child(3)').innerText.trim()
                            let upc = item.querySelector('.table > tbody:nth-child(1) > tr:nth-child(1) > td:nth-child(2)')
                                .innerText.trim()
                            let nrOfReviews = item.querySelector('.table > tbody:nth-child(1) > tr:nth-child(7) > td:nth-child(2)')
                                .innerText.trim()
                            let availability = item.querySelector('.table > tbody:nth-child(1) > tr:nth-child(6) > td:nth-child(2)')
                                .innerText.replace('In stock (', '').replace(' available)', '')
                            let details = {
                                description: description,
                                upc: upc,
                                nrOfReviews: parseInt(nrOfReviews),
                                availability: parseInt(availability)
                            }
                            return details;
                        } catch (error) {
                            console.log(error)
                            return error
                        }

                    }, command)))
                    console.log(this.booksDetails)
                    return true
                } catch (error) {
                    console.log("error", error)
                    return false
                }
    }

getItemDetailsの場合は、command.typegetItemDetailsと等しい場合にトリガーされます。 frame.evaluate()および.querySelector()メソッドを再度使用して、ブラウザーコンテキストを切り替え、DOMを解析しました。 しかし今回は、Webページの特定のframeにある各本の欠落している詳細を取得しました。 次に、これらの欠落している詳細をbooksDetailsオブジェクトに割り当てました。

これで、executeCommand()メソッドが完了しました。 puppeteerManager.jsファイルは次のようになります。

puppeteerManager.js
class PuppeteerManager {
    constructor(args) {
        this.url = args.url
        this.existingCommands = args.commands
        this.nrOfPages = args.nrOfPages
        this.allBooks = [];
        this.booksDetails = {}
    }

    async runPuppeteer() {
        const puppeteer = require('puppeteer')
        let commands = []
        if (this.nrOfPages > 1) {
            for (let i = 0; i < this.nrOfPages; i++) {
                if (i < this.nrOfPages - 1) {
                    commands.push(...this.existingCommands)
                } else {
                    commands.push(this.existingCommands[0])
                }
            }
        } else {
            commands = this.existingCommands
        }
        console.log('commands length', commands.length)

        const browser = await puppeteer.launch({
            headless: true,
            args: [
                "--no-sandbox",
                "--disable-gpu",
            ]
        });

        let page = await browser.newPage()
        await page.setRequestInterception(true);
        page.on('request', (request) => {
            if (['image'].indexOf(request.resourceType()) !== -1) {
                request.abort();
            } else {
                request.continue();
            }
        });

        await page.on('console', msg => {
            for (let i = 0; i < msg._args.length; ++i) {
                msg._args[i].jsonValue().then(result => {
                    console.log(result);
                })

            }
        });

        await page.goto(this.url);

        let timeout = 6000
        let commandIndex = 0
        while (commandIndex < commands.length) {
            try {
         
                console.log(`command ${(commandIndex + 1)}/${commands.length}`)
                let frames = page.frames()
                await frames[0].waitForSelector(commands[commandIndex].locatorCss, { timeout: timeout })
                await this.executeCommand(frames[0], commands[commandIndex])
                await this.sleep(1000)
            } catch (error) {
                console.log(error)
                break
            }
            commandIndex++
        }
        console.log('done')
        await browser.close();
    }

    async executeCommand(frame, command) {
        await console.log(command.type, command.locatorCss)
        switch (command.type) {
            case "click":
                try {
                    await frame.$eval(command.locatorCss, element => element.click());
                    return true
                } catch (error) {
                    console.log("error", error)
                    return false
                }
            case "getItems":
                try {
                    let books = await frame.evaluate((command) => {
                        function wordToNumber(word) {
                            let number = 0
                            let words = ["zero","one","two","three","four","five"]
                            for(let n=0;n<words.length;words++){
                                if(word == words[n]){
                                    number = n
                                    break
                                }
                            }  
                            return number
                        }
                        try {
                            let parsedItems = [];
                            let items = document.querySelectorAll(command.locatorCss);
                            
                            items.forEach((item) => {
                                let link = 'http://books.toscrape.com/catalogue/' + item.querySelector('div.image_container a').getAttribute('href').replace('catalogue/', '')
                                let starRating = item.querySelector('p.star-rating').getAttribute('class').replace('star-rating ', '').toLowerCase().trim()
                                let title = item.querySelector('h3 a').getAttribute('title')
                                let price = item.querySelector('p.price_color').innerText.replace('£', '').trim()
                                let book = {
                                    title: title,
                                    price: parseInt(price),
                                    rating: wordToNumber(starRating),
                                    url: link
                                }
                                parsedItems.push(book)
                            })
                            return parsedItems;
                        } catch (error) {
                            console.log(error)
                        }
                    }, command).then(result => {
                        this.allBooks.push.apply(this.allBooks, result)
                        console.log('allBooks length ', this.allBooks.length)
                    })
                    return true
                } catch (error) {
                    console.log("error", error)
                    return false
                }
            case "getItemDetails":
                try {
                    this.booksDetails = JSON.parse(JSON.stringify(await frame.evaluate((command) => {
                        try {
                            let item = document.querySelector(command.locatorCss);
                            let description = item.querySelector('.product_page > p:nth-child(3)').innerText.trim()
                            let upc = item.querySelector('.table > tbody:nth-child(1) > tr:nth-child(1) > td:nth-child(2)')
                                .innerText.trim()
                            let nrOfReviews = item.querySelector('.table > tbody:nth-child(1) > tr:nth-child(7) > td:nth-child(2)')
                                .innerText.trim()
                            let availability = item.querySelector('.table > tbody:nth-child(1) > tr:nth-child(6) > td:nth-child(2)')
                                .innerText.replace('In stock (', '').replace(' available)', '')
                            let details = {
                                description: description,
                                upc: upc,
                                nrOfReviews: parseInt(nrOfReviews),
                                availability: parseInt(availability)
                            }
                            return details;
                        } catch (error) {
                            console.log(error)
                            return error
                        }

                    }, command))) 
                    console.log(this.booksDetails)
                    return true
                } catch (error) {
                    console.log("error", error)
                    return false
                }
        }
    }
}

これで、PuppeteerManagerクラスの3番目のメソッドsleep()を作成する準備が整いました。

sleep()メソッドのコーディング

executeCommand()メソッドを作成したら、次のステップはsleep()メソッドを作成することです。 このメソッドは、コードを特定の時間待機させてから、次のコード行を実行します。 これは、crawl rateを減らすために不可欠です。 この予防措置がないと、スクレーパーは、たとえば、ページAのボタンをクリックしてから、ページBが読み込まれる前にページBの要素を検索する可能性があります。

PuppeteerManagerクラスの下部に、次のコードを追加します。

puppeteerManager.js
. . .
    sleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms))
    }

sleep()メソッドに整数を渡しています。 この整数は、コードが待機する必要のあるミリ秒単位の時間です。

次に、PuppeteerManagerクラス内の最後の2つのメソッドgetAllBooks()getBooksDetails()をコーディングします。

getAllBooks()およびgetBooksDetails()メソッドのコーディング

sleep()メソッドを作成したら、getAllBooks()メソッドを作成します。 server.jsファイル内の関数がこの関数を呼び出します。 getAllBooks()は、runPuppeteer()を呼び出し、特定のページに書籍を表示してから、取得した書籍をserver.jsファイルで呼び出した関数に戻します。

PuppeteerManagerクラスの下部に、次のコードを追加します。

puppeteerManager.js
. . .
    async getAllBooks() {
        await this.runPuppeteer()
        return this.allBooks
    }

このブロックが別のPromiseをどのように使用しているかに注意してください。

これで、最終的なメソッドgetBooksDetails()を作成できます。 getAllBooks()と同様に、server.js内の関数がこの関数を呼び出します。 ただし、getBooksDetails()は、各本の不足している詳細を取得する責任があります。 また、これらの詳細をserver.jsファイルで呼び出した関数に返します。

PuppeteerManagerクラスの下部に、次のコードを追加します。

puppeteerManager.js
. . .
    async getBooksDetails() {
        await this.runPuppeteer()
        return this.booksDetails
    }

これで、puppeteerManager.jsファイルのコーディングが完了しました。

このセクションで説明する5つのメソッドを追加すると、完成したファイルは次のようになります。

puppeteerManager.js
class PuppeteerManager {
    constructor(args) {
        this.url = args.url
        this.existingCommands = args.commands
        this.nrOfPages = args.nrOfPages
        this.allBooks = [];
        this.booksDetails = {}
    }

    async runPuppeteer() {
        const puppeteer = require('puppeteer')
        let commands = []
        if (this.nrOfPages > 1) {
            for (let i = 0; i < this.nrOfPages; i++) {
                if (i < this.nrOfPages - 1) {
                    commands.push(...this.existingCommands)
                } else {
                    commands.push(this.existingCommands[0])
                }
            }
        } else {
            commands = this.existingCommands
        }
        console.log('commands length', commands.length)

        const browser = await puppeteer.launch({
            headless: true,
            args: [
                "--no-sandbox",
                "--disable-gpu",
            ]
        });

        let page = await browser.newPage()
        await page.setRequestInterception(true);
        page.on('request', (request) => {
            if (['image'].indexOf(request.resourceType()) !== -1) {
                request.abort();
            } else {
                request.continue();
            }
        });

        await page.on('console', msg => {
            for (let i = 0; i < msg._args.length; ++i) {
                msg._args[i].jsonValue().then(result => {
                    console.log(result);
                })

            }
        });

        await page.goto(this.url);

        let timeout = 6000
        let commandIndex = 0
        while (commandIndex < commands.length) {
            try {
         
                console.log(`command ${(commandIndex + 1)}/${commands.length}`)
                let frames = page.frames()
                await frames[0].waitForSelector(commands[commandIndex].locatorCss, { timeout: timeout })
                await this.executeCommand(frames[0], commands[commandIndex])
                await this.sleep(1000)
            } catch (error) {
                console.log(error)
                break
            }
            commandIndex++
        }
        console.log('done')
        await browser.close();
    }

    async executeCommand(frame, command) {
        await console.log(command.type, command.locatorCss)
        switch (command.type) {
            case "click":
                try {
                    await frame.$eval(command.locatorCss, element => element.click());
                    return true
                } catch (error) {
                    console.log("error", error)
                    return false
                }
            case "getItems":
                try {
                    let books = await frame.evaluate((command) => {
                        function wordToNumber(word) {
                            let number = 0
                            let words = ["zero","one","two","three","four","five"]
                            for(let n=0;n<words.length;words++){
                                if(word == words[n]){
                                    number = n
                                    break
                                }
                            }  
                            return number
                        }

                        try {
                            let parsedItems = [];
                            let items = document.querySelectorAll(command.locatorCss);
                            
                            items.forEach((item) => {
                                let link = 'http://books.toscrape.com/catalogue/' + item.querySelector('div.image_container a').getAttribute('href').replace('catalogue/', '')
                                let starRating = item.querySelector('p.star-rating').getAttribute('class').replace('star-rating ', '').toLowerCase().trim()
                                let title = item.querySelector('h3 a').getAttribute('title')
                                let price = item.querySelector('p.price_color').innerText.replace('£', '').trim()
                                let book = {
                                    title: title,
                                    price: parseInt(price),
                                    rating: wordToNumber(starRating),
                                    url: link
                                }
                                parsedItems.push(book)
                            })
                            return parsedItems;
                        } catch (error) {
                            console.log(error)
                        }
                    }, command).then(result => {
                        this.allBooks.push.apply(this.allBooks, result)
                        console.log('allBooks length ', this.allBooks.length)
                    })
                    return true
                } catch (error) {
                    console.log("error", error)
                    return false
                }
            case "getItemDetails":
                try {
                    this.booksDetails = JSON.parse(JSON.stringify(await frame.evaluate((command) => {
                        try {
                            let item = document.querySelector(command.locatorCss);
                            let description = item.querySelector('.product_page > p:nth-child(3)').innerText.trim()
                            let upc = item.querySelector('.table > tbody:nth-child(1) > tr:nth-child(1) > td:nth-child(2)')
                                .innerText.trim()
                            let nrOfReviews = item.querySelector('.table > tbody:nth-child(1) > tr:nth-child(7) > td:nth-child(2)')
                                .innerText.trim()
                            let availability = item.querySelector('.table > tbody:nth-child(1) > tr:nth-child(6) > td:nth-child(2)')
                                .innerText.replace('In stock (', '').replace(' available)', '')
                            let details = {
                                description: description,
                                upc: upc,
                                nrOfReviews: parseInt(nrOfReviews),
                                availability: parseInt(availability)
                            }
                            return details;
                        } catch (error) {
                            console.log(error)
                            return error
                        }

                    }, command))) 
                    console.log(this.booksDetails)
                    return true
                } catch (error) {
                    console.log("error", error)
                    return false
                }
        }
    }

    sleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms))
    }

    async getAllBooks() {
        await this.runPuppeteer()
        return this.allBooks
    }

    async getBooksDetails() {
        await this.runPuppeteer()
        return this.booksDetails
    }
}

module.exports = { PuppeteerManager }

このステップでは、モジュールPuppeteerを使用してpuppeteerManager.jsファイルを作成しました。 このファイルは、スクレーパーのコアを形成します。 次のセクションでは、server.jsファイルを作成します。

ステップ4—2番目のスクレーパーファイルを作成する

このステップでは、server.jsファイル(アプリケーションサーバーの後半)を作成します。 このファイルは、どのデータをスクレイプするかを指示する情報を含むリクエストを受信し、そのデータをクライアントに返します。

server.jsファイルを作成し、それを開きます。

  1. nano server.js

次のコードを追加します。

server.js
const express = require('express');
const bodyParser = require('body-parser')
const os = require('os');

const PORT = 5000;
const app = express();
let timeout = 1500000

app.use(bodyParser.urlencoded({ extended: true }))
app.use(bodyParser.json())

let browsers = 0
let maxNumberOfBrowsers = 5

このコードブロックでは、モジュールexpressおよびbody-parserが必要でした。 これらのモジュールは、HTTP要求を処理できるアプリケーションサーバーを作成するために必要です。 expressモジュールはアプリケーションサーバーを作成し、body-parserモジュールはミドルウェア内の着信要求本文を解析してから、本文の内容を取得します。 次に、osモジュールが必要でした。このモジュールは、アプリケーションを実行しているマシンの名前を取得します。 その後、アプリケーションのポートを指定し、変数browsersおよびmaxNumberOfBrowsersを作成しました。 これらの変数は、サーバーが作成できるブラウザーインスタンスの数を管理するのに役立ちます。 この場合、アプリケーションは5つのブラウザインスタンスの作成に制限されています。つまり、スクレーパーは5つのページから同時にデータを取得できます。

当社のWebサーバーには、//api/books、および/api/booksDetailsのルートがあります。

server.jsファイルの下部で、次のコードを使用して/ルートを定義します。

server.js
. . .

app.get('/', (req, res) => {
  console.log(os.hostname())
  let response = {
    msg: 'hello world',
    hostname: os.hostname().toString()
  }
  res.send(response);
});

/ルートを使用して、アプリケーションサーバーが実行されているかどうかを確認します。 このルートに送信されたGETリクエストは、「helloworld」とだけ表示されるmsgとマシンを識別するhostnameの2つのプロパティを含むオブジェクトを返します。アプリケーションサーバーのインスタンスが実行されています。

次に、/api/booksルートを定義します。

server.jsファイルの下部に、次のコードを追加します。

server.js
. . .

app.post('/api/books', async (req, res) => {
  req.setTimeout(timeout);
  try {
    let data = req.body
    console.log(req.body.url)
    while (browsers == maxNumberOfBrowsers) {
      await sleep(1000)
    }
    await getBooksHandler(data).then(result => {
      let response = {
        msg: 'retrieved books ',
        hostname: os.hostname(),
        books: result
      }
      console.log('done')
      res.send(response)
    })
  } catch (error) {
    res.send({ error: error.toString() })
  }
});

/api/booksルートは、スクレーパーに特定のWebページ上の本に関連するメタデータを取得するように要求します。 このルートへのPOSTリクエストは、実行中のbrowsersの数がmaxNumberOfBrowsersと等しいかどうかを確認し、等しくない場合は、メソッド[X147Xを呼び出します。 ]。 このメソッドは、PuppeteerManagerクラスの新しいインスタンスを作成し、本のメタデータを取得します。 メタデータを取得すると、応答本文でクライアントに返されます。 応答オブジェクトには、retrieved books を読み取る文字列msg、メタデータを含む配列books、および別の文字列hostnameが含まれます。これにより、アプリケーションが実行されているマシン/コンテナ/ポッドの名前が返されます。

定義する最後のルートが1つあります:/api/booksDetails

server.jsファイルの最後に次のコードを追加します。

server.js
. . .

app.post('/api/booksDetails', async (req, res) => {
  req.setTimeout(timeout);
  try {
    let data = req.body
    console.log(req.body.url)
    while (browsers == maxNumberOfBrowsers) {
      await sleep(1000)
    }
    await getBookDetailsHandler(data).then(result => {
      let response = {
        msg: 'retrieved book details',
        hostname: os.hostname(),
        url: req.body.url,
        booksDetails: result
      }
      console.log('done', response)
      res.send(response)
    })
  } catch (error) {
    res.send({ error: error.toString() })
  }
});

POSTリクエストを/api/booksDetailsルートに送信すると、スクレーパーは特定の本の不足している情報を取得するように求められます。 アプリケーションサーバーは、実行中のbrowsersの数がmaxNumberOfBrowsersと等しいかどうかを確認します。 そうである場合は、sleep()メソッドを呼び出し、1秒待ってから再度チェックします。等しくない場合は、メソッドgetBookDetailsHandler()を呼び出します。 getBooksHandler()メソッドと同様に、このメソッドはPuppeteerManagerクラスの新しいインスタンスを作成し、不足している情報を取得します。

次に、プログラムは、取得したデータを応答本文でクライアントに返します。 応答オブジェクトには、retrieved book detailsという文字列msg、アプリケーションを実行しているマシンの名前を返す文字列hostname、および別の文字列[ X179X]