آموزش Web Scraping با Puppeteer


۱۸ بهمن ۱۳۹۹
آموزش web scraping با puppeteer

Puppeteer کتابخانه‌ای است که با ‌Node.js توسعه داده شده و یک high-level API را برای کنترل مرورگر Chrome یا Chromium از طریق پروتکل DevTools فراهم می‌کند. اسکریپت‌هایی که با این کتابخانه توسعه داده می‌شوند به‌صورت پیش‌فرض، headless هستند اما شما می‌توانید آن‌ها را به‌گونه‌ای پیکربندی کنید که به‌صورت non-headless اجرا شوند.

در این مقاله سعی داریم تا با Puppeteer که از آن برای پروژه‌های Automated Browsing و Web Scraping استفاده می‌شود، پروژه‌ای کوچک را پیاده‌سازی کنیم. اسکریپت ما قادر خواهد بود تا با دریافت ورودی کاربر و جستجوی آن از میان متدهای مربوط به Array در مستندات زبان JavaScript سایت MDN، یک نتیجه را به ما برگرداند.

حال در کنار Puppeteer تصمیم گرفته‌ایم تا از پکیج‌های Chalk و Inquirer استفاده کنیم. پکیج Chalk به ما کمک می‌کند تا خروجی‌های خود را در Terminal سیستم‌عامل استایل‌دهی کرده و با Inquirer قادر خواهیم بود تا رابط کاربری Command line را ایجاد کنیم.

برای این پروژه یک پوشه ایجاد کرده و دستور زیر را در آن مسیر اجرا کنید:

npm init

پس از اجرای دستور فوق، مواردی از شما خواسته می‌شود که می‌توانید آن‌ها را با دقت وارد کرده یا فقط Enter بزنید و در مرحله‌ی آخر کلمه‌ی yes را تایپ کنید تا فایلی با نام package.json در مسیر پروژه ایجاد شود. در مرحله‌ی بعد بایستی پیش‌نیازهای پروژه که پکیج‌های Puppeteer، Chalk و Inquirer هستند را با اجرای دستور زیر نصب کنید:

npm i puppeteer chalk inquirer

پس از نصب پیش‌نیازهای پروژه به سراغ ساخت فایل اصلی پروژه‌ می‌رویم. به‌همین منظور یک فایل JavaScript با نام app.js در مسیر پروژه ایجاد کنید و یک دستور start در فایل package.json به‌صورت زیر تعریف کنید:

...
"scripts": {
  "test": "echo \"Error: no test specified\" && exit 1",
  "start": "node app.js"
}
...

با تعریف این بخش از این پس می‌توانید با اجرای دستور npm start فایل app.js را با Node.js اجرا کنید.

دریافت ورودی از کاربر

برای دریافت ورودی از کاربر، پکیج inquirer را در فایل app.js فراخوانی می‌کنیم:

const inquirer = require('inquirer');

برای این پروژه از متد prompt() استفاده می‌کنیم و type آن را input قرار می‌دهیم. سپس نام ورودی دریافت شده توسط کاربر را userInput می‌گذاریم و پیغامی که در ابتدا به کاربر نمایش داده می‌شود Enter array method to search for خواهد بود:

inquirer.prompt({
  type: 'input',
  name: 'userInput',
  message: 'Enter array method to search for'
})

پس از دریافت ورودی کاربر می‌توانیم متغیر userInput که قبلا تعریف کرده بودیم را استفاده کرده و آن را در وبسایت MDN، با استفاده از Puppeteer جستجو کنیم و سپس نتایج را به کاربر برگردانیم. به‌دلیل اینکه متد prompt() یک Promise به ما برمی‌گرداند می‌توانیم از .then() استفاده کرده تا ورودی کاربر را به فانکشن getResults() که به کمک Puppeteer ایجاد شده، پاس بدهیم. همچنین می‌توانیم تمام این فرایند را در یک wrap با نام start() انجام دهیم. از آنجا که فانکشن start() برای داشتن عملکرد صحیح به فانکشن getResults() نیاز دارد بایستی در مرحله‌ی بعد آن را تعریف ‌کنیم:

const start = () => {
	inquirer.prompt({
		type: 'input',
		name: 'userInput',
		message: 'Enter array method to search for'
	})
		.then(resp => getResults(resp.userInput))
}

Web Scraping با Puppeteer

در این مرحله پیاده‌سازی فانکشن getResults() را با فراخوانی پکیج Puppeteer در ابتدای فایل app.js شروع خواهیم کرد:

const puppeteer = require('puppeteer')

فانکشن getResults() را به‌صورت asynchronous تعریف خواهیم کرد. به‌همین منظور یک instance از مرورگر Chromium ایجاد کرده و یک صفحه‌ی جدید در آن مرورگر باز می‌کنیم:

async function getResults(term) {
	const browser = await puppeteer.launch()
	const page = await browser.newPage()
}

متغیر term همان ورودی کاربر است که توسط فانکشن start() دریافت می‌شود.puppeteer.launch() یک instance از مرورگر Chromium به‌صورت headless در اختیار ما قرار می‌دهد که می‌توانیم با browser.newPage() یک تب جدید باز کنیم.

