On my , my interests lie within machine learning (ML) and artificial intelligence (AI), and the language I’ve chosen to master is Python. self-taught programming journey My skills in Python are basic, so if you’re here with not a lot of skills in coding, I hope this guide helps you gain more knowledge and understanding. The Perfect Beginner Project To source data for ML, AI, or data science projects, you’ll often rely on databases, APIs, or ready-made CSV datasets. But what if you can’t find a dataset you want to use and analyze? That’s where a web scraper comes in. Working on projects is crucial to solidifying the knowledge you gain. When I began this project, I was a little overwhelmed because I truly didn’t know a thing. Sticking with it, finding answers to my questions on Stack Overflow, and a lot of trial and error helped me really understand how programming works — how web pages work, how to use loops, and how to build functions and keep data clean. It makes building a web scraper the perfect beginner project for anyone starting out in Python. What we’ll cover This guide will take you through understanding HTML web pages, building a web scraper using Python, and creating a DataFrame with pandas. It’ll cover data quality, data cleaning, and data-type conversion — entirely step by step and with instructions, code, and explanations on how every piece of it works. I hope you code along and enjoy! Disclaimer Websites can restrict or ban scraping data from their website. Users can be subject to legal ramifications depending on where and how you attempt to scrape information. Websites usually describe this in their terms of use and in their robots.txt file found at their site, which usually looks something like this: . So scrape responsibly, and respect the robots.txt. www.example.com/robots.txt What’s Web Scraping? consists of gathering data available on websites. This can be done manually by a human or by using a bot. Web scraping A is a program you build that helps you extract the data you need much quicker than a human’s hand and eyes can. bot What Are We Going to Scrape? It’s essential to identify the goal of your scraping right from the start. We don’t want to scrape any data we don’t actually need. For this project, we’ll scrape data from , specifically the top 50 movies on this page. Here is the information we’ll gather from each movie listing: IMDb’s “Top 1,000” movies The title The year it was released How long the movie is IMDb’s rating of the movie The Metascore of the movie How many votes the movie got The U.S. gross earnings of the movie How Do Web Scrapers Work? Web scrapers gather website data in the same way a human would: They go to a web page of the website, get the relevant data, and move on to the next web page — only much faster. Every website has a different structure. These are a few important things to think about when building a web scraper: What’s the structure of the web page that contains the data you’re looking for? How do we get to those web pages? Will you need to gather more data from the next page? The URL To begin, let’s look at the . URL of the page we want to scrape This is what we see in the URL: We notice a few things about the URL: acts as a separator — it indicates the end of the URL resource path and the start of the parameters ? specifies what the page will be about groups=top_1000 takes us to the the next or the previous page. The reference is the page we’re currently on. and are two possible values — translated to and &ref_adv_prv adv_nxt adv_prv advance to next page advance to previous page. When you navigate back and forth through the pages, you’ll notice only the parameters change. Keep this structure in mind as it’s helpful to know as we build the scraper. The HTML HTML stands for and most web pages are written using it. Essentially, HTML is two computers speak to each other over the internet, and websites are they say. hypertext markup language, how what When you access an URL, your computer sends a request to the server that hosts the site. Any technology can be running on that server (JavaScript, Ruby, Java, etc.) to process your request. Eventually, the server returns a response to your browser; oftentimes, that response will be in the form of an HTML page for your browser to display. HTML describes the structure of a web page semantically, and originally included cues for the appearance of the document. Inspect HTML Chrome, Firefox, and Safari users can examine the HTML structure of any page by right-clicking your mouse and pressing the Inspect option. A menu will appear on the bottom or right-hand side of your page with a long list of all the HTML tags housing the information displayed to your browser window. If you’re in Safari (photo above), you’ll want to press the button to the left of the search bar, which looks like a target. If you’re in Chrome or Firefox, there’s a small box with an arrow icon in it at the top left that you’ll use to inspect. Once clicked, if you move your cursor over any element of the page, you’ll notice it’ll get highlighted along with the HTML tags in the menu that they’re associated with, as seen above. Knowing how to read the basic structure of a page’s HTML page is important so we can turn to Python to help us extract the HTML from the page. Tools The tools we’re going to use are: (optional) is a simple, interactive computer-programming environment used via your web browser. I recommend using this just for code-along purposes if you don’t already have an IDE. If you use Repl, make sure you’re using the Python environment. Repl will allow us to send HTTP requests to get HTML files Requests will help us parse the HTML files BeautifulSoup will help us assemble the data into a DataFrame to clean and analyze it pandas will add support for mathematical functions and tools for working with arrays NumPy Now, Let’s Code You can follow along below inside your Repl environment or IDE, or you can go directly to . Have fun! the entire code here Import tools First, we’ll import the tools we’ll need so we can use them to help us build the scraper and get the data we need. Movies in English It’s very likely when we run our code to scrape some of these movies, we’ll get the movie names translated into the main language of the country the movie originated in. Use this code to make sure we get English-translated titles from all the movies we scrape: Request contents of the URL Get the contents of the page we’re looking at by requesting the URL: Breaking URL requests down: is the variable we create and assign the URL to url is the variable we create to store our request.get action results is the method we use to grab the contents of the URL. requests.get(url, headers=headers) The part tells our scraper to bring us English, based on our previous line of code. headers Using BeautifulSoup Make the content we grabbed easy to read by using BeautifulSoup: Breaking BeautifulSoup down: is the variable we create to assign the method BeatifulSoup to, which specifies a desired format of results using the HTML parser — this allows Python to read the components of the page rather than treating it as one long string soup will print what we’ve grabbed in a more structured tree format, making it easier to read print(soup.prettify()) The results of the print will look more ordered, like this: Initialize your storage When we write code to extract our data, we need somewhere to store that data. Create variables for each type of data you’ll extract, and assign an empty list to it, indicated by square brackets . Remember the list of information we wanted to grab from each movie from earlier: [] Your code should now look something like this. Note that we can delete our function until we need to use it again. print Find the right div container It’s time to check out the HTML code in our web page. Go to the web page we’re scraping, inspect it, and hover over a single movie in its entirety, like below: We need to figure out what distinguishes each of these from other div containers we see. You‘ll notice the list of elements to the right with a attribute that has two values: and . div class lister-item mode-advanced If you click on each of those, you’ll notice it’ll highlight each movie container on the left of the page, like above. If we do a quick search within inspect (press Ctrl+F and type ), we’ll see 50 matches representing the 50 movies displayed on a single page. We now know all the information we seek lies within this specific tag. lister-item mode-advanced div Find all divs lister-item mode-advanced Our next move is to tell our scraper to find all of these lister-item mode-advanced divs: Breaking find_all down: is the variable we’ll use to store all of the div containers with a class of movie_div lister-item mode-advanced the extracts all the div containers that have a class attribute of lister-item mode-advanced from what we have stored in our variable soup. find_all() method Get ready to extract each item If we look at the first movie on our list: We’re missing gross earnings! If you look at the second movie, they’ve included it there. Something to always consider when building a web scraper is the idea that not all the information you seek will be available for you to gather. In these cases, we need to make sure our web scraper doesn’t stop working or break when it reaches missing data and build around the idea we just don’t know whether or not that’ll happen. Getting into each div lister-item mode-advanced When we grab each of the items we need in a single container, we need the scraper to loop to the next container and grab those movie items too. And then it needs to loop to the next one and so on — 50 times for each page. For this to execute, we’ll need to wrap our scraper in a . lister-item mode-advanced div lister-item mode-advanced div for loop Breaking down the for loop: A loop is used for iterating over a sequence. Our sequence being every container that we stored in for lister-item mode-advanced div movie_div is the name of the variable that enters each div. You can name this whatever you want ( , , , ), and it wont change the function of the loop. container x loop banana cheese It can be read like this: Extract the title of the movie Beginning with the movie’s name, let’s locate its corresponding HTML line by using inspect and clicking on the title. We see the name is contained within an anchor tag, . This tag is nested within a header tag, . The tag is nested within a tag. This is the third of the s nested in the container of the first movie. <a> <h3> <h3> <div> <div> div Breaking titles down: is the variable we’ll use to store the title data we find name is what used in our loop — it’s used for iterating over each time. container for and is attribute notation and tells the scraper to access each of those tags. h3 .a tells the scraper to grab the text nested in the tag text <a> tells the scraper to take what we found and stored in name and to add it into our empty list called titles, which we created in the beginning titles.append(name) Extract year of release Let’s locate the movie’s year and its corresponding HTML line by using inspect and clicking on the year. We see this data is stored within the tag below the tag that contains the title of the movie. The dot notation, which we used for finding the title data ( ), worked because it was the first tag after the tag. Since the tag we want is the second tag, we have to use a different method. <span> <a> .h3.a <a> h3 <span> <span> Instead, we can tell our scraper to search by the distinctive mark of the second . We’ll use the , which is similar to except it only returns the first match. <span> find() method find_all() Breaking years down: is the variable we’ll use to store the year data we find year is what we used in our for loop — it’s used for iterating over each time. container is attribute notation, which tells the scraper to access that tag. h3 is a method we’ll use to access this particular tag find() <span> ( ) is the distinctive tag we want ‘span’, class_ = ‘lister-item-year’ <span> tells the scraper to take what we found and stored in year and to add it into our empty list called years (which we created in the beginning) years.append(year) Extract length of movie Locate the movie’s length and its correspondent HTML line by using inspect and clicking on the total minutes. The data we need can be found in a tag with a class of runtime. Like we did with year, we can do something similar: <span> Breaking time down: is the variable we’ll use to store the time data we find runtime is what we used in our for loop — it’s used for iterating over each time. container is a method we’ll use to access this particular tag find() <span> ( ) is the distinctive tag we want ‘span’, class_ = ‘runtime’ <span> says if there’s data there, grab it — but if the data is missing, then put a dash there instead. if container.p.find(‘span’, class_=’runtime’) else ‘-’ tells the scraper to grab that text in the tag text <span> tells the scraper to take what we found and stored in runtime and to add it into our empty list called time (which we created in the beginning) time.append(runtime) Extract IMDb ratings Find the movie’s IMDb rating and its corresponding HTML line by using inspect and clicking on the IMDb rating. Now, we’ll focus on extracting the IMDb rating. The data we need can be found in a tag. Since I don’t see any other tags, we can use attribute notation (dot notation) to grab this data. <strong> <strong> Breaking IMDb ratings down: is the variable we’ll use to store the IMDB ratings data it finds imdb is what we used in our loop — it’s used for iterating over each time. container for is attribute notation that tells the scraper to access that tag. strong tells the scraper to grab that text text The turns the text we find into a float — which is a decimal float() method tells the scraper to take what we found and stored in and to add it into our empty list called (which we created in the beginning). imdb_ratings.append(imdb) imdb imdb_ratings Extract Metascore Find the movie’s Metascore rating and its corresponding HTML line by using inspect and clicking on the Metascore number. The data we need can be found in a tag that has a class that says . <span> metascore favorable Before we settle on that, you should notice that, of course, a 96 for “Parasite” shows a favorable rating, but are the others favorable? If you highlight the next movie’s Metascore, you’ll see “JoJo Rabbit” has a class that says . Since these tags are different, it’d be safe to tell the scraper to use just the class when scraping: metascore mixed metascore Breaking Metascores down: is the variable we’ll use to store the Metascore-rating data it finds m_score is what we used in our loop — it’s used for iterating over each time. container for is a method we’ll use to access this particular tag find() <span> ( ) is the distinctive tag we want. ‘span’, class_ = ‘metascore’ <span> tells the scraper to grab that text text says if there is data there, grab it — but if the data is missing, then put a dash there if container.find(‘span’, class_=’metascore’) else ‘-’ The turns the text we find into an integer int() method tells the scraper to take what we found and stored in and to add it into our empty list called (which we created in the beginning) metascores.append(m_score) m_score metascores Extract votes and gross earnings We’re finally onto the final two items we need to extract, but we saved the toughest for last. Here’s where things get a little tricky. As mentioned earlier, you should have noticed that when we look at the first movie on this list, we don’t see a gross-earnings number. When we look at the second movie on the list, we can see both. Let’s just have a look at the second movie’s HTML code and go from there. Both the votes and the gross are highlighted on the right. After looking at the votes and gross containers for movie #2, what do you notice? As you can see, both of these are in a tag that has a attribute that equals and a attribute that holds the values of the distinctive number we need for each. <span> name nv data-value How can we grab the data for the second one if the search parameters for the first one are the same? How do we tell our scraper to skip over the first one and scrape the second? This one took a lot of brain flexing, tons of coffee, and a couple late nights to figure out. Here’s how I did it: Breaking votes and gross down: is an entirely new variable we’ll use to hold both the votes and the gross tags nv <span> is what we used in our loop for iterating over each time container for is the method we’ll use to grab both of the tags find_all() <span> ( ) is how we can grab attributes of that specific tag. ‘span’, attrs = ‘name’ : ’nv’ is the variable we’ll use to store the votes we find in the tag vote nv tells the scraper to go into the tag and grab the first data in the list — which are the votes because votes comes first in our HTML code (computers count in binary — they start count at 0, not 1). nv[0] nv tells the scraper to grab that text text tells the scraper to take what we found and stored in and to add it into our empty list called (which we created in the beginning) votes.append(vote) vote votes is the variable we’ll use to store the gross we find in the tag grosses nv tells the scraper to go into the tag and grab the second data in the list — which is gross because gross comes second in our HTML code nv[1] nv says if the length of is greater than one, then find the second datum that’s stored. But if the data that’s stored in isn’t greater than one — meaning if the gross is missing — then put a dash there. nv[1].text if len(nv) > 1 else ‘-’ nv nv tells the scraper to take what we found and stored in and to add it into our empty list called (which we created in the beginning) us_gross.append(grosses) grosses us_grosses Your code should now look like this: Let’s See What We Have So Far Now that we’ve told our scraper what elements to scrape, let’s use the print function to print out each list we’ve sent our scraped data to: Our lists looks like this ['Parasite', 'Jojo Rabbit', ' ', 'Knives Out', 'Uncut Gems', 'Once Upon a Time... in Hollywood', 'Joker', 'The Gentlemen', 'Ford v Ferrari', 'Little Women', 'The Irishman', 'The Lighthouse', 'Toy Story ', 'Marriage Story', 'Avengers: Endgame', 'The Godfather', 'Blade Runner ', 'The Shawshank Redemption', 'The Dark Knight', 'Inglourious Basterds', 'Call Me by Your Name', 'The Two Popes', 'Pulp Fiction', 'Inception', 'Interstellar', 'Green Book', 'Blade Runner', 'The Wolf of Wall Street', 'Gone Girl', 'The Shining', 'The Matrix', 'Titanic', 'The Silence of the Lambs', 'Three Billboards Outside Ebbing, Missouri', , 'The Peanut Butter Falcon', 'The Handmaiden', 'Memories of Murder', 'The Lord of the Rings: The Fellowship of the Ring', 'Gladiator', 'The Martian', 'Bohemian Rhapsody', 'Watchmen', 'Forrest Gump', 'Thor: Ragnarok', 'Casino Royale', 'The Breakfast Club', 'The Godfather: Part II', 'Django Unchained', 'Baby Driver'] ['( )', '( )', '( )', '( )', '( )', '( )', '( )', '( )', '( )', '( )', '( )', '(I) ( )', '( )', '( )', '( )', '( )', '( )', '( )', '( )', '( )', '( )', '( )', '( )', '( )', '( )', '( )', '( )', '( )', '( )', '( )', '( )', '( )', '( )', '( )', '( )', '( )', '( )', '( )', '( )', '( )', '( )', '( )', '( )', '( )', '( )', '( )', '( )', '( )', '( )', '( )'] [' min', ' min', ' min', ' min', ' min', ' min', ' min', ' min', ' min', ' min', ' min', ' min', ' min', ' min', ' min', ' min', ' min', ' min', ' min', ' min', ' min', ' min', ' min', ' min', ' min', ' min', ' min', ' min', ' min', ' min', ' min', ' min', ' min', ' min', ' min', ' min', ' min', ' min', ' min', ' min', ' min', ' min', ' min', ' min', ' min', ' min', ' min', ' min', ' min', ' min'] [ , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , ] [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '] [' , ', ' , ', ' , ', ' , ', ' , ', ' , ', ' , ', ' , ', ' , ', ' , ', ' , ', ' , ', ' , ', ' , ', ' , ', ' , , ', ' , ', ' , , ', ' , , ', ' , , ', ' , ', ' , ', ' , , ', ' , , ', ' , , ', ' , ', ' , ', ' , , ', ' , ', ' , ', ' , , ', ' , ', ' , , ', ' , ', ' , ', ' , ', ' , ', ' , ', ' , , ', ' , , ', ' , ', ' , ', ' , ', ' , , ', ' , ', ' , ', ' , ', ' , , ', ' , , ', ' , '] ['-', '$ M', '-', '-', '-', '$ M', '$ M', '-', '-', '-', '-', '$ M', '$ M', '-', '$ M', '$ M', '$ M', '$ M', '$ M', '$ M', '$ M', '-', '$ M', '$ M', '$ M', '$ M', '$ M', '$ M', '$ M', '$ M', '$ M', '$ M', '$ M', '$ M', '$ M', '$ M', '$ M', '$ M', '$ M', '$ M', '$ M', '$ M', '$ M', '$ M', '$ M', '$ M', '$ M', '$ M', '$ M', '$ M'] 1917 4 2049 "Harry Potter and the Sorcerer's Stone" 2019 2019 2019 2019 2019 2019 2019 2019 2019 2019 2019 2019 2019 2019 2019 1972 2017 1994 2008 2009 2017 2019 1994 2010 2014 2018 1982 2013 2014 1980 1999 1997 1991 2017 2001 2019 2016 2003 2001 2000 2015 2018 2009 1994 2017 2006 1985 1974 2012 2017 132 108 119 131 135 161 122 113 152 135 209 109 100 137 181 175 164 142 152 153 132 125 154 148 169 130 117 180 149 146 136 194 118 115 152 97 145 132 178 155 144 134 162 142 130 144 97 202 165 113 8.6 8.0 8.5 8.0 7.6 7.7 8.6 8.1 8.2 8.0 8.0 7.7 7.8 8.0 8.5 9.2 8.0 9.3 9.0 8.3 7.9 7.6 8.9 8.8 8.6 8.2 8.1 8.2 8.1 8.4 8.7 7.8 8.6 8.2 7.6 7.7 8.1 8.1 8.8 8.5 8.0 8.0 7.6 8.8 7.9 8.0 7.9 9.0 8.4 7.6 96 58 78 82 90 83 59 51 81 91 94 83 84 93 78 100 81 80 84 69 93 75 94 74 74 69 84 75 79 66 73 75 85 88 64 70 84 82 92 67 80 49 56 82 74 80 62 90 81 86 282 699 142 517 199 638 195 728 108 330 396 071 695 224 42 015 152 661 65 234 249 950 77 453 160 180 179 887 673 115 1 511 929 414 992 2 194 397 2 176 865 1 184 882 178 688 76 291 1 724 518 1 925 684 1 378 968 293 695 656 442 1 092 063 799 696 835 496 1 580 250 994 453 1 191 182 383 958 595 613 34 091 92 492 115 125 1 572 354 1 267 310 715 623 410 199 479 811 1 693 344 535 065 555 756 330 308 1 059 089 1 271 569 398 553 0.35 135.37 192.73 0.43 433.03 858.37 134.97 92.05 28.34 534.86 120.54 18.10 107.93 292.58 188.02 85.08 32.87 116.90 167.77 44.02 171.48 659.33 130.74 54.51 317.58 13.12 2.01 0.01 315.54 187.71 228.43 216.43 107.51 330.25 315.06 167.45 45.88 57.30 162.81 107.83 So far so good, but we aren’t quite there yet. We need to clean up our data a bit. Looks like we have some unwanted elements in our data: dollar signs,Ms, mins, commas, parentheses, and extra white space in the Metascores. Building a DataFrame With pandas The next order of business is to build a DataFrame with pandas to store the data we have nicely in a table to really understand what’s going on. Here’s how we do it: Breaking our dataframe down: is what we’ll name our DataFrame movies is how we initialize the creation of a DataFrame with pandas pd.DataFrame The keys on the left are the column names The values on the right are our lists of data we’ve scraped View Our DataFrame We can see how it all looks by simply using the print function on our DataFrame — which we called movies — at the bottom of our program: Our pandas DataFrame looks like this Data Quality Before embarking on projects like this, you must know what your data-quality criteria is — meaning, what rules or constraints should your data follow. Here are some examples: Values in your columns must be a particular data type: numeric, boolean, date, etc. Data-type constraints: : Certain columns can’t be empty Mandatory constraints ext fields that have to be in a certain pattern, like phone numbers Regular expression patterns: T What Is Data Cleaning? is the process of detecting and correcting or removing corrupt or inaccurate records from your dataset. Data cleaning When doing data analysis, it’s also important to make sure we’re using the correct data types. Checking Data Types We can check what our data types look like by running this print function at the bottom of our program: Our data type results Lets analyze this: Our movie data type is an object, which is the same as a string, which would be correct considering they’re titles of movies. Our IMDb score is also correct because we have floating-point numbers in this column (decimal numbers). But our , , , and show they’re objects when they should be integer data types, and our is an object instead of a data type. How did this happen? year timeMin metascore votes us_grossMillions float Initially, when we were telling our scraper to grab these values from each HTML container, we were telling it to grab specific values from a string. A string represents text rather than numbers — it’s comprised of a set of characters that can contain numbers. also For example, the word and the are both strings. If we were to get rid of everything except the from the string, it’s still a string — but now it’s one that only says . cheese phrase I ate 10 blocks of cheese 10 I ate 10 blocks of cheese 10 Data Cleaning With pandas Now that we have a clear idea of what our data looks like right now, it’s time to start cleaning it up. This can be a tedious task, but it’s one that’s very important. Cleaning year data To remove the parentheses from our year data and to convert the object into an integer data type, we’ll do this: Breaking cleaning year data down: tells pandas to go to the column year in our movies[‘year’] DataFrame this method: says to extract all the digits in the string .str.extract(‘(\d+’) (‘(\d+’) The converts the result to an integer .astype(int) method Now, if we run into the bottom of our program to see what our year data looks like, this is the result: print(movies[‘year’]) You should see your list of years without any parentheses. And the data type showing is now an integer. Our year data is officially cleaned. Cleaning time data We’ll do exactly what we did cleaning our year data above to our time data by grabbing only the digits and converting our data type to an integer. Cleaning Metascore data The only cleaning we need to do here is converting our object data type into an integer: Cleaning votes With votes, we need to remove the commas and convert it into an integer data type: Breaking cleaning votes down: is our votes data in our movies . We’re assigning our new cleaned up data to our votes . movies[‘votes’] DataFrame DataFrame grabs the string and uses the to replace the commas with an empty quote (nothing) .str.replace(‘ , ’ , ‘’) replace method The converts the result into an integer .astype(int) method Cleaning gross data The gross data involves a few hurdles to jump. What we need to do is remove the dollar sign and the Ms from the data and convert it into a floating-point number. Here’s how to do it: Breaking cleaning gross down: Top cleaning code: is our gross data in our movies . We’ll be assigning our new cleaned up data to our column. movies[‘us_grossMillions’] DataFrame us_grossMillions tells pandas to go to the in our movies[‘us_grossMillions’] column us_grossMillions DataFrame The calls the specified function for each item of an iterable .map() function is an anonymous functions in Python (one without a name). Normal functions are defined using the keyword. lambda x: x def is our function arguments. This tells our function to strip the from the left side and strip the from the right side. lstrip(‘$’).rstrip(‘M’) $ M Bottom conversion code: is stripped of the elements we don’t need, and now we’ll assign the conversion code data to it to finish it up movies[‘us_grossMillions’] is a we can use to change this column to a float. The reason we use this is because we have a lot of dashes in this column, and we can’t just convert it to a float using .astype(float) — this would catch an error. pd.to_numeric method will transform the nonnumeric values, our dashes, into NaN (not-a-number ) values because we have dashes in place of the data that’s missing errors=’coerce’ Review the Cleaned and Converted Code Let’s see how we did. Run the print function to see our data and the data types we have: The result of our cleaned data The result of our data types Looks good! Final Finished Code Here’s the final code of your single page web scraper: Saving Your Data to a CSV What’s the use of our scraped data if we can’t save it for any future projects or analysis? Below is the code you can add to the bottom of your program to save your data to a CSV file: Breaking the CSV file down: movies.to_csv('movies.csv') In order for this code to run successfully, you’ll need to create an empty file and name it whatever you want — making sure it has the extension. I named mine , as you can see above, but feel free to name it whatever you like. Just make sure to change the code above to match it. .csv movies.csv If you’re in Repl, you can create an empty CSVfile by hovering near Files and clicking the “Add file” option. Name it, and save it with a extension. Then, add the code to the end of your program: .csv movies.to_csv(‘the_name_of_your_csv_here.csv’) All your data should populate over into your CSV. Once you download it onto your computer/open it up, your file will look like this: Conclusion We’ve come a long way from requesting the HTML content of our web page to cleaning our entire DataFrame. You should now know how to scrape web pages with the same HTML and URL structure I’ve shown you above. Here’s a summary of what we’ve accomplished: Next steps I hope you had fun making this! If you’d like to build on what you’ve learned, here are a few ideas to try out: Grab the movie data for all 1,000 movies on that list Scrape other data about each movie — e.g., genre, director, starring, or the summary of the movie Find a different website to scrape that interests you In my next piece, I’ll explain how to loop through all of the pages of this IMDb list to grab all of the 1,000 movies, which will involve a few alterations to the final code we have here. Happy coding! Previously published at https://medium.com/better-programming/the-only-step-by-step-guide-youll-need-to-build-a-web-scraper-with-python-e79066bd895a