FAScraping: When a Movie Ratings Export Becomes a Fight With the Web
A small Filmaffinity-to-Letterboxd bridge that reveals how scraping changed from simple HTML parsing to stealth automation, caching, and locale-aware cleanup.
- FAScraping is less a scraper than a migration tool that turns a locked-in ratings history into portable CSV output.
- Its history shows how scraping evolved from raw HTML string slicing into stealthy browser automation and local caching.
- The repository’s real product is date cleanup, column mapping, and format compatibility, not page downloading.
- The project reads like a small archive of the web getting harder to read.
A tiny exporter that had to evolve
FAScraping starts with a simple promise: get your Filmaffinity ratings out and into something portable, especially Letterboxd. That sounds mundane until you look at the code. The older PHP path treats HTML like a text stream, while the newer Python path brings in Selenium, stealth settings, Cloudscraper, caching, and locale-aware parsing just to keep the export alive.
That shift is the real story. The repo is not a general-purpose scraping framework. It is a personal utility that documents what happens when one site decides that exporting your own history should be a little harder than it used to be.
Why movie fans need a bridge at all
That matters because film databases are social products, but they are also personal archives. If you have spent years rating movies on one platform, the data has value beyond that platform’s walls. A bridge like this one exists because continuity matters. People do not just want to remember what they watched. They want to keep that history usable elsewhere.
The project also sits inside a broader migration habit. Letterboxd is the destination for many film fans who want a cleaner social layer, while Filmaffinity often holds the older record. FAScraping translates between those two worlds.
The original trick: scrape fast, then slice
The original PHP version is almost defiantly low-level. It opens pages, hunts for the pager, counts pages, and then walks through them with string functions. Instead of treating HTML as a DOM, it treats it like a long piece of text that can be cut into useful segments.
$html = file_get_html($url);
$pager = stristr($html, 'pager');
$NP = intval(substr($pager, strpos($pager, 'data-pages="') + 12, 1));
$i = 1;
while ($i <= $NP) {
$page = file_get_html($url . '?page=' . $i);
$chunk = stristr($page, 'mc-title');
$dateStart = strpos($chunk, 'Votada el d') + 11;
$date = substr($chunk, $dateStart, 11);
$i++;
}
That approach is brittle, but it is also brutally direct. If you know the site’s markup well enough, and the page is stable enough, raw string slicing can be faster than a full DOM walk. The trade-off is obvious. One class name change, one layout tweak, and the offsets stop lining up.
The important distinction is philosophical. This is not HTML parsing as abstraction. It is HTML parsing as carpentry. Measure, cut, fit, repeat.
Why the Python rewrite exists
The Python version exists because the web pushed back. Filmaffinity’s protections made naive fetching less reliable, so the tool had to become a browser impersonator. Selenium provides the browser shell, stealth settings reduce the obvious automation fingerprints, and Cloudscraper helps with the parts that behave more like a gate than a webpage.
It also adds resilience in a quieter way. The script caches fetched HTML locally, so repeated runs do not have to hit the site again. That matters when the site is sensitive to repeat requests, and it matters when you are iterating on parsing logic.
| Dimension | PHP path | Python path |
|---|---|---|
| Fetch method | Direct page fetch with simple HTML loading | Browser-backed acquisition with Selenium and Cloudscraper |
| Parsing style | String seeking with strpos and substr | BeautifulSoup element selection after cached retrieval |
| Resilience | Fast, but tied tightly to markup shape | Slower, but better at surviving anti-bot friction |
| Date handling | Manual cleanup around page text | Explicit locale-aware normalization for Spanish and English |
| Maintenance cost | Low at first, then fragile when the site shifts | Higher setup cost, but easier to adapt once the workflow is in place |
Generating illustration...
The rewrite also makes the parser more explicit. BeautifulSoup is used where structure matters, and the date logic becomes a first-class concern instead of an afterthought. That is what software looks like when the web no longer lets you get away with shortcuts.
CSV is the real product
It is tempting to say FAScraping is about scraping Filmaffinity. It is more accurate to say it is about producing a CSV that another system can accept without complaint. The scraping part is just upstream work. The real deliverable is a clean, importable file with the right fields in the right order.
| Export concern | Why it matters |
|---|---|
| Column names | They have to match the destination importer, not the source site |
| Date formats | A rating date that looks fine to a human can fail an import if the locale is wrong |
| Encoding and cleanup | Movie titles and metadata must survive translation between page HTML and CSV text |
| Field order | Letterboxd-style imports are picky about structure, not just content |
from datetime import datetime
MONTHS_ES = {
'enero': 'January', 'febrero': 'February', 'marzo': 'March',
'abril': 'April', 'mayo': 'May', 'junio': 'June'
}
def normalize_date(raw_date):
for es, en in MONTHS_ES.items():
raw_date = raw_date.replace(es, en)
return datetime.strptime(raw_date, '%d %B %Y').strftime('%Y-%m-%d')
That kind of cleanup is easy to overlook, but it is the difference between a file and a useful file. The exporter has to understand how Filmaffinity names dates, how Letterboxd expects them, and how much correction is needed along the way. In other words, data portability is mostly formatting work.
Brittle by design, practical in the real world
| Question | Brittle PHP approach | Defensive Python approach |
|---|---|---|
| How fast is it to write? | Very fast | Slower |
| How easy is it to patch? | Easy if the failure is obvious | Moderate, because the stack is larger |
| How does it feel on a stable site? | Elegant enough | Overbuilt but safe |
| How does it feel on a changing site? | Falls apart quickly | Keeps going with more maintenance |
The PHP version’s fragility is not a bug in the story. It is the point. Solo tools often begin with the fastest path to a working result, and that usually means a little ugliness. If the page structure is stable, string-seeking can be an entirely rational choice.
Then the maintenance burden arrives. Once a site starts changing its markup or gating requests more aggressively, the cheap solution becomes the expensive one. FAScraping captures that transition cleanly.
What this repo says about modern scraping
FAScraping is small, but it is not trivial. It shows how scraping has shifted from simple requests and ad hoc parsing toward a layered survival stack: fetch, disguise, cache, parse, normalize, export. That stack is the real shape of modern scraping, even for a one-person utility.
The broader lesson is uncomfortable. The more a platform resists export, the more exporter code starts to resemble a defensive system. This repo is a tiny record of that escalation, and that is why it is more interesting than a plain scraper script.





