ferret-crawler 1.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,106 @@
1
+ Metadata-Version: 2.3
2
+ Name: ferret-crawler
3
+ Version: 1.1.0
4
+ Summary: Save a website or intranet as Markdown files, one per page (for search indexes and LLM tools)
5
+ Requires-Dist: playwright>=1.48 ; extra == 'browser'
6
+ Requires-Python: >=3.11
7
+ Project-URL: Homepage, https://github.com/MayankPunghal/ferret
8
+ Provides-Extra: browser
9
+ Description-Content-Type: text/markdown
10
+
11
+ <p align="center"><img src="src/site2md/static/favicon.svg" width="88" alt=""></p>
12
+
13
+ <h1 align="center">Ferret Crawler</h1>
14
+
15
+ <p align="center">Part of <a href="../README.md">Ferret</a>. Command-line name: <code>site2md</code> (or <code>ferret-crawler</code>).</p>
16
+
17
+
18
+ Save a website or intranet as Markdown files, one per page, for a search index (such as Ferret),
19
+ an LLM tool or an offline copy. Standard-library Python; a real browser is optional for sites behind a login.
20
+
21
+ ## The easy way (no setup)
22
+
23
+ 1. Unzip this folder anywhere.
24
+ 2. Double-click **`Crawl Website.bat`** (Windows) or **`Crawl Website.command`** (macOS; on Linux run
25
+ `bash "Crawl Website.command"`). A page opens in your browser; keep the small black window open while you
26
+ copy (closing it stops the crawler).
27
+ 3. **Copy a website** → paste the address. The page checks it at once ("Found “Engineering Wiki”", or "this
28
+ address asks you to sign in"). Give the copy a name and press **Start copying**. For a site behind a
29
+ sign-in, a browser window opens: sign in there; the page notices and starts copying by itself.
30
+
31
+ While it copies you see each section of the site filling in, with Pause and Stop. When it is done, the page
32
+ lists what was not copied, grouped by reason ("page not found · 12"), with **Try these again**, and can
33
+ **send the copy to Ferret**.
34
+
35
+ The copies are in `output/<name>/`, one Markdown file per page, next to the original PDF, Word and
36
+ PowerPoint files the pages link to or embed. The first run installs what it needs (uv, Python, about 1–2
37
+ minutes; the internet is needed for that). **Your copies** lists them, with **Update** (copy the site again;
38
+ the old copy is kept until the new one is finished), **Continue** for a stopped copy, Open folder and Delete.
39
+
40
+ Sign-ins use Google Chrome or Microsoft Edge already on the computer (or a Chromium downloaded with
41
+ `uv run --extra browser playwright install chromium`). The sign-in is kept on your computer only, in
42
+ `~/.site2md/profile-<browser>`. Delete that folder to sign out. The crawler never sees your password.
43
+
44
+ **Original HTML (optional):** *More options → Also keep each page's original HTML* saves each page's HTML in
45
+ `output/<name>-html/`, next to the copy rather than in it, to check what a page held. It is not sent to
46
+ Ferret: the Markdown already keeps the structure that search uses (headings, lists, tables, code,
47
+ links), and indexing the same page twice would crowd results with duplicates.
48
+
49
+ The page listens on 127.0.0.1 only, at an address with a one-time key, so other computers and other web
50
+ pages cannot use it. `site2md ui --copies <folder>` opens it from a command line; `site2md <url> --out
51
+ <folder>` still copies without the page.
52
+
53
+ ## Install as a command (technical users)
54
+
55
+ Needs Python 3.11+ (or [uv](https://docs.astral.sh/uv/), which installs Python for you).
56
+
57
+ ```bash
58
+ uv tool install "site2md[browser] @ ./site2md" # this folder; or: pipx install "./site2md[browser]"
59
+ ```
60
+
61
+ ## Use
62
+
63
+ ```bash
64
+ site2md https://docs.example.com/ --out example-docs
65
+ ```
66
+
67
+ Sites behind a company login (Google Sites, SharePoint, Confluence with SSO) — sign in once in a browser
68
+ window, then crawl with that session (kept in `~/.site2md/profile-<channel>`, on your machine only):
69
+
70
+ ```bash
71
+ site2md "https://sites.google.com/acme.com/intranet/home" --out intranet --browser --login --channel chrome
72
+ site2md "https://sites.google.com/acme.com/intranet/home" --out intranet --browser --channel chrome # later runs
73
+ ```
74
+
75
+ `--channel chrome` or `--channel msedge` uses the browser already installed. `--channel firefox` or
76
+ `--channel chromium` (the default) uses Playwright's own copy: run `playwright install firefox` (or
77
+ `chromium`) once. Other Chromium-based browsers (Brave, Vivaldi): `--browser-path` to the executable.
78
+
79
+ ## What it does
80
+
81
+ - Follows **every link on every page** — content, navigation bar, header, footer, sidebars, frames,
82
+ `<link rel=next>` — plus `sitemap.xml`, until no new page is found (`--max-pages`, default 10,000).
83
+ - Stays under the start URL's folder (`https://host/docs/intro.html` → `https://host/docs/`); `--prefix` sets
84
+ other prefixes (repeatable), `--allow-host` adds whole hosts.
85
+ - Never follows logout, delete, edit, print or feed links; respects robots.txt unless `--ignore-robots`.
86
+ - Writes `<page>.md` for every page, with front matter (`title`, `source_url`, `crawled_at`) and the page's
87
+ text (menus and footers dropped; when the page's `<main>` element misses part of its text, as on Google
88
+ Sites, the whole page is used). Scrolls through each page first, so text drawn when it comes into view is
89
+ included.
90
+ - Saves linked PDF, Word and PowerPoint files **as they are**, under their own names ("Leave Policy (1).pdf") — nothing
91
+ is converted, so nothing can be lost; a search index reads them itself (Ferret does, page by page).
92
+ - Downloads the Google Drive files, Docs (as Word) and Slides (as PDF) a page embeds or links to — the PDF
93
+ viewers on Google Sites pages — and the files in embedded Drive folders, with the same sign-in, and links
94
+ them from the page ("Attached documents"). Only PDF, Word and PowerPoint files are kept, up to 500 MB:
95
+ videos and spreadsheets uploaded to Drive are recognised by name before downloading and skipped (listed in
96
+ the manifest). For files over 100 MB it answers Google's "Download anyway" prompt itself.
97
+ - `--save-html` also keeps the HTML each page was converted from (in `<out>-html/`), to check what a page
98
+ held.
99
+ - Saves a page reached under several URLs once; rewrites links between crawled pages to the local files;
100
+ removes a title suffix shared by most pages (" | Company Wiki").
101
+ - `manifest.json`: every URL → file, failures (e.g. "sign-in page"), skips and the remaining queue;
102
+ `--resume` continues an interrupted crawl. Ctrl+C stops a crawl cleanly (links fixed, manifest saved).
103
+
104
+ Pages with no text of their own (only a title or headings) are not written; the manifest lists them under
105
+ `skipped`. Without `--browser`, pages that build their content with JavaScript are among them. `--resume` also retries the URLs that failed. Embedded Google Sheets and Calendars, forms,
106
+ videos and files whose owner disabled downloads are not saved; the manifest lists them. The copy has no access control: keep it where only the intended readers can open it.
@@ -0,0 +1,96 @@
1
+ <p align="center"><img src="src/site2md/static/favicon.svg" width="88" alt=""></p>
2
+
3
+ <h1 align="center">Ferret Crawler</h1>
4
+
5
+ <p align="center">Part of <a href="../README.md">Ferret</a>. Command-line name: <code>site2md</code> (or <code>ferret-crawler</code>).</p>
6
+
7
+
8
+ Save a website or intranet as Markdown files, one per page, for a search index (such as Ferret),
9
+ an LLM tool or an offline copy. Standard-library Python; a real browser is optional for sites behind a login.
10
+
11
+ ## The easy way (no setup)
12
+
13
+ 1. Unzip this folder anywhere.
14
+ 2. Double-click **`Crawl Website.bat`** (Windows) or **`Crawl Website.command`** (macOS; on Linux run
15
+ `bash "Crawl Website.command"`). A page opens in your browser; keep the small black window open while you
16
+ copy (closing it stops the crawler).
17
+ 3. **Copy a website** → paste the address. The page checks it at once ("Found “Engineering Wiki”", or "this
18
+ address asks you to sign in"). Give the copy a name and press **Start copying**. For a site behind a
19
+ sign-in, a browser window opens: sign in there; the page notices and starts copying by itself.
20
+
21
+ While it copies you see each section of the site filling in, with Pause and Stop. When it is done, the page
22
+ lists what was not copied, grouped by reason ("page not found · 12"), with **Try these again**, and can
23
+ **send the copy to Ferret**.
24
+
25
+ The copies are in `output/<name>/`, one Markdown file per page, next to the original PDF, Word and
26
+ PowerPoint files the pages link to or embed. The first run installs what it needs (uv, Python, about 1–2
27
+ minutes; the internet is needed for that). **Your copies** lists them, with **Update** (copy the site again;
28
+ the old copy is kept until the new one is finished), **Continue** for a stopped copy, Open folder and Delete.
29
+
30
+ Sign-ins use Google Chrome or Microsoft Edge already on the computer (or a Chromium downloaded with
31
+ `uv run --extra browser playwright install chromium`). The sign-in is kept on your computer only, in
32
+ `~/.site2md/profile-<browser>`. Delete that folder to sign out. The crawler never sees your password.
33
+
34
+ **Original HTML (optional):** *More options → Also keep each page's original HTML* saves each page's HTML in
35
+ `output/<name>-html/`, next to the copy rather than in it, to check what a page held. It is not sent to
36
+ Ferret: the Markdown already keeps the structure that search uses (headings, lists, tables, code,
37
+ links), and indexing the same page twice would crowd results with duplicates.
38
+
39
+ The page listens on 127.0.0.1 only, at an address with a one-time key, so other computers and other web
40
+ pages cannot use it. `site2md ui --copies <folder>` opens it from a command line; `site2md <url> --out
41
+ <folder>` still copies without the page.
42
+
43
+ ## Install as a command (technical users)
44
+
45
+ Needs Python 3.11+ (or [uv](https://docs.astral.sh/uv/), which installs Python for you).
46
+
47
+ ```bash
48
+ uv tool install "site2md[browser] @ ./site2md" # this folder; or: pipx install "./site2md[browser]"
49
+ ```
50
+
51
+ ## Use
52
+
53
+ ```bash
54
+ site2md https://docs.example.com/ --out example-docs
55
+ ```
56
+
57
+ Sites behind a company login (Google Sites, SharePoint, Confluence with SSO) — sign in once in a browser
58
+ window, then crawl with that session (kept in `~/.site2md/profile-<channel>`, on your machine only):
59
+
60
+ ```bash
61
+ site2md "https://sites.google.com/acme.com/intranet/home" --out intranet --browser --login --channel chrome
62
+ site2md "https://sites.google.com/acme.com/intranet/home" --out intranet --browser --channel chrome # later runs
63
+ ```
64
+
65
+ `--channel chrome` or `--channel msedge` uses the browser already installed. `--channel firefox` or
66
+ `--channel chromium` (the default) uses Playwright's own copy: run `playwright install firefox` (or
67
+ `chromium`) once. Other Chromium-based browsers (Brave, Vivaldi): `--browser-path` to the executable.
68
+
69
+ ## What it does
70
+
71
+ - Follows **every link on every page** — content, navigation bar, header, footer, sidebars, frames,
72
+ `<link rel=next>` — plus `sitemap.xml`, until no new page is found (`--max-pages`, default 10,000).
73
+ - Stays under the start URL's folder (`https://host/docs/intro.html` → `https://host/docs/`); `--prefix` sets
74
+ other prefixes (repeatable), `--allow-host` adds whole hosts.
75
+ - Never follows logout, delete, edit, print or feed links; respects robots.txt unless `--ignore-robots`.
76
+ - Writes `<page>.md` for every page, with front matter (`title`, `source_url`, `crawled_at`) and the page's
77
+ text (menus and footers dropped; when the page's `<main>` element misses part of its text, as on Google
78
+ Sites, the whole page is used). Scrolls through each page first, so text drawn when it comes into view is
79
+ included.
80
+ - Saves linked PDF, Word and PowerPoint files **as they are**, under their own names ("Leave Policy (1).pdf") — nothing
81
+ is converted, so nothing can be lost; a search index reads them itself (Ferret does, page by page).
82
+ - Downloads the Google Drive files, Docs (as Word) and Slides (as PDF) a page embeds or links to — the PDF
83
+ viewers on Google Sites pages — and the files in embedded Drive folders, with the same sign-in, and links
84
+ them from the page ("Attached documents"). Only PDF, Word and PowerPoint files are kept, up to 500 MB:
85
+ videos and spreadsheets uploaded to Drive are recognised by name before downloading and skipped (listed in
86
+ the manifest). For files over 100 MB it answers Google's "Download anyway" prompt itself.
87
+ - `--save-html` also keeps the HTML each page was converted from (in `<out>-html/`), to check what a page
88
+ held.
89
+ - Saves a page reached under several URLs once; rewrites links between crawled pages to the local files;
90
+ removes a title suffix shared by most pages (" | Company Wiki").
91
+ - `manifest.json`: every URL → file, failures (e.g. "sign-in page"), skips and the remaining queue;
92
+ `--resume` continues an interrupted crawl. Ctrl+C stops a crawl cleanly (links fixed, manifest saved).
93
+
94
+ Pages with no text of their own (only a title or headings) are not written; the manifest lists them under
95
+ `skipped`. Without `--browser`, pages that build their content with JavaScript are among them. `--resume` also retries the URLs that failed. Embedded Google Sheets and Calendars, forms,
96
+ videos and files whose owner disabled downloads are not saved; the manifest lists them. The copy has no access control: keep it where only the intended readers can open it.
@@ -0,0 +1,24 @@
1
+ [project]
2
+ name = "ferret-crawler"
3
+ version = "1.1.0"
4
+ description = "Save a website or intranet as Markdown files, one per page (for search indexes and LLM tools)"
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ dependencies = []
8
+
9
+ [project.optional-dependencies]
10
+ browser = ["playwright>=1.48"]
11
+
12
+ [project.scripts]
13
+ ferret-crawler = "site2md:main"
14
+ site2md = "site2md:main"
15
+
16
+ [project.urls]
17
+ Homepage = "https://github.com/MayankPunghal/ferret"
18
+
19
+ [build-system]
20
+ requires = ["uv_build>=0.11.32,<0.12.0"]
21
+ build-backend = "uv_build"
22
+
23
+ [tool.uv.build-backend]
24
+ module-name = "site2md"
@@ -0,0 +1,24 @@
1
+ [project]
2
+ name = "ferret-crawler"
3
+ version = "1.1.0"
4
+ description = "Save a website or intranet as Markdown files, one per page (for search indexes and LLM tools)"
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ dependencies = []
8
+
9
+ [project.optional-dependencies]
10
+ browser = ["playwright>=1.48"]
11
+
12
+ [project.scripts]
13
+ ferret-crawler = "site2md:main"
14
+ site2md = "site2md:main"
15
+
16
+ [project.urls]
17
+ Homepage = "https://github.com/MayankPunghal/ferret"
18
+
19
+ [build-system]
20
+ requires = ["uv_build>=0.11.32,<0.12.0"]
21
+ build-backend = "uv_build"
22
+
23
+ [tool.uv.build-backend]
24
+ module-name = "site2md"
@@ -0,0 +1,68 @@
1
+ """site2md: save a website or intranet as Markdown files, one per page (see crawl.py)."""
2
+
3
+ import argparse
4
+ from pathlib import Path
5
+
6
+ __version__ = "1.1.0"
7
+
8
+
9
+ def main(argv: list[str] | None = None) -> None:
10
+ import sys
11
+
12
+ args = sys.argv[1:] if argv is None else argv
13
+ if not args or args[0] == "ui": # the page in the browser (Crawl Website launchers)
14
+ from . import ui
15
+
16
+ p = argparse.ArgumentParser(prog="site2md ui", description="Open Ferret Crawler's page in your browser.")
17
+ p.add_argument("--copies", default="", help="folder for the copies (default ~/site2md)")
18
+ p.add_argument("--no-browser", action="store_true", help="only print the address")
19
+ a = p.parse_args(args[1:])
20
+ ui.main(a.copies, open_browser=not a.no_browser)
21
+ return
22
+ from .crawl import BrowserFetcher, Crawler
23
+
24
+ ap = argparse.ArgumentParser(prog="site2md", description="Save a website as Markdown files, one per page.")
25
+ ap.add_argument("url", help="start page")
26
+ ap.add_argument("--out", required=True, help="output directory")
27
+ ap.add_argument("--max-pages", type=int, default=10_000)
28
+ ap.add_argument("--prefix", action="append", help="only URLs starting with this (repeatable; default: the start URL's folder)")
29
+ ap.add_argument("--allow-host", action="append", help="also follow links anywhere on this host (repeatable)")
30
+ ap.add_argument("--header", action="append", default=[], help="extra request header, e.g. 'Authorization: Bearer ...'")
31
+ ap.add_argument("--workers", type=int, default=4)
32
+ ap.add_argument("--delay", type=float, default=0.2, help="seconds each worker waits between requests")
33
+ ap.add_argument("--ignore-robots", action="store_true", help="ignore robots.txt (only for your own sites)")
34
+ ap.add_argument("--no-files", action="store_true", help="do not save linked PDF/Word/text files")
35
+ ap.add_argument("--resume", action="store_true", help="continue an interrupted crawl in --out")
36
+ ap.add_argument("--follow", choices=("site", "linked", "everything"), default="linked",
37
+ help="links leaving the start folder: site = never; linked (default) = a link in a page's content "
38
+ "brings in the folder it points to; everything = every link anywhere, up to --max-pages")
39
+ b = ap.add_argument_group("browser (sites behind a login or built with JavaScript; needs: pip install playwright)")
40
+ b.add_argument("--browser", action="store_true", help="fetch pages with a real browser")
41
+ b.add_argument("--login", action="store_true", help="open a browser window to sign in first (saved for next time)")
42
+ b.add_argument("--channel", default="chromium", help="chrome or msedge (the installed browser), or firefox / "
43
+ "chromium (Playwright's own, once: playwright install firefox|chromium); default chromium")
44
+ b.add_argument("--profile", default="", help="where the browser keeps the sign-in "
45
+ "(default: ~/.site2md/profile-<channel>, one per browser)")
46
+ b.add_argument("--show-browser", action="store_true", help="show the browser window while crawling")
47
+ b.add_argument("--browser-path", default="", help="path to a Chromium/Chrome executable")
48
+ ap.add_argument("--save-html", action="store_true", help="also keep the HTML each page was converted from, "
49
+ "in <out>-html (to check what a page held)")
50
+ a = ap.parse_args(args)
51
+ fetcher = None
52
+ a.profile = a.profile or str(Path.home() / ".site2md" / f"profile-{a.channel}")
53
+ if a.browser or a.login:
54
+ Path(a.profile).mkdir(parents=True, exist_ok=True)
55
+ if a.login:
56
+ login = BrowserFetcher(Path(a.profile), a.channel, headless=False, executable=a.browser_path)
57
+ login.login(a.url)
58
+ login.close()
59
+ fetcher = BrowserFetcher(Path(a.profile), a.channel, headless=not a.show_browser, executable=a.browser_path)
60
+ headers = dict(h.split(":", 1) for h in a.header)
61
+ try:
62
+ Crawler(a.url, Path(a.out), prefixes=a.prefix, allow_hosts=a.allow_host, max_pages=a.max_pages,
63
+ workers=a.workers, delay=a.delay, headers={k.strip(): v.strip() for k, v in headers.items()},
64
+ robots=not a.ignore_robots, files=not a.no_files, browser=fetcher,
65
+ save_html=Path(str(Path(a.out)) + "-html") if a.save_html else None, follow=a.follow).run(resume=a.resume)
66
+ finally:
67
+ if fetcher:
68
+ fetcher.close()
@@ -0,0 +1,3 @@
1
+ from . import main
2
+
3
+ main()