Mistral Web Scraping: Reading Data Hidden in HTML Attributes
Scraping and Proxy Management Expert
TL;DR:
- Mistral reads data that isn't in the visible text at all, and a live run proves it. Fed a rendered laptop catalogue page,
mistralai/ministral-3b-2512returned 6 products with the full, untruncated product name and a numeric rating — both values that live only in HTML attributes, not in the text a reader sees. - The current cheapest Mistral tier is a dedicated small-model line, not a shrunk-down flagship. Ministral 3 is Mistral's purpose-built efficient family; this guide checked the live OpenRouter catalog for the current cheapest entry rather than reusing an older "Mistral 7B" name from memory.
- The model still cannot fetch. Rendering, sessions, and access challenges belong to a fetch layer; this guide's is one POST per page through the Scrapeless Universal Scraping API.
- Attribute-only data is a real extraction trap. A page's visible link text can read
"Packard 255 G2..."while the true product name sits whole in atitleattribute a few characters away — this guide's schema and prompt name exactly where to read from. - No Mistral key yet? The identical request runs through OpenRouter. This guide executed it live on
mistralai/ministral-3b-2512and shows the captured output. - Free to start on the fetch side. Create your Scrapeless API key at app.scrapeless.com.
Can Mistral scrape websites?
Mistral extracts; it does not collect. Send it page text and a schema and it returns the fields you name — cheaply, since the model family's small tier prices among the lowest of any current API. What it cannot do is retrieve a URL, execute the JavaScript that assembles a page, or manage sessions and access challenges at scraping volume. That work belongs to a fetch layer, kept separate from the model on purpose.
This is a genuinely open surface: no earlier post on this site walks through pairing Mistral with a live fetch layer for web extraction. For the adjacent question of which model family to reach for at all, the LLM scraper explainer and the ranked list of LLM scrapers cover the category.
Install
The official Mistral SDK covers the native path, openai covers the OpenRouter path, and requests covers the fetch layer:
bash
pip install "mistralai==2.7.2" "openai==2.48.0" requests
Configure
bash
export MISTRAL_API_KEY="your_mistral_key"
export SCRAPELESS_API_KEY="sk_your_scrapeless_key"
Fetch a page where the real data hides in attributes
The demo target is a public laptop-catalogue sandbox where the visible product link is truncated ("Packard 255 G2...") but the full name lives in that same link's title attribute, and the star rating is drawn not from icon markup but from a data-rating value on a wrapping element. One POST to the Universal Scraping API returns the rendered page:
python
# fetch_laptops.py — one POST returns the rendered laptop catalogue page
import os
import requests
resp = requests.post(
"https://api.scrapeless.com/api/v2/unlocker/request",
headers={
"Content-Type": "application/json",
"x-api-token": os.environ["SCRAPELESS_API_KEY"],
},
json={
"actor": "unlocker.webunlocker",
"input": {
"url": "https://webscraper.io/test-sites/e-commerce/static/computers/laptops",
"method": "GET",
"js_render": True,
},
},
timeout=120,
)
resp.raise_for_status()
html = resp.json().get("data", "")
print(f"fetched {len(html):,} characters")
print("product cards:", html.count('class="card thumbnail"'))
with open("page.html", "w", encoding="utf-8") as f:
f.write(html)
The run prints 6 product cards from a live catalogue page hosted on a public scraping-practice site — a small, deliberately bounded page rather than the full multi-hundred-item listing the same domain also serves.
Basic implementation: Mistral as the extractor
The official SDK's chat.complete method takes response_format={"type": "json_object"}, documented in Mistral's JSON-mode reference. The current cheapest Mistral tier is ministral-3b-2512 — the smallest model in the Ministral 3 family, not an older Mistral 7B name still circulating in older tutorials.
Note: This block needs a
MISTRAL_API_KEYwith credit — the one prerequisite this guide does not assume. The next section runs the identical extraction live through OpenRouter, with captured output.
python
# extract_mistral.py — native Mistral extraction (requires MISTRAL_API_KEY)
import json
import os
from mistralai.client import Mistral
client = Mistral(api_key=os.environ["MISTRAL_API_KEY"])
page_html = open("page.html", encoding="utf-8").read()
response = client.chat.complete(
model="ministral-3b-2512",
temperature=0,
max_tokens=2000,
response_format={"type": "json_object"},
messages=[
{
"role": "system",
"content": 'Extract every laptop. Reply ONLY with JSON: '
'{"laptops":[{"title":str,"price_usd":number,"rating":int,"review_count":int}]}. '
"title is the full product name from the link's title attribute, not the truncated visible text. "
"rating is 1-5, read it from the data-rating attribute.",
},
{"role": "user", "content": page_html},
],
)
data = json.loads(response.choices[0].message.content)
print(f"extracted {len(data['laptops'])} laptops")
No prefill trick, no separate schema object — response_format alone is enough for Mistral's JSON mode.
No Mistral key? Run it through OpenRouter
Because both surfaces speak the same JSON-mode contract, the aggregator variant differs by little more than the client and base URL. This is the version this guide executed for real, fetch and extraction in one self-contained script:
python
# extract_openrouter.py — the same extraction, executed via OpenRouter
import json
import os
import requests
from openai import OpenAI
resp = requests.post(
"https://api.scrapeless.com/api/v2/unlocker/request",
headers={
"Content-Type": "application/json",
"x-api-token": os.environ["SCRAPELESS_API_KEY"],
},
json={
"actor": "unlocker.webunlocker",
"input": {
"url": "https://webscraper.io/test-sites/e-commerce/static/computers/laptops",
"method": "GET",
"js_render": True,
},
},
timeout=120,
)
resp.raise_for_status()
page_html = resp.json().get("data", "")
client = OpenAI(base_url="https://openrouter.ai/api/v1", api_key=os.environ["OPENROUTER_API_KEY"])
completion = client.chat.completions.create(
model="mistralai/ministral-3b-2512",
temperature=0,
max_tokens=2000,
response_format={"type": "json_object"},
messages=[
{
"role": "system",
"content": 'Extract every laptop. Reply ONLY with JSON: '
'{"laptops":[{"title":str,"price_usd":number,"rating":int,"review_count":int}]}. '
"title is the full product name from the link's title attribute, not the truncated visible text. "
"rating is 1-5, read it from the data-rating attribute.",
},
{"role": "user", "content": page_html},
],
)
data = json.loads(completion.choices[0].message.content)
print(f"extracted {len(data['laptops'])} laptops")
for row in data["laptops"]:
print(json.dumps(row, ensure_ascii=False))
The live run returned all 6 laptops, every field matching the source page exactly:
text
extracted 6 laptops
{"title": "Packard 255 G2", "price_usd": 416.99, "rating": 2, "review_count": 2}
{"title": "Aspire E1-510", "price_usd": 306.99, "rating": 3, "review_count": 2}
{"title": "ThinkPad T540p", "price_usd": 1178.99, "rating": 1, "review_count": 2}
{"title": "ProBook", "price_usd": 739.99, "rating": 4, "review_count": 8}
{"title": "ThinkPad X240", "price_usd": 1311.99, "rating": 3, "review_count": 12}
{"title": "Aspire E1-572G", "price_usd": 581.99, "rating": 1, "review_count": 2}
Every title, price, rating, and review count matches the source markup one for one — the model read the title attribute for the full name instead of the truncated link text, and the data-rating attribute instead of counting icon markup. The whole call: 28,797 tokens, almost all of it input, because the fetched page carries a full page of navigation and script chrome around six small product cards.
Get your API key on the free plan: app.scrapeless.com
Advanced patterns
- Say exactly where a value lives when it isn't in the visible text. "Read the full title from the
titleattribute" and "read rating fromdata-rating" are what produced correct output here — a prompt that only says "extract the title" invites the model to return the truncated string it can see. - Watch the input-to-output token ratio. This run spent 28,554 tokens on input against only 243 on output — the page's surrounding chrome, not the six records, dominates the bill. Trimming to the product-grid container before sending would cut that by a large margin with no loss of extractable content.
- Keep temperature at zero for extraction. Ministral models respond well to
temperature=0for structured tasks; anything higher reintroduces the paraphrasing risk that defeats exact-match fields like a title attribute. - Reserve the larger Mistral tiers for genuinely hard pages. The 3B tier read attribute-encoded data with zero errors here; escalate only if a specific field keeps coming back wrong on otherwise-clean input.
Troubleshooting
- Titles come back truncated, matching only the visible text. The prompt didn't say to read the attribute. Name the exact source ("the link's
titleattribute") rather than the field alone. - Ratings are wrong or all identical. The value usually lives in an attribute (
data-rating) rather than a countable icon or a plain number in text — say where it lives, the way this guide's prompt does. - Fewer laptops than the page actually has. Check the fetched HTML's card count first, the way the fetch script above does — a short fetch is a rendering issue, not something the extraction prompt can fix.
- Output varies between identical runs. Set
temperature=0; extraction is a transcription task, not a generative one.
Conclusion
Mistral's cheapest current tier handled a real trap cleanly: a truncated visible title, a rating hidden in a data attribute, and it read both correctly with nothing more than a response_format flag and two sentences telling it where the real values live. What the model still cannot do is fetch that page in the first place — one POST through a dedicated fetch layer supplies the HTML, and the six-line schema above turns it into typed records with no selector code anywhere in the pipeline.
Ready to Feed Mistral Real Pages?
The fetch layer here is the Universal Scraping API — plans and request volumes are on the pricing page, with every unlocker.webunlocker parameter in the developer docs. Create a key on the free plan at app.scrapeless.com and both scripts run as written.
FAQ
Q: Can Mistral scrape websites by itself?
No. The Mistral API extracts from text you provide; it cannot issue HTTP requests, render JavaScript, or hold a session. Every working Mistral scraping setup pairs it with a fetch layer that returns faithful page content.
Q: Which Mistral model should I use for web-scraping extraction?
ministral-3b-2512 — the current cheapest tier in the dedicated Ministral 3 small-model family, checked against the live OpenRouter catalog rather than an older "Mistral 7B" name. The live run in this guide used it and returned all 6 records with every field matching the source page exactly.
Q: How does Mistral handle data that's hidden in HTML attributes rather than visible text?
Correctly, when the prompt says where to look. This guide's system message named the exact attribute for both the untruncated title and the numeric rating; without that instruction, a model tends to return the truncated text it can visually see instead.
Q: Local Mistral via Ollama versus the API — which should I use for scraping extraction?
The API path in this guide needs no local GPU or model download and returns results in one request; a local Ollama deployment trades that convenience for zero per-token cost and full data locality. Both point the same fetch-then-extract architecture at different execution environments — pick the API for low-volume or bursty jobs, local for high-volume or data-sensitive ones.
Q: Is scraping with Mistral legal?
The extraction layer does not change collection rules. Fetch public pages only, respect site terms and the robots directives standardized by the Robots Exclusion Protocol, keep volumes bounded, and handle any personal data under applicable law — plus Mistral's usage terms on the model side.
At Scrapeless, we only access publicly available data while strictly complying with applicable laws, regulations, and website privacy policies. The content in this blog is for demonstration purposes only and does not involve any illegal or infringing activities. We make no guarantees and disclaim all liability for the use of information from this blog or third-party links. Before engaging in any scraping activities, consult your legal advisor and review the target website's terms of service or obtain the necessary permissions.



