MIL-004: top 100 movies scraper #38
@@ -161,6 +161,52 @@ A short summary in our own words; it is not legal advice.
|
||||
- **Commercial use** of scraped data invites expensive disputes even where the scraping itself was allowed.
|
||||
- **Be a good guest:** prefer an official API when there is one, read the site's `robots.txt` to see what it allows, and send at most one request a minute. This project fetches one page per run and identifies itself with a `User-Agent`.
|
||||
|
||||
### Project: 100 movies to watch
|
||||
|
||||
The assignment: scrape Empire's "100 Greatest Movies" and write `movies.txt` with the titles numbered from `1)` upwards. The code is `infrastructure/empire_movies.py` (parsing), `domain/movie.py` (reverse and number) and `infrastructure/movie_file.py` (writing). `--source wayback` (default) reads the Internet Archive snapshot of May 2020 that the assignment pins, `--source live` reads the current Empire page and `--source fixture` reads the saved headings in `tests/fixtures/`.
|
||||
|
||||
**1. Choose the page** (the saved headings of the page)
|
||||
|
||||

|
||||
|
||||
**2. Inspect the markup** (the 2020 page has `<h3 class="title">`; the current page has `<h2><strong>` headings that start with a rank)
|
||||
|
||||
```bash
|
||||
python -m scrape_movies movies markup
|
||||
```
|
||||
|
||||

|
||||
|
||||
**3. Titles in page order** (`select("h3.title")` and a list comprehension over `get_text()`; the page counts down from 100)
|
||||
|
||||
```bash
|
||||
python -m scrape_movies movies titles
|
||||
```
|
||||
|
||||

|
||||
|
||||
**4. Reverse the list** (`titles[::-1]`, so the list counts up from 1)
|
||||
|
||||
```bash
|
||||
python -m scrape_movies movies reversed
|
||||
```
|
||||
|
||||

|
||||
|
||||
**5. Write `movies.txt`** (a `with open(...)` block, one title per line)
|
||||
|
||||
```bash
|
||||
python -m scrape_movies movies write
|
||||
```
|
||||
|
||||

|
||||
|
||||
**6. The result**
|
||||
|
||||

