Raw text is full of useful information hiding in plain sight — email addresses, dates, file names, prices. Finding them with string methods means scanning character by character, tracking indices, and writing fragile code that snaps when the format changes slightly.
In the previous module you learned regex as a pattern language. This milestone puts it to work. You will write small, focused patterns that pull specific entities out of documents and hand them back as clean, structured data. That is the exact skill that powers search engines, contact scrapers, and log analysers.
A well-written extraction function is a filter: messy text goes in, a list of verified matches comes out. The patterns you write here will become the detection layer of your finished text analyzer.
flowchart LR T["Raw text"] --> E["re.findall<br/>pattern"] E --> L["List of<br/>matches"] style E fill:#3776ab,color:#fff
Extracting emails
An email has a recognizable shape: some characters, an @, more characters, a dot, and a domain ending. A naive pattern like \w+@\w+\.\w+ catches many real addresses, but it also misses dots in the user part and allows invalid domain lengths.
A more practical pattern anchors to word boundaries and allows dots and hyphens where they actually appear: r'[\w.-]+@[\w.-]+\.\w{2,}'. The character class \w covers letters, digits, and underscores; the quantifier {2,} insists on at least two characters after the final dot, which rejects a@b.c while accepting a@b.co.uk.
re.findall with this pattern returns every match as a string. If you also need the parts — user and domain — wrap them in parentheses and read .group(1) and .group(2) from each match object. This is how a scraper separates the recipient from the host before storing either one.
import re
text = 'Contact ada.lovelace@example.com or bob@test.io for details.'
emails = re.findall(r'[\w.-]+@[\w.-]+\.\w{2,}', text)
print('Found:', emails)
m = re.search(r'([\w.-]+)@([\w.-]+\.\w{2,})', text)
if m:
print('User:', m.group(1))
print('Host:', m.group(2))
Extracting dates
Dates appear in many formats: 2024-03-15, 15/03/2024, or Mar 15, 2024. A single pattern cannot catch them all unless it is enormous and unreadable. The better approach is to pick one format, write a tight pattern for it, and document the assumption.
For ISO-style dates — four digits, a hyphen, two digits, a hyphen, two digits — the pattern is r'\d{4}-\d{2}-\d{2}'. The braces {4} and {2} are exact counts, so \d{4} means 'exactly four digits' and nothing else will pass.
When you need the year, month, and day separately, capture each block: r'(\d{4})-(\d{2})-(\d{2})'. After a successful search, .group(1) holds the year, .group(2) the month, and .group(3) the day. You can then convert them to integers and build a dict or tuple for downstream code.
If your input mixes formats, run multiple patterns and merge the results rather than trying to invent one pattern that does everything.
flowchart TD
P["Pattern<br/>#quot;(\d{4})-(\d{2})-(\d{2})#quot;"] --> G1["group 1 = year"]
P --> G2["group 2 = month"]
P --> G3["group 3 = day"]
style P fill:#3776ab,color:#fff
import re
log = 'Session started 2024-03-15, ended 2024-04-01, backup 2024-04-02'
dates = re.findall(r'\d{4}-\d{2}-\d{2}', log)
print('Dates:', dates)
m = re.search(r'(\d{4})-(\d{2})-(\d{2})', log)
if m:
print('Year:', m.group(1))
print('Month:', m.group(2))
print('Day:', m.group(3))
The over-broad trap
The most common extraction bug is a pattern that matches too much. r'<.*>' applied to HTML does not stop at the first tag — it keeps going to the last closing angle bracket in the entire string. In email extraction, r'\w+@\w+\.\w+' silently skips addresses that contain dots in the user name, because \w does not match a dot.
The fix is usually a character class that is generous where it needs to be and strict where it matters. [\w.-]+ for the user part allows dots and hyphens, while \w{2,} at the end insists on a real top-level domain. Anchors like \b (word boundary) or ^ and $ (start and end of string) stop the match from drifting into neighbouring text.
Word boundaries are especially useful when the target sits inside a larger sentence. r'\b[\w.-]+@[\w.-]+\.\w{2,}\b' will not accidentally include the trailing comma or period that often follows an email in running prose.
flowchart LR G["Greedy .*"] --> R1["Matches too much"] T["Tight \d+\.\d+"] --> R2["Exact match"] style G fill:#b45309,color:#fff style T fill:#3776ab,color:#fff
Building an extraction pipeline
Real analysis rarely needs just one entity. A document might contain emails, dates, and prices, and your program needs all three. The clean design is a small function per entity — extract_emails(text), extract_dates(text) — and a top-level function that calls each one and assembles the results into a dict.
This mirrors how professional libraries are structured: small, testable pieces that compose into larger workflows. It also makes debugging far easier, because when a date goes missing you know exactly which function to inspect.
Write extract_emails(text) that returns a list of all email addresses in text. Use re.findall with the pattern r'[\w.-]+@[\w.-]+\.\w{2,}'.
import re
def extract_emails(text):
# Return every email address found in text
pass
re.findall returns every non-overlapping match as a list of strings.
Remember to import re at the top of your function.
import re
def extract_emails(text):
return re.findall(r'[\w.-]+@[\w.-]+\.\w{2,}', text)
Predict exactly what this script prints. The pattern uses a capturing group.
import re
text = 'Start: 2024-03-15, End: 2024-04-01'
m = re.search(r'(\d{4})-(\d{2})-(\d{2})', text)
print(m.group(1))
print(m.group(2))
m.group(1) is the first capture: the four-digit year.
m.group(2) is the second capture: the two-digit month.
Write extract_iso_dates(text) that returns a list of dicts, each with keys 'year', 'month', and 'day' as integers. Use re.finditer with the pattern r'(\d{4})-(\d{2})-(\d{2})' and build a dict for each match.
import re
def extract_iso_dates(text):
# Return a list of dicts with year, month, day as ints
pass
re.finditer yields match objects one at a time.
Convert each group to int() before putting it in the dict.
import re
def extract_iso_dates(text):
dates = []
for m in re.finditer(r'(\d{4})-(\d{2})-(\d{2})', text):
dates.append({
'year': int(m.group(1)),
'month': int(m.group(2)),
'day': int(m.group(3))
})
return dates
This function is meant to return the first price in text like $42.50, but it matches everything from the first dollar sign to the last dot in the string. Fix the pattern to use a tight character class instead of the greedy .*.
import re
def first_price(text):
m = re.search(r'\$.*\.\d+', text)
return m.group(0) if m else ''
Replace .* with \d+.\d+ to match digits-dot-digits exactly.
The $ is a literal dollar sign, escaped as $ in the pattern.
import re
def first_price(text):
m = re.search(r'\$\d+\.\d+', text)
return m.group(0) if m else ''
Write analyze_text(text) that returns a dict with two keys: 'emails' containing all found emails, and 'dates' containing all ISO-style dates (as strings, not parsed). Use the patterns r'[\w.-]+@[\w.-]+\.\w{2,}' for emails and r'\d{4}-\d{2}-\d{2}' for dates.
import re
def analyze_text(text):
# Return {'emails': [...], 'dates': [...]}
pass
Call re.findall once for emails and once for dates.
Return a dict with both lists under the correct keys.
import re
def analyze_text(text):
return {
'emails': re.findall(r'[\w.-]+@[\w.-]+\.\w{2,}', text),
'dates': re.findall(r'\d{4}-\d{2}-\d{2}', text)
}
Recap
- Extraction turns messy text into structured data using focused regex patterns.
re.findallcollects every match;re.searchgives you the first match object with captured groups.- Character classes like
[\w.-]+are more precise than bare\w+for real-world data. - Anchors and exact counts (
{2,},\b) stop patterns from matching too much. - Regex validates shape, not truth — always sanity-check extracted values before acting on them.
- When data arrives in mixed formats, prefer several tight patterns over one giant expression.
In the next milestone you will package these extractions into a clean JSON report, turning raw findings into a shareable analysis payload.