序章

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に移動します。 データがどのように構造化されているか、および同時スクレイピングが最適なソリューションである理由を調べます。

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

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

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

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

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

すべての本は中にあります <section> タグ、および各本は独自の下にリストされています <li> 鬼ごっこ。 それぞれの中に <li> タグがあります <article> タグ付き class 等しい属性 product_pod. これが私たちが削りたい要素です。

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

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

抽出したい不足している情報は、ここでも、 <article> タグ付き class 等しい属性 product_page.

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

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

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

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

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

  1. mkdir concurrent-webscraper

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

  1. cd ./concurrent-webscraper

次に、という名前の3つのサブディレクトリを作成します server, client、 と k8s:

  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"
  }
}

ここで変更しました mainscripts プロパティ、およびあなたも編集しました 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"
}

ここで変更しました mainscripts プロパティ。

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

  1. npm install axios lowdb --save

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

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

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

このステップとステップ4では、サーバー側にスクレーパーを作成します。 このアプリケーションは、次の2つのファイルで構成されます。 puppeteerManager.jsserver.js. The 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 次のプロパティがあります。 description, locatorCss、 と type. description 何を教えてくれます command します、 locatorCss で適切な要素を見つけます DOM、 と type 特定のアクションを選択します。
  • nrOfPages:このプロパティは整数を保持します。これは、アプリケーションが何回かを決定するために使用します。 commands 繰り返す必要があります。 たとえば、 books.toscrape.com は、1ページあたり20冊の本しか表示しないため、20ページすべてで400冊すべてを取得するには、このプロパティを使用して既存の本を繰り返します。 commands 20回。

このコードブロックでは、受け取ったオブジェクトのプロパティもコンストラクター変数に割り当てました url, existingCommands、 と nrOfPages. 次に、2つの追加の変数を作成しました。 allBooksbooksDetails. 変数を使用します 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 ページごとに commands 配列。 ただし、最後のページに到達しても、最後のページは追加されません command の中に existingCommands への配列 commands 最後の配列 command 次のページボタンをクリックします。

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

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

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

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

        . . .
    }

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

これで、新しい動作にいくつかの動作を追加する準備ができました page URLをナビゲートする方法を含むオブジェクト。

の下部に 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’)イベントを使用して、ブラウザコンテキストでメッセージを表示しようとする試みをインターセプトします。 The page 次に、特定の場所に移動します url page.goto()メソッドを使用します。

次に、いくつかの動作を追加します 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()
    }

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

内部 while ループ、コードはすべてを通過します command の中に commands 配列。 まず、 page.frames()メソッドを使用して、ページにアタッチされたすべてのフレームの配列を作成します。 でDOM要素を検索します frame のオブジェクト page frame.waitForSelector()メソッドlocatorCss 財産。 要素が見つかった場合、それは呼び出します executeCommand() メソッドとパス frame そしてその command パラメータとしてのオブジェクト。 後に executeCommand 戻ります、それは呼び出します sleep() メソッド。コードを1秒間待機させてから、次のコードを実行します。 command. 最後に、コマンドがなくなると、 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();
    }
}

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

コーディング executeCommand() 方法

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

の下部に 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() 方法。 このメソッドは、2つの引数を想定しています。 frame ページ要素と command コマンドを含むオブジェクト。 このメソッドは、 switch 次の場合のステートメント: click, getItems、 と getItemDetails.

を定義する click 場合。

交換 break; 下に case "click": 次のコードで:

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
                }
        . . .        
    }

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

次をプログラムします case 声明。

交換 break; 下に case "getItems": 次のコードで:

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
                }
        . . .
    }

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

これで、ファイナルを定義できます case 声明。

交換 break; 下に case "getItemDetails" 次のコードで:

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
                }
    }

The getItemDetails ケースは次の場合にトリガーされます command.type に等しい getItemDetails. あなたは frame.evaluate().querySelector() ブラウザのコンテキストを切り替えて解析するためのメソッド DOM. しかし今回は、特定の本の不足している詳細を取得しました 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
                }
        }
    }
}

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

コーディング sleep() 方法

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

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

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

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

次に、最後の2つのメソッドを PuppeteerManager クラス: 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

このコードブロックでは、モジュールが必要でした expressbody-parser. これらのモジュールは、処理可能なアプリケーションサーバーを作成するために必要です。 HTTP リクエスト。 The express モジュールはアプリケーションサーバーを作成し、 body-parser モジュールは、本文の内容を取得する前に、ミドルウェア内の着信要求本文を解析します。 次に、 os モジュール。アプリケーションを実行しているマシンの名前を取得します。 その後、アプリケーションのポートを指定し、変数を作成しました browsersmaxNumberOfBrowsers. これらの変数は、サーバーが作成できるブラウザーインスタンスの数を管理するのに役立ちます。 この場合、アプリケーションは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);
});

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

次に、 /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() })
  }
});

