-
Section 1: Getting Started
-
Section 2: Core Syntax and Data Types
-
Section 3: Collections
-
39: Set Operations: Union, Intersection, Difference
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Turtle Graphics and Early Practice Projects
-
Section 7: Working with Files and I/O
-
Section 8: Regular Expressions
-
Section 9: Object-Oriented Python
-
Section 10: Error Handling
-
Section 11: Modules and Packages
-
Section 12: Iterators, Generators, and Functional Tools
-
Section 13: Decorators and Metaprogramming
-
Section 14: Concurrency and Parallelism
-
Section 15: Working with Dates, Times, and Numbers
-
Section 16: Standard Library Deep Dive I: Data Structures
-
Section 17: Standard Library Deep Dive II: System and Introspection
-
Section 18: Standard Library Deep Dive III: Security and Encoding
-
Section 19: Standard Library Deep Dive IV: Text and Data Utilities
-
Section 20: Networking and Web Basics
-
Section 21: Working with Databases
-
Section 22: Testing and Quality
-
Section 23: Advanced Typing
-
Section 24: Context Managers and Resource Handling
-
Section 25: Text, Unicode, and Binary Data
-
Section 26: More Functional and Iteration Tools
-
Section 27: Data Validation and Configuration
-
Section 28: Working with Images and Media
-
Section 29: Property-Based and Documentation Testing
-
Section 30: Packaging and Deployment
-
Section 31: Performance and Internals
-
Section 32: Design Patterns in Python
-
Section 33: GUI Programming
-
Section 34: Security Basics
-
Section 35: Data Structures and Algorithms
-
Section 36: Practical Projects
-
Section 37: Capstone Projects
-
Section 38: Interview and Algorithm Practice
-
Section 39: Writing Idiomatic Python
216: Practice Exercise: Building a Concurrent Web Scraper
Why can't I just use a for-loop with the requests library?
You technically can, but you're wasting an enormous amount of time. When you use requests.get(), your code stops everything and just sits there—literally doing nothing—while it waits for the server to send a response back over the wire. If you have 100 URLs and each takes one second, you've just spent a minute and a half staring at a blinking cursor.
By using asyncio and aiohttp, we change the game. Instead of waiting for one request to finish before starting the next, we fire off a request and immediately tell Python, "Hey, while we're waiting for that server to wake up, go ahead and start the next five requests." It's the difference between a waiter who takes one order, walks it to the kitchen, and waits for the food to be cooked before greeting the next customer, versus a waiter who takes five orders in a row and then delivers the plates as they come out of the kitchen.
How do I stop myself from accidentally DDOSing the site I'm scraping?
This is where a lot of beginners trip up. If you have 1,000 URLs and you use asyncio.gather(*tasks) without any constraints, Python will attempt to open 1,000 simultaneous TCP connections. Most servers will see that as an attack and either block your IP immediately or just crash. I've definitely seen a few internal company tools get blacklisted because someone forgot this part.
The solution is a Semaphore. Think of it as a bouncer at a club who only lets a specific number of people in at a time. Here is how I usually implement it:
import asyncio
import aiohttp
async def fetch_url(session, url, semaphore):
async with semaphore: # This ensures only X requests run at once
async with session.get(url) as response:
return await response.text()
async def main():
urls = ["https://example.com/page1", "https://example.com/page2", "..."]
semaphore = asyncio.Semaphore(10) # Limit to 10 concurrent requests
async with aiohttp.ClientSession() as session:
tasks = [fetch_url(session, url, semaphore) for url in urls]
results = await asyncio.gather(*tasks)
return results
What's the best way to handle a few failed requests without crashing the whole script?
In a synchronous loop, a try/except block is straightforward. In an async environment, if one of your gathered tasks raises an unhandled exception, it can get messy. You don't want one 404 error or a timeout on the 50th URL to kill the remaining 950 requests you've already started.
I recommend wrapping the logic inside your worker function. Instead of letting the exception bubble up to asyncio.gather, catch it right where it happens and return a specific value (like None or an error message) so you can filter it out later. Check out this pattern:
async def fetch_url(session, url, semaphore):
async with semaphore:
try:
async with session.get(url, timeout=10) as response:
# Raise an exception for 4xx or 5xx errors
response.raise_for_status()
return await response.text()
except Exception as e:
print(f"Failed to fetch {url}: {e}")
return None # Return None so we know this one failed
Then, when you get your list of results back from gather(), you can just do a quick list comprehension to remove the None values: valid_results = [r for r in results if r is not None].
📋 Practical Task
Exercise: Building a Concurrent Product Price Tracker
Your goal is to build a scraper that fetches the HTML content of 20 different product pages from a mock e-commerce site (you can use https://books.toscrape.com/catalogue/page-X.html where X is 1 through 20).
- Use
aiohttpfor the requests andasynciofor concurrency. - Implement an
asyncio.Semaphoreto ensure no more than 5 requests are happening at any single moment. - Wrap your request logic in a
try/exceptblock to handle potential connection errors or timeouts gracefully. - After gathering all responses, print the total number of successfully retrieved pages and the average size (in bytes) of the HTML content returned.
There are no comments for now.