How to Beat a Typing Speed Test in Less Than 30 Lines of Python Code.
This blog shows and explains how to write a bot that can enter a typing speed test and outperform all other users. It does so in less than 30 lines of code. It provides a brief overview of using the Selenium WebDriver in Python.
We all know this feeling: we're visiting a website, browsing a little bit on Instagram, or even playing an online game - only to find it swarmed with bots. If you've ever wondered why you encounter bots on practically any platform with a social network, I can provide an easy answer: it's incredibly easy to set up. To showcase the simplicity of doing so, we'll create a bot using the Selenium WebDriver programming interface, which will compete and dominate in a typing-speed-measuring website in less than 30 lines of Python code.
What is the Selenium Webdriver?
Before we look into building the project, let's take a moment to understand what Selenium is and how it functions. Selenium is an open-source project encompassing various browser automation tools. For the purpose of this blog, we'll focus on one of its key tools: the Selenium WebDriver. As the official documentation describes:
Webdriver uses browser automation APIs provided by browser vendors to control the browser and run tests. This is as if a real user is operating the browser.
The Webdriver enables us to analyze and manipulate websites. For instance, we can instruct our bot to click buttons, send keystrokes, or move the mouse according to the specifications outlined in our program.
What's the project?
play.typeracer.com is a website where users can measure their typing speed in words per minute (wpm). When visiting the site, users are presented with a text to type, and a timer begins counting down. Once the timer ends, users compete against four other players to be the first to finish typing the text. Progress is visually represented by cars moving from left to right, showing the amount of text typed.
How do we implement the project?
The first step is to install the necessary requirements. Make sure you have Python 3 and the Selenium library installed. Next, we'll create an instance of our driver within our script. This will initiate a driver session, enabling us to read information from or interact with websites within the session. Let's us also navigate to the website with our driver.
from selenium import webdriver
# Initialize Webdriver
driver = webdriver.Chrome()
driver.get("https://play.typeracer.com/")
If you run the code, you should be able to see a browser window popping up, navigating to the website we have specified. It should look like this:
But the window closes almost immediately - because we haven't told the driver yet what to do. As a first step, we would like that annoying privacy window to close. We need the driver to click on that "DISAGREE" button. But how do we tell the driver which button we exactly mean? One way to do so is by specifying the XPath. XPath expressions allow us to query and navigate an HTML page and search for elements by specifying their contents or structure. For example: //div/a
returns all a-tags whose parent tag is a div. //input[@class='txtInput']
returns all input tags whose class attribute equals "txtInput". If you're unfamiliar with XPath, take a quick look at this tutorial.
To find the XPath of the website we're trying to access, let's use the "inspect" function that basically every browser has. It allows us to manipulate and - as the name already implies - inspect the HTML and CSS of a webpage. Here, we're attempting to find an XPath expression that uniquely identifies our target. Let's give it a shot with the disagree button. We know from inspecting the button that it's inside a span-tag, but there are many span-tags on this webpage. We need a characteristic that is unique to this button - perhaps a unique class, title, or structure? In our example, what immediately comes to mind is the title "DISAGREE". So, let's write our XPath:
//span[contains(text(), 'DISAGREE')]
Let's search for this expression - and voilà, only one element fits this XPath and it's the disagree button.
Now, let's add the code that clicks the button using its XPath. To achieve this, we'll need to import an additional library for XPath functionality as well.
from selenium.webdriver.common.by import By
# Click the disagree cookies button
driver.find_element(By.XPATH, "//span[contains(text(), 'DISAGREE')]").click()
Good, now let's enter a race by clicking on the "Enter a Race" button!
# Click the "Enter a Typing Race" button
driver.find_element(By.XPATH, "//a[contains(text(), 'Enter a Typing Race')]").click()
But... what is this? We get an exception which claims that this element does not exist. But if we manually click on the disagree button, we can immediately see the "Enter a Race" button.
selenium.common.exceptions.NoSuchElementException: Message: no such element: Unable to locate element: {"method":"xpath","selector":"//a[contains(text(), 'Enter a Typing Race')]"}
The problem arises when the element we're trying to access hasn't finished loading by the time our script executes. While our script runs in microseconds, web page content could take a couple seconds to load completely. As a result, the WebDriver fails to locate the specified element.
A solution to this is to wait for an element to appear and then click on it. Fortunately, there is already a library that does exactly that. To use this functionality, we need to import two additional libraries and create an instance of the 'WebDriverWait' class. Then, we specify the condition for when we should look for the element (e.g., if it has been loaded or if it is clickable). Let's take a look at how our code looks now.
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
# Initialize WebDriver
driver = webdriver.Chrome()
wait = WebDriverWait(driver, timeout=25)
# Navigate to the webpage
driver.get("https://play.typeracer.com/")
# Click the disagree cookies button
wait.until(EC.element_to_be_clickable((By.XPATH, "//span[contains(text(), 'DISAGREE')]"))).click()
# Click the "Enter a Typing Race" button
wait.until(EC.element_to_be_clickable((By.XPATH, "//a[contains(text(), 'Enter a Typing Race')]"))).click()
The code looks a bit different now, but it's essentially the same. The 'expected_conditions' library (EC) specifies the condition we're waiting for, 'wait' is the WebDriverWait instance, and 'until()' waits until the condition is met and then returns the specified element.
Now that the code has executed, we've arrived at the page where the typing challenge takes place.
The next steps are to read in the text and then typing it into that input field. As you are already familiar now with how these steps work, I'll explain them in less detail.
# Wait until the time "remainig element" is visible
wait.until(EC.visibility_of_element_located((By.XPATH, \
"//span[@title='Time remaining']")))
# Find the elements containing the text to type
text_elements = wait.until(EC.presence_of_all_elements_located((By.XPATH, "//span[@unselectable='on']")))
# Merge the text of the elements to a single string
race_text = race_text = ''.join(i.text for i in text_elements[:-1]) + " " + text_elements[-1].text
We know that the 'Time Remaining' element, which counts the remaining seconds we have for inputting the text, appears precisely when the race begins. So we just wait until that happens. Then, we read in the text (Since the layout of the website is still changing, we can't read the text at first - it could cause an "NoSuchElementException" I previously mentioned. Actually, we could, but it would exceed the 30-line limit for this program.). Initially, it seems as if the text were in a single HTML tag. When inspecting the website, however, we notice that the text is split up into two or three different HTML tags, so we need to merge their text.
Now, we have all the ingredients we need. Everything that's left to do is to input everything into the input field. To do so, we'll first locate the input field and then use the "send_keys" function to input every character of the string into that text field.
# Find input field
input_field = driver.find_element(By.XPATH, "//input[@class='txtInput']")
# Type the text
for char in race_text:
input_field.send_keys(char)
Let's try the code out. Feel free to copy the code down below or to watch this short video for a code demonstration.
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
# Initialize Webdriver and WebDriverWait
driver = webdriver.Chrome()
wait = WebDriverWait(driver, timeout=25)
# Navigate to the webpage
driver.get("https://play.typeracer.com/")
# Click the disagree cookies button
wait.until(EC.element_to_be_clickable((By.XPATH, "//span[contains(text(), 'DISAGREE')]"))).click()
# Click the "Enter a Typing Race" button
wait.until(EC.element_to_be_clickable((By.XPATH, "//a[contains(text(), 'Enter a Typing Race')]"))).click()
# Wait until the time "remainig element" is visible
wait.until(EC.visibility_of_element_located((By.XPATH, "//span[@title='Time remaining']")))
# Find the elements containing the text to type
text_elements = wait.until(EC.presence_of_all_elements_located((By.XPATH, "//span[@unselectable='on']")))
# Merge the text of the elements to a single string
race_text = race_text = ''.join(i.text for i in text_elements[:-1]) + " " + text_elements[-1].text
# Find input field
input_field = driver.find_element(By.XPATH, "//input[@class='txtInput']")
# Type the text
for char in race_text:
input_field.send_keys(char)