The /api/books routeは、スクレーパーに、特定のWebページ上の本に関連するメタデータを取得するように要求します。 A POST このルートへのリクエストは、 browsers 実行中は maxNumberOfBrowsers、そうでない場合は、メソッドを呼び出します getBooksHandler(). このメソッドは、の新しいインスタンスを作成します PuppeteerManager クラスを作成し、本のメタデータを取得します。 メタデータを取得すると、応答本文でクライアントに返されます。 応答オブジェクトには文字列が含まれます。 msg、それは読む retrieved books 、配列、 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 クラスを作成し、不足している情報を取得します。

次に、プログラムは、取得したデータを応答本文でクライアントに返します。 応答オブジェクトには文字列が含まれます。 msg、と言って retrieved book details、 文字列、 hostname、アプリケーションを実行しているマシンの名前と別の文字列を返します。 url、プロジェクトページのURLを含みます。 配列も含まれます、 booksDetails、本の不足しているすべての情報が含まれています。

Webサーバーには次の機能もあります。 getBooksHandler(), getBookDetailsHandler()、 と sleep().

から始めます getBooksHandler() 関数。

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

server.js
. . .

async function getBooksHandler(arg) {
  let pMng = require('./puppeteerManager')
  let puppeteerMng = new pMng.PuppeteerManager(arg)
  browsers += 1
  try {
    let books = await puppeteerMng.getAllBooks().then(result => {
      return result
    })
    browsers -= 1
    return books
  } catch (error) {
    browsers -= 1
    console.log(error)
  }
}

The getBooksHandler() 関数は、の新しいインスタンスを作成します PuppeteerManager クラス。 それはの数を増やします browsers 1つずつ実行し、本を取得するために必要な情報を含むオブジェクトを渡してから、 getAllBooks() 方法。 データが取得された後、それはの数を減らします browsers 1つ実行してから、新しく取得したデータを /api/books ルート。

次に、次のコードを追加して、 getBookDetailsHandler() 関数:

server.js
. . .

async function getBookDetailsHandler(arg) {
  let pMng = require('./puppeteerManager')
  let puppeteerMng = new pMng.PuppeteerManager(arg)
  browsers += 1
  try {
    let booksDetails = await puppeteerMng.getBooksDetails().then(result => {
      return result
    })
    browsers -= 1
    return booksDetails
  } catch (error) {
    browsers -= 1
    console.log(error)
  }
}

The getBookDetailsHandler() 関数は、の新しいインスタンスを作成します PuppeteerManager クラス。 それはちょうどのように機能します getBooksHandler() 各本の欠落しているメタデータを処理し、それを /api/booksDetails ルート。

あなたの下部に server.js ファイルに次のコードを追加して、 sleep() 関数:

server.js
  function sleep(ms) {
    console.log(' running maximum number of browsers')
    return new Promise(resolve => setTimeout(resolve, ms))
  }

The sleep() 関数は、コードを特定の時間待機させます。 browsers に等しい maxNumberOfBrowsers. この関数に整数を渡します。この整数は、コードがチェックできるようになるまで待機する必要がある時間をミリ秒単位で表します。 browsers に等しい maxNumberOfBrowsers.

これでファイルが完成しました。

必要なすべてのルートと機能を作成した後、 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

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

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() })
  }
});


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() })
  }
});

async function getBooksHandler(arg) {
  let pMng = require('./puppeteerManager')
  let puppeteerMng = new pMng.PuppeteerManager(arg)
  browsers += 1
  try {
    let books = await puppeteerMng.getAllBooks().then(result => {
      return result
    })
    browsers -= 1
    return books
  } catch (error) {
    browsers -= 1
    console.log(error)
  }
}

async function getBookDetailsHandler(arg) {
  let pMng = require('./puppeteerManager')
  let puppeteerMng = new pMng.PuppeteerManager(arg)
  browsers += 1
  try {
    let booksDetails = await puppeteerMng.getBooksDetails().then(result => {
      return result
    })
    browsers -= 1
    return booksDetails
  } catch (error) {
    browsers -= 1
    console.log(error)
  }
}

function sleep(ms) {
  console.log(' running maximum number of browsers')
  return new Promise(resolve => setTimeout(resolve, ms))
}

app.listen(PORT);
console.log(`Running on port: ${PORT}`);

この手順で、アプリケーションサーバーの作成が完了しました。 次のステップでは、アプリケーションサーバーのイメージを作成し、それをKubernetesクラスターにデプロイします。

ステップ5—Dockerイメージを構築する

このステップでは、スクレーパーアプリケーションを含むDockerイメージを作成します。 ステップ6では、そのイメージをKubernetesクラスターにデプロイします。

アプリケーションのDockerイメージを作成するには、Dockerfileを作成してから、コンテナーをビルドする必要があります。

あなたがまだにいることを確認してください ./server フォルダ。

次に、Dockerfileを作成して開きます。

  1. nano Dockerfile

中に次のコードを書いてください Dockerfile:

Dockerfile
FROM node:10

RUN apt-get update

RUN apt-get install -yyq ca-certificates

RUN apt-get install -yyq libappindicator1 libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libnss3 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6

