Node.jsとPuppeteerを使用してWebサイトをスクレイプする方法
著者は、 Write for DOnations プログラムの一環として、 Free and Open SourceFundを選択して寄付を受け取りました。
序章
Webスクレイピングは、Webからのデータ収集を自動化するプロセスです。 このプロセスは通常、Webを自動的にサーフィンし、選択したページからデータを取得する「クローラー」をデプロイします。 データをスクレイピングする理由はたくさんあります。 主に、手動のデータ収集プロセスを排除することにより、データ収集をはるかに高速化します。 スクレイピングは、データ収集が必要または必要であるが、WebサイトがAPIを提供していない場合の解決策でもあります。
このチュートリアルでは、Node.jsとPuppeteerを使用してWebスクレイピングアプリケーションを構築します。 進行するにつれて、アプリは複雑になります。 まず、 Chromium を開き、ウェブスクレイピングサンドボックスとして設計された特別なウェブサイトbooks.toscrape.comを読み込むようにアプリをコーディングします。 次の2つのステップでは、books.toscrapeの1ページにあるすべての本をスクレイプしてから、複数のページにまたがるすべての本をスクレイプします。 残りの手順では、本のカテゴリでスクレイピングをフィルタリングしてから、データをJSONファイルとして保存します。
警告:ウェブスクレイピングの倫理と合法性は非常に複雑で、常に進化しています。 また、場所、データの場所、および問題のWebサイトによっても異なります。 このチュートリアルでは、スクレーパーアプリケーションをテストするために特別に設計された特別なWebサイトbooks.toscrape.comをスクレイピングします。 他のドメインをスクレイピングすることは、このチュートリアルの範囲外です。
前提条件
- 開発マシンにインストールされているNode.js。 このチュートリアルは、Node.jsバージョン12.18.3およびnpmバージョン6.14.6でテストされました。 このガイドに従ってmacOSまたはUbuntu18.04にNode.jsをインストールするか、このガイドに従ってPPAを使用してUbuntu18.04にNode.jsをインストールできます。
ステップ1—Webスクレイパーを設定する
Node.jsをインストールすると、Webスクレイパーのセットアップを開始できます。 まず、プロジェクトのルートディレクトリを作成してから、必要な依存関係をインストールします。 このチュートリアルに必要な依存関係は1つだけで、Node.jsのデフォルトのパッケージマネージャーnpmを使用してインストールします。 npmにはNode.jsがプリインストールされているため、インストールする必要はありません。
このプロジェクトのフォルダーを作成してから、次の場所に移動します。
mkdir book-scraper cd book-scraper
以降のすべてのコマンドは、このディレクトリから実行します。
npmまたはノードパッケージマネージャーを使用して1つのパッケージをインストールする必要があります。 プロジェクトの依存関係とメタデータを管理するpackages.json
ファイルを作成するために、最初にnpmを初期化します。
プロジェクトのnpmを初期化します。
npm init
npmは一連のプロンプトを表示します。 すべてのプロンプトでENTER
を押すか、パーソナライズされた説明を追加できます。 必ずENTER
を押し、entry point:
およびtest command:
の入力を求められたら、デフォルト値のままにしておきます。 または、y
フラグをnpm
— npm init -y
に渡すと、すべてのデフォルト値が送信されます。
出力は次のようになります。
Output{ "name": "sammy_scraper", "version": "1.0.0", "description": "a web scraper", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "sammy the shark", "license": "ISC" } Is this OK? (yes) yes
yes
と入力し、ENTER
を押します。 npmは、この出力をpackage.json
ファイルとして保存します。
次に、npmを使用してPuppeteerをインストールします。
npm install --save puppeteer
このコマンドは、Puppeteerと、PuppeteerチームがAPIで動作することがわかっているバージョンのChromiumの両方をインストールします。
Linuxマシンでは、Puppeteerにいくつかの追加の依存関係が必要になる場合があります。
Ubuntu 18.04を使用している場合は、Puppeteerのトラブルシューティングドキュメントの「ChromeヘッドレスがUNIXで起動しない」セクション内の「Debian依存関係」ドロップダウンを確認してください。 次のコマンドを使用して、不足している依存関係を見つけることができます。
ldd chrome | grep not
npm、Puppeteer、およびその他の依存関係がインストールされている場合、コーディングを開始する前に、package.json
ファイルに最後の構成が1つ必要です。 このチュートリアルでは、npm run start
を使用してコマンドラインからアプリを起動します。 このstart
スクリプトに関する情報をpackage.json
に追加する必要があります。 具体的には、start
コマンドに関して、scripts
ディレクティブの下に1行追加する必要があります。
お好みのテキストエディタでファイルを開きます。
nano package.json
scripts:
セクションを見つけて、次の構成を追加します。 test
スクリプト行の最後にコンマを置くことを忘れないでください。そうしないと、ファイルが正しく解析されません。
Output{ . . . "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "start": "node index.js" }, . . . "dependencies": { "puppeteer": "^5.2.1" } }
また、ファイルの終わり近くのdependencies
の下にpuppeteer
が表示されることにも気付くでしょう。 package.json
ファイルにこれ以上のリビジョンは必要ありません。 変更を保存して、エディターを閉じます。
これで、スクレーパーのコーディングを開始する準備が整いました。 次のステップでは、ブラウザインスタンスを設定し、スクレーパーの基本機能をテストします。
ステップ2—ブラウザインスタンスの設定
従来のブラウザを開くと、ボタンをクリックしたり、マウスでナビゲートしたり、入力したり、開発ツールを開いたりすることができます。 Chromiumのようなヘッドレスブラウザを使用すると、これらと同じことができますが、プログラムによって、ユーザーインターフェイスなしで実行できます。 このステップでは、スクレーパーのブラウザーインスタンスを設定します。 アプリケーションを起動すると、Chromiumが自動的に開き、books.toscrape.comに移動します。 これらの最初のアクションは、プログラムの基礎を形成します。
Webスクレイパーには、browser.js
、index,js
、pageController.js
、およびpageScraper.js
の4つの.js
ファイルが必要です。 このステップでは、4つのファイルすべてを作成し、プログラムが高度化するにつれてそれらを継続的に更新します。 browser.js
から始めます。 このファイルには、ブラウザを起動するスクリプトが含まれています。
プロジェクトのルートディレクトリから、テキストエディタでbrowser.js
を作成して開きます。
nano browser.js
まず、require
Puppeteerを作成してから、startBrowser()
というasync
関数を作成します。 この関数はブラウザを起動し、そのインスタンスを返します。 次のコードを追加します。
./book-scraper/browser.js
const puppeteer = require('puppeteer'); async function startBrowser(){ let browser; try { console.log("Opening the browser......"); browser = await puppeteer.launch({ headless: false, args: ["--disable-setuid-sandbox"], 'ignoreHTTPSErrors': true }); } catch (err) { console.log("Could not create a browser instance => : ", err); } return browser; } module.exports = { startBrowser };
Puppeteerには、ブラウザのインスタンスを起動する.launch()メソッドがあります。 このメソッドはPromiseを返すため、.thenまたはawaitブロックを使用してPromiseが解決されることを確認する必要があります。
await
を使用して、Promiseが解決されることを確認し、このインスタンスを try-catchコードブロックでラップしてから、ブラウザーのインスタンスを返します。
.launch()
メソッドは、いくつかの値を持つJSONパラメーターを受け取ることに注意してください。
- ヘッドレス-
false
は、ブラウザがインターフェイスで実行されるため、スクリプトの実行を監視できることを意味し、true
は、ブラウザがヘッドレスモードで実行されることを意味します。 ただし、スクレーパーをクラウドにデプロイする場合は、headless
をtrue
に戻すことに注意してください。 ほとんどの仮想マシンはヘッドレスであり、ユーザーインターフェイスが含まれていないため、ブラウザはヘッドレスモードでのみ実行できます。 Puppeteerにはheadful
モードも含まれていますが、これはテスト目的でのみ使用する必要があります。 - ignoreHTTPSErrors -
true
を使用すると、安全なHTTPSプロトコルでホストされていないWebサイトにアクセスし、HTTPS関連のエラーを無視できます。
ファイルを保存して閉じます。
次に、2番目の.js
ファイルindex.js
を作成します。
nano index.js
ここでは、require
browser.js
とpageController.js
になります。 次に、startBrowser()
関数を呼び出し、作成されたブラウザインスタンスをページコントローラに渡します。ページコントローラはそのアクションを指示します。 次のコードを追加します。
./book-scraper/index.js
const browserObject = require('./browser'); const scraperController = require('./pageController'); //Start the browser and create a browser instance let browserInstance = browserObject.startBrowser(); // Pass the browser instance to the scraper controller scraperController(browserInstance)
ファイルを保存して閉じます。
3番目の.js
ファイルpageController.js
を作成します。
nano pageController.js
pageController.js
は、スクレイピングプロセスを制御します。 ブラウザインスタンスを使用して、すべてのスクレイピングスクリプトが実行されるpageScraper.js
ファイルを制御します。 最終的には、これを使用して、スクレイプする本のカテゴリを指定します。 ただし、今のところ、Chromiumを開いてWebページに移動できることを確認する必要があります。
./book-scraper/pageController.js
const pageScraper = require('./pageScraper'); async function scrapeAll(browserInstance){ let browser; try{ browser = await browserInstance; await pageScraper.scraper(browser); } catch(err){ console.log("Could not resolve the browser instance => ", err); } } module.exports = (browserInstance) => scrapeAll(browserInstance)
このコードは、ブラウザインスタンスを取り込んで、それをscrapeAll()
という関数に渡す関数をエクスポートします。 次に、この関数は、このインスタンスを引数としてpageScraper.scraper()
に渡し、これを使用してページをスクレイプします。
ファイルを保存して閉じます。
最後に、最後の.js
ファイルpageScraper.js
を作成します。
nano pageScraper.js
ここでは、url
プロパティとscraper()
メソッドを使用してオブジェクトリテラルを作成します。 url
はスクレイピングするWebページのWebURLであり、scraper()
メソッドには実際のスクレイピングを実行するコードが含まれていますが、この段階ではURLに移動するだけです。 次のコードを追加します。
./book-scraper/pageScraper.js
const scraperObject = { url: 'http://books.toscrape.com', async scraper(browser){ let page = await browser.newPage(); console.log(`Navigating to ${this.url}...`); await page.goto(this.url); } } module.exports = scraperObject;
Puppeteerには、ブラウザに新しいページインスタンスを作成するnewPage()メソッドがあり、これらのページインスタンスはかなりの数のことを実行できます。 scraper()
メソッドでは、ページインスタンスを作成し、 page.goto()メソッドを使用してbooks.toscrape.comホームページに移動しました。
ファイルを保存して閉じます。
これで、プログラムのファイル構造が完成しました。 プロジェクトのディレクトリツリーの最初のレベルは次のようになります。
Output. ├── browser.js ├── index.js ├── node_modules ├── package-lock.json ├── package.json ├── pageController.js └── pageScraper.js
次に、コマンドnpm run start
を実行し、スクレーパーアプリケーションが実行されるのを確認します。
npm run start
Chromiumブラウザインスタンスが自動的に開き、ブラウザで新しいページが開き、books.toscrape.comに移動します。
このステップでは、Chromiumを開き、ダミーのオンライン書店books.toscrape.comのホームページをロードするPuppeteerアプリケーションを作成しました。 次のステップでは、そのホームページ上のすべての本のデータをスクレイピングします。
ステップ3—単一のページからデータをスクレイピングする
スクレーパーアプリケーションに機能を追加する前に、お好みのWebブラウザーを開き、ブックに手動で移動してホームページをスクレイプします。 サイトを閲覧して、データがどのように構造化されているかを理解してください。
左側にカテゴリセクションがあり、右側に本が表示されています。 書籍をクリックすると、ブラウザはその特定の書籍に関連する情報を表示する新しいURLに移動します。
このステップでは、この動作を複製しますが、コードを使用します。 あなたはウェブサイトをナビゲートしてそのデータを消費するビジネスを自動化します。
まず、ブラウザ内のDev Toolsを使用してホームページのソースコードを調べると、ページにsection
タグの下に各書籍のデータがリストされていることがわかります。 section
タグ内では、すべての本がlist
(li
)タグの下にあり、ここに本の専用ページ、価格、および在庫あり。
これらの本のURLをスクレイピングし、在庫のある本をフィルタリングし、個々の本のページに移動して、その本のデータをスクレイピングします。
pageScraper.js
ファイルを再度開きます。
nano pageScraper.js
次の強調表示されたコンテンツを追加します。 await page.goto(this.url);
内に別のawait
ブロックをネストします。
./book-scraper/pageScraper.js
const scraperObject = { url: 'http://books.toscrape.com', async scraper(browser){ let page = await browser.newPage(); console.log(`Navigating to ${this.url}...`); // Navigate to the selected page await page.goto(this.url); // Wait for the required DOM to be rendered await page.waitForSelector('.page_inner'); // Get the link to all the required books let urls = await page.$$eval('section ol > li', links => { // Make sure the book to be scraped is in stock links = links.filter(link => link.querySelector('.instock.availability > i').textContent !== "In stock") // Extract the links from the data links = links.map(el => el.querySelector('h3 > a').href) return links; }); console.log(urls); } } module.exports = scraperObject;
このコードブロックでは、 page.waitForSelector()メソッドを呼び出しました。 これは、すべての本関連情報を含むdivがDOMにレンダリングされるのを待ってから、ページを呼び出しました。$$ eval()メソッド。 このメソッドは、セレクターsection ol li
を使用してURL要素を取得します(page.$eval()
およびpage.$$eval()
メソッドからは常に文字列または数値のみを返すようにしてください)。
すべての本には2つのステータスがあります。 本はIn Stock
またはOut of stock
のいずれかです。 In Stock
の本だけをこすりたい。 page.$$eval()
は一致するすべての要素の配列を返すため、この配列をフィルター処理して、在庫のある本のみを操作するようにしました。 これを行うには、クラス.instock.availability
を検索して評価します。 次に、本のリンクのhref
プロパティをマップし、メソッドから返しました。
ファイルを保存して閉じます。
アプリケーションを再実行します。
npm run start
ブラウザが開き、Webページに移動し、タスクが完了すると閉じます。 次に、コンソールを確認します。 削り取られたすべてのURLが含まれます。
Output> [email protected] start /Users/sammy/book-scraper > node index.js Opening the browser...... Navigating to http://books.toscrape.com... [ 'http://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html', 'http://books.toscrape.com/catalogue/tipping-the-velvet_999/index.html', 'http://books.toscrape.com/catalogue/soumission_998/index.html', 'http://books.toscrape.com/catalogue/sharp-objects_997/index.html', 'http://books.toscrape.com/catalogue/sapiens-a-brief-history-of-humankind_996/index.html', 'http://books.toscrape.com/catalogue/the-requiem-red_995/index.html', 'http://books.toscrape.com/catalogue/the-dirty-little-secrets-of-getting-your-dream-job_994/index.html', 'http://books.toscrape.com/catalogue/the-coming-woman-a-novel-based-on-the-life-of-the-infamous-feminist-victoria-woodhull_993/index.html', 'http://books.toscrape.com/catalogue/the-boys-in-the-boat-nine-americans-and-their-epic-quest-for-gold-at-the-1936-berlin-olympics_992/index.html', 'http://books.toscrape.com/catalogue/the-black-maria_991/index.html', 'http://books.toscrape.com/catalogue/starving-hearts-triangular-trade-trilogy-1_990/index.html', 'http://books.toscrape.com/catalogue/shakespeares-sonnets_989/index.html', 'http://books.toscrape.com/catalogue/set-me-free_988/index.html', 'http://books.toscrape.com/catalogue/scott-pilgrims-precious-little-life-scott-pilgrim-1_987/index.html', 'http://books.toscrape.com/catalogue/rip-it-up-and-start-again_986/index.html', 'http://books.toscrape.com/catalogue/our-band-could-be-your-life-scenes-from-the-american-indie-underground-1981-1991_985/index.html', 'http://books.toscrape.com/catalogue/olio_984/index.html', 'http://books.toscrape.com/catalogue/mesaerion-the-best-science-fiction-stories-1800-1849_983/index.html', 'http://books.toscrape.com/catalogue/libertarianism-for-beginners_982/index.html', 'http://books.toscrape.com/catalogue/its-only-the-himalayas_981/index.html' ]
これは素晴らしいスタートですが、URLだけでなく、特定の本に関連するすべてのデータを取得する必要があります。 次に、これらのURLを使用して各ページを開き、書籍のタイトル、著者、価格、在庫状況、UPC、説明、画像のURLを取得します。
pageScraper.js
を再度開きます:
nano pageScraper.js
次のコードを追加します。このコードは、スクレイプされた各リンクをループし、新しいページインスタンスを開いてから、関連するデータを取得します。
./book-scraper/pageScraper.js
const scraperObject = { url: 'http://books.toscrape.com', async scraper(browser){ let page = await browser.newPage(); console.log(`Navigating to ${this.url}...`); // Navigate to the selected page await page.goto(this.url); // Wait for the required DOM to be rendered await page.waitForSelector('.page_inner'); // Get the link to all the required books let urls = await page.$$eval('section ol > li', links => { // Make sure the book to be scraped is in stock links = links.filter(link => link.querySelector('.instock.availability > i').textContent !== "In stock") // Extract the links from the data links = links.map(el => el.querySelector('h3 > a').href) return links; }); // Loop through each of those links, open a new page instance and get the relevant data from them let pagePromise = (link) => new Promise(async(resolve, reject) => { let dataObj = {}; let newPage = await browser.newPage(); await newPage.goto(link); dataObj['bookTitle'] = await newPage.$eval('.product_main > h1', text => text.textContent); dataObj['bookPrice'] = await newPage.$eval('.price_color', text => text.textContent); dataObj['noAvailable'] = await newPage.$eval('.instock.availability', text => { // Strip new line and tab spaces text = text.textContent.replace(/(\r\n\t|\n|\r|\t)/gm, ""); // Get the number of stock available let regexp = /^.*\((.*)\).*$/i; let stockAvailable = regexp.exec(text)[1].split(' ')[0]; return stockAvailable; }); dataObj['imageUrl'] = await newPage.$eval('#product_gallery img', img => img.src); dataObj['bookDescription'] = await newPage.$eval('#product_description', div => div.nextSibling.nextSibling.textContent); dataObj['upc'] = await newPage.$eval('.table.table-striped > tbody > tr > td', table => table.textContent); resolve(dataObj); await newPage.close(); }); for(link in urls){ let currentPageData = await pagePromise(urls[link]); // scrapedData.push(currentPageData); console.log(currentPageData); } } } module.exports = scraperObject;
すべてのURLの配列があります。 この配列をループして、新しいページでURLを開き、そのページのデータを取得し、そのページを閉じて、配列内の次のURLの新しいページを開きます。 このコードをPromiseでラップしていることに注意してください。 これは、ループ内の各アクションが完了するのを待つことができるようにするためです。 したがって、各Promiseは新しいURLを開き、プログラムがURL上のすべてのデータをスクレイプし、そのページインスタンスが閉じるまで解決されません。
警告:for-in
ループを使用してPromiseを待機したことに注意してください。 他のループでも十分ですが、forEach
などの配列反復法、またはコールバック関数を使用する他の方法を使用してURL配列を反復処理することは避けてください。 これは、コールバック関数が最初にコールバックキューとイベントループを通過する必要があるためです。したがって、複数のページインスタンスが一度に開きます。 これはあなたの記憶にはるかに大きな負担をかけます。
pagePromise
関数を詳しく見てください。 スクレーパーは最初にURLごとに新しいページを作成し、次にpage.$eval()
関数を使用して、新しいページでスクレイプしたい関連する詳細のセレクターをターゲットにしました。 一部のテキストには、正規表現を使用して削除した空白、タブ、改行、およびその他の英数字以外の文字が含まれています。 次に、このページでスクレイピングされたすべてのデータの値をオブジェクトに追加し、そのオブジェクトを解決しました。
ファイルを保存して閉じます。
スクリプトを再度実行します。
npm run start
ブラウザはホームページを開き、次に各本のページを開き、それらの各ページからスクレイピングされたデータをログに記録します。 この出力はコンソールに出力されます。
OutputOpening the browser...... Navigating to http://books.toscrape.com... { bookTitle: 'A Light in the Attic', bookPrice: '£51.77', noAvailable: '22', imageUrl: 'http://books.toscrape.com/media/cache/fe/72/fe72f0532301ec28892ae79a629a293c.jpg', bookDescription: "It's hard to imagine a world without A Light in the Attic. [...]', upc: 'a897fe39b1053632' } { bookTitle: 'Tipping the Velvet', bookPrice: '£53.74', noAvailable: '20', imageUrl: 'http://books.toscrape.com/media/cache/08/e9/08e94f3731d7d6b760dfbfbc02ca5c62.jpg', bookDescription: `"Erotic and absorbing...Written with starling power."--"The New York Times Book Review " Nan King, an oyster girl, is captivated by the music hall phenomenon Kitty Butler [...]`, upc: '90fa61229261140a' } { bookTitle: 'Soumission', bookPrice: '£50.10', noAvailable: '20', imageUrl: 'http://books.toscrape.com/media/cache/ee/cf/eecfe998905e455df12064dba399c075.jpg', bookDescription: 'Dans une France assez proche de la nôtre, [...]', upc: '6957f44c3847a760' } ...
このステップでは、 books.toscrape.com のホームページにあるすべての本の関連データをスクレイプしましたが、さらに多くの機能を追加することができます。 たとえば、本の各ページはページ付けされています。 これらの他のページからどのように本を入手しますか? また、ウェブサイトの左側には、本のカテゴリがあります。 すべての本が必要ではなく、特定のジャンルの本だけが必要な場合はどうなりますか? 次に、これらの機能を追加します。
ステップ4—複数のページからデータをスクレイピングする
ページが表示されているbooks.toscrape.comのページには、コンテンツの下にnext
ボタンがありますが、ページが表示されていないページにはありません。
このボタンの存在を使用して、ページがページ分割されているかどうかを判断します。 各ページのデータは同じ構造であり、同じマークアップを持っているため、すべての可能なページにスクレーパーを作成することはありません。 むしろ、再帰のプラクティスを使用します。
まず、コードの構造を少し変更して、複数のページに再帰的に移動できるようにする必要があります。
pagescraper.js
を再度開きます:
nano pagescraper.js
scrapeCurrentPage()
という新しい関数をscraper()
メソッドに追加します。 この関数には、特定のページからデータを取得し、存在する場合は[次へ]ボタンをクリックするすべてのコードが含まれます。 次の強調表示されたコードを追加します。
./book-scraper/pageScraper.js scarer()
const scraperObject = { url: 'http://books.toscrape.com', async scraper(browser){ let page = await browser.newPage(); console.log(`Navigating to ${this.url}...`); // Navigate to the selected page await page.goto(this.url); let scrapedData = []; // Wait for the required DOM to be rendered async function scrapeCurrentPage(){ await page.waitForSelector('.page_inner'); // Get the link to all the required books let urls = await page.$$eval('section ol > li', links => { // Make sure the book to be scraped is in stock links = links.filter(link => link.querySelector('.instock.availability > i').textContent !== "In stock") // Extract the links from the data links = links.map(el => el.querySelector('h3 > a').href) return links; }); // Loop through each of those links, open a new page instance and get the relevant data from them let pagePromise = (link) => new Promise(async(resolve, reject) => { let dataObj = {}; let newPage = await browser.newPage(); await newPage.goto(link); dataObj['bookTitle'] = await newPage.$eval('.product_main > h1', text => text.textContent); dataObj['bookPrice'] = await newPage.$eval('.price_color', text => text.textContent); dataObj['noAvailable'] = await newPage.$eval('.instock.availability', text => { // Strip new line and tab spaces text = text.textContent.replace(/(\r\n\t|\n|\r|\t)/gm, ""); // Get the number of stock available let regexp = /^.*\((.*)\).*$/i; let stockAvailable = regexp.exec(text)[1].split(' ')[0]; return stockAvailable; }); dataObj['imageUrl'] = await newPage.$eval('#product_gallery img', img => img.src); dataObj['bookDescription'] = await newPage.$eval('#product_description', div => div.nextSibling.nextSibling.textContent); dataObj['upc'] = await newPage.$eval('.table.table-striped > tbody > tr > td', table => table.textContent); resolve(dataObj); await newPage.close(); }); for(link in urls){ let currentPageData = await pagePromise(urls[link]); scrapedData.push(currentPageData); // console.log(currentPageData); } // When all the data on this page is done, click the next button and start the scraping of the next page // You are going to check if this button exist first, so you know if there really is a next page. let nextButtonExist = false; try{ const nextButton = await page.$eval('.next > a', a => a.textContent); nextButtonExist = true; } catch(err){ nextButtonExist = false; } if(nextButtonExist){ await page.click('.next > a'); return scrapeCurrentPage(); // Call this function recursively } await page.close(); return scrapedData; } let data = await scrapeCurrentPage(); console.log(data); return data; } } module.exports = scraperObject;
最初にnextButtonExist
変数をfalseに設定してから、ボタンが存在するかどうかを確認します。 next
ボタンがある場合は、nextButtonExists
をtrue
に設定し、next
ボタンをクリックして、この関数を再帰的に呼び出します。
nextButtonExists
がfalseの場合、通常どおりscrapedData
配列を返します。
ファイルを保存して閉じます。
スクリプトを再度実行します。
npm run start
これは完了するまでに時間がかかる場合があります。 結局のところ、アプリケーションは800冊を超える本からデータをスクレイピングしています。 ブラウザを閉じるか、CTRL + C
を押してプロセスをキャンセルしてください。
これでスクレーパーの機能が最大化されましたが、その過程で新しい問題が発生しました。 ここで問題となるのは、データが少なすぎることではなく、データが多すぎることです。 次のステップでは、アプリケーションを微調整して、本のカテゴリでスクレイピングをフィルタリングします。
ステップ5—カテゴリ別のデータのスクレイピング
カテゴリ別にデータをスクレイピングするには、pageScraper.js
ファイルとpageController.js
ファイルの両方を変更する必要があります。
テキストエディタでpageController.js
を開きます。
nano pageController.js
旅行の本だけをこすり取るようにスクレーパーを呼び出します。 次のコードを追加します。
./book-scraper/pageController.js
const pageScraper = require('./pageScraper'); async function scrapeAll(browserInstance){ let browser; try{ browser = await browserInstance; let scrapedData = {}; // Call the scraper for different set of books to be scraped scrapedData['Travel'] = await pageScraper.scraper(browser, 'Travel'); await browser.close(); console.log(scrapedData) } catch(err){ console.log("Could not resolve the browser instance => ", err); } } module.exports = (browserInstance) => scrapeAll(browserInstance)
ここで、2つのパラメーターをpageScraper.scraper()
メソッドに渡します。2番目のパラメーターは、スクレイプする本のカテゴリー(この例ではTravel
)です。 ただし、pageScraper.js
ファイルはまだこのパラメーターを認識していません。 このファイルも調整する必要があります。
ファイルを保存して閉じます。
pageScraper.js
を開きます:
nano pageScraper.js
次のコードを追加します。これにより、カテゴリパラメータが追加され、そのカテゴリページに移動して、ページ付けされた結果のスクレイピングを開始します。
./book-scraper/pageScraper.js
const scraperObject = { url: 'http://books.toscrape.com', async scraper(browser, category){ let page = await browser.newPage(); console.log(`Navigating to ${this.url}...`); // Navigate to the selected page await page.goto(this.url); // Select the category of book to be displayed let selectedCategory = await page.$$eval('.side_categories > ul > li > ul > li > a', (links, _category) => { // Search for the element that has the matching text links = links.map(a => a.textContent.replace(/(\r\n\t|\n|\r|\t|^\s|\s$|\B\s|\s\B)/gm, "") === _category ? a : null); let link = links.filter(tx => tx !== null)[0]; return link.href; }, category); // Navigate to the selected category await page.goto(selectedCategory); let scrapedData = []; // Wait for the required DOM to be rendered async function scrapeCurrentPage(){ await page.waitForSelector('.page_inner'); // Get the link to all the required books let urls = await page.$$eval('section ol > li', links => { // Make sure the book to be scraped is in stock links = links.filter(link => link.querySelector('.instock.availability > i').textContent !== "In stock") // Extract the links from the data links = links.map(el => el.querySelector('h3 > a').href) return links; }); // Loop through each of those links, open a new page instance and get the relevant data from them let pagePromise = (link) => new Promise(async(resolve, reject) => { let dataObj = {}; let newPage = await browser.newPage(); await newPage.goto(link); dataObj['bookTitle'] = await newPage.$eval('.product_main > h1', text => text.textContent); dataObj['bookPrice'] = await newPage.$eval('.price_color', text => text.textContent); dataObj['noAvailable'] = await newPage.$eval('.instock.availability', text => { // Strip new line and tab spaces text = text.textContent.replace(/(\r\n\t|\n|\r|\t)/gm, ""); // Get the number of stock available let regexp = /^.*\((.*)\).*$/i; let stockAvailable = regexp.exec(text)[1].split(' ')[0]; return stockAvailable; }); dataObj['imageUrl'] = await newPage.$eval('#product_gallery img', img => img.src); dataObj['bookDescription'] = await newPage.$eval('#product_description', div => div.nextSibling.nextSibling.textContent); dataObj['upc'] = await newPage.$eval('.table.table-striped > tbody > tr > td', table => table.textContent); resolve(dataObj); await newPage.close(); }); for(link in urls){ let currentPageData = await pagePromise(urls[link]); scrapedData.push(currentPageData); // console.log(currentPageData); } // When all the data on this page is done, click the next button and start the scraping of the next page // You are going to check if this button exist first, so you know if there really is a next page. let nextButtonExist = false; try{ const nextButton = await page.$eval('.next > a', a => a.textContent); nextButtonExist = true; } catch(err){ nextButtonExist = false; } if(nextButtonExist){ await page.click('.next > a'); return scrapeCurrentPage(); // Call this function recursively } await page.close(); return scrapedData; } let data = await scrapeCurrentPage(); console.log(data); return data; } } module.exports = scraperObject;
このコードブロックは、渡されたカテゴリを使用して、そのカテゴリの書籍が存在するURLを取得します。
page.$$eval()
は、引数を3番目のパラメーターとして$$eval()
メソッドに渡し、コールバックの3番目のパラメーターとして次のように定義することで引数を取り込むことができます。
サンプルページ。$$eval()関数
page.$$eval('selector', function(elem, args){ // ....... }, args)
これはあなたがあなたのコードでしたことでした。 削りたい本のカテゴリを渡し、すべてのカテゴリをマッピングして、どれが一致するかを確認してから、このカテゴリのURLを返しました。
次に、このURLを使用して、page.goto(selectedCategory)
メソッドを使用してスクレイプする書籍のカテゴリを表示するページに移動します。
ファイルを保存して閉じます。
アプリケーションを再度実行します。 Travel
カテゴリに移動し、そのカテゴリの本をページごとに再帰的に開き、結果をログに記録します。
npm run start
このステップでは、複数のページにまたがってデータをスクレイピングしてから、1つの特定のカテゴリから複数のページにまたがってデータをスクレイピングしました。 最後のステップでは、スクリプトを変更して複数のカテゴリにまたがるデータをスクレイピングし、このスクレイピングされたデータを文字列化されたJSONファイルに保存します。
ステップ6—複数のカテゴリからデータをスクレイピングし、データをJSONとして保存する
この最後のステップでは、スクリプトで必要な数のカテゴリからデータを取得し、出力の方法を変更します。 結果をログに記録するのではなく、data.json
という構造化ファイルに保存します。
スクレイプにカテゴリをすばやく追加できます。 これを行うには、ジャンルごとに1行追加するだけで済みます。
pageController.js
を開きます:
nano pageController.js
追加のカテゴリを含めるようにコードを調整します。 次の例では、HistoricalFiction
とMystery
を既存のTravel
カテゴリに追加します。
./book-scraper/pageController.js
const pageScraper = require('./pageScraper'); async function scrapeAll(browserInstance){ let browser; try{ browser = await browserInstance; let scrapedData = {}; // Call the scraper for different set of books to be scraped scrapedData['Travel'] = await pageScraper.scraper(browser, 'Travel'); scrapedData['HistoricalFiction'] = await pageScraper.scraper(browser, 'Historical Fiction'); scrapedData['Mystery'] = await pageScraper.scraper(browser, 'Mystery'); await browser.close(); console.log(scrapedData) } catch(err){ console.log("Could not resolve the browser instance => ", err); } } module.exports = (browserInstance) => scrapeAll(browserInstance)
ファイルを保存して閉じます。
スクリプトを再度実行し、3つのカテゴリすべてのデータを取得するのを確認します。
npm run start
スクレーパーが完全に機能するようになったら、最後のステップとして、データをより便利な形式で保存します。 ここで、Node.jsのfsモジュールを使用してJSONファイルに保存します。
まず、pageController.js
を再度開きます。
nano pageController.js
次の強調表示されたコードを追加します。
./book-scraper/pageController.js
const pageScraper = require('./pageScraper'); const fs = require('fs'); async function scrapeAll(browserInstance){ let browser; try{ browser = await browserInstance; let scrapedData = {}; // Call the scraper for different set of books to be scraped scrapedData['Travel'] = await pageScraper.scraper(browser, 'Travel'); scrapedData['HistoricalFiction'] = await pageScraper.scraper(browser, 'Historical Fiction'); scrapedData['Mystery'] = await pageScraper.scraper(browser, 'Mystery'); await browser.close(); fs.writeFile("data.json", JSON.stringify(scrapedData), 'utf8', function(err) { if(err) { return console.log(err); } console.log("The data has been scraped and saved successfully! View it at './data.json'"); }); } catch(err){ console.log("Could not resolve the browser instance => ", err); } } module.exports = (browserInstance) => scrapeAll(browserInstance)
まず、pageController.js
にNode、jsのfs
モジュールが必要です。 これにより、データをJSONファイルとして保存できるようになります。 次に、スクレイピングが完了してブラウザが閉じたときに、プログラムがdata.json
という名前の新しいファイルを作成するようにコードを追加します。 data.json
の内容は文字列化されたJSONであることに注意してください。 したがって、data.json
のコンテンツを読み取るときは、データを再利用する前に、常にそれをJSONとして解析してください。
ファイルを保存して閉じます。
これで、複数のカテゴリにわたって本をスクレイピングし、スクレイピングしたデータをJSONファイルに保存するWebスクレイピングアプリケーションを構築しました。 アプリケーションの複雑さが増すにつれて、このスクレイピングされたデータをデータベースに保存するか、APIを介して提供することをお勧めします。 このデータがどのように消費されるかは、本当にあなた次第です。
結論
このチュートリアルでは、複数のページにまたがるデータを再帰的にスクレイピングするWebクローラーを作成し、それをJSONファイルに保存しました。 つまり、Webサイトからのデータ収集を自動化する新しい方法を学びました。
Puppeteerには、このチュートリアルの範囲外の機能がたくさんあります。 詳細については、ヘッドレスクロームを簡単に制御するためのPuppeteerの使用をご覧ください。 Puppeteerの公式ドキュメントにもアクセスできます。