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.

8 min read • View on GitHub • More from fsanchez83

A wide editorial scene shows a film ratings ledger on one side of a narrow bridge and a clean CSV sheet on the other. Defensive gates, shields, and warning lights sit under the bridge, hinting at the friction between a user's saved ratings and a portable export. The image explains that the tool exists to move personal movie data across platform boundaries.
The repo is a bridge: one side is Filmaffinity’s stored history, the other is a CSV that another service can actually ingest.
Key Takeaways

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

A close-up editorial scene shows a film rating card being lowered into a machine that stamps it into a CSV grid. Thin rails, funnels, and a small transfer arm suggest a conversion pipeline rather than a simple download. The image explains that the repository is about transforming one site's output into another site's input.
The key job is not scraping pages. It is converting one platform’s private format into another platform’s import shape.

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 pipeline changes shape as the site gets harder to scrape. The left path is quick and brittle. The right path is slower but built to survive.

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.

DimensionPHP pathPython path
Fetch methodDirect page fetch with simple HTML loadingBrowser-backed acquisition with Selenium and Cloudscraper
Parsing styleString seeking with strpos and substrBeautifulSoup element selection after cached retrieval
ResilienceFast, but tied tightly to markup shapeSlower, but better at surviving anti-bot friction
Date handlingManual cleanup around page textExplicit locale-aware normalization for Spanish and English
Maintenance costLow at first, then fragile when the site shiftsHigher setup cost, but easier to adapt once the workflow is in place

Generating illustration...

The rewrite is not just a language change. It is a change in how much friction the script is prepared to absorb.

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 concernWhy it matters
Column namesThey have to match the destination importer, not the source site
Date formatsA rating date that looks fine to a human can fail an import if the locale is wrong
Encoding and cleanupMovie titles and metadata must survive translation between page HTML and CSV text
Field orderLetterboxd-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

QuestionBrittle PHP approachDefensive Python approach
How fast is it to write?Very fastSlower
How easy is it to patch?Easy if the failure is obviousModerate, because the stack is larger
How does it feel on a stable site?Elegant enoughOverbuilt but safe
How does it feel on a changing site?Falls apart quicklyKeeps 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.