Skip to content
Build a Product Attribute Classifier with LangChain and MrScraper
Article

Build a Product Attribute Classifier with LangChain and MrScraper

Engineering

Build a LangChain workflow that extracts product evidence with MrScraper, normalizes attributes and validates classification results.

By MrScraper Team 16 min read

LangChain product attribute classification turns messy product text into typed, consistent fields. MrScraper's LangChain tools collect product listings with their source URLs. A LangChain chat model then maps each listing to a fixed schema, and Python rules convert units, check values and keep anything missing as unknown instead of guessing.

TL;DR

  • Use the langchain-mrscraper tools to collect product evidence, and keep the raw text and source URL with every record.
  • Use a LangChain chat model with structured output to classify attributes into a fixed schema, then normalize units in plain Python.
  • Validate every record, flag doubtful ones for review and measure accuracy against products a person labeled by hand.

How does LangChain product attribute classification work?

It works as three separate steps: collect, classify and normalize. MrScraper collects product text from the page. A LangChain model reads that text and fills typed fields. Python code then converts units and checks the results. Each step has one job, so you can test and fix each one alone.

Separate collection, normalization and model decisions

Product data is messy because every listing is written by a different person. In our test data, the same kind of fact shows up in many forms:

  • Storage: 500GB, 256GB SSD, 1TB + 128GB SSD, 256GB SSD + 1TB SSHD
  • Operating system: Windows 10 Home, Win7 Pro 64bit, DOS, Linux + Windows 10 Home
  • Screen: 15.6", 14" FHD, 17.3" FHD IPS A good pipeline gives each kind of work to the right tool:
Step Tool Job Should not
Collect MrScraper LangChain tools Return product text, price and URL Decide categories
Classify LangChain chat model Read text and fill typed fields Do unit math or guess missing specs
Normalize Python Convert units, apply category rules Read free text
Validate Python + a person Flag bad or missing values Silently fix them

MrScraper does not perform the classification. It supplies the evidence. The classifier is your own application logic.

For this tutorial we use the laptop section of the Web Scraper e-commerce test site, a sandbox store built for scraping practice. It has 117 laptops across 20 listing pages, and each listing has a short spec line that is written a little differently every time.

How do you connect the MrScraper LangChain tools?

Install the langchain-mrscraper package, store your MrScraper API token in an environment variable and load only the tools you need. Each tool is a standard LangChain tool, so you can call it directly or give it to an agent.

Package installation, secure token and selected tools

Install the packages and pin the versions you tested:

bash
pip install "langchain-mrscraper==0.3.0" langchain-anthropic python-dotenv pandas

Create a .env file in your project folder. Keep it out of version control.

bash
MRSCRAPER_API_KEY=your_mrscraper_token
ANTHROPIC_API_KEY=your_anthropic_key

Then load just one tool. The JSON extraction tool is all this workflow needs.

python
import json
from dotenv import load_dotenv
from langchain_mrscraper import load_mrscraper_tools
 
load_dotenv()  # reads MRSCRAPER_API_KEY from .env
 
(scrape_json,) = load_mrscraper_tools(tool_names=["mrscraper_scraper_json"])
print(scrape_json.name)  # mrscraper_scraper_json

Loading one tool keeps your code simple. If you later give tools to an agent, it also keeps the agent from calling things it does not need.

We tested this tutorial on October 23, 2026 with these versions:

Package or item Version
Python 3.14.0
langchain-mrscraper 0.3.0
mrscraper-sdk 0.7.0
langchain-core 1.7.0
langchain-anthropic 1.7.6
pydantic 2.14.1
Model claude-haiku-5-5

How do you extract product evidence?

Choose the collection method that matches the page, ask MrScraper for only the fields you need and save the raw text with its source URL. That raw text is your evidence. Every attribute you create later should trace back to it.

Choose page/listing collection and retrieve completed results

The langchain-mrscraper package offers several ways to collect product data:

Tool Best for
mrscraper_scraper_json One page, extracted into a shape you describe
mrscraper_scraper_multiple_page Listings that span several pages
mrscraper_scraper_markdown Page text you want to parse yourself