در مرحله‌ی بعد گزینه‌ی headless را در puppeteer.launch() برابر با false قرار می‌دهیم تا روند کار اسکریپت را بهتر متوجه شویم. در انتهای آدرس URL سایت MDN مقدار locale=en-US را تنظیم کرده‌ایم تا مطمئن شویم حتما نتایج جستجو به زبان انگلیسی نمایش داده می‌شوند.

پس از باز شدن URL سایت MDN بایستی ورودی کاربر را در نوار جستجو که id آن را در اسکریپت تعریف کرده‌ایم، تایپ کرده و دکمه‌ی Enter را وارد کنیم تا جستجو انجام شود:

const mdnUrl = 'https://developer.mozilla.org/en-US/search?locale=en-US'

async function getResults(term) {
	const browser = await puppeteer.launch({ headless: false })
	const page = await browser.newPage()
	await page.goto(mdnUrl)
	await page.type('#main-q', `Array.prototype.${term}`)
	await page.keyboard.down('Enter')
}

با استفاده از page.type المنتی که می‌خواهیم ورودی کاربر را در آن وارد کنیم، انتخاب کرده و با پارامتر دوم، ورودی کاربر را در آن المنت تایپ می‌کنیم. همان‌طور که قبل‌تر گفتیم جستجوی ما به متدهای مرتبط با Array محدود می‌شود بنابراین بخشی از جستجو‌ی ما ثابت است. به‌همین دلیل مقدار Array.prototype را به‌صورت ثابت قرار داده‌ایم و سپس ورودی کاربر را در انتهای آن اضافه می‌کنیم.

پس از بارگیری نتایج جستجو بایستی اولین نتیجه را دریافت کنیم. تمام نتایج موجود در صفحه دارای یک class با نام result هستند و هر عنوان دارای یک class با نام result-title است. اگر یک querySelector بر روی result-title پیاده‌سازی کنیم می‌توانیم اولین نتیجه را همراه با URL آن دریافت کنیم:

const mdnUrl = 'https://developer.mozilla.org/en-US/search?locale=en-US'

async function getResults(term) {
	const browser = await puppeteer.launch({ headless: false })
	const page = await browser.newPage()
	await page.goto(mdnUrl)
	await page.type('#main-q', `Array.prototype.${term}`)
	await page.keyboard.down('Enter')
	await page.waitForSelector('.result')

	const resultUrl = await page.evaluate(() => {
		const topResult = document.querySelector('.result-title')
		return topResult.href
	})
}

حال بایستی یک تعریف ساده از متد جستجو شده را در Console به کاربر نمایش دهیم. به همین منظور URL اولین عنوانی که در جستجو دریافت کرده‌ایم را در یک صفحه‌ی جدید مرورگر باز کرده و اولین تگ <p> که در صفحه وجود دارد را با استفاده از querySelector انتخاب می‌کنیم و innerText آن را در Console به کاربر نمایش می‌دهیم:

const mdnUrl = 'https://developer.mozilla.org/en-US/search?locale=en-US'

async function getResults(term) {
	const browser = await puppeteer.launch({ headless: false })
	const page = await browser.newPage()
	await page.goto(mdnUrl)
	await page.type('#main-q', `Array.prototype.${term}`)
	await page.keyboard.down('Enter')
	await page.waitForSelector('.result')

	const resultUrl = await page.evaluate(() => {
		const topResult = document.querySelector('.result-title')
		return topResult.href
	})

	await page.goto(resultUrl)

	const resultDef = await page.evaluate(() => {
		const termDef = document.querySelector('p')
		return termDef.innerText
	})

	browser.close()
}

نمایش خروجی در Console

برای استایل‌دهی اسکریپت در Console، از پکیج Chalk استفاده خواهیم کرد به‌همین منظور در ابتدای فایل app.js پکیج Chalk را فراخوانی می‌کنیم:

const chalk = require('chalk')

این پکیج گزینه‌های زیادی را برای استایل‌دهی به متن‌ها در اختیار ما قرار می‌دهد و برترین مزیت آن نسبت به دیگر پکیج‌ها، استفاده آسان و بدون نیاز به تعریف مقادیر hex یا RGB است. البته اگر خودتان بخواهید می‌توانید از مقادیر RGB استفاده کنید ولی ما برای ساده نگه داشتن آموزش از مقادیر RGB استفاده نخواهیم کرد:

console.log(chalk.magentaBright(resultDef))
console.log(chalk.underline.dim(resultUrl))

با استفاده از کدهای فوق، خروجی متنی ما به رنگ magenta روشن و URL با رنگ پیش‌فرض Console اما کم رنگ‌تر همراه با underline نمایش داده می‌شود.

اکنون می‌توانید { headless: false } را از puppeteer.launch() حذف کرده و اگر همه‌ی مراحل را به‌درستی انجام داده باشید، کدهای شما به‌صورت زیر خواهند بود:

const puppeteer = require('puppeteer');
const chalk = require('chalk');
const inquirer = require('inquirer')

const mdnUrl = 'https://developer.mozilla.org/en-US/search?locale=en-US'

