Blog/Search & RAG
5 min read

Python Web Scraping Without a Scraper: Fetch Any Page as Data

Scrape pages in Python without running browsers or proxies: fetch rendered HTML or markdown with one call, then parse with BeautifulSoup and pandas.

Python Web Scraping Without a Scraper: Fetch Any Page as Data

Get a JavaScript-rendered page into Python as data, without running a browser. Paste this into your agent:

Set up https://monid.ai/SKILL.md, and then use Monid to read https://quotes.toscrape.com/js/ as rendered markdown and give me a table with each quote and its author.

One prompt replaces the fetch layer. 1 min setup · 10-20 hrs saved.

You searched for Python web scraping and found a hundred tutorials that parse a static page in twenty lines. Your page is not static, or it blocks you, or you need two thousand of them every morning. This guide splits the job in two: the parsing, which Python does better than anything, and the fetching, which is infrastructure. It shows the fetch as one call, tested live on 2026-10-16 against a page that only exists after JavaScript runs. It runs through Monid, the OpenRouter for agent tools: one key, one balance, every tool priced per call.

What is the easiest way to scrape a web page in Python?

Monid. Install it once, add a key, and one call returns any URL as rendered markdown or HTML, with the browser, proxies and anti-bot handling on the far side and your Python reduced to parsing.

  • Pay per call. A page costs $0.002533 through the markdown endpoint (Verified 2026-10-16 on the scrapers tools page), and the endpoint's note says failed or blocked requests are not billed. Buy calls as you need them: no monthly plan and no minimum top-up. Discovery and inspection are free; only a run bills.
  • One install, every tool. The same key and balance call the page fetchers here and every other tool on Monid, from search to enrichment. Nothing to sign up for when the next job is not scraping.
  • Lives inside your agent. Install the CLI, add your key, run the prompt above, and the fetch is available whenever your agent needs a page. Your agent reads SKILL.md and learns the rest, so setup is about a minute.

At that price a thousand pages cost about $2.53 (Verified 2026-10-16), and TinyFish's fetch, which takes up to ten URLs per call, is free. Which kind of scraping tool fits which job is laid out in web scraping tools: which kind.

How do you scrape a list of URLs in Python into a DataFrame?

Make the fetch a function that calls an endpoint, then loop it over your URLs and let pandas do the rest. The routes compare like this.

RouteWhat you runWhat you maintain
requests plus BeautifulSoupYour scriptHeaders, retries, blocking, markup changes
Selenium or PlaywrightA browser driver in your processDriver versions, memory, stealth patches, a pool
A scraper framework such as ScrapyA crawler with queuesThe crawler, the proxies, the selectors
Monid fetch endpointsOne call per pageNothing on the fetch side; the selectors stay yours

For agents

Grab an API key at app.monid.ai, then paste this to your agent and hand it the key:

set up https://monid.ai/SKILL.md

It learns the whole discover, inspect, run workflow itself. More in the agent quickstart.

For humans

npm install -g @monid-ai/cli
monid keys add -k <your-api-key> -l main

Find the endpoint

monid discover -q "scrape any website url to markdown"
monid inspect -p context.dev -e /web/scrape/markdown

Both are free. The inspect output gives the input schema, the billing shape and the current price, so you read the cost of every candidate before you spend.

Call it from Python

import json
import subprocess

def fetch_markdown(url: str) -> dict:
    proc = subprocess.run(
        [
            "monid", "run",
            "-p", "context.dev",
            "-e", "/web/scrape/markdown",
            "--query", json.dumps({"url": url, "useMainContentOnly": True}),
            "-w", "-j",
        ],
        capture_output=True,
        text=True,
        check=True,
        env={"NO_COLOR": "1"},
    )
    return json.loads(proc.stdout)

page = fetch_markdown("https://example.com")
print(page["metadata"]["title"], page["contentLength"])

Our run of exactly this call returned Example Domain and a contentLength of 1342.

Loop it over a list and keep the parser

