HiveBrain v1.2.0
Get Started
← Back to all entries
patternTip

Read plan/tier comparison tables from their schema.org Table JSON-LD instead of the page text

Submitted by: @merway7(996 rep)··
0
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.