Price monitoring starts with a saved observation. It does not start with a
claim that a scraper keeps a historical-price database.
The Shopee Product Scraper returns product rows from a run. If you save the
Dataset export from two dated runs, you can compare those observations locally
with a small Python command. The helper in the examples repository reads two
JSON arrays, matches the same listing, and reports four outcomes:
price_changed, unchanged, new, and missing.
This is deliberately a developer workflow. The comparator makes no storefront
request, Apify request, login, or currency conversion. It cannot reconstruct a
price that you did not save.
Disclosure: I build and monetize this Actor. This is an affiliate link;
I may earn a commission if you subscribe through it.
The row identity matters
The comparison key is the exact tuple:
(market, shop_id, product_id)
A product ID by itself is not enough. The same seller and product identifiers
must stay attached to the storefront where they were observed. Keep at least
these fields in every downloaded row:
| Field | Why keep it |
|---|---|
market |
Separates storefront observations |
shop_id |
Identifies the seller |
product_id |
Identifies the listing |
currency |
Makes the numeric amount interpretable |
price |
The observed numeric price; zero is valid |
canonical_url |
Lets a reviewer inspect the exact listing reference |
source_fetched_at |
Records when the selected source supplied the row |
source_mode |
Preserves whether the row came from public search or an index |
Keep IDs as strings when they arrive as strings. That preserves leading zeroes
and very large IDs. Do not deduplicate by title: two sellers can use similar
titles, and one search result can be adjacent to the query you entered.
Create two local exports
Run the Actor separately when you need a later observation, then download each
default Dataset as JSON. Name the files by their observation time, such as
before.json and after.json. The Actor is the collection step; the helper is
the local comparison step.
For a fully local smoke test, create a fresh temporary directory first. The
commands below use shell noclobber as a second guard, so they cannot replace
saved exports elsewhere. These are test-only rows, not evidence from Shopee:
git clone https://github.com/DataKazKN/shopee-product-scraper-examples.git
cd shopee-product-scraper-examples
git checkout --detach d857edaecbc66f1b2dbb8112fc3c4e5f8b3fad59
repo_dir="$PWD"
fixture_dir="$(mktemp -d "${TMPDIR:-/tmp}/shopee-price-fixture.XXXXXX")"
(
set -C
cd "$fixture_dir"
cat > before.json <<'JSON'
[
{
"market": "th",
"shop_id": "demo-shop-1",
"product_id": "demo-product-1",
"currency": "THB",
"price": 1200,
"canonical_url": "https://example.test/th/demo-shop-1/demo-product-1",
"source_fetched_at": "2026-09-01T08:00:00Z",
"source_mode": "search_index"
},
{
"market": "th",
"shop_id": "demo-shop-2",
"product_id": "demo-product-2",
"currency": "THB",
"price": 800,
"canonical_url": "https://example.test/th/demo-shop-2/demo-product-2",
"source_fetched_at": "2026-09-01T08:00:00Z",
"source_mode": "shopee_public_search"
}
]
JSON
cat > after.json <<'JSON'
[
{
"market": "th",
"shop_id": "demo-shop-1",
"product_id": "demo-product-1",
"currency": "THB",
"price": 1300,
"canonical_url": "https://example.test/th/demo-shop-1/demo-product-1",
"source_fetched_at": "2026-09-05T08:00:00Z",
"source_mode": "search_index"
},
{
"market": "th",
"shop_id": "demo-shop-3",
"product_id": "demo-product-3",
"currency": "THB",
"price": 950,
"canonical_url": "https://example.test/th/demo-shop-3/demo-product-3",
"source_fetched_at": "2026-09-05T08:00:00Z",
"source_mode": "shopee_public_search"
}
]
JSON
python3 "$repo_dir/python/compare_snapshots.py" before.json after.json --pretty
)
The example.test URLs above are intentionally synthetic, and the temporary
directory is separate from the repository. Replace the fixture workflow with
your saved Dataset exports for a real comparison; do not point --output at an
existing export.
Run the standard-library CLI
Use the reviewed Shopee examples commit from the setup above, then run:
python3 python/compare_snapshots.py before.json after.json --pretty
To keep the report, choose a new filename:
python3 python/compare_snapshots.py before.json after.json \
--output price-report.json --pretty
The output path uses exclusive creation. It refuses to replace an input, an
existing report, or a symlink, so an accidental rerun cannot destroy the
source bytes. Use another report name when the path already exists.
For the synthetic files above, an abridged result shape is:
{
"currency": "THB",
"price_changed": [
{
"market": "th",
"shop_id": "demo-shop-1",
"product_id": "demo-product-1",
"currency": "THB",
"canonical_url": "https://example.test/th/demo-shop-1/demo-product-1",
"previous_price": 1200,
"current_price": 1300,
"previous_canonical_url": "https://example.test/th/demo-shop-1/demo-product-1",
"current_canonical_url": "https://example.test/th/demo-shop-1/demo-product-1",
"source_fetched_at": "2026-09-05T08:00:00Z",
"previous_source_fetched_at": "2026-09-01T08:00:00Z",
"current_source_fetched_at": "2026-09-05T08:00:00Z"
}
],
"unchanged": [],
"new": [
{
"market": "th",
"shop_id": "demo-shop-3",
"product_id": "demo-product-3",
"current_price": 950,
"current_source_fetched_at": "2026-09-05T08:00:00Z"
}
],
"missing": [
{
"market": "th",
"shop_id": "demo-shop-2",
"product_id": "demo-product-2",
"previous_price": 800,
"previous_source_fetched_at": "2026-09-01T08:00:00Z"
}
]
}
The real report also includes schema_version, snapshot row counts, and a
counts object. Every row retains its exact IDs and any available canonical
URL or timestamp; the abbreviated view above is still synthetic, not Actor
evidence.
Read the four categories correctly
-
price_changedmeans the identity exists in both arrays and the numeric price differs. -
unchangedmeans the identity exists in both arrays and the price is equal. -
newmeans the identity appears only in the later saved search. -
missingmeans the identity appears only in the earlier saved search.
missing is not out_of_stock. A listing can disappear because the later
search returned a different slice, an index changed, the source was incomplete,
or the query limits changed. The comparator does not convert absence into an
inventory claim. Keep the Actor's stock evidence beside the row; labels such as
public_search_listing_available or indexed_variations_available describe
what that selected source exposed, not an exact unit quantity.
Timestamps and indexed rows
Check source_fetched_at yourself before interpreting the two files as earlier
and later observations. The helper carries timestamp strings through; it does
not parse dates or enforce chronological order. The field records when the selected
source supplied the row, not a guarantee that the marketplace changed at that
instant. Preserve source_mode as well: public search and indexed results are
different evidence lanes. Keep the original exports beside the report: the
comparator report does not include source_mode. An indexed row may be stale between indexing and
collection, so it should not be described as live inventory.
The comparator validates every input row before matching. Duplicate identities,
missing market, shop_id, or product_id, negative, missing or non-finite prices,
booleans and null prices, and mixed currencies fail with a diagnostic. A
zero price is valid. Different currencies fail rather than being silently
converted.
Run the local contract tests with:
python3 -m unittest discover -s tests -v
The test rows are synthetic and are not source evidence. The command does not
mutate either input; it only reads the files and writes a new report when you
select a path that does not exist.
What the dated owner check proves
On 5 September 2026, public build 0.1.24 was checked in the owner UI with
run oBQBFizSZ6AXwliIF. It returned 10 Thailand iPhone 17-family rows in 27
seconds, with observed THB prices from 29,700 to 44,980. The rating scope was
seller; stock evidence was public_search_listing_available; optional
images, reviews, and sold fields were absent. The selector lists 14 storefronts
plus automatic URL detection, but this single run does not verify every market.
Argentina keyword search is unavailable. It is one dated
owner observation, not historical-price, savings, customer, or guarantee
proof.
The useful next step is simple: save the next compatible Dataset export and
compare it with the first one under the same market and currency rules.
Run Shopee Product Scraper on Apify
I build and monetize this Actor. This is an affiliate link; I may earn a commission if you subscribe through it.
Top comments (0)