開発者ドキュメント

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

序章

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

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

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

前提条件

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

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

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

  1. mkdir book-scraper
  2. cd book-scraper

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

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

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

  1. npm init

npmは一連のプロンプトを表示します。 押すことができます ENTER すべてのプロンプトに追加するか、パーソナライズされた説明を追加できます。 必ず押してください ENTER プロンプトが表示されたら、デフォルト値のままにします entry point:test command:. または、合格することもできます y フラグを立てる npmnpm init -y—そしてそれはあなたのためにすべてのデフォルト値を提出します。

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

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

タイプ yes を押して ENTER. npmはこの出力をあなたの package.json ファイル。

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

  1. npm install --save puppeteer

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

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

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

  1. ldd chrome | grep not

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

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

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

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

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

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

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

Webスクレイパーには4つ必要です .js ファイル: browser.js, index,js, pageController.js、 と pageScraper.js. このステップでは、4つのファイルすべてを作成し、プログラムが高度化するにつれてそれらを継続的に更新します。 皮切りに browser.js; このファイルには、ブラウザを起動するスクリプトが含まれています。

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

  1. nano browser.js

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

./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パラメーターを取ります。

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

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

  1. nano index.js

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

./book-scraper/index.js
const browserObject = require('./browser');
const scraperController = require('./pageController');

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

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

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

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

  1. 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:

  1. nano pageScraper.js

ここでは、オブジェクトリテラルを作成します url プロパティと scraper() 方法。 The url は、スクレイピングするWebページのWeb URLですが、 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 スクレーパーアプリケーションの実行を監視します。

  1. npm run start

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

In this step, you created a Puppeteer application that opened Chromium and loaded the homepage for a dummy online bookstore—books.toscrape.com. 次のステップでは、そのホームページ上のすべての本のデータをスクレイピングします。

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

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

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

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

まず、ブラウザ内の開発ツールを使用してホームページのソースコードを調べると、ページに各書籍のデータが一覧表示されていることがわかります。 section 鬼ごっこ。 内部 section すべての本の下にタグを付ける list (li)タグを付けると、本の専用ページへのリンク、価格、在庫状況がここに表示されます。

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

再開します pageScraper.js ファイル:

  1. nano pageScraper.js

次の強調表示されたコンテンツを追加します。 別のネストします await 内部のブロック await page.goto(this.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}...`);
		// 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()メソッド。 このメソッドは、セレクターでURL要素を取得します section ol li (常に文字列または数値のみを返すようにしてください page.$eval()page.$$eval() メソッド)。

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

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

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

  1. npm run start

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

Output
> book-scraper@1.0.0 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:

  1. 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 ループ。 他のループでも十分ですが、次のような配列反復法を使用してURL配列を反復処理することは避けてください。 forEach、またはコールバック関数を使用するその他のメソッド。 これは、コールバック関数が最初にコールバックキューとイベントループを通過する必要があるためです。したがって、複数のページインスタンスが一度に開きます。 これはあなたの記憶にはるかに大きな負担をかけます。

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

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

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

  1. npm run start

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

Output
Opening 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:

  1. nano pagescraper.js

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

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

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

module.exports = scraperObject;

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

もしも nextButtonExists falseの場合、 scrapedData いつものように配列。

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

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

  1. 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:

  1. 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を取得します。

The page.$$eval() 引数を3番目のパラメータとして $$eval() メソッド、およびそれをコールバックの3番目のパラメーターとして定義します。

サンプルページ。$$eval()関数
page.$$eval('selector', function(elem, args){
	// .......
}, args)

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

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

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

アプリケーションを再度実行します。 あなたはそれがにナビゲートすることに気付くでしょう Travel カテゴリ、そのカテゴリの本をページごとに再帰的に開き、結果をログに記録します。

  1. npm run start

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

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

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

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

開ける pageController.js:

  1. nano pageController.js

追加のカテゴリを含めるようにコードを調整します。 以下の例は追加します HistoricalFictionMystery 私たちの既存に Travel カテゴリー:

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

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

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

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

  1. npm run start

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

まず、再開します pageController.js:

  1. 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)

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

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

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

結論

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

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

モバイルバージョンを終了