DS205 – Advanced Data Manipulation
17 Feb 2025
10:03 – 10:15
A few unusual Python concepts that might have left you a bit puzzled plus some new debugging tools.
You will probably have found it unusual that we’re not using Jupyter Notebooks much in this course. This is because for code that needs to be run in a production environment, Jupyter Notebooks are not a perfect fit.
👉 We want our code to run without a graphical user interface
Jupyter Notebooks
Python Scripts
yield Keyword in ScrapyYou are aware of lists in Python, right? But maybe you don’t know about generators?
In the words of the Python documentation:
Regular functions compute a value and return it, but generators return an iterator that returns a stream of values.
This is useful when you want to process items one at a time, without loading them all into memory. It’s a type of lazy evaluation, a concept from functional programming in computer science.
The command above will not return the item immediately. Instead, it will yield the item to the Scrapy engine, which will process it later.
In the end, as the user of a function, you will perceive this as if the function was returnning a kind of list of items for you.
ipdbKey Commands
Common Commands
n: Next lines: Step into functionc: Continue executionp variable: Print variablell: List source codeq: Quit debuggerprint()
You can also monitor your Python code using the Python logging library.
Logging is a more professional way to monitor your code. You can enable different severity levels, making it easier to filter later on.
💡 Logging is the professional way to monitor your spiders
You can customise how the logs are displayed. Here is the way I like to do it. I create a custom formatter that adds colors and mimics the Scrapy logging style:
The string "\033[32m" is the ANSI escape code for green. ANSI is a standard for controlling the formatting of text output on terminals.
This way, instead of seeing:
Parsing https://climateactiontracker.org/countries/brazil/
Successfully parsed data for Brazil
You see a colourful output that is more like this:
2025-02-16 12:21:08 [climate_tracker.spiders] INFO: Parsing https://climateactiontracker.org/countries/brazil/ 2025-02-16 12:21:09 [climate_tracker.spiders] DEBUG: Successfully parsed data for Brazil
10:15 – 10:40
Back to the world of web scraping…
Remember how last week we used CSS selectors to extract data?
There’s another way to select elements: XPath.
It’s more powerful but also more verbose 👇
CSS Selector
# Find elements containing specific text
response.xpath('//p[contains(text(), "climate")]')
# Complex conditions
response.xpath('//div[@class="rating" and @data-value > 5]')
# Navigate up the tree
response.xpath('//span[@class="price"]/ancestor::div')
# Select nth child
response.xpath('//ul/li[2]') # second list item💡 We’ll use both CSS and XPath in our spiders - each has its strengths
Common CSS Selectors
Scrapy-specific Pseudo-elements
Note there is a difference between response.css('p::text').get() and response.css('p ::text').get().
The > Combinator
<span>
<em> <!-- selected by both 'span *' and 'span > *' -->
<strong> <!-- selected by 'span *' only -->
text <!-- selected by 'span *' only -->
</strong>
</em>
<b> <!-- selected by both 'span *' and 'span > *' -->
<i> <!-- selected by 'span *' only -->
more <!-- selected by 'span *' only -->
</i>
</b>
</span>In Scrapy
💡 The space in 'span *' means “any descendant”, while 'span > *' means “direct child”
Click here for the full list of CSS selectors.
XPath looks more like working with paths in a file system.
Basic Selection
Attributes and Text
Navigation
Indexing
Click here to see the full XPath spec
10:40 – 11:05
Just like we did with our API in Weeks 2 & 3, we need to make sure our spider:
Python Unit Tests
Remember the ascor-api tests?
Key Concepts
Test one thing at a time
Arrange-Act-Assert pattern
Mock external dependencies
Clear test names
Isolated tests
Challenges
Network dependencies
Dynamic content
Rate limiting
State management
Complex setup
Web Scraping Needs
Test selectors
Validate data formats
Check pagination
Handle failures
Test pipelines
💡 Contracts are docstring-based tests
Unit Tests
More flexible
Better for complex logic
Can mock dependencies
Standard Python tools
IDE integration
Contracts
Spider-specific
Built into Scrapy
Tests real responses
Simpler to write
Self-documenting
API Models (W02-W03)
Pydantic models served as a good example of how to enforce data structure.
11:05 – 11:15

After the break:
11:15 – 11:35
Spider -> Item -> Pipeline -> OutputThen add this to your settings.py:
Add logging to your pipeline to help debug.
You can run your spider with a specific type of log level.
🐞 Current bug: the log level is not being applied to our custom logging, only to Scrapy’s default logging. If anyone finds a fix, send a PR!
When Pipelines Run
Common Issues
class CountryFlagsPipeline(FilesPipeline):
def get_media_requests(self, item, info):
"""Request SVG download if URL is present."""
if item.get('flag_url'):
yield Request(item['flag_url'])
def file_path(self, request, response=None, info=None, *, item=None):
"""Generate file path for storing the SVG."""
country = item['country_name'].lower().replace(' ', '_')
return f'flags/{country}.svg'We can use Scrapy’s Feed Exports to control the output format. They are very similar to Item Pipelines, but they are built into Scrapy.
💡 Use built-in feed exports instead of custom pipelines when possible
You can also specify the output file when running the spider:
⚠️ Common gotchas:
11:35 – 12:00
Static Approach
Dynamic Discovery
The .follow() method is used to follow a link and call a different callback function to parse the response.
Main Parser
💡 Each callback function has a specific responsibility
By the way, you can also handle pagination in your callback functions.
💡 Separating item parsing into its own method makes the code more maintainable
URL Management
Error Handling
THE END
![]()
LSE DS205 (2024/25)