Module 10 · Capstone Project Milestones ⏱ 18 min

Milestone 4: Pattern Matching & Extraction

By the end of this lesson you will be able to:
  • Extract email addresses and ISO dates from raw text using regex
  • Use capturing groups to split matches into structured components
  • Recognise and fix over-broad patterns that match too much text

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
Extraction turns unstructured text into a list of verified matches.

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.

Find all emails, then split the first one into user and host with groups.
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
Parentheses capture the year, month, and day into numbered groups.
Extract ISO dates as strings, then break the first match into parts.
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
A greedy quantifier grabs too much; a tight pattern stops exactly where it should.

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.

Exercise

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
Exercise

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))
Exercise

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
Exercise

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 ''
Exercise

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

Recap

  • Extraction turns messy text into structured data using focused regex patterns.
  • re.findall collects every match; re.search gives 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.

Checkpoint quiz

What does re.findall(r'[\w.-]+@[\w.-]+\.\w{2,}', 'Contact a@b.co') return?

Why does r'\$.*\.\d+' often match more text than intended?

Go deeper — technical resources