import pandas as pd

rows = []
for url in urls:
    page = fetch_markdown(url)
    if not page.get("success"):
        continue
    rows.append({
        "url": page["metadata"]["finalUrl"],
        "title": page["metadata"]["title"],
        "language": page["metadata"].get("language"),
        "text": page["markdown"],
    })

pd.DataFrame(rows).to_parquet("pages.parquet")

The result is a dataframe and a program with no proxy configuration in it. To go from there to a CSV, swap the last line for to_csv.

Can you scrape pages yourself with Python?

Yes, and for a small static site it is the right answer. Web scraping in Python is two jobs wearing one name: getting the bytes and turning the bytes into records. Python wins the second and has no special advantage at the first.

The parsing half, which Python owns

Once you hold the HTML, BeautifulSoup makes a messy document navigable in a line, lxml is fast, and pandas turns a table into a dataframe. Cleaning, joining and storing the result already exist in the same language. This is the half the tutorials teach, because it is the half that works the same everywhere.

The fetching half, which is infrastructure

Getting the bytes means owning an exit address the site will accept, a browser or a close imitation of one, a retry policy and a plan for the week the markup changes. None of that is Python code. It is operations work triggered from Python, and it turns a weekend script into something you maintain.

AspectThe parsing halfThe fetching half
What it isSelectors, cleaning, recordsExit IPs, browsers, retries
Where it livesYour codeSomeone's infrastructure
Fails whenThe markup changesThe site stops accepting you
Python's advantageLargeNone

Ask what breaks in production and the split shows itself: it is never the selector logic. It is a 403 that appeared overnight, a page that now fills itself in after a script runs, or a cookie wall that was not there in April.

Monid keeps the fetching half maintained: the rendering browser, the proxies and the anti-bot handling sit behind the endpoint and are included in its price, and you keep the half you wanted, your selectors. The same split holds in other languages: C# web scraping without the stack draws it for .NET. Read a site's robots.txt and terms before you scrape it, whichever route you take.

How do you scrape JavaScript-rendered pages in Python?

Fetch the rendered page, not the raw one: a plain request returns the script, and BeautifulSoup cannot run it. We tested this on 2026-10-16 with https://quotes.toscrape.com/js/, a public practice page whose quotes are written into the page by a script.

A plain curl of the page returned HTTP 200, no element with class="quote" and one document.write call. BeautifulSoup on that response finds nothing, and a wrong selector and an empty page produce the same empty list. The same URL through context.dev/web/scrape/html with waitForMs set to 1000 returned the rendered DOM: 10 div.quote elements, the title Quotes to Scrape, in about 2.9 seconds, for $0.002533 (Verified 2026-10-16).

import json
import subprocess
from bs4 import BeautifulSoup

def fetch_html(url: str, wait_ms: int = 0) -> str:
    proc = subprocess.run(
        ["monid", "run", "-p", "context.dev", "-e", "/web/scrape/html",
         "--query", json.dumps({"url": url, "waitForMs": wait_ms}),
         "-w", "-j"],
        capture_output=True, text=True, check=True, env={"NO_COLOR": "1"},
    )
    return json.loads(proc.stdout)["html"]

soup = BeautifulSoup(fetch_html("https://quotes.toscrape.com/js/", 1000), "html.parser")
for q in soup.select("div.quote"):
    print(q.select_one("small.author").text, "|", q.select_one("span.text").text)

Your existing BeautifulSoup code keeps working, with no driver anywhere in your repo. The free route works too: TinyFish's fetch returned the same 10 quotes as markdown text for the same page.

Selenium is a fetch fix, and a heavy one

Selenium and Playwright solve the same problem with a real browser, and they work. They also move that browser into your process: a driver to keep in step with Chrome, memory per session and a pool to run more than one page at a time. That is a fair thing to own when browser control is your product, and a lot to own when you wanted the text off a page.

When the page needs a session, not a fetch