RUN apt-get install -yyq gconf-service lsb-release wget xdg-utils

RUN apt-get install -yyq fonts-liberation

WORKDIR /usr/src/app

COPY package*.json ./

RUN npm install

COPY . .

EXPOSE 5000
CMD [ "node", "server.js" ]

このブロックのコードのほとんどは、Dockerfileの標準のコマンドラインコードです。 から画像を作成しました node:10 画像。 次に、 RUN DockerコンテナでPuppeteerを実行するために必要なパッケージをインストールするコマンドを実行してから、アプリディレクトリを作成しました。 スクレーパーをコピーしました package.json ファイルをアプリディレクトリに追加し、内部で指定された依存関係をインストールしました package.json ファイル。 最後に、アプリソースをバンドルし、ポートでアプリを公開しました 5000、および選択 server.js エントリファイルとして。

次に、 .dockerignore ファイルを開いて開きます。 これにより、機密性の高い不要なファイルをバージョン管理できなくなります。

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

  1. nano .dockerignore

次のコンテンツをファイルに追加します。

./server/.dockerignore
node_modules
npm-debug.log

作成後 Dockerfile そしてその .dockerignore ファイルの場合、アプリケーションのDockerイメージをビルドして、DockerHubアカウントのリポジトリにプッシュできます。 イメージをプッシュする前に、DockerHubアカウントにサインインしていることを確認してください。

Docker Hubにサインインします:

  1. docker login --username=your_username --password=your_password

イメージを作成します。

  1. docker build -t your_username/concurrent-scraper .

次に、スクレーパーをテストします。 このテストでは、各ルートにリクエストを送信します。

まず、アプリを起動します。

  1. docker run -p 5000:5000 -d your_username/concurrent-scraper

今すぐ使用 curl 送信するには GET にリクエスト / ルート:

  1. curl http://localhost:5000/

を送信することによって GET にリクエスト / ルート、あなたはを含む応答を受け取る必要があります msg 言って hello worldhostname. これ hostname DockerコンテナのIDです。 これと同様の出力が表示されますが、マシンの一意のIDは次のとおりです。

Output
{"msg":"hello world","hostname":"0c52d53f97d3"}

今すぐ送信します POST にリクエスト /api/books 1つのWebページに表示されるすべての本のメタデータを取得するためのルート:

  1. curl --header "Content-Type: application/json" --request POST --data '{"url": "http://books.toscrape.com/index.html" , "nrOfPages":1 , "commands":[{"description": "get items metadata", "locatorCss": ".product_pod","type": "getItems"},{"description": "go to next page","locatorCss": ".next > a:nth-child(1)","type": "Click"}]}' http://localhost:5000/api/books

を送信することによって POST にリクエスト /api/books ルートあなたはを含む応答を受け取ります msg 言って retrieved books hostname 前のリクエストと同様で、 books books.toscrapeWebサイトの最初のページに表示される20冊すべての本を含む配列。 次のような出力が表示されますが、マシンの一意のIDは次のとおりです。

Output
{"msg":"retrieved books ","hostname":"0c52d53f97d3","books":[{"title":"A Light in the Attic","price":null,"rating":0,"url":"http://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html"},{"title":"Tipping the Velvet","price":null,"rating":0,"url":"http://books.toscrape.com/catalogue/tipping-the-velvet_999/index.html"}, [ . . . ] }]}

今すぐ送信します POST にリクエスト /api/booksDetails ランダムな本の不足している情報を取得するためのルート:

  1. curl --header "Content-Type: application/json" --request POST --data '{"url": "http://books.toscrape.com/catalogue/slow-states-of-collapse-poems_960/index.html" , "nrOfPages":1 , "commands":[{"description": "get item details", "locatorCss": "article.product_page","type": "getItemDetails"}]}' http://localhost:5000/api/booksDetails

を送信することによって POST にリクエスト /api/booksDetails ルートあなたはを含む応答を受け取ります msg 言って retrieved book detailsbooksDetails この本の欠落している詳細を含むオブジェクト、 url 製品のページのアドレス、および hostname 前のリクエストのように。 次のような出力が表示されます。

Output
{"msg":"retrieved book details","hostname":"0c52d53f97d3","url":"http://books.toscrape.com/catalogue/slow-states-of-collapse-poems_960/index.html","booksDetails":{"description":"The eagerly anticipated debut from one of Canada’s most exciting new poets In her debut collection, Ashley-Elizabeth Best explores the cultivation of resilience during uncertain and often trying times [...]","upc":"b4fd5943413e089a","nrOfReviews":0,"availability":17}}

もしあなたの curl コマンドが正しい応答を返さない場合は、ファイル内のコードを確認してください puppeteerManager.jsserver.js 前の2つのステップの最後のコードブロックと一致します。 また、Dockerコンテナが実行されており、クラッシュしていないことを確認してください。 これを行うには、Dockerイメージを実行しようとします。 -d オプション(このオプションはDockerイメージをデタッチモードで実行します)、次に送信します HTTP ルートの1つにリクエストします。

Dockerイメージを実行しようとしてもエラーが発生する場合は、実行中のすべてのコンテナーを停止し、スクレーパーイメージを実行せずに実行してみてください。 -d オプション。

まず、すべてのコンテナを停止します。

  1. docker stop $(docker ps -a -q)

次に、Dockerコマンドを実行します。 -d 国旗:

  1. docker run -p 5000:5000 your_username/concurrent-scraper

エラーが発生しない場合は、ターミナルウィンドウをクリーンアップします。

  1. clear

画像のテストに成功したので、画像をリポジトリに送信できます。 イメージをDockerHubアカウントのリポジトリにプッシュします。

  1. docker push your_username/concurrent-scraper:latest

スクレーパーアプリケーションがDockerHubでイメージとして利用できるようになったので、Kubernetesにデプロイする準備が整いました。 これが次のステップになります。

ステップ6—スクレーパーをKubernetesにデプロイする

スクレーパーイメージがビルドされてリポジトリにプッシュされると、デプロイの準備が整います。

まず、 kubectl と呼ばれる新しい名前空間を作成します concurrent-scraper-context:

  1. kubectl create namespace concurrent-scraper-context

設定 concurrent-scraper-context デフォルトのコンテキストとして:

  1. kubectl config set-context --current --namespace=concurrent-scraper-context

アプリケーションのデプロイメントを作成するには、次のファイルを作成する必要があります。 app-deployment.yaml、ただし、最初に、に移動する必要があります k8s プロジェクト内のディレクトリ。 これは、すべてのKubernetesファイルを保存する場所です。

に移動します k8s プロジェクト内のディレクトリ:

  1. cd ../k8s

を作成します app-deployment.yaml ファイルを開いて開きます。

  1. nano app-deployment.yaml

中に次のコードを書いてください app-deployment.yaml. 必ず交換してください your_DockerHub_username 一意のユーザー名を使用:

./k8s/app-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: scraper
  labels:
    app: scraper
spec:
  replicas: 5
  selector:
    matchLabels:
      app: scraper
  template:
    metadata:
      labels:
        app: scraper
    spec:
      containers:
      - name: concurrent-scraper
        image: your_DockerHub_username/concurrent-scraper
        ports:
        - containerPort: 5000

前のブロックのコードのほとんどは、Kubernetesの標準です deployment ファイル。 まず、アプリのデプロイの名前をに設定します scraper、次にポッドの数をに設定します 5、次にコンテナの名前をに設定します concurrent-scraper. その後、アプリのビルドに使用する画像を次のように指定しました your_DockerHub_username/concurrent-scraper、ただし、実際のDockerHubユーザー名を使用します。 最後に、アプリでポートを使用するように指定しました 5000.

デプロイファイルを作成すると、アプリをクラスターにデプロイする準備が整います。

アプリをデプロイします。

  1. kubectl apply -f app-deployment.yaml

次のコマンドを実行して、展開のステータスを監視できます。

  1. kubectl get deployment -w

コマンドを実行すると、次のような出力が表示されます。

Output
NAME READY UP-TO-DATE AVAILABLE AGE scraper 0/5 5 0 7s scraper 1/5 5 1 23s scraper 2/5 5 2 25s scraper 3/5 5 3 25s scraper 4/5 5 4 33s scraper 5/5 5 5 33s

すべてのデプロイメントが実行を開始するまでに数秒かかりますが、実行が開始されると、スクレーパーの5つのインスタンスが実行されます。 各インスタンスは5ページを同時にスクレイプできるため、25ページを同時にスクレイプできるため、400ページすべてをスクレイプするのに必要な時間が短縮されます。

クラスタの外部からアプリにアクセスするには、を作成する必要があります service. これ service ロードバランサーになり、と呼ばれるファイルが必要になります load-balancer.yaml.

を作成します load-balancer.yaml ファイルを開いて開きます。

  1. nano load-balancer.yaml

中に次のコードを書いてください load-balancer.yaml:

load-balancer.yaml
apiVersion: v1
kind: Service
metadata:
  name: load-balancer
  labels:
    app: scraper
spec:
  type: LoadBalancer
  ports:
  - port: 80
    targetPort: 5000
    protocol: TCP
  selector:
    app: scraper

前のブロックのコードのほとんどは、 service ファイル。 まず、サービスの名前をに設定します load-balancer. サービスタイプを指定してから、ポートでサービスにアクセスできるようにしました 80. 最後に、このサービスがアプリ用であることを指定しました。 scraper.

これで、 load-balancer.yaml ファイル、サービスをクラスターにデプロイします。

サービスを展開します。

  1. kubectl apply -f load-balancer.yaml

次のコマンドを実行して、サービスのステータスを監視します。

  1. kubectl get services -w

このコマンドを実行すると、次のような出力が表示されますが、外部IPが表示されるまでに数秒かかります。

Output
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE load-balancer LoadBalancer 10.245.91.92 <pending> 80:30802/TCP 10s load-balancer LoadBalancer 10.245.91.92 161.35.252.69 80:30802/TCP 69s

