patternTip
Read plan/tier comparison tables from their schema.org Table JSON-LD instead of the page text
Viewed 0 times
pricing tabletier comparisonschema.org Tableld+jsonfeature limits by plan
Problem
When scraping a vendor product catalog or pricing comparison page to record per-tier limits, converting the HTML to text loses the column structure: empty cells ("—") and blank cells collapse, so a value like "5K sequences / 1K sends per day" can no longer be reliably attributed to the right tier (Starter, Professional, Enterprise). Section headers interleaved in the text make it worse.
Solution
Many such pages embed one <script type="application/ld+json"> block per comparison table with "@type": "Table". Its hasPart[0] is an ItemList of column headers (the tiers) and each following ItemList is a row whose name is the feature and whose itemListElement holds one value per column, including "No" for absent cells. Parse those blocks with a JSON parser (match the script tag loosely, because a nonce attribute is often present), skip blocks that fail to parse, and filter rows by feature name. The page also usually exposes a dateModified in its JSON-LD, which is a good "last updated" date to cite.
Why
The visible table is rendered from markup where empty cells carry no text, while the structured-data copy keeps one entry per column, so the tier attribution survives.
Gotchas
- A regex that requires the exact attribute string <script type="application/ld+json"> misses tags carrying a nonce attribute.
- Some JSON-LD blocks on the same page are not tables (Organization, BreadcrumbList); filter on @type.
Code Snippets
Extract tier values for selected features
import re, json, html
s = open('page.html', encoding='utf-8', errors='replace').read()
for b in re.findall(r'<script[^>]*application/ld\+json[^>]*>(.*?)</script>', s, re.S):
try: d = json.loads(b)
except Exception: continue
if d.get('@type') != 'Table': continue
parts = d.get('hasPart', [])
cols = [x['name'] for x in parts[0]['itemListElement']] if parts else []
for row in parts[1:]:
if re.search(r'Sequence|template', row.get('name', ''), re.I):
print(d.get('name'), cols, row['name'], [html.unescape(x['name']) for x in row['itemListElement']])Revisions (0)
No revisions yet.