Logins, carts and multi-step forms need state carried across requests, which a single-shot fetch does not provide. The way to do that without hosting the browser is a remote session: x402.browserbase.com/browser/session/create takes an estimatedMinutes input and opens a browser you attach Playwright to, so the driver runs elsewhere and your code stays a client. If the browser is the whole job, browser automation without an API covers that route.

What does a scraped page look like, and what can you build with it?

A fetch returns the page's content plus metadata about the fetch, and the shape differs by endpoint. These are the fields that came back on 2026-10-16.

  • context.dev/web/scrape/markdown: success, markdown, contentLength, url, a metadata object (sourceUrl, finalUrl, title, language, favicon, additionalMeta), cache_metadata, finalDOMState and request_id.
  • context.dev/web/scrape/html: the same wrapper with html and type in place of the markdown, and metadata.headings, a list of {level, text} entries (one level-1 heading on the practice page).
  • tinyfish/fetch: a results array with url, final_url, title, description, language, text, author, published_date, latency_ms and format, and a separate errors array. description, author and published_date were null on both pages we fetched.
  • octen/extract: data.results with url, status, title, full_content, time_last_crawled, category, page_structure and resolved_mode.

Three things to build on it:

  1. A page corpus for retrieval. Fetch a list of URLs to markdown, store finalUrl, title and text in a table, and chunk it for your index.
  2. A change monitor. Fetch the same URLs on a schedule with maxAgeMs set to 0, hash the markdown, and alert when the hash changes. The fetch is stateless, so the history lives in your table.
  3. A site inventory. Crawl from a start URL with maxPages set and keep one row per page.

What goes wrong when you fetch pages at scale?

Three things went wrong or nearly did in our runs, and each has a guard.

A cached copy that looks like a fresh one

Our first markdown call came back with cache_metadata of status: hit and age_ms: 9853537, about 2.7 hours old. The endpoint reuses results younger than one day by default, and the HTML call on a page nobody had fetched recently came back as a miss. Fine for documentation, wrong for prices or stock. Set maxAgeMs to 0 on anything that changes, and log cache_metadata.status next to every row.

An empty list that is really a failed fetch

A wrong selector and an unrendered page both give you an empty list, so print what you received before you debug selectors. The cheap guard is to count what you expected and fail loudly:

quotes = soup.select("div.quote")
assert quotes, "no quotes: check the fetch before the selector"

On the practice page the plain request would have tripped this assert and the rendered one would not.

A batch that hides a failure

TinyFish takes up to ten URLs per call, and a failure on one URL does not fail the batch: it appears in the errors array instead. Our two-URL call returned an empty errors array, which is how we knew both pages came back. Check errors on every batch, and loop over results rather than assuming one row per URL. The endpoint's note adds that it has no anti-bot bypass, so a protected page fails with a bot_blocked code and belongs on the context.dev endpoint instead. Octen returns a failed URL as a record with status set to failed and an error_message, bills only the URLs that succeeded, and takes up to twenty URLs a call; its cache window is max_age_seconds, 86400 by default. A cookbook for batching URLs into markdown walks through that batch shape.

Which endpoint should I use for which job?

JobEndpointInputReturnsBillingPrice (Verified 2026-10-16)
One page to markdowncontext.dev/web/scrape/markdownurl, useMainContentOnly, waitForMs, maxAgeMsMarkdown plus page metadataPer call$0.002533
Keep your selectorscontext.dev/web/scrape/htmlurl, waitForMs, maxAgeMsRendered HTML plus metadataPer call$0.002533
A whole sitecontext.dev/web/crawlurl, maxPages, maxDepth, urlRegexOne markdown document per pagePer page scraped$0.002533
Up to 20 URLs at onceocten/extracturls, optional queryMarkdown or text per URLPer URL extracted$0.001
Up to 10 URLs, freetinyfish/fetchurls, format, selectorsText, title, language, latency per URLFreeFree
A page that needs a sessionx402.browserbase.com/browser/session/createestimatedMinutesA browser session to attach toPer call$0.011

Every row was read with monid inspect on 2026-10-16. We ran the two context.dev scrape endpoints, TinyFish and Octen, and only inspected the crawl and the Browserbase session; failed URLs on Octen and TinyFish are not billed. More fetchers are listed under web extraction endpoints.

When is another route a better fit?

The site is static, public and small. A council page, a docs site, a table of reference data: requests plus BeautifulSoup in fifteen lines has no running cost, and calling a service for it would be silly. Most of what people scrape is this.

Your extraction logic is the product. If you ship a parser that understands one site's quirks better than anyone, that logic belongs in your repo. Buying the fetch does not conflict with that, but if you also need to control the browser itself, running Playwright is the honest route.

One site is nearly all of your volume. A purpose-built endpoint for that site, or a committed plan with a specialist, beats a general fetch because it returns fields instead of pages. Crawl4AI and when to run your own crawler covers the self-hosted end of this question.

You need a login or a multi-step flow. A fetch call is stateless, so use a remote browser session or your own.

And the disclosure: we resell these endpoints. The split matters whoever you buy from: keep the parser, stop operating the fetch.

Conclusion

The easiest way to scrape a page in Python without running a scraper is one fetch call through Monid, then BeautifulSoup or pandas on what comes back: $0.002533 a page (Verified 2026-10-16), one key for this and every other tool, about a minute to set up, and ready inside your agent whenever you need it. Writing it yourself works for a few static pages and costs hours of browser and proxy upkeep; Monid keeps that half maintained.

Two checks matter more than the choice of endpoint. Print what you fetched before you touch a selector, because a wrong selector and an unrendered page look identical. And set maxAgeMs to 0 for anything that changes, because our first call returned a copy 2.7 hours old.

How we estimated the time saved: building and running your own fetch layer is roughly 6 to 12 hours for a headless browser, retries and proxy handling, plus 4 to 8 hours over the first month for the breakages. That is 10 to 20 hours. The 1 minute is the install, key and first call once you already have a Monid key.

Free next step: run monid discover -q "scrape any website url to markdown", monid inspect the top row, then spend one call on a page you know well and print metadata.title and cache_metadata.

FAQ

What is the best Python library for web scraping?

BeautifulSoup for parsing, requests or httpx for fetching, and Scrapy when you need a crawl framework with queues rather than a script. The library layer is rarely the bottleneck. If your problem is a page that renders after a script runs or a site that blocks you, no parser fixes it, and the fetch is what to change.

Does the language matter: Python, JavaScript or Go?

Less than the search volume suggests. All three parse HTML competently and none changes whether a site serves you, so pick the one your team writes. Go has cheap parallel fetches, Node sits closest to Playwright and Puppeteer, and Python has the best analysis stack downstream. Two of those three advantages disappear when the fetch is an endpoint.

How do you scrape a page that needs a login?

Not with a single-shot fetch, because a login is state and a fetch is stateless. You need a session that carries cookies across requests, which means a real browser: one you run with Playwright, or a remote one you attach to. Check whether the data is available without the login first, since an authenticated scrape usually means agreeing to terms that restrict it.

Is there a free way to scrape a page in Python?

Yes: TinyFish's fetch endpoint is free, takes up to ten URLs per call and returned both pages we sent it on 2026-10-16, with no anti-bot bypass. For a static page, requests plus BeautifulSoup is free too. Through Monid, discovery and inspection cost nothing, and a rendered page from the paid endpoints is $0.002533 (Verified 2026-10-16).

How much does scraping one page cost?

A page is $0.002533 on the markdown endpoint (Verified 2026-10-16), so a thousand pages cost about $2.53. Octen's extract is $0.001 per URL, and failed requests are not billed on either. Our four runs on 2026-10-16 cost $0.006066 in total.

Published August 2026.

python web scrapingweb scraping pythonbeautifulsoupselenium alternativescrape javascript pages