-
Notifications
You must be signed in to change notification settings - Fork 0
How the Scraper Works
tgju.org has no API. The prices are read out of the HTML its pages are built from, which means this project has exactly one long term risk: the day tgju restyles a page. Everything below is about making that day cheap.
All three boards render the same markup. One <table class="... market-table ..."> per
section, and one row per instrument:
<table class="data-table market-table">
<thead><tr>
<th>عنوان</th><th>قیمت زنده</th><th>تغییر</th>
<th>کمترین</th><th>بیشترین</th><th>زمان</th><th></th>
</tr></thead>
<tbody>
<tr data-market-nameslug="price_dollar_rl" data-price="1,864,000">
<th><span class="mini-flag flag-us"></span>دلار</th>
<td class="nf">1,864,000</td>
<td class="nf"><span class="low">(0.32%) 6,050</span></td>
<td>1,860,800</td>
<td>1,869,100</td>
<td>۱۱:۴۹:۴۵</td>
<td class="chart-td"><a href="profile/price_dollar_rl"></a></td>
</tr>
</tbody>
</table>Three details are worth knowing:
-
data-market-nameslugis the identity. It is the key you store. The parser falls back to the last segment of the chart link, and drops a row that has neither — a spacer, an advertisement, a footer. -
The direction lives in a class, not in the number.
(0.32%) 6,050is unsigned; onlyclass="low"on the inner<span>says the price fell. That is whyChange.PercentandChange.Amountare always positive andChange.Statuscarries the sign. -
The time column is not always a time. Actively traded instruments show a clock;
stale ones show a Persian date such as
۲۴ مرداد. It is passed through as text, because the site gives no year and no timezone, and inventing either would be a lie.
internal/scrape ← the only package that knows what HTML looks like
internal/dom ← a query layer, no tgju knowledge
internal/numfmt ← Persian digits, separators, number parsing
convert.go ← maps a scraped row onto the domain model
internal/scrape deals in tables, cells and slugs and knows nothing about currencies,
gold or rials. It returns text. convert.go turns that text into an Item. When tgju
changes something, the fix is almost always in internal/scrape alone, and the fixtures
prove it.
Tolerated:
- Columns reordered. The layout is read from the
<thead>captions —قیمت زنده,تغییر,کمترین,بیشترین,زمان— so swapping the low and the high upstream swaps them in the parser too, rather than silently mislabelling every price. - Extra classes on the table, extra attributes on a row, new columns at the end.
- A missing
<thead>. The historical column order is used as a fallback. - Persian, Arabic-Indic and ASCII digits,
٬and,separators,٫and.decimals, zero-width joiners in titles. - A row with no chart link, or no daily extremes.
Not tolerated, by design:
- The
market-tableclass disappearing. That producesErrParseimmediately rather than a half-parsed page, because a scraper that guesses is worse than one that stops. - A page with tables but no rows. That is
ErrEmpty, which is a different problem fromErrParseand is reported as such.
The tgju changed its markup
issue template asks for the one thing that makes a fix a single commit: the new markup.
The repair loop:
make fixtures # refetch and re-trim the saved pages
go test ./... # the failures point at what moved
# fix internal/scrape, and the pinned values in the tests if prices changed
make cimake fixtures writes to internal/fixture/testdata/. The diff is the review: it
should be small and it should be about markup. If it is large, look at what else changed
on the page before trusting it.
Every unit test runs against saved pages, which means the whole suite can be green while
the live site has moved on. The live job in the CI workflow fetches all three boards
from the real tgju.org on every push to main. It does not block a merge — a red build
because a third party site is down helps nobody — but it is the canary, and it is the
first thing to look at when someone reports that prices stopped updating.
https://www.tgju.org/crypto builds its table with client-side JavaScript. Reading it
would mean shipping a headless browser, and a headless browser has no place in a library
you import to look up an exchange rate. If tgju ever server-renders that board, adding it
is the ten lines described in Adding a market.