Our test store uses simple numbered listing pages, so we call mrscraper_scraper_json once per page. If one page fails, you only retry that page.

Each call returns its result in the same response. The tool output is a JSON string with three keys: status_code, data and headers. The extracted records sit inside data.

python
BASE = "https://webscraper.io/test-sites/e-commerce/static/computers/laptops"
 
PROMPT = """Extract every product card on this page.
Json Schema:
{"products": [{"title": "string", "description": "string",
               "price": "string", "url": "string (absolute URL)"}]}"""
 
 
def find_products(payload):
    """Return the first list of product dicts inside the API response."""
    if isinstance(payload, dict):
        if isinstance(payload.get("products"), list):
            return payload["products"]
        for value in payload.values():
            found = find_products(value)
            if found:
                return found
    return []
 
 
records, failures = [], []
for page in range(1, 21):
    url = f"{BASE}?page={page}"
    try:
        result = json.loads(scrape_json.invoke({"url": url, "prompt": PROMPT}))
    except Exception as exc:  # network or API error
        failures.append({"url": url, "error": str(exc)})
        continue
    if result["status_code"] != 200:
        failures.append({"url": url, "error": result["status_code"]})
        continue
    for item in find_products(result["data"]):
        records.append({**item, "listing_url": url})
 
print(len(records), "products,", len(failures), "failed pages")

The find_products helper looks for the products list wherever it sits in the response. That keeps the code working if the response nesting changes slightly between API versions.

Check your output before moving on. You should see about 117 products, each with a title, description, price and product URL.

In our test run, the loop collected 117 unique laptops from all 20 pages, with no failed pages. These are the first three records:

json
[
  {
    "title": "Packard 255 G2",
    "description": "15.6\", AMD E2-3800 1.3GHz, 4GB, 500GB, Windows 8.1",
    "price": "$416.99",
    "url": "https://webscraper.io/test-sites/e-commerce/static/product/31"
  },
  {
    "title": "Aspire E1-510",
    "description": "15.6\", Pentium N3520 2.16GHz, 4GB, 500GB, Linux",
    "price": "$306.99",
    "url": "https://webscraper.io/test-sites/e-commerce/static/product/32"
  },
  {
    "title": "ThinkPad T540p",
    "description": "15.6\", Core i5-4200M, 4GB, 500GB, Win7 Pro 64bit",
    "price": "$1178.99",
    "url": "https://webscraper.io/test-sites/e-commerce/static/product/33"
  }
]

How do you classify and normalize attributes?

Give the model a typed schema with fixed choices, ask it to copy only what the text supports, and leave unit conversion to Python. Fixed choices stop the model from inventing new labels. Python makes the math the same every time.

Typed fields, unit conversions and category rules

Start with the schema. Listings can name more than one drive or operating system, so those fields are lists. Every other field can be null or unknown, because many listings leave specs out.

python
from typing import Literal, Optional
from pydantic import BaseModel, Field
 
 
class Drive(BaseModel):
    amount: float = Field(description="Storage number as written.")
    unit: Literal["GB", "TB"]
    type: Literal["SSD", "HDD", "SSHD", "eMMC", "unknown"]
 
 
class LaptopAttributes(BaseModel):
    screen_size_in: Optional[float] = Field(None, description="Screen size in inches, only if stated.")
    cpu_brand: Literal["Intel", "AMD", "Apple", "unknown"]
    cpu_model: Optional[str] = Field(None, description="Processor name as written, such as 'Core i5-7200U'.")
    ram_gb: Optional[int] = Field(None, description="Memory in GB, only if stated.")
    drives: list[Drive] = Field(default_factory=list, description="One entry per storage drive listed.")
    operating_systems: list[Literal["Windows", "macOS", "Linux", "Chrome OS", "none"]] = Field(
        default_factory=list, description="Every operating system named, in order. Empty if none is named.")

Next, write the category rules into the prompt. These rules are decisions you make for your catalog, so write them down where reviewers can see them.

python
from langchain_anthropic import ChatAnthropic
from langchain_core.prompts import ChatPromptTemplate
 