|
||||
|
||||
The committed [`movies.txt`](./movies.txt) was produced with `--source fixture` on 2026-10-28 from the current Empire list, so its ranking differs from the 2020 list in the lecture video. The Internet Archive answered `429 Too Many Requests` while this project was built, so the snapshot is read live only; its `h3.title` layout is covered by a hand-written sample in the tests.
|
||||
|
||||
## 🧪 Tests
|
||||
|
||||
```bash
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 84 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 9.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
+59
-56
@@ -1,56 +1,59 @@
|
||||
"""@brief Save a screenshot of a web page with a headless Chromium browser.
|
||||
|
||||
A headless browser draws into memory, so no desktop window is opened and no
|
||||
mouse or keyboard is used. It needs Edge, Chrome or Chromium on the machine.
|
||||
"""
|
||||
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
CANDIDATES = (
|
||||
r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe",
|
||||
r"C:\Program Files\Microsoft\Edge\Application\msedge.exe",
|
||||
r"C:\Program Files\Google\Chrome\Application\chrome.exe",
|
||||
"google-chrome",
|
||||
"chromium",
|
||||
"chromium-browser",
|
||||
)
|
||||
WINDOW = "1280,1000"
|
||||
|
||||
|
||||
def find_browser() -> str:
|
||||
"""@brief Locate a Chromium-based browser.
|
||||
|
||||
@return The path or command name of the browser.
|
||||
@throws FileNotFoundError When none is installed.
|
||||
"""
|
||||
for candidate in CANDIDATES:
|
||||
if Path(candidate).exists() or shutil.which(candidate):
|
||||
return candidate
|
||||
raise FileNotFoundError("Install Edge, Chrome or Chromium to capture pages.")
|
||||
|
||||
|
||||
def capture_page(url: str, target: Path) -> Path:
|
||||
"""@brief Render a page and save it as a PNG.
|
||||
|
||||
@param url The page address.
|
||||
@param target The PNG file to write.
|
||||
@return The path of the PNG.
|
||||
"""
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
subprocess.run(
|
||||
[
|
||||
find_browser(),
|
||||
"--headless=new",
|
||||
"--disable-gpu",
|
||||
"--hide-scrollbars",
|
||||
f"--window-size={WINDOW}",
|
||||
f"--screenshot={target}",
|
||||
url,
|
||||
],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
timeout=120,
|
||||
)
|
||||
return target
|
||||
"""@brief Save a screenshot of a web page with a headless Chromium browser.
|
||||
|
||||
A headless browser draws into memory, so no desktop window is opened and no
|
||||
mouse or keyboard is used. It needs Edge, Chrome or Chromium on the machine.
|
||||
"""
|
||||
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
CANDIDATES = (
|
||||
r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe",
|
||||
r"C:\Program Files\Microsoft\Edge\Application\msedge.exe",
|
||||
r"C:\Program Files\Google\Chrome\Application\chrome.exe",
|
||||
"google-chrome",
|
||||
"chromium",
|
||||
"chromium-browser",
|
||||
)
|
||||
WINDOW = "1280,1000"
|
||||
|
||||
|
||||
def find_browser() -> str:
|
||||
"""@brief Locate a Chromium-based browser.
|
||||
|
||||
@return The path or command name of the browser.
|
||||
@throws FileNotFoundError When none is installed.
|
||||
"""
|
||||
for candidate in CANDIDATES:
|
||||
if Path(candidate).exists() or shutil.which(candidate):
|
||||
return candidate
|
||||
raise FileNotFoundError("Install Edge, Chrome or Chromium to capture pages.")
|
||||
|
||||
|
||||
def capture_page(url: str, target: Path) -> Path:
|
||||
"""@brief Render a page and save it as a PNG.
|
||||
|
||||
@param url The page address.
|
||||
@param target The PNG file to write.
|
||||
@return The path of the PNG.
|
||||
"""
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
browser = find_browser()
|
||||
for mode in ("new", "old"):
|
||||
result = subprocess.run(
|
||||
[
|
||||
browser,
|
||||
f"--headless={mode}",
|
||||
"--disable-gpu",
|
||||
"--hide-scrollbars",
|
||||
f"--window-size={WINDOW}",
|
||||
f"--screenshot={target}",
|
||||
url,
|
||||
],
|
||||
capture_output=True,
|
||||
timeout=120,
|
||||
)
|
||||
if result.returncode == 0 and target.exists():
|
||||
return target
|
||||
raise RuntimeError(f"The browser could not capture {url}")
|
||||
|
||||
@@ -29,10 +29,20 @@ STEPS: dict[str, list[str]] = {
|
||||
"hn-4-links": ["hacker-news", "links"],
|
||||
"hn-5-upvotes": ["hacker-news", "upvotes"],
|
||||
"hn-6-top": ["hacker-news", "top"],
|
||||
"movies-2-markup": ["movies", "markup", "--source", "fixture"],
|
||||
"movies-3-titles": ["movies", "titles", "--source", "fixture"],
|
||||
"movies-4-reversed": ["movies", "reversed", "--source", "fixture"],
|
||||
"movies-5-write": ["movies", "write", "--source", "fixture"],
|
||||
}
|
||||
## Screenshots of a file's first lines: image name, file and number of lines.
|
||||
FILES: dict[str, tuple[str, int]] = {"movies-6-file": ("movies.txt", 15)}
|
||||
## Screenshots of real web pages: image name and the address of the page.
|
||||
PAGES: dict[str, str] = {
|
||||
"hn-1-page": "https://appbrewery.github.io/news.ycombinator.com/",
|
||||
# The live page opens a consent wall, so the saved headings are drawn instead.
|
||||
"movies-1-page": (
|
||||
ROOT / "tests" / "fixtures" / "empire_live_excerpt.html"
|
||||
).as_uri(),
|
||||
}
|
||||
|
||||
|
||||
@@ -63,6 +73,13 @@ def main() -> None:
|
||||
for name, args in STEPS.items():
|
||||
if name.startswith(prefix):
|
||||
print(capture(name, args))
|
||||
for name, (file_name, count) in FILES.items():
|
||||
if name.startswith(prefix):
|
||||
text = (ROOT / file_name).read_text(encoding="utf-8")
|
||||
target = IMAGES / f"{name}.png"
|
||||
shown = os.linesep.join(text.splitlines()[:count])
|
||||
render(f"head -n {count} {file_name}", shown, target)
|
||||
print(target)
|
||||
for name, url in PAGES.items():
|
||||
if name.startswith(prefix):
|
||||
print(capture_page(url, IMAGES / f"{name}.png"))
|
||||
|
||||
Reference in New Issue
Block a user