Playwright Guide - Downloading A File
Downloading files is a common requirement in many web scraping scenarios, and being able to handle file downloads effectively is crucial for successful scraping processes.
This is an in-depth tutorial that covers everything from finding elements on the page to finding URLs with Playwright and downloading a ton of them with Axios.
TLDR: How to Download A File Using Playwright
Today, we're going to learn how to download files using Playwright. There is actually more than one way to do this, and we're going to explore them all with an excruciating level of detail.
Don't have time for the whole thing? Take a look at this script:
const playwright = require("playwright");const path = require("path");
async function main() { //create a browser object const browser = await playwright.chromium.launch(); //set a new context for the browser const context = await browser.newContext({ acceptDownloads: true }); //open a new page with our custom context const page = await context.newPage(); //navigate to the site await page.goto("https://nakamotoinstitute.org/library/bitcoin") //xpath variable const xPath = "/html/body/main/article/div/a[2]" //create a promise that resolves once a download event has taken place const downloadPromise = page.waitForEvent("download"); //locate the link by its xpath const link = page.locator(`xpath=${xPath}`); //const link = page.locator(`href="https://nakamotoinstitute.org/library/bitcoin"`) //click the link await link.click(); //create a download object out of our promise const download = await downloadPromise; //create a path to the file const filePath = path.join(process.cwd(), download.suggestedFilename()); //save the file at the specified path await download.saveAs(filePath); //close the browser await browser.close(); } main();When downloading a file, you need to ensure that you take the following steps:
- Create a
Promiseobject usingpage.waitForEvent("download"). - Begin your download, the example above uses
click()to do so, but you can also do this withpage.goto() - Create a
downloadobject from yourPromise - Save your download at a specified path with
download.saveAs()
Why Automated File Downloads Matter?
Why would anybody want to automate file downloads?
Actually there are many reasons to automate your downloads. The list below outlines some of the reasons you may wish to automate a file download.
-
Web Scraping for Content Aggregation: Quite often, data collection is the main reason that we'd need to scrape a site. If we need to aggregate relevant content, automating the download is a much faster and more efficient way to do it than manually downloading all the content.
-
Social Media Data Collection: Quite often, different companies will routinely scrape the social media posts of their competitors. When scraping in this scenario, one can easily and quickly create a summary of what their competitor is doing.
-
Email Attachment Automation: In larger companies especially, it is very important to backup data. What better way to backup email attachments than to build a bot that does this in seconds?
-
Continuous Integration and Deployment (CI/CD): For large download sites that change often, they may wish to create a suite of tests that check all the downloads. Automated testing is great because when you change a large piece of software (your website), you know immediately if your changes break anything, and if your tests are written correctly, you'll know where things are breaking.
Understanding File Downloads
To understand file downloads, first we need a basic understanding of HTTP requests. There are four main requests that you should be aware of in web development:
- GET: gets information from a server
- POST: posts information to a server.
- PUT: modifies existing content on a server.
- DELETE: deletes content from a server.
In this article, we're really only going focus on GET requests. GET is very self-explanatory. You send a GET request whenever you wish to get content. When you go to a website, your browser sends a GET request to the domain. After receiving the request, the server finds the required information (in this case an HTML file), and sends it back to you.
The same happens when we download a file. The main difference in the process:
- When we're viewing a site: Our browser reads the file and displays the HTML content to us as website.
- When we're downloading: Instead of being read and displayed by the browser, our file is saved to the local machine to be viewed later.
Downloading a File in Playwright
Downloading a file is actuallly quite simple. There are only a few steps we need to follow:
- Find download button or download url
- Start the download by either clicking the button or sending a
GETrequest to it. - Wait for the download to complete.
- Verify that the download took place.
There are numerous other things we can to in order to tweak and customize this process, but the steps listed above are the most important. We always need to find, start, wait, and verify.
Locating the Download Link or Button
As mentioned above, our first step is to find the download. Using Playwright, we can find pretty much anything using its ID, Class, or XPATH. The code below finds several elements on the page:
const playwright = require("playwright");
async function main() { //create a browser object const browser = await playwright.chromium.launch(); //set a new context for the browser const context = await browser.newContext({ acceptDownloads: true }); //open a new page with our custom context const page = await context.newPage(); //navigate to the site await page.goto("https://nakamotoinstitute.org/library/bitcoin/"); //xpath variable const xPath = ("/html/body/main/article/div/a[2]"); //locate the link by its xpath const link = page.locator(`xpath=${xPath}`); //await the link's text content const downloadText = await link.textContent(); //log the link's text content console.log("Link download text:", downloadText); //get the href const href = await link.getAttribute("href"); //log the href console.log("Actual link:", href); //close the browser await browser.close();}
main();In the code above, we:
- Import Playwright with
require() - Create an
asyncfunction,main() - Open a browser with
playwright.chromium.launch() - Allow our browser to download files with
browser.newContext() - Create a
pageobject withcontext.newPage() - Navigate to the site with
page.goto() - Find the link by its xpath with
page.locator(`xpath=${xpath}`)and log its text to the console - Use
link.getAttribute("href")to get the actualhrefof the link and print it to the console - Close the browser with
browser.close()
If you would like to get more information about how to select/locate elements with CSS selectors, check our How To Find Elements by CSS Selector Playwright Guide.
Clicking the Download Link or Button
Now that we know how to find elements on the page, we need to find and click the download button. Let's create some code that finds the button download link and clicks on it with click(). While this example is a bit similar to our first one, you should pay attention to the differences, they're very important.
const playwright = require("playwright");const path = require("path");
async function main() { //create a browser object const browser = await playwright.chromium.launch(); //set a new context for the browser const context = await browser.newContext({ acceptDownloads: true }); //open a new page with our custom context const page = await context.newPage(); //navigate to the site await page.goto("https://nakamotoinstitute.org/library/bitcoin") //xpath variable const xPath = "/html/body/main/article/div/a[2]" //create a promise that resolves once a download event has taken place const downloadPromise = page.waitForEvent("download"); //locate the link by its xpath const link = page.locator(`xpath=${xPath}`); //const link = page.locator(`href="https://nakamotoinstitute.org/library/bitcoin"`) //click the link await link.click(); //create a download object out of our promise const download = await downloadPromise; //create a path to the file const filePath = path.join(process.cwd(), download.suggestedFilename()); //save the file at the specified path await download.saveAs(filePath); //close the browser await browser.close(); } main();Key differences from our first example:
downloadPromise = page.waitForEvent("download");creates aPromiseobject that cannot resolve until a"download"event has taken place.const download = await downloadPromise;creates a download object out of our Promise objectconst filePath = path.join(process.cwd(), download.suggestedFilename());joins our current working directory (cwd) and the suggested filename of the download object to create a path for our new file.await download.saveAs(filePath);saves the file at the path that we just created
Setting a Custom Download Behaviour
If you followed the example above, you've already set a custom download behaviour. Setting custom behaviours consists of the following steps:
- Set the download path: When we set the download path, we create a PATH object in our filesystem. We'll use this later on.
- Create an event listener: If you recall from the previous example, we used
page.waitForEvent("download")to create an event listener. This creates aPromisewhich resolves once the event has taken place. - Manage the completion of the download:
await download.saveAs(filePath);tells Playwright toawaitthe download object and save it at the PATH that we created in Step 1.
Let's take our previous example and expand upon it a bit more.
const playwright = require("playwright");const path = require("path");const fs = require("fs");
async function main() { //create a browser object const browser = await playwright.chromium.launch(); //get the current directory const currentFolder = process.cwd(); //name of the downloads folder const downloadsFolder = "./pdfDownloads"; //if the downloads folder doesn't exist, create it if (!fs.existsSync(downloadsFolder)) { fs.mkdirSync(downloadsFolder) } //path for our downloads const downloadsPath = path.join(currentFolder, downloadsFolder); //set a new context for the browser const context = await browser.newContext({ acceptDownloads: true, }); //open a new page with our custom context const page = await context.newPage(); //navigate to the site await page.goto("https://nakamotoinstitute.org/library/bitcoin/"); //xpath variable const xPath = "/html/body/main/article/div/a[2]"; //create a promise that resolves once a download event has taken place const downloadPromise = page.waitForEvent("download"); //xpath of the download button await page.locator(`xpath=${xPath}`).click(); //create a download object from the promise const download = await downloadPromise; //save the file to our new custom pdfDownloads folder await download.saveAs(path.join(downloadsPath, "CUSTOM-BITCOIN.pdf")); //close the browser await browser.close();}
main();Here are some of the important differences you should notice with this script:
require(fs)imports the filesystemif (!fs.existsSync(downloadsFolder)), tells NodeJS to check if ourdownloadsFoldervariable exists- If it doesn't exist, we create it with
fs.mkdirSync(downloadsFolder) - We then make a new path by combining our current folder and the
downloadsFolderwithpath.join() - We then create another path when saving our file,
"CUSTOM_BITCOIN.pdf"withdownloadsPath. This path gets passed intodownload.saveAs()to tell Playwright to save our download into this new custom folder with the new custom name.
Handling File Download Dialogs
Sometimes when handling a file download, you may need to click pop-ups or close them out. The example below shows you how to handle a pop-up with Playwright. We simply locate the element as we did earlier when learning how to locate elements, then we click the button.
const playwright = require("playwright");
async function main() { //create a browser object const browser = await playwright.chromium.launch(); //set a new context for the browser const context = await browser.newContext({ acceptDownloads: true, }); //open a new page with our custom context const page = await context.newPage(); //navigate to the site await page.goto("https://designsystem.digital.gov/components/modal/"); //xpath to launch the modal const xPathModalLauncher = ("/html/body/div[2]/div/main/div/div[2]/div/div[1]/a"); //click to launch the modal await page.locator(`xpath=${xPathModalLauncher}`).click(); //take a screenshot interacting with the modal await page.screenshot({ path:"before-accepting.png" }); //xpath to the "continue without saving" button const xPathModalCloser = "/html/body/div[4]/div/div/div/div/div[2]/ul/li[1]/button" //close the modal await page.locator(`xpath=${xPathModalCloser}`).click() //take a screenshot after clicking the "continue without saving" button await page.screenshot({ path: "after-accepting.png" }); //close the browser await browser.close();}
main();Notice the similarities between this example and our first:
- We find the button to launch the pop-up using
page.locator() - We also find the "continue without saving" button using
page.locator() - We then
click()on the button to take care of the pop-up
Here is the screenshot before clicking the button:
image
Here is the screenshot after the button has been clicked:
image
Downloading Multiple Files
Perhaps the most important reason to download with Playwright: We can download a lot of stuff...fast. In the example below, we navigate to the same website we used before for the Bitcoin Whitepaper, but this time, we attempt to download all of the PDF files from the site.
const { chromium } = require('playwright');const path = require('path');
async function downloadFile(context, linkHref) { //open a new page const page = await context.newPage(); try { const [download] = await Promise.all([ //create a download listener page.waitForEvent("download"), //go to the download url page.goto(linkHref), ]); const downloadPath = path.join(process.cwd(), download.suggestedFilename()); await download.saveAs(downloadPath); console.log(`Downloaded file saved to ${downloadPath}`); } catch (err) { console.log(`Failed to download from ${linkHref}: ${err}`); }}
async function main() { //open a new browser const browser = await chromium.launch(); //create a new context that accepts downloads const context = await browser.newContext({ acceptDownloads: true, }); //open a new page const page = await context.newPage(); //base url const url = "https://file-examples.com/index.php/sample-documents-download/sample-pdf-download/"; //navigate to the url await page.goto(url); //find every pdf link on the page const listOfLinks = await page.$$eval("a[href$='.pdf']", links => links.map(link => link.href)); //create an array of async download promises const downloadPromises = listOfLinks.map(linkHref => downloadFile(context, linkHref)); //await all the promises await Promise.all(downloadPromises); //close the browser await browser.close();}
main();Key differences you should notice in the code above:
downloadFile()is an async function we use to download filesconst downloadPromises = listOfLinks.map(linkHref => downloadFile(context, linkHref))runsdownloadFile()on each of the links in the list
Downloading All the Images from a WebPage
Now that we've downloaded all the PDFs from a site, let's take this a step further. With the help of axios, we're going to download all images from a website asynchronously. In the code below, we create a function that asynchronously downloads an image. Then we take a a notch further and create an array of Promise objects that only resolves once all of our images have finished downloading.
const { chromium } = require('playwright');const axios = require('axios');const fs = require('fs');const path = require('path');
//if we don't have a download folder, create oneconst downloadDir = path.join(__dirname, 'downloads');if (!fs.existsSync(downloadDir)) { fs.mkdirSync(downloadDir, { recursive: true });}//function to download an imageasync function downloadImage(url, filepath) { //get the image as a stream const response = await axios({ url, method: 'GET', responseType: 'stream', }); //write the stream const writer = fs.createWriteStream(filepath); //pipe the writer object response.data.pipe(writer); //return a new Promise object return new Promise((resolve, reject) => { //if we succeed, resolve the Promise writer.on("finish", () => { writer.close(); // Close the stream resolve(); }); //if we fail, reject the Promise writer.on("error", reject); });}//main functionasync function main() { //launch chromium const browser = await chromium.launch(); //open a new page const page = await browser.newPage(); //navigate to the site await page.goto('https://www.unsplash.com'); //get all image urls on the page const imageUrls = await page.$$eval('img', images => images.map(img => img.src)); //create a Promise for each image in our list const downloadPromises = imageUrls.map(url => { //name of the image const imageName = path.basename(new URL(url).pathname); //path to the image const filepath = path.join(downloadDir, `${imageName}.png`); //return the downloaded image and log it to the console return downloadImage(url, filepath).then(() => { console.log(`Downloaded image: ${imageName}`); //if we encounter an error, log it to the console }).catch(error => { console.error(`Error downloading ${imageName}:`, error); }); }); //wait for ALL downloadPromises to resolve or reject await Promise.all(downloadPromises); //close the browser await browser.close();}main();In the code above, we:
- Create a function to download images asynchronously,
downloadImage(). - After finding the url of each image (similar to how we found all the PDF links), we create an array
downloadPromiseswhich contains aPromisefor each image we'd like to download. - When downloading each image, we pass the url and our expected filepath into the download function. If the image downloads, we get a .png image inside of our downloads folder.
Monitoring Download Progress
In the example below, we once again create a function to download a file using axios. When we create a download stream, we receive our data in chunks. If we know the total size of the stream, we can update our progress each time we receive a new chunk. The code below does exactly that.
const axios = require("axios");const { chromium } = require("playwright");const fs = require("fs");const path = require("path");//function to download a file and log the progressasync function downloadFileWithProgress(url, outputPath) { //make a get request const response = await axios({ url, method: "GET", responseType: "stream", }); //save the content length const totalLength = response.headers["content-length"]; //start at zero let receivedLength = 0; //when we receive a chunk response.data.on("data", (chunk) => { //add the chunk to our received length receivedLength += chunk.length; //log the current received length and the percentage of the total console.log(`Received ${receivedLength} of ${totalLength} bytes (${((receivedLength / totalLength) * 100).toFixed(2)}%)`); }); //create a write stream const writer = fs.createWriteStream(outputPath); //pipe the output response.data.pipe(writer); //return a promise return new Promise((resolve, reject) => { writer.on("finish", resolve); writer.on("error", reject); });}//main functionasync function main() { //launch chromium const browser = await chromium.launch(); //open a new page const page = await browser.newPage(); //go to the site await page.goto("https://bitcoin.org/"); //url of the file we'd like to download const fileUrl = "https://bitcoin.org/bitcoin.pdf"; //path for the download const downloadPath = path.join(__dirname, "bitcoin.pdf"); //try to dowload the file try { await downloadFileWithProgress(fileUrl, downloadPath); console.log("Download completed"); //log any errors } catch (error) { console.error("Download failed:", error); } //close the browser await browser.close();}main();In the code example above, we follow the same basic structure as our example that downloads images. The major differences here lie in the downloadFileWithProgress() function. Key points in this function:
- We make a
GETrequest to the download url, and pass the parameter to stream. - We find the
content-lengthfrom the headers. This tells us the full size of the file. We save this as a variable,totalLength - We create a counter,
receivedLengthand start it at zero. - Each time we receive a data chunk, we update
receivedLengthand log our download progress to the console. - After receiving all of our data, we then write the file to
outputPath. - If everything runs properly, we resolve the
Promise, if the writer returns an error, we reject it.
From inside the main function, we call it and get an output that looks like this:
Alt text
As you can see from the screenshot, each time we received a chunk, our progress was logged to the console. After we finished the download, we log "Download completed".
Verifying File Download
There are multiple ways to ensure that your download took place. We can either do this manually with the File Explorer or automate this process with code. In this section we'll take a look at how to do both. Now let's verify the download from our previous example.
Verify With Your File Explorer
To check with your file explorer, simply open the folder where you downloaded the file. If you download succeeded, you should be able to open the file.
Step 1: Open the Folder
Take a look at the screenshot below. The downloaded file is highlighted.
Alt text
Step 2: Open the File
Next, we open our file. If you downloaded a PDF, open it with a PDF viewer, if you downloaded an image, open it with an image viewer, and so on and so forth.
Alt text
Verify Using Your Code
Now let's take a look at how we can verify this with our code. The code below is largely the same as our previous example, but at the end, we double check to ensure that the file exists.
const axios = require('axios');const { chromium } = require('playwright');const fs = require('fs');const path = require('path');//function to download a file and log the progressasync function downloadFileWithProgress(url, outputPath) { //make a get request const response = await axios({ url, method: 'GET', responseType: 'stream', }); //save the content length const totalLength = response.headers['content-length']; //start at zero let receivedLength = 0; //when we receive a chunk response.data.on('data', (chunk) => { //add the chunk to our received length receivedLength += chunk.length; //log the current received length and the percentage of the total console.log(`Received ${receivedLength} of ${totalLength} bytes (${((receivedLength / totalLength) * 100).toFixed(2)}%)`); }); //create a write stream const writer = fs.createWriteStream(outputPath); //pipe the output response.data.pipe(writer); //return a promise return new Promise((resolve, reject) => { writer.on('finish', resolve); writer.on('error', reject); });}//main functionasync function main() { //launch chromium const browser = await chromium.launch(); //open a new page const page = await browser.newPage(); //go to the site await page.goto("https://bitcoin.org/"); //url of the file we'd like to download const fileUrl = "https://bitcoin.org/bitcoin.pdf"; //path for the download const downloadPath = path.join(__dirname, "bitcoin.pdf"); //try to dowload the file try { await downloadFileWithProgress(fileUrl, downloadPath); console.log("Download completed"); //log any errors } catch (error) { console.error('Download failed:', error); } //check if the new file exists const fileExists = fs.existsSync(downloadPath); //log the result of the check if (fileExists) { console.log(`File found at: ${downloadPath}`); } else { console.log(`Failed to find file at: ${downloadPath}`); } //close the browser await browser.close();}main();In this example, we:
- Download the file as we previously did. This informs us of the progress and logs a message that the download did complete.
- After completing the download, we create a boolean using
fs.existsSync()to check the existence of the file. - Then, we log the
fileExistsanddownloadPathvariables to the console
Take a look at the screenshot below, our code works! The file finishes downloading and tells us when it's done. After completing the download, we double-check to ensure that the file exists, it does.
Alt text
Increasing Download Speed
Unlike Selenium, Playwright does not offer very good built-in support for downloading a bunch of files asynchronously. Recall the example where we downloaded all those Unsplash images. We only used Playwright to scrape the url of each image. We actually used axios to download the images.
Take a closer look this function:
//function to download an imageasync function downloadImage(url, filepath) { //get the image as a stream const response = await axios({ url, method: 'GET', responseType: 'stream', }); //write the stream const writer = fs.createWriteStream(filepath); //pipe the writer object response.data.pipe(writer); //return a new Promise object return new Promise((resolve, reject) => { //if we succeed, resolve the Promise writer.on("finish", () => { writer.close(); // Close the stream resolve(); }); //if we fail, reject the Promise writer.on("error", reject); });}This function creates a Promise object. While we run this function, the rest of the code in our script continues to execute...But how do we access the results when the Promise resolves?
Take a look at this code from that same example:
const downloadPromises = imageUrls.map(url => { //name of the image const imageName = path.basename(new URL(url).pathname); //path to the image const filepath = path.join(downloadDir, `${imageName}.png`); //return the downloaded image and log it to the console return downloadImage(url, filepath).then(() => { console.log(`Downloaded image: ${imageName}`); //if we encounter an error, log it to the console }).catch(error => { console.error(`Error downloading ${imageName}:`, error); }); }); //wait for ALL downloadPromises to resolve or reject await Promise.all(downloadPromises);In this portion, we create an array of Promise objects, downloadPromises. Each Promise in our array is being resolved in parallel. To access this array when it's finished resolving everything, we await Promise.all(downloadPromises);. When we get to this portion of the code, NodeJS will automatically await until all of our downloadPromises are either rejected or resolved.
Downloading Very Large Files With Playwright
When downloading extremely large files, it is actually better to find the download url and use a dedicated download manager than to simply hang there with Playwright waiting for the file to finish downloading.
You can use Free Download Manager or a similar tool to easily accomplish your download. Download managers like this can support pausing the download, retries and sometimes even multithreaded downloads.
If you need to download large files, it's much better to use an existing solution built for the problem than to "reinvent the wheel".
Handling Different File Types
When downloading different file types, you should always use a strategy tailored to your individual needs. When downloading images, axios was our best bet. This allowed us to download all of our files at once. When the same strategy was used for downloading PDF files, we actually received many errors from the server and got blocked on more than one occasion.
If you choose to use a regular HTTP client (such as axios), you may need to ensure that you're sending the right headers and even possibly using fake user agents. This all depends on the type of file that your downloading. On the web, images are often the easiest to download without getting blocked. Any time you view an image on the web, a GET is already sent to the server containing that image in order to retrieve it. When a server receives a bunch of requests for images, this is pretty standard. When a server receives a bunch of requests for other types of documents, this doesn't look normal and you can be spotted as a bot.
Handling Authentication for Secure Downloads
Sometimes, you'll need to login to a website in order to download its content. Playwright offers a great API for filling and submitting forms on the web. The example below shows you how to fill and submit a simple login form.
const playwright = require("playwright");
async function main() { //create a browser object const browser = await playwright.chromium.launch(); //set a new context for the browser const context = await browser.newContext({ acceptDownloads: true }); //open a new page with our custom context const page = await context.newPage(); //navigate to the site await page.goto("https://quotes.toscrape.com/login"); //take a screenshot before filling the forms await page.screenshot({ path: "before-login.png" }); //find the username const usernameBox = page.locator("#username"); //find the password const passwordBox = page.locator("#password"); //fill the username await usernameBox.fill("ScrapeOps"); //fill the password await passwordBox.fill("MY-SUPER-SECRET-PASSWORD"); //take a screenshot after filling the forms await page.screenshot({ path: "after-login.png" }); //close the browser await browser.close();}
main();This example uses the same locator method we've used previously to find elements on the page. There are a couple things you really need to pay attention to here:
usernameBox.fill()fills the username boxpasswordBox.fill()fills the password box
Here is the page before entering the information:
image
Here it is after:
image
Error Handling for Download Failures
As you probably noticed, in most of our in-depth examples, we used JavaScript's builtin error handling. In this tutorial we've used error handling for the following:
- Incomplete Downloads: In the example where we showed the download progress, this snippet handled incomplete downloads:
JavaScripttry {await downloadFileWithProgress(fileUrl, downloadPath);console.log("Download completed");//log any errors} catch (error) {console.error('Download failed:', error);}
- Incorrect File Format: When downloading images, and PDFs, we always checked the file extension in the code:
- Images
JavaScript//in this example, we automatically add the .png file extensionconst filepath = path.join(downloadDir, `${imageName}.png`);There are many ways to check file extensions in your code and most of them involve only basic string operations. Take the extra minute to ensure that your file extensions are correct.JavaScript//create a list ONLY containing objects with the .pdf extensionconst listOfLinks = await page.$$eval("a[href$='.pdf']", links => links.map(link => link.href)); - Network Latency: Network latency can definitely present difficulties. If you run into timeout errors, it is very easy to manually change your timeout. We set our timeout in milliseconds.
JavaScript//don't wait for timeoutawait page.goto('https://example.com', { timeout: 0 });//wait 60 seconds for the page to timeoutawait page.goto('https://example.com', { timeout: 60000 });
Conclusion
Congratulations! You've made it to the end of this tutorial. You now have should now have a decent grasp of the following topics:
- Locating elements with Playwright
- Clicking buttons with Playwright
- Downloading files with Playwright
- Taking screenshots with Playwright
- Downloading files with Axios
- Basic error handling in JavaScript
- Basic filesystem operations in JavaScript
Take your new knowledge and go build something. You've now got a great start on not just the fundamentals of Playwright, but Web Development fundamentals as well. If you'd like to learn more, take a look at the following links:
More Playwright Web Scraping Guides
Then check out ScrapeOps, the complete toolkit for web scraping.