RULES = """Classify the laptop's attributes from its title and description only.
Rules:
- Copy a value only if the text states it. Otherwise use null, "unknown" or an empty list.
- RAM is the memory size. Do not count graphics card memory, such as "GTX 1060 6GB".
- drives: one entry for each storage drive, such as "128GB SSD + 1TB" (two drives).
  Set type only if the text says it (SSD, HDD, SSHD, eMMC). Otherwise "unknown".
- operating_systems: list every operating system named. Any Windows version is
  "Windows". Linux distributions such as Ubuntu or Endless OS are "Linux".
  "DOS", "FreeDOS" and "No OS" are "none".
- cpu_brand: Core, Celeron, Pentium and Atom are Intel. Ryzen, A-series
  and E-series are AMD. Use "unknown" if the processor is not named."""
 
prompt = ChatPromptTemplate.from_messages([
    ("system", RULES),
    ("human", "Title: {title}\nDescription: {description}"),
])
 
model = ChatAnthropic(
    model="claude-haiku-5-5",
    max_tokens=512,
    thinking={"type": "disabled"},
    output_config={"effort": "low"},
)
classifier = prompt | model.with_structured_output(LaptopAttributes, include_raw=True)

Setting include_raw=True returns the raw model reply along with the parsed result. If parsing fails, you get parsing_error instead of a crash, which you will count in the validation step.

Now turn the lists into catalog fields in plain Python. Writing these rules in code means they work the same for every product and are easy to audit.

python
def normalize(attrs: LaptopAttributes) -> dict:
    row = attrs.model_dump()
    drives, systems = row.pop("drives"), row.pop("operating_systems")
    # Rule: 1 TB = 1,000 GB, matching how storage is usually sold.
    # Rule: storage_gb is the total of all drives; two or more drives are "multiple".
    sizes = [d["amount"] * 1000 if d["unit"] == "TB" else d["amount"] for d in drives]
    row["storage_gb"] = sum(sizes) if sizes else None
    row["storage_type"] = ("unknown" if not drives else
                           drives[0]["type"] if len(drives) == 1 else "multiple")
    # Rule: one OS family keeps its name; two different families are "multiple".
    families = set(systems)
    row["os_family"] = ("unknown" if not families else
                        families.pop() if len(families) == 1 else "multiple")
    return row

Each rule settles a real case from our test data:

Listing text Rule Result
500GB Type not stated storage_gb: 500, storage_type: "unknown"
256GB SSD + 1TB HDD Add all drives; two drives are "multiple" storage_gb: 1256, storage_type: "multiple"
DOS DOS, FreeDOS and No OS mean no usable OS os_family: "none"
Linux + Windows 10 Home Two different families os_family: "multiple"
Windows 10 Home + Windows 10 Home Same family twice os_family: "Windows"

Note the first row. "500GB" with no other words stays unknown. Many of those drives are probably hard disks, but the listing does not say so. A catalog that guesses here will be wrong some of the time, and nobody will know which rows.

We wrote these rules after our first labeling pass turned up two-drive and two-OS listings that the original rules did not cover. That is normal. Fix the rules in one place, then apply them to the prompt, the code and your hand labels together, so the accuracy check stays fair.

How do you validate model output?

Check every record against required fields, allowed ranges and simple consistency rules. Then flag anything that fails for a person to review. Validation does not fix values. It decides which records can be used as-is and which need a second look.

Required fields, unsupported values and unknowns

python
INTEL = ("intel", "core", "celeron", "pentium", "atom")
AMD = ("amd", "ryzen", "a4", "a6", "a8", "a9", "a10", "a12", "e1", "e2")
 
 
def check(row: dict) -> list[str]:
    flags = []
    if row["screen_size_in"] is not None and not 10 <= row["screen_size_in"] <= 18.4:
        flags.append("screen_size_out_of_range")
    if row["ram_gb"] is not None and row["ram_gb"] not in {2, 4, 6, 8, 12, 16, 24, 32, 64}:
        flags.append("unusual_ram")
    if row["storage_gb"] is not None and not 16 <= row["storage_gb"] <= 4000:
        flags.append("storage_out_of_range")
    model_name = (row["cpu_model"] or "").lower()
    if model_name.startswith(INTEL) and row["cpu_brand"] != "Intel":
        flags.append("cpu_brand_conflict")
    if model_name.startswith(AMD) and row["cpu_brand"] != "AMD":
        flags.append("cpu_brand_conflict")
    if all(row[k] in (None, "unknown") for k in ("screen_size_in", "ram_gb", "storage_gb", "os_family")):
        flags.append("no_specs_found")
    return flags
 
 
