Web Scraper
Last updated
Last updated
In this tutorial youâll learn advanced Python web automation techniques: using Selenium with a âheadlessâ browser, exporting the scraped data to CSV files, and wrapping your scraping code in a Python class. Remove ads
Suppose that you have been listening to music on bandcamp for a while now, and you find yourself wishing you could remember a song you heard a few months back.
Sure, you could dig through your browser history and check each song, but that might be a painâĶ All you remember is that you heard the song a few months ago and that it was in the electronic genre.
âWouldnât it be great,â you think to yourself, âif I had a record of my listening history? I could just look up the electronic songs from two months ago, and Iâd surely find it.â
Today, you will build a basic Python class, called BandLeader
that connects to bandcamp.com, streams music from the âdiscoveryâ section of the front page, and keeps track of your listening history.
The listening history will be saved to disk in a CSV file. You can then explore that CSV file in your favorite spreadsheet application or even with Python.
If you have had some experience with web scraping in Python, you are familiar with making HTTP requests and using Pythonic APIs to navigate the DOM. You will do more of the same today, except with one difference.
Today you will use a full-fledged browser running in headless mode to do the HTTP requests for you.
A headless browser is just a regular web browser, except that it contains no visible UI element. Just like youâd expect, it can do more than make requests: it can also render HTML (though you cannot see it), keep session information, and even perform asynchronous network communications by running JavaScript code.
If you want to automate the modern web, headless browsers are essential.
Free Bonus: Click here to download a "Python + Selenium" project skeleton with full source code that you can use as a foundation for your own Python web scraping and automation apps.
Your first step, before writing a single line of Python, is to install a Selenium supported WebDriver for your favorite web browser. In what follows, you will be working with Firefox, but Chrome could easily work too.
Assuming that the path ~/.local/bin
is in your execution PATH
, hereâs how you would install the Firefox WebDriver, called geckodriver
, on a Linux machine:
Next, you install the selenium package, using pip
or whatever you like. If you made a virtual environment for this project, you just type:
Note: If you ever feel lost during the course of this tutorial, the full code demo can be found on GitHub.
Now itâs time for a test drive.
To test that everything is working, you decide to try out a basic web search via DuckDuckGo. You fire up your preferred Python interpreter and type the following:>>>
So far, you have created a headless Firefox browser and navigated to https://duckduckgo.com
. You made an Options
instance and used it to activate headless mode when you passed it to the Firefox
constructor. This is akin to typing firefox -headless
at the command line.
Now that a page is loaded, you can query the DOM using methods defined on your newly minted browser
object. But how do you know what to query?
The best way is to open your web browser and use its developer tools to inspect the contents of the page. Right now, you want to get ahold of the search form so you can submit a query. By inspecting DuckDuckGoâs home page, you find that the search form <input>
element has an id
attribute "search_form_input_homepage"
. Thatâs just what you needed:>>>
You found the search form, used the send_keys
method to fill it out, and then the submit
method to perform your search for "Real Python"
. You can checkout the top result:>>>
Everything seems to be working. In order to prevent invisible headless browser instances from piling up on your machine, you close the browser object before exiting your Python session:>>>
Youâve tested that you can drive a headless browser using Python. Now you can put it to use:
You want to play music.
You want to browse and explore music.
You want information about what music is playing.
To start, you navigate to https://bandcamp.com and start to poke around in your browserâs developer tools. You discover a big shiny play button towards the bottom of the screen with a class
attribute that contains the value"playbutton"
. You check that it works:
You should hear music! Leave it playing and move back to your web browser. Just to the side of the play button is the discovery section. Again, you inspect this section and find that each of the currently visible available tracks has a class
value of "discover-item"
, and that each item seems to be clickable. In Python, you check this out:>>>
A new track should be playing! This is the first step to exploring bandcamp using Python! You spend a few minutes clicking on various tracks in your Python environment but soon grow tired of the meagre library of eight songs.
Looking a back at your browser, you see the buttons for exploring all of the tracks featured in bandcampâs music discovery section. By now, this feels familiar: each button has a class
value of "item-page"
. The very last button is the ânextâ button that will display the next eight tracks in the catalogue. You go to work:>>>
Great! Now you want to look at the new tracks, so you think, âIâll just repopulate my tracks
variable like I did a few minutes ago.â But this is where things start to get tricky.
First, bandcamp designed their site for humans to enjoy using, not for Python scripts to access programmatically. When you call next_button.click()
, the real web browser responds by executing some JavaScript code.
If you try it out in your browser, you see that some time elapses as the catalogue of songs scrolls with a smooth animation effect. If you try to repopulate your tracks
variable before the animation finishes, you may not get all the tracks, and you may get some that you donât want.
Whatâs the solution? You can just sleep for a second, or, if you are just running all this in a Python shell, you probably wonât even notice. After all, it takes time for you to type too.
Another slight kink is something that can only be discovered through experimentation. You try to run the same code again:>>>
But you notice something strange. len(tracks)
is not equal to 8
even though only the next batch of 8
should be displayed. Digging a little further, you find that your list contains some tracks that were displayed before. To get only the tracks that are actually visible in the browser, you need to filter the results a little.
After trying a few things, you decide to keep a track only if its x
coordinate on the page fall within the bounding box of the containing element. The catalogueâs container has a class
value of "discover-results"
. Hereâs how you proceed:>>>
If you are growing weary of retyping the same commands over and over again in your Python environment, you should dump some of it into a module. A basic class for your bandcamp manipulation should do the following:
Initialize a headless browser and navigate to bandcamp
Keep a list of available tracks
Support finding more tracks
Play, pause, and skip tracks
Hereâs the basic code, all in one go:
Pretty neat. You can import this into your Python environment and run bandcamp programmatically! But wait, didnât you start this whole thing because you wanted to keep track of information about your listening history?
Your final task is to keep track of the songs that you actually listened to. How might you do this? What does it mean to actually listen to something anyway? If you are perusing the catalogue, stopping for a few seconds on each song, do each of those songs count? Probably not. You are going to allow some âexplorationâ time to factor in to your data collection.
Your goals are now to:
Collect structured information about the currently playing track
Keep a âdatabaseâ of tracks
Save and restore that âdatabaseâ to and from disk
You decide to use a namedtuple to store the information that you track. Named tuples are good for representing bundles of attributes with no functionality tied to them, a bit like a database record:
In order to collect this information, you add a method to the BandLeader
class. Checking back in with the browserâs developer tools, you find the right HTML elements and attributes to select all the information you need. Also, you only want to get information about the currently playing track if there music is actually playing at the time. Luckily, the page player adds a "playing"
class to the play button whenever music is playing and removes it when the music stops.
With these considerations in mind, you write a couple of methods:
For good measure, you also modify the play()
method to keep track of the currently playing track:
Next, youâve got to keep a database of some kind. Though it may not scale well in the long run, you can go far with a simple list. You add self.database = []
to BandCamp
âs __init__()
method. Because you want to allow for time to pass before entering a TrackRec
object into the database, you decide to use Pythonâs threading tools to run a separate process that maintains the database in the background.
Youâll supply a _maintain()
method to BandLeader
instances that will run in a separate thread. The new method will periodically check the value of self._current_track_record
and add it to the database if it is new.
You will start the thread when the class is instantiated by adding some code to __init__()
:
If youâve never worked with multithreaded programming in Python, you should read up on it! For your present purpose, you can think of thread as a loop that runs in the background of the main Python process (the one you interact with directly). Every twenty seconds, the loop checks a few things to see if the database needs to be updated, and if it does, appends a new record. Pretty cool.
The very last step is saving the database and restoring from saved states. Using the csv package, you can ensure your database resides in a highly portable format and remains usable even if you abandon your wonderful BandLeader
class!
The __init__()
method should be yet again altered, this time to accept a file path where youâd like to save the database. Youâd like to load this database if it is available, and youâd like to save it periodically, whenever it is updated. The updates look like this:
Voilà ! You can listen to music and keep a record of what you hear! Amazing.
Something interesting about the above is that using a namedtuple
really begins to pay off. When converting to and from CSV format, you take advantage of the ordering of the rows in the CSV file to fill in the rows in the TrackRec
objects. Likewise, you can create the header row of the CSV file by referencing the TrackRec._fields
attribute. This is one of the reasons using a tuple ends up making sense for columnar data.
You could do loads more! Here are a few quick ideas that would leverage the mild superpower that is Python + Selenium:
You could extend the BandLeader
class to navigate to album pages and play the tracks you find there.
You might decide to create playlists based on your favorite or most frequently heard tracks.
Perhaps you want to add an autoplay feature.
Maybe youâd like to query songs by date or title or artist and build playlists that way.
Free Bonus: Click here to download a "Python + Selenium" project skeleton with full source code that you can use as a foundation for your own Python web scraping and automation apps.
You have learned that Python can do everything that a web browser can do, and a bit more. You could easily write scripts to control virtual browser instances that run in the cloud. You could create bots that interact with real users or mindlessly fill out forms! Go forth and automate!
Reference : https://realpython.com/modern-web-automation-with-python-and-selenium/