Removing everything except digits can make a scraped field look clean while changing its meaning. A range becomes one number. A delivery estimate becomes a price. A blank becomes zero because a later step needs something numeric.
A useful cleaner should tell you when its rule does not apply. Preserve the source value, derive a separate amount, and attach a status that the next stage can inspect.
Here is a deliberately narrow Python example for a site whose documented field contract is GBP amounts with a pound sign, a decimal point, and exactly two fractional digits. It is application-side code using synthetic inputs. It does not reproduce a BrowserAct Bot's internal implementation or report a new website test.
Define the grammar before accepting the number
The example accepts strings such as £45.17 and permits surrounding whitespace. It rejects grouped amounts, ranges, negative values, other currencies, and explanatory suffixes. The nine-digit whole-number limit is an explicit example constraint, not a currency standard.
Those rejected strings may be valid source values. They simply require a different rule. The cleaner should expose that mismatch instead of quietly expanding its interpretation.
Use this contract only after confirming the source field represents the intended price. A string matching the grammar can still be a delivery fee or an unrelated amount. Parsing syntax does not establish business meaning.
import re from decimal import Decimal GBP_PRICE = re.compile(r"£(0|[1-9][0-9]{0,8})\.([0-9]{2})") def clean_site_price(raw): result = { "raw_price": raw, "price_amount": None, "currency": None, "parse_status": "unrecognized_format", "rule_version": "site-gbp-v1", } if raw is None: result["parse_status"] = "missing_input" return result if not isinstance(raw, str): result["parse_status"] = "unexpected_type" return result match = GBP_PRICE.fullmatch(raw.strip()) if match is None: return result amount = Decimal(match.group(1) + "." + match.group(2)) result.update( price_amount=format(amount, ".2f"), currency="GBP", parse_status="parsed", ) return result The derived amount is a decimal string, suitable for an agreed serialization contract. A downstream calculation can construct a Decimal from it. Do not assume a JSON or spreadsheet importer will infer that type correctly.
The Python decimal documentation explains construction from strings. The regular expression documentation describes fullmatch, which checks the whole candidate string. Here, the grammar controls which strings reach decimal construction.
Preserve the evidence, including whitespace
The code strips whitespace from a working copy for matching. It retains the exact original string in raw_price, so a reviewer can see that cleanup occurred.
For example:
assert clean_site_price(" £45.17 ") == { "raw_price": " £45.17 ", "price_amount": "45.17", "currency": "GBP", "parse_status": "parsed", "rule_version": "site-gbp-v1", } assert clean_site_price("£0.00")["price_amount"] == "0.00" assert clean_site_price(None)["price_amount"] is None assert clean_site_price("£10.00–£20.00")["parse_status"] == "unrecognized_format" A stated zero remains zero. A missing input remains missing. An unrecognized range remains available for review rather than being flattened into a plausible-looking amount.
Notice the name missing_input: the function only knows what it received. It cannot tell whether the source lacked a price, the scraper selected the wrong element, or access failed. Keep extraction and access evidence elsewhere in the job record so those causes can be investigated separately.
Inspect the exceptions before widening the rule
Count statuses alongside record counts. A sudden increase in unrecognized_format deserves a source inspection. It might indicate a formatting change, a new currency, promotional text, or an extraction mistake.
Do not solve every exception by deleting more characters. Add a new grammar only when the field meaning and source examples justify it. Give the changed rule a new version and retain the old raw inputs so you can compare interpretations.
Grouped thousands and decimal commas require their own conventions. A price range needs a model with two endpoints or an explicit range value. A missing unit needs investigation. These are changes to the data contract, not cosmetic replacements.
For a repeated collection, retain the source URL, observation time, raw value, derived fields, and rule version together. A product can legitimately have different prices at different times. Deduplicating every repeated product URL would destroy that history if each row represents an observation.
Validate the export as another transformation
After writing CSV or JSON, read representative records back and compare their raw values, statuses, amounts, and units. Confirm that missing derived amounts did not become zero and identifiers did not change during import.
Keep the check proportional to the promise. A parser test establishes behavior for specified input strings. It does not prove that the scraper collected all intended records, that the source price was current, or that every page shares the same format.
I work on BrowserAct. Our scraped-data cleaning guide illustrates preserving original and derived fields in a separate sandbox workflow. This tutorial develops the narrow-parser principle with its own synthetic code; it does not claim to rerun that workflow.
