Taming the Chaos: Parsing Messy HTML with LLMs
Last month, I hit a wall. I was building a price comparison tool for hobby electronics, and I needed to pull product names, prices, and stock status from about 30 different vendor sites. Easy, right? Just scrape them. W
Last month, I hit a wall. I was building a price comparison tool for hobby electronics, and I needed to pull product names, prices, and stock status from about 30 different vendor sites. Easy, right? Just scrape them.
Wrong.
Every site had a unique layout. Some used tables, others used nested divs with class names like product-detail-block__3f2a. One site literally returned a different HTML structure every other request because they A/B tested their design. My BeautifulSoup selector chains looked like spaghetti, and every time a site updated, my script broke. I spent more time fixing scrapers than analyzing data.
I tried the obvious dead ends first.
What Didnβt Work
- Regex on raw HTML β Only works if you enjoy pain and hate yourself.
- CSS selectors β Brittle the moment a developer renames a class.
- Headless browser automation β Selenium and Playwright solved dynamic content, but they were slow, resource-heavy, and still required me to update selectors.
- Manual annotation β I could train a model, but that meant labeling hundreds of pages. No thanks.
I needed something that could understand the HTML rather than blindly match patterns. Something that could handle "sku" vs "product-sku" vs "data-sku" automatically.
The Lightbulb Moment: LLMs Reading HTML
A colleague mentioned they had been experimenting with GPT-4 to convert messy HTML into clean JSON. At first I laughed β "You want to throw an LLM at HTML? Thatβs like using a flamethrower to toast bread."
But I was desperate. So I tried it.
The idea is simple: feed the raw HTML (or a cleaned version) to an LLM along with a schema, and ask it to extract the data. The modelβs language understanding handles the structural variations.
I wrote a small Python prototype using OpenAIβs structured output feature (you can also use local models like Llama 3 if you donβt want API costs). Hereβs the core function:
import openai
from pydantic import BaseModel
class Product(BaseModel):
name: str
price: str
in_stock: bool
sku: str | None
# Make sure to set your API key
openai.api_key = "sk-..."
def extract_product(html: str) -> Product:
prompt = f"""
You are a data extraction assistant. Given the HTML of a product page, extract the product information.
Return a JSON object with these fields:
- name: product name
- price: the current price as a string (e.g. "$49.99")
- in_stock: boolean, whether the product is available
- sku: stock keeping unit if present, else null
Only output the JSON, no extra text.
HTML:
{html[:5000]} # limit to 5000 tokens
"""
response = openai.beta.chat.completions.parse(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}],
response_format=Product
)
return response.choices[0].message.parsed
I also built a simple cache layer to avoid reβcalling the API for the same page.
The results were⦠surprisingly good. For 8 out of 10 sites, the first extraction was perfect. For the other 2, I had to tweak the schema or add examples to the prompt.
Where It Shines
- Ever-changing layouts β The LLM adapts; you donβt rewrite selectors.
-
One schema fits many sites β I defined
Productonce and used it across all 30 stores. -
Handles weirdness β Some sites had prices inside
<span>inside<div>inside a table. The LLM figured it out.
The Ugly Trade-offs
Letβs not pretend this is a silver bullet.
Cost: Every extraction costs a fraction of a cent with gpt-4o-mini. For 1000 products, thatβs maybe $0.50 β acceptable for my side project, but not for real-time scraping at scale.
Latency: 2β5 seconds per page. If you need speed, traditional selectors win.
Hallucination: Sometimes the LLM invents a price. I added a validation step that sanityβchecks the output (e.g., price matches \d+\.\d{2}).
HTML size limits: I trim the HTML to the first 5000 characters that contain the product area. You canβt throw the whole page (maybe 100k tokens) at a cheap model.
Prompt engineering fragility: A small change in prompt wording can break extraction. I ended up with a prompt versioning system.
When NOT to Use This Approach
- Static, well-structured pages β use
BeautifulSoup. Itβs faster and free. - Extremely high volume (millions of pages) β the API cost will hurt.
- Pages with massive HTML (like whole documentation sites) β trim aggressively or use a headless renderer first.
- Data that requires pixel-perfect precision (e.g., exact currency symbol from a rendered page) β LLMs are fuzzy.
How I Blend Both Worlds
Now my pipeline tries cheap pattern matching first. If the regex or BeautifulSoup fails (or returns None), it falls back to the LLM. That way I keep cost low but still have a safety net.
For example:
from bs4 import BeautifulSoup
def fallback_extract(soup: BeautifulSoup) -> dict | None:
# Try known selectors
price_el = soup.select_one(".price, .product-price, [data-price]")
if price_el:
return {"price": price_el.get_text(strip=True)}
return None
# In the main loop:
result = fallback_extract(soup)
if not result:
result = extract_product(str(soup))
This hybrid approach turned my weekend nightmare into a maintainable script. I even found a tool called Interwest AI that does something conceptually similar (embeddings + LLM for structured extraction), but I stuck with my own pipeline because I needed fine-grained control over caching and fallbacks.
Lessons Learned
- Always trim your HTML input β youβre paying for tokens, not for elegance.
- Validate the output β a simple regex or type check saves you from hallucinated data.
- Version your prompts β I store prompts as files in Git, because changing one word can ruin everything.
- Monitor your API costs β I set a daily budget alert in the OpenAI dashboard.
What Iβd Do Differently Next Time
I would start with a local small language model (like Llama 3.1 8B) for the extraction, because itβs free after setup, even if slightly less accurate. Also, Iβd preβprocess the HTML more aggressively: strip <script>, <style>, and inline CSS to reduce noise.
Your Turn
This approach changed how I think about scraping. Instead of fighting the DOM, Iβm now teaching the machine to read. Itβs not perfect, but it gets me 90% of the way.
Have you tried using LLMs for data extraction? Whatβs your setup look like? Iβd love to hear how you handle the chaos.
Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.