Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Decision framework for choosing between regex and LLM when parsing structured text — start with regex, add LLM only for low-confidence edge cases.
.claude/skills/loulanyue-regex-vs-llm-structured-text/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-04 | ✗→✓ | ▲ Improved | 64% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 49% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 26% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 71% | 0% |
| case-15 | ✗→✓ | ▲ Improved | 106% | 0% |
A practical decision framework for parsing structured text (quizzes, forms, invoices, documents). The key insight: regex handles 95-98% of cases cheaply and deterministically. Reserve expensive LLM calls for the remaining edge cases.
Is the text format consistent and repeating?
├── Yes (>90% follows a pattern) → Start with Regex
│ ├── Regex handles 95%+ → Done, no LLM needed
│ └── Regex handles <95% → Add LLM for edge cases only
└── No (free-form, highly variable) → Use LLM directlySource Text
│
▼
[Regex Parser] ─── Extracts structure (95-98% accuracy)
│
▼
[Text Cleaner] ─── Removes noise (markers, page numbers, artifacts)
│
▼
[Confidence Scorer] ─── Flags low-confidence extractions
│
├── High confidence (≥0.95) → Direct output
│
└── Low confidence (<0.95) → [LLM Validator] → Outputpythonimport re from dataclasses import dataclass @dataclass(frozen=True) class ParsedItem: id: str text: str choices: tuple[str, ...] answer: str confidence: float = 1.0 def parse_structured_text(content: str) -> list[ParsedItem]: """Parse structured text using regex patterns.""" pattern = re.compile( r"(?P<id>\d+)\.\s*(?P<text>.+?)\n" r"(?P<choices>(?:[A-D]\..+?\n)+)" r"Answer:\s*(?P<answer>[A-D])", re.MULTILINE | re.DOTALL, ) items = [] for match in pattern.finditer(content): choices = tuple( c.strip() for c in re.findall(r"[A-D]\.\s*(.+)", match.group("choices")) ) items.append(ParsedItem( id=match.group("id"), text=match.group("text").strip(), choices=choices, answer=match.group("answer"), )) return items
Flag items that may need LLM review:
python@dataclass(frozen=True) class ConfidenceFlag: item_id: str score: float reasons: tuple[str, ...] def score_confidence(item: ParsedItem) -> ConfidenceFlag: """Score extraction confidence and flag issues.""" reasons = [] score = 1.0 if len(item.choices) < 3: reasons.append("few_choices") score -= 0.3 if not item.answer: reasons.append("missing_answer") score -= 0.5 if len(item.text) < 10: reasons.append("short_text") score -= 0.2 return ConfidenceFlag( item_id=item.id, score=max(0.0, score), reasons=tuple(reasons), ) def identify_low_confidence( items: list[ParsedItem], threshold: float = 0.95, ) -> list[ConfidenceFlag]: """Return items below confidence threshold.""" flags = [score_confidence(item) for item in items] return [f for f in flags if f.score < threshold]
pythondef validate_with_llm( item: ParsedItem, original_text: str, client, ) -> ParsedItem: """Use LLM to fix low-confidence extractions.""" response = client.messages.create( model="claude-haiku-4-5-20251001", # Cheapest model for validation max_tokens=500, messages=[{ "role": "user", "content": ( f"Extract the question, choices, and answer from this text.\n\n" f"Text: {original_text}\n\n" f"Current extraction: {item}\n\n" f"Return corrected JSON if needed, or 'CORRECT' if accurate." ), }], ) # Parse LLM response and return corrected item... return corrected_item
pythondef process_document( content: str, *, llm_client=None, confidence_threshold: float = 0.95, ) -> list[ParsedItem]: """Full pipeline: regex -> confidence check -> LLM for edge cases.""" # Step 1: Regex extraction (handles 95-98%) items = parse_structured_text(content) # Step 2: Confidence scoring low_confidence = identify_low_confidence(items, confidence_threshold) if not low_confidence or llm_client is None: return items # Step 3: LLM validation (only for flagged items) low_conf_ids = {f.item_id for f in low_confidence} result = [] for item in items: if item.id in low_conf_ids: result.append(validate_with_llm(item, content, llm_client)) else: result.append(item) return result
From a production quiz parsing pipeline (410 items):
| Metric | Value | |--------|-------| | Regex success rate | 98.0% | | Low confidence items | 8 (2.0%) | | LLM calls needed | ~5 | | Cost savings vs all-LLM | ~95% | | Test coverage | 93% |
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-03 | pass→pass | 17,304 | 12,692 | -27% | 1 | 1 | 0% | 2,741 | 3,835 | +40% | 0 | 0 | — |
case-01 | pass→pass | 20,415 | 15,348 | -25% | 1 | 1 | 0% | 3,351 | 4,563 | +36% | 0 | 0 | — |
case-02 | fail→fail | 15,787 | 16,188 | +3% | 1 | 1 | 0% | 2,401 | 4,417 | +84% | 0 | 0 | — |
case-04 | fail→pass | 12,480 | 10,052 | -19% | 1 | 1 | 0% | 2,055 | 3,379 | +64% | 0 | 0 | — |
case-05 | pass→pass | 11,447 | 2,198 | -81% | 1 | 1 | 0% | 1,904 | 2,176 | +14% | 0 | 0 | — |
case-06 | fail→pass | 9,477 | 2,171 | -77% | 1 | 1 | 0% | 1,424 | 2,122 | +49% | 0 | 0 | — |
case-07 | fail→pass | 10,823 | 3,156 | -71% | 1 | 1 | 0% | 1,859 | 2,351 | +26% | 0 | 0 | — |
case-08 | pass→pass | 16,513 | 11,043 | -33% | 1 | 1 | 0% | 2,558 | 3,729 | +46% | 0 | 0 | — |
case-09 | fail→pass | 15,080 | 14,045 | -7% | 1 | 1 | 0% | 2,496 | 4,258 | +71% | 0 | 0 | — |
case-10 | fail→fail | 20,240 | 19,991 | -1% | 1 | 1 | 0% | 3,025 | 5,069 | +68% | 0 | 0 | — |
case-11 | pass→pass | 13,235 | 8,887 | -33% | 1 | 1 | 0% | 1,920 | 3,179 | +66% | 0 | 0 | — |
case-12 | pass→pass | 11,180 | 14,346 | +28% | 1 | 1 | 0% | 1,740 | 4,335 | +149% | 0 | 0 | — |
case-13 | pass→pass | 9,797 | 6,554 | -33% | 1 | 1 | 0% | 1,862 | 3,001 | +61% | 0 | 0 | — |
case-14 | pass→pass | 9,303 | 3,935 | -58% | 1 | 1 | 0% | 1,585 | 2,470 | +56% | 0 | 0 | — |
case-15 | fail→pass | 7,597 | 3,734 | -51% | 1 | 1 | 0% | 1,186 | 2,445 | +106% | 0 | 0 | — |
case-16 | pass→pass | 11,825 | 2,982 | -75% | 1 | 1 | 0% | 2,055 | 2,405 | +17% | 0 | 0 | — |
case-17 | pass→pass | 10,397 | 5,390 | -48% | 1 | 1 | 0% | 1,618 | 2,826 | +75% | 0 | 0 | — |
case-18 | pass→pass | 13,657 | 3,420 | -75% | 1 | 1 | 0% | 2,199 | 2,365 | +8% | 0 | 0 | — |
case-19 | pass→pass | 15,826 | 9,295 | -41% | 1 | 1 | 0% | 2,678 | 3,385 | +26% | 0 | 0 | — |
case-20 | pass→pass | 14,083 | 9,802 | -30% | 1 | 1 | 0% | 2,268 | 3,377 | +49% | 0 | 0 | — |
case-21 | fail→pass | 19,771 | 10,981 | -44% | 1 | 1 | 0% | 2,999 | 3,675 | +23% | 0 | 0 | — |
case-22 | fail→pass | 5,274 | 2,320 | -56% | 1 | 1 | 0% | 832 | 2,257 | +171% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +32 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.