const start = async () => {
	inquirer.prompt({
		type: 'input',
		name: 'userInput',
		message: 'Enter array method to search for'
	})
		.then(res => getResults(res.userInput))
}

async function getResults(term) {
	const browser = await puppeteer.launch();
	const page = await browser.newPage();
	await page.goto(mdnUrl);
	await page.type('#main-q', `Array.prototype.${term}`);
	await page.keyboard.down('Enter');
	await page.waitForSelector('.result');

	const resultUrl = await page.evaluate(() => {
		const topResult = document.querySelector('.result-title');
		return topResult.href;
	});

	await page.goto(resultUrl);

	const resultDef = await page.evaluate(() => {
		const termDef = document.querySelector('p');
		return termDef.innerText;
	});

	browser.close();

	console.log(chalk.magentaBright(resultDef));
	console.log(chalk.underline.dim(resultUrl));

}

start()
web scraping با puppeteer

منبع: https://levelup.gitconnected.com/playing-with-puppeteer-1438af6bf95e

برچسب‌ها:

خدمات رایگان لیارا

۲.۵ گیگابایت فضای ذخیره‌سازی ابری رایگان

۲.۵ گیگابایت Object Storage سازگار با پروتکل S3 با دیسک‌های SSD به‌صورت رایگان دریافت کنید.

هاست رایگان برای دیتابیس‌

دیتابیس‌های MariaDB، PostgreSQL و Redis را فقط با یک کلیک و به‌صورت رایگان تهیه کنید.

سرویس DNS رایگان

به سادگی دامنه‌تان را اضافه کنید و به صورت رایگان رکورد‌های آن را مدیریت کنید.

۱۰۰ هزار تومان اعتبار اولیه

بعد از ثبت نام در لیارا مبلغ ۱۰۰ هزار تومان اعتبار هدیه دریافت می‌کنید که با توجه به ساعتی بودن هزینه سرویس‌ها، می‌توانید تمامی خدمات پولی را برای چندین هفته رایگان استفاده کنید.

ارسال ۱۰۰ ایمیل تراکنشی رایگان در هر ماه

در سرویس ایمیل لیارا شما می‌توانید تا ۱۰۰ ایمیل رایگان در هر ماه ارسال کنید و فقط برای بیش از آن هزینه پرداخت کنید. (به‌همراه دسترسی SMTP)

هاست رایگان برای انواع وبسایت

تفاوتی ندارد برای وبسایت خود از Node استفاده می‌کنید یا Laravel و Django، در لیارا می‌توانید به صورت کاملا رایگان آن را میزبانی کنید.

توسعه‌دهندگان درباره‌ی ما چه می‌گویند

تجربه کار باliara_cloud@امروز خیلی خوب بود. یکی از سرویس هام رو منتقل کردم روش و راضیم. انقد سریع و جذاب کارم راه افتادم اصن باورم نمیشد! برعکس سرویس های PaaS دیگه با اون همه پیچیدگیشون. دمتون گرم
...

MohammadReza
liara testimonial
keikaavousi

بعد از بسته شدن @fandoghpaas و ناراحتی همه‌مون از اینکه یه سرویس خوب و صادق نمی‌تونه از پس هزینه‌ها بر بیاد، سرویسم رو منتقل کردم به پاس لیارا (https://liara.ir @liara_cloud) . تجربه راحت و خوب. تفاوت‌هایی داشت که کمی کار می‌خواست ولی تا الان کاملا راضی.

jadi
liara testimonial
jadi

یه خسته نباشید باید به تصمیمliara_cloud@بگم،
بعد از چندین روز سرکله زدن با سرویس های مشابه بالاخره تصمیم گرفتم لیارا رو امتحان کنم و باور نمیشه ۱۰ دقیقه بیشتر وقت نبرد،
دمتون گرم.

Arch
liara testimonial
EbadiDev

واسه سرویس PaaS با اختلاف لیارا بهترین رابط کاربری داره و یکی از مزیت‌های سرویس دیتابیس‌شون اینه که خودشون به صورت دوره‌ای بکآپ میگیرن.
...

Ali Najafi
liara testimonial
me_ali_najafi

یکی از کارهای خوبی که جدیداً میکنم اینه که یه دیتابیس روی لیارا میسازم و به پروژه وصل میکنم اینطوری هم خونه و هم محل کار دیتابیس بروز رو دارم و راحت میتونم ادامه بدم کار روliara_cloud@

Navid
liara testimonial
1navid

عاشقliara_cloud@شدم درسته در حد AWS نیست ولی خب تجربه خوبی واسه پروژه های داخل ایران ارائه میده، میتونم رو CD هم اجراش کنم

Amir H Shekari
liara testimonial
vanenshi

همراه شما هستیم

در خصوص سفارش یا استفاده از سرویس‌ها سوالی دارید؟
تلفن واحد فروش:
۰۲۵-۳۳۵۵۷۶۱۹ (روزهای کاری ۹ الی ۱۷)
تلفن واحد فروش: ۳۳۵۵۷۶۱۹-۰۲۵ (روزهای کاری ۹ الی ۱۷)