- GitHub is where people build software. More than 56 million people use GitHub to discover, fork, and contribute to over 100 million projects.
- Nodejs Web Scraper Docs Blog. Nodejs Web Scraper. Docs; Blog; Nodejs Web Scraper A simplified web scraper for Nodejs. Recursive scraping. Scrape/crawl deeply nested server-side rendered websites. Data collection. Hook into various steps of the scraping/crawling tree and collect the data.
Let’s start with a little section on what web scraping actually means. All of us use web scraping in our everyday lives. It merely describes the process of extracting information from a website. Hence, if you copy and paste a recipe of your favorite noodle dish from the internet to your personal notebook, you are performing web scraping.
When using this term in the software industry, we usually refer to the automation of this manual task by using a piece of software. Sticking to our previous “noodle dish” example, this process usually involves two steps:
Nodejs-web-scraper is a simple tool for scraping/crawling server-side rendered pages. It supports features like recursive scraping (pages that 'open' other pages), file download and handling, automatic retries of failed requests, concurrency limitation, pagination, request delay, etc. Tested on Node 10 and 12 (Windows 7, Linux Mint). A simple Python module to bypass Cloudflare's anti-bot page (also known as 'I'm Under Attack Mode', or IUAM), implemented with Requests.Cloudflare changes their techniques periodically, so I will update this repo frequently.
- Fetching the page
We first have to download the page as a whole. This step is like opening the page in your web browser when scraping manually. - Parsing the data
Now, we have to extract the recipe in the HTML of the website and convert it to a machine-readable format like JSON or XML.
In the past, I have worked for many companies as a data consultant. I was amazed to see how many data extractions, aggregation, and enrichment tasks are still done manually although they easily could be automated with just a few lines of code. That is exactly what web scraping is all about for me: extracting and normalizing valuable pieces of information from a website to fuel another value-driving business process.
During this time, I saw companies use web scraping for all sorts of use cases. Investment firms were primarily focused on gathering alternative data, like product reviews, price information, or social media posts to underpin their financial investments.
Here’s one example. A client approached me to scrape product review data for an extensive list of products from several e-commerce websites, including the rating, location of the reviewer, and the review text for each submitted review. The result data enabled the client to identify trends about the product’s popularity in different markets. This is an excellent example of how a seemingly “useless” single piece of information can become valuable when compared to a larger quantity.
Other companies accelerate their sales process by using web scraping for lead generation. This process usually involves extracting contact information like the phone number, email address, and contact name for a given list of websites. Automating this task gives sales teams more time for approaching the prospects. Hence, the efficiency of the sales process increases.
Stick To The Rules
In general, web scraping publicly available data is legal, as confirmed by the jurisdiction of the Linkedin vs. HiQ case. However, I have set myself an ethical set of rules that I like to stick to when starting a new web scraping project. This includes:
- Checking the robots.txt file.
It usually contains clear information about which parts of the site the page owner is fine to be accessed by robots & scrapers and highlights the sections that should not be accessed. - Reading the terms and conditions.
Compared to the robots.txt, this piece of information is not available less often, but usually states how they treat data scrapers. - Scraping with moderate speed.
Scraping creates server load on the infrastructure of the target site. Depending on what you scrape and at which level of concurrency your scraper is operating, the traffic can cause problems for the target site’s server infrastructure. Of course, the server capacity plays a big role in this equation. Hence, the speed of my scraper is always a balance between the amount of data that I aim to scrape and the popularity of the target site. Finding this balance can be achieved by answering a single question: “Is the planned speed going to significantly change the site’s organic traffic?”. In cases where I am unsure about the amount of natural traffic of a site, I use tools like ahrefs to get a rough idea.
Selecting The Right Technology
In fact, scraping with a headless browser is one of the least performant technologies you can use, as it heavily impacts your infrastructure. One core from your machine’s processor can approximately handle one Chrome instance.
Let’s do a quick example calculation to see what this means for a real-world web scraping project.
Scenario
- You want to scrape 20,000 URLs.
- The average response time from the target site is 6 seconds.
- Your server has 2 CPU cores.
The project will take 16 hours to complete.
Hence, I always try to avoid using a browser when conducting a scraping feasibility test for a dynamic website.
Here is a small checklist that I always go through:
- Can I force the required page state through GET-parameters in the URL? If yes, we can simply run an HTTP-request with the appended parameters.
- Are the dynamic information part of the page source and available through a JavaScript object somewhere in the DOM? If yes, we can again use a normal HTTP-request and parse the data from the stringified object.
- Are the data fetched through an XHR-request? If so, can I directly access the endpoint with an HTTP-client? If yes, we can send an HTTP-request to the endpoint directly. A lot of times, the response is even formatted in JSON, which makes our life much easier.
If all questions are answered with a definite “No”, we officially run out of feasible options for using an HTTP-client. Of course, there might be more site-specific tweaks that we could try, but usually, the required time to figure them out is too high, compared to the slower performance of a headless browser. The beauty of scraping with a browser is that you can scrape anything that is subject to the following basic rule:
If you can access it with a browser, you can scrape it.
Let’s take the following site as an example for our scraper: https://quotes.toscrape.com/search.aspx. It features quotes from a list of given authors for a list of topics. All data is fetched via XHR.
Whoever took a close look at the site’s functioning and went through the checklist above probably realized that the quotes could actually be scraped using an HTTP client, as they can be retrieved by making a POST-request on the quotes endpoint directly. But since this tutorial is supposed to cover how to scrape a website using Puppeteer, we will pretend this was impossible.
Installing Prerequisites
Since we are going to build everything using Node.js, let’s first create and open a new folder, and create a new Node project inside, running the following command:
Please make sure you have already installed npm. The installer will ask us a few questions about meta-information about this project, which we can all skip, hitting Enter.
Installing Puppeteer
We have been talking about scraping with a browser before. Puppeteer is a Node.js API that allows us to talk to a headless Chrome instance programmatically.
Node Js Web Scraper Login
Let’s install it using npm:
Building Our Scraper
Now, let’s start to build our scraper by creating a new file, called scraper.js.
First, we import the previously installed library, Puppeteer:
As a next step, we tell Puppeteer to open up a new browser instance inside an asynchronous and self-executing function:
Note: By default, the headless mode is switched off, as this increases performance. However, when building a new scraper, I like to turn off the headless mode. This allows us to follow the process the browser is going through and see all rendered content. This will help us debug our script later on.
Inside our opened browser instance, we now open a new page and direct towards our target URL:
Node Js Web Scraper Free
As part of the asynchronous function, we will use the await
statement to wait for the following command to be executed before proceeding with the next line of code.
Now that we have successfully opened a browser window and navigated to the page, we have to create the website’s state, so the desired pieces of information become visible for scraping.
The available topics are generated dynamically for a selected author. Hence, we will first select ‘Albert Einstein’ and wait for the generated list of topics. Once the list has been fully generated, we select ‘learning’ as a topic and select it as a second form parameter. We then click on submit and extract the retrieved quotes from the container that is holding the results.
As we will now convert this into JavaScript logic, let’s first make a list of all element selectors that we have talked about in the previous paragraph:
Author select field | #author |
Tag select field | #tag |
Submit button | input[type='submit'] |
Quote container | .quote |
Before we start interacting with the page, we will ensure that all elements that we will access are visible, by adding the following lines to our script:
Next, we will select values for our two select fields:
We are now ready to conduct our search by hitting the “Search” button on the page and wait for the quotes to appear:
Since we are now going to access the HTML DOM-structure of the page, we are calling the provided page.evaluate()
function, selecting the container that is holding the quotes (it is only one in this case). We then build an object and define null as the fallback-value for each object
parameter:
We can make all results visible in our console by logging them:
Finally, let’s close our browser and add a catch statement:
The complete scraper looks like the following:
Let’s try to run our scraper with:
And there we go! The scraper returns our quote object just as expected:
Advanced Optimizations
Our basic scraper is now working. Let’s add some improvements to prepare it for some more serious scraping tasks.
Setting A User-Agent
By default, Puppeteer uses a user-agent that contains the string HeadlessChrome
. Quite a few websites look out for this sort of signature and block incoming requests with a signature like that one. To avoid that from being a potential reason for the scraper to fail, I always set a custom user-agent by adding the following line to our code:
This could be improved even further by choosing a random user-agent with each request from an array of the top 5 most common user-agents. A list of the most common user-agents can be found in a piece on Most Common User-Agents.
Implementing A Proxy
Puppeteer makes connecting to a proxy very easy, as the proxy address can be passed to Puppeteer on launch, like this:
sslproxies provides a large list of free proxies that you can use. Alternatively, rotating proxy services can be used. As proxies are usually shared between many customers (or free users in this case), the connection becomes much more unreliable than it already is under normal circumstances. This is the perfect moment to talk about error handling and retry-management.
Error And Retry-Management
A lot of factors can cause your scraper to fail. Hence, it is important to handle errors and decide what should happen in case of a failure. Since we have connected our scraper to a proxy and expect the connection to be unstable (especially because we are using free proxies), we want to retry four times before giving up.
Also, there is no point in retrying a request with the same IP address again if it has previously failed. Hence, we are going to build a small proxy rotating system.
First of all, we create two new variables:
Each time we are running our function scrape()
, we will increase our retry variable by 1. We then wrap our complete scraping logic with a try and catch statement so we can handle errors. The retry-management happens inside our catch
function:
The previous browser instance will be closed, and if our retry variable is smaller than our maxRetries
variable, the scrape function is called recursively.
Our scraper will now look like this:
Now, let us add the previously mentioned proxy rotator.
Let’s first create an array containing a list of proxies:
Now, pick a random value from the array:
We can now run the dynamically generated proxy together with our Puppeteer instance:
Of course, this proxy rotator could be further optimized to flag dead proxies, and so on, but this would definitely go beyond the scope of this tutorial.
This is the code of our scraper (including all improvements):
Voilà! Running our scraper inside our terminal will return the quotes.
Playwright As An Alternative To Puppeteer
Puppeteer was developed by Google. At the beginning of 2020, Microsoft released an alternative called Playwright. Microsoft headhunted a lot of engineers from the Puppeteer-Team. Hence, Playwright was developed by a lot of engineers that already got their hands working on Puppeteer. Besides being the new kid on the blog, Playwright’s biggest differentiating point is the cross-browser support, as it supports Chromium, Firefox, and WebKit (Safari).
Performance tests (like this one conducted by Checkly) show that Puppeteer generally provides about 30% better performance, compared to Playwright, which matches my own experience — at least at the time of writing.
Other differences, like the fact that you can run multiple devices with one browser instance, are not really valuable for the context of web scraping.
Resources And Additional Links
- Web Scraping with Javascript by Zenscrape