results = []
for rec in records:
    out = classifier.invoke({"title": rec["title"], "description": rec["description"]})
    if out["parsing_error"] or out["parsed"] is None:
        results.append({**rec, "valid_output": False, "flags": ["invalid_output"], "needs_review": True})
        continue
    row = normalize(out["parsed"])
    flags = check(row)
    results.append({**rec, **row, "valid_output": True, "flags": flags, "needs_review": bool(flags)})

Three kinds of problems get caught here:

Problem Example Flag
Invalid output Model reply does not match the schema invalid_output
Unsupported value 300 GB of RAM, a 25-inch laptop unusual_ram, screen_size_out_of_range
Rule conflict "Core i5" labeled as AMD cpu_brand_conflict

unknown is a valid answer, not an error. Only flag it when a record has no usable specs at all.

How do you evaluate and export the results?

Measure accuracy against products a person labeled by hand from the listing text, then export every record with its source URL, raw text and flags. Hand labels must be made without looking at the model's answers. Otherwise you are only measuring agreement with yourself.

Labeled attribute checks and source-linked records

Label a random sample of at least 40 products in a spreadsheet. Use the same rules as the prompt, including unknown when the text does not say. Save it as labels.csv with one column per attribute and the product url.

Then compare field by field:

python
import pandas as pd
 
fields = ["screen_size_in", "cpu_brand", "ram_gb", "storage_gb", "storage_type", "os_family"]
pred = pd.DataFrame(results).set_index("url")
pred[fields] = pred[fields].astype(object)  # lets number columns hold "invalid"
pred.loc[~pred["valid_output"], fields] = "invalid"  # invalid output never counts as correct
gold = pd.read_csv("labels.csv").set_index("url")
gold[fields] = gold[fields].astype(object)
 
 
def same(a, b):
    """Numbers match by value (500 == 500.0); text matches ignoring case."""
    try:
        return float(a) == float(b)
    except (TypeError, ValueError):
        return str(a).strip().lower() == str(b).strip().lower()
 
 
rows = []
for f in fields:
    # Blank cells mean "not stated", so they are compared as "unknown" too.
    pairs = gold[[f]].join(pred[[f]].add_suffix("_pred"), how="inner").fillna("unknown")
    correct = sum(same(a, b) for a, b in zip(pairs[f], pairs[f + "_pred"]))
    rows.append({"attribute": f, "labeled": len(pairs), "correct": correct,
                 "accuracy": round(correct / len(pairs), 3)})
print(pd.DataFrame(rows).to_string(index=False))

We labeled 40 randomly chosen laptops by hand from the listing text. For 10 cells on listings with two drives or two operating systems, the values were filled mechanically from the written rules. Here is how the classifier did on that sample:

Attribute Labeled Correct Accuracy
screen_size_in 40 40 100%
cpu_brand 40 40 100%
ram_gb 40 40 100%
storage_gb 40 40 100%
storage_type 40 40 100%
os_family 40 40 100%

The model matched every hand label in the sample, including all seven two-drive listings and the dual-OS listings. Read that result with three limits in mind:

  • The sample is small. A perfect score on 40 products still allows a true error rate of up to about 7.5% at 95% confidence.
  • The rules were tuned on this sample. Our first labeling pass found two-drive and two-OS listings, and we added rules for them before running the classifier. That makes this a development check, not a fully held-out test.
  • The test data is tidy. Each listing is one short spec line in a mostly fixed order. Real catalogs with long descriptions, missing fields and mixed languages will be harder. The 77 products outside the sample were useful too. Validation flagged none of the 117 records. Two patterns stood out:
Listing text Model output What it shows
Apple MacBook Air 13", Intel Core i5 1.8GHz, 8GB, 256GB SSD os_family: "unknown" The listing never names an OS, so the model did not guess macOS
500GB + 16GB SSD Cache storage_gb: 516, storage_type: "multiple" A rule gap: a small cache drive is not extra storage for files

The second row is the kind of case you will keep finding. Decide a rule, such as "ignore cache drives in storage_gb," add it to the prompt, the code and your labeling guide together, then rerun the check.

The full run of 117 products used 156,577 input tokens and 24,268 output tokens, about $0.03 at Claude Haiku 5.5 list prices, with a median of 1.4 seconds per product.

Finally, export everything a reviewer needs:

python
export_cols = ["url", "title", "description", "price", *fields, "cpu_model",
               "valid_output", "flags", "needs_review", "listing_url"]
pd.DataFrame(results)[export_cols].to_csv("laptop_attributes.csv", index=False)

Each row keeps the raw description next to the normalized fields. A reviewer can see the evidence and the answer side by side, then fix flagged rows before they reach your catalog.

Here are the first two rows from our export:

csv
url,title,description,price,screen_size_in,cpu_brand,ram_gb,storage_gb,storage_type,os_family,cpu_model,valid_output,flags,needs_review,listing_url
https://webscraper.io/test-sites/e-commerce/static/product/31,Packard 255 G2,"15.6"", AMD E2-3800 1.3GHz, 4GB, 500GB, Windows 8.1",$416.99,15.6,AMD,4,500.0,unknown,Windows,E2-3800,True,[],False,https://webscraper.io/test-sites/e-commerce/static/computers/laptops?page=1
https://webscraper.io/test-sites/e-commerce/static/product/32,Aspire E1-510,"15.6"", Pentium N3520 2.16GHz, 4GB, 500GB, Linux",$306.99,15.6,Intel,4,500.0,unknown,Linux,Pentium N3520,True,[],False,https://webscraper.io/test-sites/e-commerce/static/computers/laptops?page=1

No row was flagged in our run. In your own data, a flagged row has needs_review set to True and lists the reasons in flags, such as cpu_brand_conflict.

FAQs

Does MrScraper perform the downstream classification? No. MrScraper collects product evidence, such as titles, descriptions, prices and URLs, and returns it as structured data. The attribute classification in this tutorial is your own application logic, built with a LangChain chat model, a typed schema and Python rules you control and can test.

Which LangChain package should I use? Use langchain-mrscraper from PyPI and pin the exact version you tested. This tutorial uses version 0.3.0. Tool names have changed between releases, so check the package version against MrScraper's LangChain documentation before you build, and record versions with your results.

How do I handle tool outputs? Each tool returns a JSON string. Parse it with json.loads, check status_code, then read the records from data. Handle errors per page and keep a list of failed URLs, so one bad page does not stop the whole run or quietly drop products.

Should missing specifications be guessed? No. If a listing does not state a value, keep it null or unknown. A guessed value looks the same as a real one in your catalog, so errors become impossible to find. Flag records with no usable specs and ask a person to check the source page.

How do I compare attribute accuracy? Label a random sample of products by hand from the listing text, without looking at model output. Then compare each attribute field by field. Report accuracy per attribute, not one overall score, because a model can be strong on screen size and weak on storage type.

Sources: langchain-mrscraper 0.3.0 package source and MrScraper documentation, checked Oct 23, 2026. Test run: October 23, 2026, Python 3.14.0, langchain-mrscraper 0.3.0, langchain-anthropic 1.7.6, model claude-haiku-5-5, 117 products collected, 40 labeled by hand.

Summarize this post

Open it in your assistant of choice with the prompt ready to send.

Take a Taste of Easy Scraping!

Your choices

Cookie preferences

Necessary cookies keep your selection. Optional categories are disabled until you switch them on.

Strictly necessary

Remembers your privacy selection and keeps the site working.

Always on