Skip to content

Scraper only - #2

Merged
pras-ops merged 22 commits into
mainfrom
scraper-only
Oct 7, 2026
Merged

pras-ops merged 22 commits into
mainfrom
scraper-only

Conversation

@pras-ops

@pras-ops pras-ops commented Oct 7, 2026

Copy link
Copy Markdown
Owner

No description provided.

Remove the UI/UX testing product (Studio backend and frontend, sandbox runner, test generator, flow recorder, a11y/visual/console checks, record/test/start commands and their tests). It is preserved on the archive/ui-testing-studio branch for its own repository.

Keep the element engine (selector ladder, fingerprints, self-healing) and move the element picker to scrapewizard/engine/picker.js. Drop sqlmodel and pillow, remove the frontend CI and release steps, rewrite the README for the scraper, and add SCRAPER_PLAN.md with competitor research and the improvement plan.
Second research pass covering AutoScraper, Crawl4AI schemas, Scrapling, Spidermon, changedetection.io, mitmproxy2swagger, autopager, recipe-scrapers and others, mapped onto current gaps. Records two confirmed defects: CSV export fails on uneven records and pagination detection emits invalid selectors.
Restructure the plan into background, Part A (user flow) and Part B (how the code works). Part A defines four commands, one mode, a single preview-and-confirm question, files saved in the current folder, and the messages for login, site-change repair and failure cases, then maps each piece to a build phase.
…ss waits

write_csv now uses the union of all record keys instead of the first record's, so a later record with an extra field no longer raises ValueError.

PaginationDetector no longer emits invalid selectors (':contains', unescaped class names). It now recognises rel=next and icon-only links, numbered pages with a marked current page, and load-more buttons, and exposes find_next_url().

Build steps are printed as they happen instead of animating on a timer (about 18 seconds per build).
A recipe names a repeating container and typed fields, each with a ladder of selectors. The runtime fetches over plain HTTP by default (browser as an explicit mode), reads and cleans records, follows pagination, removes duplicates and evaluates checks (minimum records, required-field coverage).

HTTP responses without a charset header are decoded using the page's own meta charset.
The builder finds the repeating block on a page, discovers the values each item holds, types and names them, detects pagination and returns a recipe. A recipe is only returned if running it against the same page yields records.

It prefers a card over the grid cell or row that wraps it, keeps an item that merely contains a sub-list (tags), drops constant labels, cut-off copies of a title and text that only adds fixed words to another field, avoids generated class names, and merges two routes to the same value into a selector ladder. Example values (likes) choose between several lists on a page.
'scrapewizard <url>' fetches a page over plain HTTP (falling back to a browser
only when the page shows no list without JavaScript), builds a recipe with no
LLM, previews the data, asks one question (this page or all pages) and saves
the data and the recipe in the current folder. --yes, --pages, --all-pages,
--like, --format and --out answer everything up front.

'scrapewizard run <recipe>' runs a saved recipe, names the data file after the
recipe file, saves nothing when no records are found, and exits non-zero when
a check fails.

README and SCRAPER_PLAN.md updated with the new flow, a status table and
known limits.
A value repeated on every item is kept when it sits in a non-control element with a meaningful class (availability, year). Buttons, links and labels ending in a colon are still dropped.

Multi-class field selectors get single-class fallbacks in their ladder, so a state class such as 'instock' changing on one item no longer loses the value.
…te changes

Each successful run is remembered in .scrapewizard/<name>.state.json beside
the recipe: row count, per-field fill rate, an index of every record and a
sample of full records.

'scrapewizard run' now reports "N new, N changed, N removed since last run"
when the page limit matches the remembered run.

When a run finds no records, or a required or previously well-filled field
stops matching, the page is searched again for the data. The recipe's fields
are matched to what is found by comparing values with the remembered records.
A repair is verified when known items are found again. Without that, it is
accepted only if every field matches by name and type. A required field that
cannot be found, or a page with no list, is refused and nothing is saved.

Repaired recipes keep their old selectors as fallbacks and record the change
in a history section. --no-repair turns this off.
Recipes that need a browser now keep one browser open for the whole run
instead of starting one per page. Two pagination types grow the page in place:
load_more clicks a button, scroll scrolls to the bottom. Both wait until the
page has gained real content and stopped changing, rather than trusting
"network idle" (reached once per page load) or a height change (a spinner).