あなたのサービスの EXTERNAL-IPCLUSTER-IP 上記のものとは異なります。 あなたのメモをとる EXTERNAL-IP. 次のセクションで使用します。

このステップでは、スクレーパーアプリケーションをKubernetesクラスターにデプロイしました。 次のステップでは、新しくデプロイされたアプリケーションと対話するためのクライアントアプリケーションを作成します。

ステップ7—クライアントアプリケーションの作成

このステップでは、クライアントアプリケーションをビルドします。これには、次の3つのファイルが必要です。 main.js, lowdbHelper.js、 と books.json. The main.js fileは、クライアントアプリケーションのメインファイルです。 アプリケーションサーバーにリクエストを送信し、取得したデータを、内部で作成するメソッドを使用して保存します。 lowdbHelper.js ファイル。 The lowdbHelper.js fileは、データをローカルファイルに保存し、その中のデータを取得します。 The books.json fileは、スクレイピングされたすべてのデータを保存するローカルファイルです。

最初にあなたに戻る client ディレクトリ:

  1. cd ../client

彼らはよりも小さいので main.js、作成します lowdbHelper.jsbooks.json 最初にファイル。

と呼ばれるファイルを作成して開きます lowdbHelper.js:

  1. nano lowdbHelper.js

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

lowdbHelper.js
const lowdb = require('lowdb')
const FileSync = require('lowdb/adapters/FileSync')
const adapter = new FileSync('books.json')

このコードブロックでは、モジュールが必要です lowdb その後、アダプターが必要でした FileSync、データを保存して読み取る必要があります。 次に、と呼ばれるJSONファイルにデータを保存するようにプログラムに指示します。 books.json.

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

lowdbHelper.js
. . .
class LowDbHelper {
    constructor() {
        this.db = lowdb(adapter);
    }

    getData() {
        try {
            let data = this.db.getState().books
            return data
        } catch (error) {
            console.log('error', error)
        }
    }

    saveData(arg) {
        try {
            this.db.set('books', arg).write()
            console.log('data saved successfully!!!')
        } catch (error) {
            console.log('error', error)
        }
    }
}

module.exports = { LowDbHelper }

ここで、というクラスを作成しました LowDbHelper. このクラスには、次の2つのメソッドが含まれています。 getData()saveData(). 1つ目は、内部に保存されている本を取得します books.json 2番目はあなたの本を同じファイルに保存します。

完成しました lowdbHelper.js 次のようになります。

lowdbHelper.js
const lowdb = require('lowdb')
const FileSync = require('lowdb/adapters/FileSync')
const adapter = new FileSync('books.json')

class LowDbHelper {
    constructor() {
        this.db = lowdb(adapter);
    }

    getData() {
        try {
            let data = this.db.getState().books
            return data
        } catch (error) {
            console.log('error', error)
        }
    }

    saveData(arg) {
        try {
            this.db.set('books', arg).write()
            //console.log('data saved successfully!!!')
        } catch (error) {
            console.log('error', error)
        }
    }

}

module.exports = { LowDbHelper }

これで、 lowdbHelper.js ファイル、それは作成する時間です books.json ファイル。

を作成します books.json ファイルを開いて開きます。

  1. nano books.json

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

books.json
{
    "books": []
}

The books.json ファイルは、というプロパティを持つオブジェクトで構成されています books. このプロパティの初期値は空の配列です。 後で本を取得するときに、プログラムがそれらを保存する場所です。

これで、 lowdbHelper.js そしてその books.json ファイル、あなたは作成します main.js ファイル。

作成 main.js そしてそれを開きます:

  1. nano main.js

次のコードをに追加します main.js:

main.js
let axios = require('axios')
let ldb = require('./lowdbHelper.js').LowDbHelper
let ldbHelper = new ldb()
let allBooks = ldbHelper.getData()

let server = "http://your_load_balancer_external_ip_address"
let podsWorkDone = []
let booksDetails = []
let errors = []

このコードのチャンクでは、 lowdbHelper.js ファイルと呼ばれるモジュール axios. 使用します axios 送信する HTTP スクレーパーへのリクエスト。 the lowdbHelper.js ファイルは取得した本を保存し、 allBooks 変数は、に保存されたすべての本を保存します books.json ファイル。 本を取得する前に、この変数は空の配列を保持します。 the server 変数は EXTERNAL-IP 前のセクションで作成したロードバランサーの 必ずこれを一意のIPに置き換えてください。 The podsWorkDone 変数は、スクレーパーの各インスタンスが処理したページ数を追跡します。 The booksDetails 変数は、個々の本について取得された詳細を格納し、 errors 変数は、本を取得しようとしたときに発生する可能性のあるエラーを追跡します。

次に、スクレーパープロセスの各部分に対していくつかの関数を作成する必要があります。

次のコードブロックをの下部に追加します main.js ファイル:

main.js
. . .
function main() {
  let execute = process.argv[2] ? process.argv[2] : 0
  execute = parseInt(execute)
  switch (execute) {
    case 0:
      getBooks()
      break;
    case 1:
      getBooksDetails()
      break;
  }
}

これで、という関数を作成しています main()、は、いずれかを呼び出すswitchステートメントで構成されます。 getBooks() また getBooksDetails() 渡された入力に基づく関数。

交換してください break;getBooks() 次のコードで:

main.js
. . .
function getBooks() {
  console.log('getting books')
  let data = {
    url: 'http://books.toscrape.com/index.html',
    nrOfPages: 20,
    commands: [
      {
        description: 'get items metadata',
        locatorCss: '.product_pod',
        type: "getItems"
      },
      {
        description: 'go to next page',
        locatorCss: '.next > a:nth-child(1)',
        type: "Click"
      }
    ],
  }
  let begin = Date.now();
  axios.post(`${server}/api/books`, data).then(result => {
    let end = Date.now();
    let timeSpent = (end - begin) / 1000 + "secs";
    console.log(`took ${timeSpent} to retrieve ${result.data.books.length} books`)
    ldbHelper.saveData(result.data.books)
  })
}

ここでは、という関数を作成しました getBooks(). このコードは、20ページすべてをスクレイピングするために必要な情報を含むオブジェクトを次の変数に割り当てます。 data. 最初 command の中に commands このオブジェクトの配列は、ページに表示されている20冊の本すべてを取得し、2番目の本は command ページの次のボタンをクリックして、ブラウザで次のページに移動します。 これは、最初の command 20回、2回目は19回繰り返します。 A POST を使用して送信されたリクエスト axios/api/books routeはこのオブジェクトをアプリケーションサーバーに送信し、スクレーパーは books.toscrapeWebサイトの最初の20ページに表示されるすべての本の基本的なメタデータを取得します。 次に、取得したデータを使用して保存します LowDbHelper 内部のクラス lowdbHelper.js ファイル。

次に、2番目の関数をコーディングします。この関数は、個々のページのより具体的な本のデータを処理します。

交換してください break;getBooksDetails() 次のコードで:

main.js
. . .

function getBooksDetails() {
  let begin = Date.now()
  for (let j = 0; j < allBooks.length; j++) {
    let data = {
      url: allBooks[j].url,
      nrOfPages: 1,
      commands: [
        {
          description: 'get item details',
          locatorCss: 'article.product_page',
          type: "getItemDetails"
        }
      ]
    }
    sendRequest(data, function (result) {
      parseResult(result, begin)
    })
  }
}

The getBooksDetails() 関数は通過します allBooks すべての本を保持する配列、およびこの配列内の各本について、ページをスクレイピングするために必要な情報を含むオブジェクトを作成します。 このオブジェクトを作成した後、それをに渡します sendRequest() 関数。 次に、その値を使用します sendRequest() 関数はこの値を返し、呼び出された関数に渡します parseResult().

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

main.js
. . .

async function sendRequest(payload, cb) {
  let book = payload
  try {
    await axios.post(`${server}/api/booksDetails`, book).then(response => {
      if (Object.keys(response.data).includes('error')) {
        let res = {
          url: book.url,
          error: response.data.error
        }
        cb(res)
      } else {
        cb(response.data)
      }
    })
  } catch (error) {
    console.log(error)
    let res = {
      url: book.url,
      error: error
    }
    cb({ res })
  }
}

今、あなたはという関数を作成しています sendRequest(). この関数を使用して、スクレーパーを含むアプリケーションサーバーに400個のリクエストすべてを送信します。 このコードは、ページをスクレイピングするために必要な情報を含むオブジェクトを、という変数に割り当てます。 book. 次に、このオブジェクトを POST にリクエスト /api/booksDetails アプリケーションサーバーでルーティングします。 応答はに返送されます getBooksDetails() 関数。

次に、を作成します parseResult() 関数。

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

main.js
. . .

function parseResult(result, begin){
  try {
    let end = Date.now()
    let timeSpent = (end - begin) / 1000 + "secs ";
    if (!Object.keys(result).includes("error")) {
      let wasSuccessful = Object.keys(result.booksDetails).length > 0 ? true : false
      if (wasSuccessful) {
        let podID = result.hostname
        let podsIDs = podsWorkDone.length > 0 ? podsWorkDone.map(pod => { return Object.keys(pod)[0]}) : []
        if (!podsIDs.includes(podID)) {
          let podWork = {}
          podWork[podID] = 1
          podsWorkDone.push(podWork)
        } else {
          for (let pwd = 0; pwd < podsWorkDone.length; pwd++) {
            if (Object.keys(podsWorkDone[pwd]).includes(podID)) {
              podsWorkDone[pwd][podID] += 1
              break
            }
          }
        }
        booksDetails.push(result)
      } else {
        errors.push(result)
      }
    } else {
      errors.push(result)
    }
    console.log('podsWorkDone', podsWorkDone, ', retrieved ' + booksDetails.length + " books, ",
      "took " + timeSpent + ", ", "used " + podsWorkDone.length + " pods", " errors: " + errors.length)
    saveBookDetails()
  } catch (error) {
    console.log(error)
  }
}

parseResult() 受け取る result 関数の sendRequest() 不足している本の詳細が含まれています。 次に、 result とを取得します hostname リクエストを処理して割り当てたポッドの podID 変数。 これかどうかをチェックします podID すでにの一部です podsWorkDone 配列; そうでない場合は、 podIdpodsWorkDone 配列し、実行された作業の数を1に設定します。 ただし、そうであれば、このポッドによって実行される作業の数が1つ増えます。 次に、コードはを追加します resultbooksDetails 配列、全体的な進捗状況を出力します getBooksDetails() 関数を呼び出してから、 saveBookDetails() 関数。

次に、次のコードを追加して、 saveBookDetails() 関数:

main.js
. . .

function saveBookDetails() {
  let books = ldbHelper.getData()
  for (let b = 0; b < books.length; b++) {
    for (let d = 0; d < booksDetails.length; d++) {
      let item = booksDetails[d]
      if (books[b].url === item.url) {
        books[b].booksDetails = item.booksDetails
        break
      }
    }
  }
  ldbHelper.saveData(books)
}

main()

saveBookDetails() に保存されているすべての本を取得します books.json を使用してファイル LowDbHelper クラスと呼ばれる変数にそれを割り当てます books. 次に、ループします booksbooksDetails 同じ配列の両方の配列で要素が見つかるかどうかを確認する配列 url 財産。 含まれている場合は、 booksDetails 内の要素のプロパティ booksDetails 配列し、それをの要素に割り当てます books 配列。 次に、の内容を上書きします books.json の内容を含むファイル books この関数で配列がループしました。 作成後 saveBookDetails() 関数、コードはを呼び出します main() このファイルを使用可能にする関数。 そうしないと、このファイルを実行しても目的の結果が得られません。

完成しました main.js ファイルは次のようになります。

main.js
let axios = require('axios')
let ldb = require('./lowdbHelper.js').LowDbHelper
let ldbHelper = new ldb()
let allBooks = ldbHelper.getData()

let server = "http://your_load_balancer_external_ip_address"
let podsWorkDone = []
let booksDetails = []
let errors = []

function main() {
  let execute = process.argv[2] ? process.argv[2] : 0
  execute = parseInt(execute)
  switch (execute) {
    case 0:
      getBooks()
      break;
    case 1:
      getBooksDetails()
      break;
  }
}

function getBooks() {
  console.log('getting books')
  let data = {
    url: 'http://books.toscrape.com/index.html',
    nrOfPages: 20,
    commands: [
      {
        description: 'get items metadata',
        locatorCss: '.product_pod',
        type: "getItems"
      },
      {
        description: 'go to next page',
        locatorCss: '.next > a:nth-child(1)',
        type: "Click"
      }
    ],
  }
  let begin = Date.now();
  axios.post(`${server}/api/books`, data).then(result => {
    let end = Date.now();
    let timeSpent = (end - begin) / 1000 + "secs";
    console.log(`took ${timeSpent} to retrieve ${result.data.books.length} books`)
    ldbHelper.saveData(result.data.books)
  })
}

function getBooksDetails() {
  let begin = Date.now()
  for (let j = 0; j < allBooks.length; j++) {
    let data = {
      url: allBooks[j].url,
      nrOfPages: 1,
      commands: [
        {
          description: 'get item details',
          locatorCss: 'article.product_page',
          type: "getItemDetails"
        }
      ]
    }
    sendRequest(data, function (result) {
      parseResult(result, begin)
    })
  }
}

async function sendRequest(payload, cb) {
  let book = payload
  try {
    await axios.post(`${server}/api/booksDetails`, book).then(response => {
      if (Object.keys(response.data).includes('error')) {
        let res = {
          url: book.url,
          error: response.data.error
        }
        cb(res)
      } else {
        cb(response.data)
      }
    })
  } catch (error) {
    console.log(error)
    let res = {
      url: book.url,
      error: error
    }
    cb({ res })
  }
}

function parseResult(result, begin){
  try {
    let end = Date.now()
    let timeSpent = (end - begin) / 1000 + "secs ";
    if (!Object.keys(result).includes("error")) {
      let wasSuccessful = Object.keys(result.booksDetails).length > 0 ? true : false
      if (wasSuccessful) {
        let podID = result.hostname
        let podsIDs = podsWorkDone.length > 0 ? podsWorkDone.map(pod => { return Object.keys(pod)[0]}) : []
        if (!podsIDs.includes(podID)) {
          let podWork = {}
          podWork[podID] = 1
          podsWorkDone.push(podWork)
        } else {
          for (let pwd = 0; pwd < podsWorkDone.length; pwd++) {
            if (Object.keys(podsWorkDone[pwd]).includes(podID)) {
              podsWorkDone[pwd][podID] += 1
              break
            }
          }
        }
        booksDetails.push(result)
      } else {
        errors.push(result)
      }
    } else {
      errors.push(result)
    }
    console.log('podsWorkDone', podsWorkDone, ', retrieved ' + booksDetails.length + " books, ",
      "took " + timeSpent + ", ", "used " + podsWorkDone.length + " pods,", " errors: " + errors.length)
    saveBookDetails()
  } catch (error) {
    console.log(error)
  }
}

