Puppeteer、Node.js、Docker、Kubernetesを使用して同時Webスクレイパーを構築する方法
序章
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をスクレイプします。 他のドメインをスクレイピングすることは、このチュートリアルの範囲外です。
前提条件
このチュートリアルに従うには、次のマシンが必要です。
- Dockerがインストールされています。 手順については、Dockerをインストールして使用する方法に関するチュートリアルに従ってください。 DockerのWebサイトには、macOSやWindowsなどの他のオペレーティングシステムのインストール手順が記載されています。
- Dockerイメージを保存するためのDockerHubのアカウント。
- 接続構成が次のように設定されたKubernetes1.17+クラスター
kubectl
デフォルト。 DigitalOceanでKubernetesクラスタを作成するには、Kubernetesクイックスタートをお読みください。 クラスタに接続するには、DigitalOceanKubernetesクラスタに接続する方法をお読みください。 kubectl
インストールされています。 Kubernetesの使用を開始するためのこのチュートリアル:kubectl CheatSheetに従ってインストールしてください。- 開発マシンにインストールされているNode.js。 このチュートリアルは、Node.jsバージョン12.18.3およびnpmバージョン6.14.6でテストされました。 このガイドに従ってmacOSにNode.jsをインストールするか、このガイドに従ってさまざまなLinuxディストリビューションにNode.jsをインストールします。
- DigitalOcean Kubernetesを使用している場合は、パーソナルアクセストークンも必要になります。 作成するには、パーソナルアクセストークンの作成方法に関するガイドに従ってください。 このトークンを安全な場所に保存します。 アカウントへのフルアクセスを提供します。
ステップ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
:
- mkdir concurrent-webscraper
ディレクトリに移動します。
- cd ./concurrent-webscraper
次に、という名前の3つのサブディレクトリを作成します server
, client
、 と k8s
:
- mkdir server client k8s
に移動します server
ディレクトリ:
- cd ./server
新しいNode.jsプロジェクトを作成します。 npmの実行 init
コマンドは作成します package.json
ファイル。依存関係とメタデータを管理するのに役立ちます。
初期化コマンドを実行します。
- npm init
デフォルト値を受け入れるには、を押します ENTER
すべてのプロンプトに; または、応答をパーソナライズすることもできます。 npmの初期化設定の詳細については、チュートリアルのステップ1、npmおよびpackage.jsonでNode.jsモジュールを使用する方法を参照してください。
を開きます package.json
ファイルして編集します。
- nano package.json
変更する必要があります main
プロパティ、にいくつかの情報を追加します scripts
ディレクティブを作成し、 dependencies
指令。
ファイル内の内容を強調表示されたコードに置き換えます。
{
"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
ディレクトリ:
- cd ../client
別のNode.jsプロジェクトを作成します。
- npm init
同じ手順に従って、デフォルト設定を受け入れるか、応答をカスタマイズします。
を開きます package.json
ファイルして編集します。
- nano package.json
ファイル内の内容を強調表示されたコードに置き換えます。
{
"name": "client",
"version": "1.0.0",
"description": "",
"main": "main.js",
"scripts": {
"start": "node main.js"
},
"author": "",
"license": "ISC"
}
ここで変更しました main
と scripts
プロパティ。
今回は、npmを使用して必要な依存関係をインストールします。
- npm install axios lowdb --save
このコードブロックでは、 axios
と lowdb
. axios
約束に基づく HTTP
ブラウザとNode.jsのクライアント。 このモジュールを使用して非同期で送信します HTTP
へのリクエスト REST
スクレーパーと対話するためのスクレーパーのエンドポイント。 lowdb
は、Node.jsとブラウザ用の小さなJSONデータベースであり、スクレイピングされたデータを保存するために使用します。
このステップでは、プロジェクトディレクトリを作成し、スクレーパーを含むアプリケーションサーバーのNode.jsプロジェクトを初期化しました。 次に、アプリケーションサーバーと対話するクライアントアプリケーションに対して同じことを行いました。 また、Kubernetes構成ファイル用のディレクトリも作成しました。 次のステップでは、アプリケーションサーバーの構築を開始します。
ステップ3—最初のスクレーパーファイルを作成する
このステップとステップ4では、サーバー側にスクレーパーを作成します。 このアプリケーションは、次の2つのファイルで構成されます。 puppeteerManager.js
と server.js
. The puppeteerManager.js
ファイルはブラウザセッションを作成および管理し、 server.js
ファイルは、1つまたは複数のWebページをスクレイピングする要求を受け取ります。 次に、これらのリクエストは内部のメソッドを呼び出します puppeteerManager.js
これにより、特定のWebページがスクレイピングされ、スクレイピングされたデータが返されます。 このステップでは、 puppeteerManager.js
ファイル。 ステップ4では、 server.js
ファイル。
まず、サーバーディレクトリに戻り、というファイルを作成します puppeteerManager.js
.
に移動します server
フォルダ:
- cd ../server
を作成して開きます puppeteerManager.js
お好みのテキストエディタを使用したファイル:
- nano puppeteerManager.js
君の puppeteerManager.js
ファイルには、というクラスが含まれます PuppeteerManager
、およびこのクラスは、 Puppeteer
ブラウザインスタンス。 最初にこのクラスを作成してから、コンストラクターを追加します。
次のコードをに追加します 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つの追加の変数を作成しました。 allBooks
と booksDetails
. 変数を使用します allBooks
取得したすべての書籍と変数のメタデータを保存します booksDetails
特定の個々の本の不足している本の詳細を保存します。
これで、いくつかのメソッドをに追加する準備ができました。 PuppeteerManager
クラス。 このクラスには次のメソッドがあります。 runPuppeteer()
, executeCommand()
, sleep()
, getAllBooks()
、 と getBooksDetails()
. これらのメソッドはスクレーパーアプリケーションのコアを形成するため、1つずつ調べる価値があります。
コーディング runPuppeteer()
方法
内部の最初のメソッド PuppeteerManager
クラスは runPuppeteer()
. これには、Puppeteerモジュールが必要であり、ブラウザインスタンスを起動します。
の下部に PuppeteerManager
クラスには、次のコードを追加します。
. . .
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()
作成したメソッドに、次のコードを追加します。
. . .
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()
メソッドには、次のコードを追加します。
. . .
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()
メソッドは次のコードを追加します。
. . .
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つの変数を作成しました。 timeout
と commandIndex
. 最初の変数は、コードが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
ファイルは次のようになります。
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
クラスは次のコードを追加します。
. . .
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":
次のコードで:
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":
次のコードで:
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"
次のコードで:
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
ファイルは次のようになります。
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
クラスは次のコードを追加します。
. . .
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
クラスは次のコードを追加します。
. . .
async getAllBooks() {
await this.runPuppeteer()
return this.allBooks
}
このブロックが別のPromiseをどのように使用しているかに注意してください。
これで、最終的なメソッドを作成できます。 getBooksDetails()
. お気に入り getAllBooks()
、内部の関数 server.js
この関数を呼び出します。 getBooksDetails()
ただし、各本の不足している詳細を取得する責任があります。 また、これらの詳細を、で呼び出した関数に返します。 server.js
ファイル。
の下部に PuppeteerManager
クラスは次のコードを追加します。
. . .
async getBooksDetails() {
await this.runPuppeteer()
return this.booksDetails
}
これでコーディングが完了しました puppeteerManager.js
ファイル。
このセクションで説明する5つのメソッドを追加すると、完成したファイルは次のようになります。
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
ファイルを開いて開きます。
- nano 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
リクエスト。 The express
モジュールはアプリケーションサーバーを作成し、 body-parser
モジュールは、本文の内容を取得する前に、ミドルウェア内の着信要求本文を解析します。 次に、 os
モジュール。アプリケーションを実行しているマシンの名前を取得します。 その後、アプリケーションのポートを指定し、変数を作成しました browsers
と maxNumberOfBrowsers
. これらの変数は、サーバーが作成できるブラウザーインスタンスの数を管理するのに役立ちます。 この場合、アプリケーションは5つのブラウザインスタンスの作成に制限されています。つまり、スクレーパーは5つのページから同時にデータを取得できます。
Webサーバーには次のルートがあります。 /
, /api/books
、 と /api/booksDetails
.
あなたの下部に 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
ファイルに、次のコードを追加します。
. . .
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
ファイル:
. . .
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
ファイルに、次のコードを追加します。
. . .
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()
関数:
. . .
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()
関数:
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
ファイルは次のようになります。
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を作成して開きます。
- nano 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
ファイルを開いて開きます。 これにより、機密性の高い不要なファイルをバージョン管理できなくなります。
お好みのテキストエディタを使用してファイルを作成します。
- nano .dockerignore
次のコンテンツをファイルに追加します。
node_modules
npm-debug.log
作成後 Dockerfile
そしてその .dockerignore
ファイルの場合、アプリケーションのDockerイメージをビルドして、DockerHubアカウントのリポジトリにプッシュできます。 イメージをプッシュする前に、DockerHubアカウントにサインインしていることを確認してください。
Docker Hubにサインインします:
- docker login --username=your_username --password=your_password
イメージを作成します。
- docker build -t your_username/concurrent-scraper .
次に、スクレーパーをテストします。 このテストでは、各ルートにリクエストを送信します。
まず、アプリを起動します。
- docker run -p 5000:5000 -d your_username/concurrent-scraper
今すぐ使用 curl
送信するには GET
にリクエスト /
ルート:
- curl http://localhost:5000/
を送信することによって GET
にリクエスト /
ルート、あなたはを含む応答を受け取る必要があります msg
言って hello world
と hostname
. これ hostname
DockerコンテナのIDです。 これと同様の出力が表示されますが、マシンの一意のIDは次のとおりです。
Output{"msg":"hello world","hostname":"0c52d53f97d3"}
今すぐ送信します POST
にリクエスト /api/books
1つのWebページに表示されるすべての本のメタデータを取得するためのルート:
- 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
ランダムな本の不足している情報を取得するためのルート:
- 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 details
、 booksDetails
この本の欠落している詳細を含むオブジェクト、 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.js
と server.js
前の2つのステップの最後のコードブロックと一致します。 また、Dockerコンテナが実行されており、クラッシュしていないことを確認してください。 これを行うには、Dockerイメージを実行しようとします。 -d
オプション(このオプションはDockerイメージをデタッチモードで実行します)、次に送信します HTTP
ルートの1つにリクエストします。
Dockerイメージを実行しようとしてもエラーが発生する場合は、実行中のすべてのコンテナーを停止し、スクレーパーイメージを実行せずに実行してみてください。 -d
オプション。
まず、すべてのコンテナを停止します。
- docker stop $(docker ps -a -q)
次に、Dockerコマンドを実行します。 -d
国旗:
- docker run -p 5000:5000 your_username/concurrent-scraper
エラーが発生しない場合は、ターミナルウィンドウをクリーンアップします。
- clear
画像のテストに成功したので、画像をリポジトリに送信できます。 イメージをDockerHubアカウントのリポジトリにプッシュします。
- docker push your_username/concurrent-scraper:latest
スクレーパーアプリケーションがDockerHubでイメージとして利用できるようになったので、Kubernetesにデプロイする準備が整いました。 これが次のステップになります。
ステップ6—スクレーパーをKubernetesにデプロイする
スクレーパーイメージがビルドされてリポジトリにプッシュされると、デプロイの準備が整います。
まず、 kubectl
と呼ばれる新しい名前空間を作成します concurrent-scraper-context
:
- kubectl create namespace concurrent-scraper-context
設定 concurrent-scraper-context
デフォルトのコンテキストとして:
- kubectl config set-context --current --namespace=concurrent-scraper-context
アプリケーションのデプロイメントを作成するには、次のファイルを作成する必要があります。 app-deployment.yaml
、ただし、最初に、に移動する必要があります k8s
プロジェクト内のディレクトリ。 これは、すべてのKubernetesファイルを保存する場所です。
に移動します k8s
プロジェクト内のディレクトリ:
- cd ../k8s
を作成します app-deployment.yaml
ファイルを開いて開きます。
- nano app-deployment.yaml
中に次のコードを書いてください app-deployment.yaml
. 必ず交換してください your_DockerHub_username
一意のユーザー名を使用:
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
.
デプロイファイルを作成すると、アプリをクラスターにデプロイする準備が整います。
アプリをデプロイします。
- kubectl apply -f app-deployment.yaml
次のコマンドを実行して、展開のステータスを監視できます。
- kubectl get deployment -w
コマンドを実行すると、次のような出力が表示されます。
OutputNAME 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
ファイルを開いて開きます。
- nano 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
ファイル、サービスをクラスターにデプロイします。
サービスを展開します。
- kubectl apply -f load-balancer.yaml
次のコマンドを実行して、サービスのステータスを監視します。
- kubectl get services -w
このコマンドを実行すると、次のような出力が表示されますが、外部IPが表示されるまでに数秒かかります。
OutputNAME 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-IP
と CLUSTER-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
ディレクトリ:
- cd ../client
彼らはよりも小さいので main.js
、作成します lowdbHelper.js
と books.json
最初にファイル。
と呼ばれるファイルを作成して開きます lowdbHelper.js
:
- nano 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
ファイル:
. . .
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
次のようになります。
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
ファイルを開いて開きます。
- nano books.json
次のコードを追加します。
{
"books": []
}
The books.json
ファイルは、というプロパティを持つオブジェクトで構成されています books
. このプロパティの初期値は空の配列です。 後で本を取得するときに、プログラムがそれらを保存する場所です。
これで、 lowdbHelper.js
そしてその books.json
ファイル、あなたは作成します main.js
ファイル。
作成 main.js
そしてそれを開きます:
- nano 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
ファイル:
. . .
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()
次のコードで:
. . .
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()
次のコードで:
. . .
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
ファイル:
. . .
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
ファイル:
. . .
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
配列; そうでない場合は、 podId
に podsWorkDone
配列し、実行された作業の数を1に設定します。 ただし、そうであれば、このポッドによって実行される作業の数が1つ増えます。 次に、コードはを追加します result
に booksDetails
配列、全体的な進捗状況を出力します getBooksDetails()
関数を呼び出してから、 saveBookDetails()
関数。
次に、次のコードを追加して、 saveBookDetails()
関数:
. . .
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
. 次に、ループします books
と booksDetails
同じ配列の両方の配列で要素が見つかるかどうかを確認する配列 url
財産。 含まれている場合は、 booksDetails
内の要素のプロパティ booksDetails
配列し、それをの要素に割り当てます books
配列。 次に、の内容を上書きします books.json
の内容を含むファイル books
この関数で配列がループしました。 作成後 saveBookDetails()
関数、コードはを呼び出します main()
このファイルを使用可能にする関数。 そうしないと、このファイルを実行しても目的の結果が得られません。
完成しました 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
ファイル:
- npm start 0
次の出力が表示されます。
Outputgetting books
took 40.323secs to retrieve 400 books
20ページすべてに表示されている書籍のメタデータを取得するには、40.323秒かかりましたが、この値はインターネットの速度によって異なる場合があります。
ここで、に保存されているすべての本の不足している詳細を取得する必要があります。 books.json
各ポッドが処理するリクエストの数も監視しながらファイルします。
走る npm start
もう一度詳細を取得するには:
- 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倍にして、取得をさらに高速化します。
- kubectl scale deployment scraper --replicas=10
ポッドが使用可能になるまで少し時間がかかるため、次のコマンドを実行する前に少なくとも10秒待ちます。
再実行 npm start
不足している詳細を取得するには:
- 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でコーディングする方法に関するチュートリアルシリーズをご覧ください。