The builder records a detected "load more" button. When more pages are asked
for and the plain page shows no way to continue, the page is checked in a
browser for controls or scrolling that only appear once scripts have run.
With --follow, a few item pages are opened while building to learn what they
add to the list: labelled rows in tables and definition lists, embedded
JSON-LD, the heading, and the main description block. Values the list already
holds, and values that are the same on every page, are left out. The fields
are stored in the recipe's 'detail' section, so every later run collects them.

Detail selectors apply to the whole page and may be CSS, 'jsonld:path.to.key'
or 'meta:name'. An item page that cannot be read leaves its fields empty
instead of failing the run.

Plain HTTP requests now share one client, so connections are reused, and item
pages are read four at a time. Reading 40 item pages took under 9 seconds
where 20 took 41 before.
--ask "what I want" sends a pruned copy of the page to the configured model,
which replies with a recipe (container and fields), not code. --ai lets the
model try only when local analysis finds no list, and suggests better column
names. Running a recipe never uses AI.

The page is untrusted input: the prompt says so, the reply is never executed,
a proposed recipe is run against the page and accepted only if it yields at
least three rows, invented fields that match nothing are dropped, and names
are accepted only if they are plain unique identifiers.

Pruning removes scripts, styles, comments, hidden elements and most
attributes, shortens long text and caps the page at 14,000 characters.

Without a key, --ai carries on with local results and --ask stops with how to
set one up. Tested against a stand-in model; no real AI service is called by
the tests.
--login opens a visible browser. The user signs in and goes to the page with
the data, then presses Enter. The recipe is built from the page they ended on
and marked 'login: true'. The session (cookies and local storage) is saved in
.scrapewizard/<name>.session.json beside the recipe and reused by 'run'.

The recipe itself holds no credentials. The .scrapewizard folder now writes
its own .gitignore, so run memory and sign-ins are not committed by accident.

A missing session, or a run that finds nothing with a saved session, says
that the sign-in is missing or has probably expired and gives the command to
sign in again, instead of attempting a repair against a sign-in page.

Suggested commands in error messages no longer wrap in narrow terminals.
…features

The default install goes from 25 packages to 13. openai and anthropic move to
an 'ai' extra, pandas and openpyxl to 'excel', and the test tools with fastapi
and uvicorn (used only by the demo app fixture) to 'dev'. aiohttp,
python-dotenv and prompt_toolkit were not imported anywhere and are removed.
requirements.txt remains the full development set.

Verified in a fresh virtual environment with only the core installed: the
installed command builds, runs and re-runs a recipe.

A missing AI package now says which package to install. The output format,
including whether Excel export is available, is checked before anything is
fetched.

README and SCRAPER_PLAN.md describe pagination in place, item pages, run
memory, self-repair, sign-in and optional AI, with the limits that remain.
Tried on 14 real pages the builder had not seen: correct list on 8 (was 4). Hacker News now works without --like, and Wikipedia tables are read in full with column names from the table headings.

Choose between nested blocks by which fields each holds. Read records split over two neighbouring elements (dt + dd, title row + details row). Group all same-tag siblings and reach them through the nearest nameable ancestor. Name links after the text they belong to, drop spare links and images, avoid generated and layout class names. Count link-only blocks as one column, weigh small groups and position-only fields less, and let the main content landmark outrank a stray menu class.

Also fix the hardware-detection test on Windows and remove two unused imports.
Many tests drive a real browser (JavaScript pages, "load more", infinite
scroll, sign-in). The workflow installed the Python package but not the
browser itself, so those tests could not pass on GitHub.
…ming

Tables
- A table whose heading row sits among its rows (no thead) lost every
  plain text column: the heading row has th cells, so the td cells looked
  like an optional extra and were dropped. Rows that agree with each
  other now keep their cells.
- A value inside a cell (a link in the first column) takes that column's
  heading as its name instead of "text".

Naming
- A class that says "title" or "headline" now outranks plain link text.
  On arXiv the title column held the paper number; it now holds the title.