function saveBookDetails() {
  let books = ldbHelper.getData()
  for (let b = 0; b < books.length; b++) {
    for (let d = 0; d < booksDetails.length; d++) {
      let item = booksDetails[d]
      if (books[b].url === item.url) {
        books[b].booksDetails = item.booksDetails
        break
      }
    }
  }
  ldbHelper.saveData(books)
}

main()

これでクライアントアプリケーションが作成され、Kubernetesクラスターのスクレーパーとやり取りする準備が整いました。 次のステップでは、このクライアントアプリケーションとアプリケーションサーバーを使用して、400冊すべての本をスクレイプします。

ステップ8—Webサイトのスクレイピング

クライアントアプリケーションとサーバー側スクレーパーアプリケーションを作成したので、 books.toscrapeWebサイトをスクレイプします。 まず、400冊すべての本のメタデータを取得します。 次に、ページ上のすべての本の不足している詳細を取得し、各ポッドがリアルタイムで処理したリクエストの数を監視します。

の中に ./client ディレクトリで、次のコマンドを実行します。 これにより、400冊すべての書籍の基本的なメタデータが取得され、 books.json ファイル:

  1. npm start 0

次の出力が表示されます。

Output
getting books took 40.323secs to retrieve 400 books

20ページすべてに表示されている書籍のメタデータを取得するには、40.323秒かかりましたが、この値はインターネットの速度によって異なる場合があります。

ここで、に保存されているすべての本の不足している詳細を取得する必要があります。 books.json 各ポッドが処理するリクエストの数も監視しながらファイルします。

走る npm start もう一度詳細を取得するには:

  1. npm start 1

次のような出力が表示されますが、ポッドIDは異なります。

Output
. . . podsWorkDone [ { 'scraper-59cd578ff6-z8zdd': 69 }, { 'scraper-59cd578ff6-528gv': 96 }, { 'scraper-59cd578ff6-zjwfg': 94 }, { 'scraper-59cd578ff6-nk6fr': 80 }, { 'scraper-59cd578ff6-h2n8r': 61 } ] , retrieved 400 books, took 56.875secs , used 5 pods, errors: 0

Kubernetesを使用して400冊すべての書籍の欠落している詳細を取得するのに、60秒もかかりませんでした。 スクレーパーを含む各ポッドは、少なくとも60ページを削りました。 これは、1台のマシンを使用する場合に比べてパフォーマンスが大幅に向上することを表しています。

次に、Kubernetesクラスター内のポッドの数を2倍にして、取得をさらに高速化します。

  1. kubectl scale deployment scraper --replicas=10

ポッドが使用可能になるまで少し時間がかかるため、次のコマンドを実行する前に少なくとも10秒待ちます。

再実行 npm start 不足している詳細を取得するには:

  1. npm start 1

次のような出力が表示されますが、ポッドIDが異なります。

Output
. . . podsWorkDone [ { 'scraper-59cd578ff6-z8zdd': 38 }, { 'scraper-59cd578ff6-6jlvz': 47 }, { 'scraper-59cd578ff6-g2mxk': 36 }, { 'scraper-59cd578ff6-528gv': 41 }, { 'scraper-59cd578ff6-bj687': 36 }, { 'scraper-59cd578ff6-zjwfg': 47 }, { 'scraper-59cd578ff6-nl6bk': 34 }, { 'scraper-59cd578ff6-nk6fr': 33 }, { 'scraper-59cd578ff6-h2n8r': 38 }, { 'scraper-59cd578ff6-5bw2n': 50 } ] , retrieved 400 books, took 34.925secs , used 10 pods, errors: 0

ポッドの数を2倍にした後、400ページすべてをスクレイプするのに必要な時間はほぼ半分に短縮されました。 不足しているすべての詳細を取得するのに35秒もかかりませんでした。

このセクションでは、Kubernetesクラスターにデプロイされたアプリケーションサーバーに400のリクエストを送信し、短時間で400の個別のURLを取得しました。 また、パフォーマンスをさらに向上させるために、クラスター内のポッドの数を増やしました。

結論

このガイドでは、Puppeteer、Docker、Kubernetesを使用して、400のWebページを迅速にスクレイピングできる同時Webスクレイパーを構築しました。 スクレーパーと対話するために、axiosを使用して複数を送信するNode.jsアプリを作成しました HTTP スクレーパーを含むサーバーへのリクエスト。

Puppeteerには多くの追加機能が含まれています。 詳細については、Puppeteerの公式ドキュメントをご覧ください。 Node.jsの詳細については、Node.jsでコーディングする方法に関するチュートリアルシリーズをご覧ください。