






## Requirements
* nodejs version >= 20
* website-scraper version >= 5
## Installation
```sh
npm install website-scraper website-scraper-puppeteer
```
## Usage
```javascript
import scrape from 'website-scraper';
import PuppeteerPlugin from 'website-scraper-puppeteer';
await scrape({
urls: ['https://www.instagram.com/gopro/'],
directory: '/path/to/save',
plugins: [
new PuppeteerPlugin({
launchOptions: { headless: "new" }, /* optional */
gotoOptions: { waitUntil: "networkidle0" }, /* optional */
scrollToBottom: { timeout: 10000, viewportN: 10 }, /* optional */
})
]
});
```
Puppeteer plugin constructor accepts next params:
* `launchOptions` - *(optional)* - puppeteer launch options, can be found in [puppeteer docs](https://github.com/puppeteer/puppeteer/blob/main/docs/api/puppeteer.puppeteerlaunchoptions.md)
* `gotoOptions` - *(optional)* - puppeteer page.goto options, can be found in [puppeteer docs](https://github.com/puppeteer/puppeteer/blob/main/docs/api/puppeteer.frame.goto.md#parameters)
* `scrollToBottom` - *(optional)* - in some cases, the page needs to be scrolled down to render its assets (lazyloading). Because some pages can be really endless, the scrolldown process can be interrupted before reaching the bottom when one or both of the bellow limitations are reached:
* `timeout` - in milliseconds
* `viewportN` - viewport height multiplier
## How it works
It starts Chromium in headless mode which just opens page and waits until page is loaded.
It is far from ideal because probably you need to wait until some resource is loaded or click some button or log in. Currently this module doesn't support such functionality.
================================================
FILE: lib/browserUtils/.eslintrc.yml
================================================
extends: '../../.eslintrc.yml'
env:
browser: true
================================================
FILE: lib/browserUtils/scrollToBottom.js
================================================
export default async (timeout, viewportN) => {
await new Promise((resolve) => {
let totalHeight = 0, distance = 200, duration = 0, maxHeight = window.innerHeight * viewportN;
const timer = setInterval(() => {
duration += 200;
window.scrollBy(0, distance);
totalHeight += distance;
if (totalHeight >= document.body.scrollHeight || duration >= timeout || totalHeight >= maxHeight) {
clearInterval(timer);
resolve();
}
}, 200);
});
};
================================================
FILE: lib/index.js
================================================
import puppeteer from '@website-scraper/puppeteer-version-wrapper';
import logger from './logger.js';
import scrollToBottomBrowser from './browserUtils/scrollToBottom.js';
class PuppeteerPlugin {
constructor ({
launchOptions = {},
gotoOptions = {},
scrollToBottom = null,
} = {}) {
this.launchOptions = launchOptions;
this.gotoOptions = gotoOptions;
this.scrollToBottom = scrollToBottom;
this.browser = null;
this.headers = {};
logger.info('init plugin', { launchOptions, scrollToBottom });
}
apply (registerAction) {
registerAction('beforeStart', async () => {
this.browser = await puppeteer.launch(this.launchOptions);
});
registerAction('beforeRequest', async ({requestOptions}) => {
if (hasValues(requestOptions.headers)) {
this.headers = Object.assign({}, requestOptions.headers);
}
return {requestOptions};
});
registerAction('afterResponse', async ({response}) => {
const contentType = response.headers['content-type'];
const isHtml = contentType && contentType.split(';')[0] === 'text/html';
if (isHtml) {
const url = response.url;
const page = await this.browser.newPage();
if (hasValues(this.headers)) {
logger.info('set headers to puppeteer page', this.headers);
await page.setExtraHTTPHeaders(this.headers);
}
await page.goto(url, this.gotoOptions);
if (this.scrollToBottom) {
await scrollToBottom(page, this.scrollToBottom.timeout, this.scrollToBottom.viewportN);
}
const content = await page.content();
await page.close();
// convert utf-8 -> binary string because website-scraper needs binary
return Buffer.from(content).toString('binary');
} else {
return response.body;
}
});
registerAction('afterFinish', () => this.browser && this.browser.close());
}
}
function hasValues (obj) {
return obj && Object.keys(obj).length > 0;
}
async function scrollToBottom (page, timeout, viewportN) {
logger.info(`scroll puppeteer page to bottom ${viewportN} times with timeout = ${timeout}`);
await page.evaluate(scrollToBottomBrowser, timeout, viewportN);
}
export default PuppeteerPlugin;
================================================
FILE: lib/logger.js
================================================
import debug from 'debug';
const appName = 'website-scraper-puppeteer';
const logLevels = ['error', 'warn', 'info', 'debug', 'log'];
const logger = {};
logLevels.forEach(logLevel => {
logger[logLevel] = debug(`${appName}:${logLevel}`);
});
export default logger;
================================================
FILE: package.json
================================================
{
"name": "website-scraper-puppeteer",
"version": "2.0.0",
"description": "Plugin for website-scraper which returns html for dynamic websites using puppeteer",
"readmeFilename": "README.md",
"type": "module",
"exports": {
".": "./lib/index.js"
},
"keywords": [
"website-scraper",
"puppeteer",
"chromium",
"chrome",
"headless",
"html"
],
"dependencies": {
"debug": "^4.1.1",
"@website-scraper/puppeteer-version-wrapper": "^1.0.0"
},
"peerDependencies": {
"website-scraper": "^6.0.0"
},
"devDependencies": {
"c8": "^11.0.0",
"chai": "^6.0.1",
"eslint": "^8.5.0",
"finalhandler": "^2.1.0",
"fs-extra": "^11.1.0",
"mocha": "^11.0.1",
"serve-static": "^2.2.0",
"website-scraper": "^6.0.0"
},
"scripts": {
"test": "c8 --all --reporter=text --reporter=lcov mocha --recursive --timeout 15000",
"eslint": "eslint lib/**"
},
"repository": {
"type": "git",
"url": "git+https://github.com/website-scraper/website-scraper-puppeteer.git"
},
"author": "Sofiia Antypenko