Speed
- Looking for lists asked the selector engine to scan the whole page
  hundreds of times. One index of the page, built once, answers the same
  questions by lookup. A 1.7 MB Wikipedia table: 11s -> 1s. GitHub
  trending: 2.8s -> 0.7s. Results on 14 saved real pages are identical.

Tests
- 11 new offline tests for page shapes met on live sites: records split
  over dt/dd, title row + details row, heading rows, links named after
  their text, card vs its stats line, a list marked "menu" inside the
  main content, neighbouring cards, few large sections vs a long list,
  rows with mixed classes.
A list could only be reached from an ancestor with a unique id or class.
An old hand-written page names nothing, so it gave "no list found".
The body is now the anchor of last resort.
README
- New features: table headings as column names, records in two parts,
  links named after their text.
- "How well it works": results on 14 public pages the tool was not built
  against (10 right, 1 partly, 3 not), and the weak spot (column names).
- Known limits rewritten to match: bot protection, frames, generated
  class names, what has not been verified live.

SCRAPER_PLAN.md
- Status table brought up to date, with the same results, what they do
  not show, and the next steps in order of value.
- Section 1 marked as the starting point, not the present.

learn.md
- Rewritten. It described only the older AI builder. It now walks
  through the recipe path module by module, with the older builder as a
  short last section.

Also: package description no longer says "agentic AI"; a CI comment
pointed at a document that is not on this branch.
On sites built from components or utility CSS, the class names say
nothing ("d-inline-block") or are made up by a build tool ("jeApUG").
Columns came out as text, text_2, number, and selectors rested on names
that change at the site's next release.

Selectors
- Use the markers a site adds for its own tests and for search engines:
  data-testid, data-test, data-qa, data-cy, itemprop.
- Do not use short mixed-case made-up class names.
- Markers that differ only in a first word ("dundee-card", "london-card")
  are one list. BBC News: 47 cards through div[data-testid$="-card"],
  where one layout alone gave 17.
- When a larger block holds the chosen items and reads the same fields
  the same way, it is the list.

Names, tried in order when an element has no class of its own
- the marker's value (itemprop="programmingLanguage")
- the constant end of its link (/owner/repo/stargazers)
- an icon's label, or the element's accessible label
- the look of the value ("3 hours ago" -> age)
- the words after a number ("9 comments", "12 stars today")
- a label before the value ("Language: Go")
- the class of the element around it
- one long text under a title is the description
- microformat classes lose their prefix (u-author -> author)

GitHub trending, before: title, text, text_2, number, number_2, text_3
after: title, description, programming_language, stargazers, forks,
stars_today
… list

Embedded data
- A page drawn by JavaScript often ships its list as JSON in a script:
  __NEXT_DATA__, JSON-LD, or "var data = [...]". When the HTML shows no
  list (or only a menu), that data is read instead. No browser, no
  selectors. A recipe says so with a container like
  "data:props.pageProps.products"; its fields are paths ("price.amount").
- quotes.toscrape.com/js needed a browser before. It now gives all 100
  quotes over 10 pages with plain HTTP.
- Columns keep the data's own names. Constants, internal keys, tokens and
  whole article bodies are left out. Small settings lists (menus,
  language pickers) are not taken for records.
- --like works on it. Self-repair follows the list when its key is renamed.

Frames
- When a page is a shell around a frame from the same site, the list is
  read from the frame and the recipe points at the frame's address.

Menus
- A list found only in navigation is no longer returned as the answer
  straight away: embedded data and then the browser are tried first, and
  if it is still all there is, the message says it looks like the menu.

Commands
- build, list, resume and clean (the older AI builder) still work but are
  hidden from --help, which now shows six commands instead of ten.
Widening a list to "all the layouts" also took in picture-only banners
that sat among the cards (Real Python: 22 rows, 4 of them empty). The
items a larger block adds must now have the record's main value too.
Real Python is back to 18 clean rows; BBC News keeps all 47 cards.

Docs
- README: embedded data, names from the page, lasting selectors, frames;
  results now 11 of 14 right; command table matches --help; limits redone.
- SCRAPER_PLAN.md: status, a table of each gap against other tools and
  where it stands now, next steps.
- learn.md: markers and grouped layouts, the naming order, embedded data,
  frames, repair of data recipes.
@pras-ops
pras-ops merged commit 5ee064a into main Oct 7, 2026
0 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant