core

fetch, markdown, crawl, and specialized readers

fetch() reads URLs with plain HTTP or a browser-backed tier. to_md() cleans HTML. The module also crawls sites, discovers XHR calls, and reads YouTube, arXiv, and GitHub sources.


source

fossick_cache

def fossick_cache(
    path:NoneType=None
):

Get cache path for name (e.g. ‘arxiv’ or ‘fetch’)


source

Path.mk_bytes

def mk_bytes(
    data, mode:int=511, uid:int=-1, gid:int=-1
):

Make all parent dirs of self, and write data


source

syncy

def syncy(
    coro, tout:int=60
):

Run coro on the shared background loop from sync code, even inside a running loop.

Fetch

Responses expose url, status, html, data (JSON), and xhr. Use heavy, stealthy, session, or auto to select the fetch tier.


source

to_md

def to_md(
    res_or_html, # Page dict (from fetch/crawl) or raw HTML string
    sel:str=None, # CSS selector to extract before conversion; returns '' if no match
    multi:bool=False, # Return all selector matches joined
    wrap_tag:str=None, # Wrap each multi-result in <wrap_tag>...</wrap_tag>; only used when multi=True
    ignore_links:bool=True, ignore_images:bool=False,
    readability:bool=True, # False keeps the whole document, for local files and saved pages
)->str:

Convert a Page dict or HTML string to clean markdown


source

html2md

def html2md(
    s:str, ignore_links:bool=True, ignore_images:bool=False,
    readability:bool=True, # Extract the article (right for fetched pages); False converts the whole document
):

Convert s from HTML to markdown


source

html_title

def html_title(
    s:str
)->str:

The document’s own title: <title> if present, else the first <h1>

to_md/html2md run readability’s article extractor by default — right for a fetched page, since it drops nav, ads and furniture. For a document somebody saved deliberately, pass readability=False: it converts the whole document, minus script/style/noscript/template and comments.

SSRF guard

check_url blocks private/link-local/metadata targets unless FOSSICK_ALLOW_PRIVATE or allow_private=True.


source

check_url

def check_url(
    url:str, # the URL about to be fetched
    allow_private:bool=None, # None = honour $FOSSICK_ALLOW_PRIVATE (default: off)
)->str:

Raise BlockedURL if url targets a private/link-local/metadata host.


source

BlockedURL

def BlockedURL(
    *args, **kwargs
):

A fetch target that points inside the network fossick is running in.


source

get_page

def get_page(
    url, method:str='GET', payload:NoneType=None, heavy:bool=False, stealthy:bool=False, session:bool=False,
    allow_private:NoneType=None, **kw
)->Response:

Fetch url. session=True (or a port int) drives the persistent debug Chrome via CDP, reusing its logged-in cookies.

Every fetch in fossick arrives here, which is why the private-address check lives here and not in fetch: crawl, replay_xhr, download_files and http_get would each have to remember.


source

http_page

def http_page(
    url, method:str='GET', payload:NoneType=None, verify:bool=False, **kw
)->Response:

Call self as a function.


source

BrowserUnavailable

def BrowserUnavailable(
    *args, **kwargs
):

scrapling’s headless/stealth browsers could not be loaded.

r1=get_page('https://httpbun.com/get', verify=False)
r1.__dict__.keys()
from fastcore.test import test_fail
try: get_page('https://httpbun.com/put', method='PUT', verify=False)
except AssertionError as e: test_fail(str(e) == 'Only GET and POST methods are supported')
http_page('https://httpbun.com/put', method='PUT', verify=False).status

source

browser_session

def browser_session(
    stealthy:bool=False, headless:bool=True, **init_kw
):

Open ONE browser and yield a fetch(url, sel=None, **) func that reuses it across calls (skips per-URL relaunch).


source

fetch

def fetch(
    url:str, # URL to fetch
    sel:str=None, # CSS selector to extract (None = full page)
    method:str='GET', # HTTP method; 'POST' sends payload as JSON body
    payload:dict=None, # POST body (JSON) or GET query params
    heavy:bool=False, # Full JS rendering via headless browser
    stealthy:bool=False, # Anti-bot stealth fetcher (Cloudflare etc.)
    capture_xhr:bool=False, # Capture XHR calls made by the page (only works with heavy or stealthy)
    session:bool=False, # Route through the persistent debug Chrome (reuses its logged-in cookies); True or a port int
    auto:bool=False, # Auto-escalate plain->heavy->stealthy->session on bot-block detection
    allow_private:bool=None, # Allow localhost/intranet targets (default: refuse; $FOSSICK_ALLOW_PRIVATE=1)
    **kw
)->Response: # Extra kwargs passed to scrapling (e.g. verify, headers)

Fetch url, return a Response with .html (raw or sel-extracted), .xhr, .status.

Bot walls

_blocked() detects Cloudflare/Anubis-style interstitials. auto=True escalates tiers until one is clean.

# A site behind Cloudflare: a plain fetch hits the bot wall, and _blocked() flags it for escalation
cf = fetch('https://www.scrapingcourse.com/cloudflare-challenge', verify=False)
assert cf.status == 403, f'expected a Cloudflare 403 challenge, got {cf.status}'
assert _blocked(cf) is True            # fetch(auto=True) escalates from here (heavy -> stealthy -> session)
print('cloudflare:', cf.status, '| blocked?', _blocked(cf))
# A site behind Anubis (proof-of-work bot wall): _blocked() now recognizes it, so fetch(auto=True) escalates
an = fetch('https://anubis.techaro.lol/', verify=False)
assert 'making sure' in an.html.lower(), 'expected the Anubis challenge page'
assert _blocked(an) is True            # Anubis markers are in _BLOCK_MARKERS -> auto escalates past this wall
print('anubis wall detected by _blocked?', _blocked(an))
# regression: a normal page that merely *mentions* captcha must NOT read as a bot wall.
# Wikipedia embeds hCaptcha config keys in its page JS -- the word is there, but there's no challenge widget.
wiki = fetch('https://en.wikipedia.org/wiki/Anubis', verify=False)
assert wiki.status == 200 and 'captcha' in wiki.html.lower()   # the word IS present on the page...
assert _blocked(wiki) is False   # ...but _BLOCK_MARKERS matches captcha *widgets* (g-recaptcha/h-captcha/...), not the word
# fetch(auto=True): walks plain->heavy->stealthy->session, stops at the first unblocked tier, tags .tier
plain = fetch('https://example.com', auto=True, verify=False)
assert plain.status == 200 and plain.tier == 'plain'   # a static page needs no escalation
print('example.com ->', plain.tier)
# browser_session(): open ONE browser, reuse it across fetches (skips per-URL relaunch)
with browser_session() as bf:
    a = bf('https://example.com')
    b = bf('https://httpbin.org/html')
assert a.status == 200 and b.status == 200
assert len(a.html) > 0 and len(b.html) > 0
# fetch(session=True): route through the persistent debug Chrome, reusing its logged-in cookies
pg = fetch('https://httpbin.org/headers', session=True)
assert pg.status == 200
print(pg.html[:200])

source

crawl

def crawl(
    start_url:str, # URL to start from
    sel:str=None, # CSS selector to extract per page
    follow_sel:str='a[href]', # CSS selector for links to follow
    same_domain:bool=True, # Only follow links on same domain
    max_pages:int=10, # Max pages to visit
    delay:float=0, # Seconds to wait between requests (polite crawling)
    heavy:bool=False, # Full JS rendering via headless browser
    stealthy:bool=False, # Anti-bot stealth fetcher (Cloudflare etc.)
    reuse:bool=True, # For heavy/stealthy crawls, keep one browser open across pages
    **kw
)->list: # Extra kwargs passed to scrapling (e.g. verify, timeout)

Crawl from start_url, following follow_sel links, return list of Page dicts


source

fetch_all

def fetch_all(
    urls:list, # URLs to fetch
    sel:str=None, # CSS selector to extract per page (None = full page)
    concurrency:int=8, # Max parallel fetches
    heavy:bool=False, # Full JS rendering via headless browser
    stealthy:bool=False, # Anti-bot stealth fetcher (Cloudflare etc.)
    **kw
)->list: # Extra kwargs passed to fetch()

Fetch a list of URLs in parallel; returns Page dicts in the same order as urls


source

get_options

def get_options(
    page_or_html, # Page dict (from fetch) or raw HTML string
    sel:str, # CSS selector for the <select> element
)->list:

Extract options from a