Python bez serveru: profilovač CSV s Pyodide postavíte ze tří souborů

Python nemusí zpracovávat data na backendu. S Pyodide lze CPython a pandas spustit přímo v prohlížeči a výsledný nástroj nasadit jako statickou stránku. Na profilovači CSV ukážeme, jak si JavaScript s Pythonem předají soubor, proč výpočet patří do web workeru a na jaké limity tento přístup naráží.
Nálepky:
Postavíme nástroj, který po výběru CSV ukáže počet řádků a sloupců, chybějící hodnoty, duplicity a náhled prvních záznamů. V běžné webové architektuře by prohlížeč soubor odeslal přes API na server a tam by jej zpracovalo pandas. Samotná analýza je krátká, ale serverová část musí navíc řešit příjem souboru, jeho případné uložení, mazání a přístupová oprávnění.
V našem příkladu uživatel otevře stránku, vybere soubor a prohlížeč si stáhne Python spolu s pandas. Analýza proběhne na jeho zařízení a CSV se na server neposílá. Pro nasazení proto stačí statický hosting, který poskytne HTML, JavaScript, Pythonový skript a soubory runtime.

Jak se Python dostane do prohlížeče
Pyodide je port CPythonu pro WebAssembly. Nejde o jazyk, který Python pouze připomíná, ani o vzdálený interpret schovaný za HTTP požadavkem. V prohlížeči skutečně běží CPython a spolu s ním lze načítat balíčky, které jsou pro Pyodide připravené. Aktuální vydání 314.0.4 obsahuje mimo jiné pandas 3.0.2.
Úlohy rozdělíme jednoduše. JavaScript načte soubor, obslouží rozhraní a vykreslí výsledek do DOM. Python dostane text CSV, provede analýzu a vrátí JSON. Výpočet spustíme ve web workeru, takže uživatelské rozhraní zůstane během načítání runtime i práce pandas použitelné. Současná dokumentace Pyodide pro tento účel vyžaduje worker typu module; starší návody s importScripts() už pro aktuální verzi neplatí.
Tři soubory, tři odpovědnosti
Projekt rozdělíme na tři části:
csv-profiler/
├── index.html # formulář, čtení souboru a vykreslení výsledku
├── worker.mjs # načtení Pyodide a komunikace s Pythonem
└── processor.py # samotná analýza CSV
Každý soubor má vlastní úlohu. Pythonový výpočet lze upravovat a testovat samostatně, protože není uložený jako dlouhý řetězec v JavaScriptu. Worker spouští runtime mimo hlavní vlákno. Soubor index.html nemusí znát podrobnosti práce s pandas; reaguje jen na zprávy ready, result a error.
Začneme souborem processor.py, ve kterém je samotná analýza:
# processor.py
import json
from io import StringIO
import pandas as pd
def profile_csv(csv_text):
frame = pd.read_csv(StringIO(csv_text), sep=None, engine="python")
columns = []
for name in frame.columns:
series = frame[name]
missing = int(series.isna().sum())
columns.append(
{
"name": str(name),
"dtype": str(series.dtype),
"missing": missing,
"missing_percent": (missing / len(frame) * 100)
if len(frame)
else 0,
"unique": int(series.nunique(dropna=True)),
}
)
preview = frame.head(5).astype("string").fillna("")
result = {
"rows": int(len(frame)),
"columns": int(len(frame.columns)),
"duplicates": int(frame.duplicated().sum()),
"column_names": [str(name) for name in preview.columns],
"profile": columns,
"preview": preview.to_dict(orient="records"),
}
return json.dumps(result, ensure_ascii=False, default=str)
sep=None s enginem python nechá pandas odhadnout oddělovač. Stejný kód proto zvládne běžné čárkové i středníkové exporty. Nejde o spolehlivý detektor všech možných formátů, pro malý interní nástroj je však dostatečně tolerantní. Výsledek převádíme na obyčejný JSON, takže mezi Pythonem a JavaScriptem nemusíme předávat objekt DataFrame ani sadu proxy objektů.
Pyodide spustíme ve web workeru
Při první analýze musí prohlížeč stáhnout Pyodide, pandas a jejich závislosti a potom inicializovat pythonový runtime. Začneme je proto načítat až ve chvíli, kdy uživatel vybere CSV. Při dalších analýzách už stránka používá připravenou instanci.
// worker.mjs
import { loadPyodide } from
"https://cdn.jsdelivr.net/pyodide/v314.0.4/full/pyodide.mjs";
let profileCsv;
const ready = (async () => {
const pyodide = await loadPyodide();
await pyodide.loadPackage("pandas");
const response = await fetch("./processor.py");
if (!response.ok) {
throw new Error(`processor.py se nepodařilo načíst (${response.status}).`);
}
await pyodide.runPythonAsync(await response.text());
profileCsv = pyodide.globals.get("profile_csv");
self.postMessage({ type: "ready" });
})();
ready.catch((error) => {
self.postMessage({ type: "error", phase: "init", message: String(error) });
});
self.onmessage = async ({ data }) => {
if (data.type !== "profile") return;
try {
await ready;
const result = JSON.parse(profileCsv(data.csv));
self.postMessage({ type: "result", result });
} catch (error) {
self.postMessage({
type: "error",
phase: "process",
message: String(error),
});
}
};
Načtenou funkci profile_csv si uložíme do proměnné. V produkční aplikaci s dlouhou životností bychom při ukončení workeru zavolali také profileCsv.destroy(), protože jde o PyProxy. V tomto příkladu proxy zanikne společně s workerem.
CDN dodá runtime a balíčky, nikoli uživatelské CSV. Obsah souboru posílá hlavní vlákno workeru přes postMessage(), tedy uvnitř stejné stránky. V testu jsme kontrolovali URL i těla síťových požadavků; hodnoty z ukázkových CSV se v nich neobjevily. To ale samo o sobě nezaručuje soukromí. Pokud do stránky přidáte analytiku, cizí skript nebo vlastní upload, mohou data prohlížeč opustit.
HTML přečte soubor a bezpečně vykreslí výsledek
V index.html potřebujeme vstup pro soubor, volbu kódování, tlačítko, stavovou zprávu a dvě tabulky pro výsledek. Samotný HTML formulář ani CSS nepřinášejí pro práci s Pyodide nic podstatného, proto je zde nevypisujeme. Kompletní soubor včetně vzhledu použitého na screenshotu výše najdete v ukázkovém projektu ke stažení.
V článku proto ukážeme jen propojení formuláře s workerem. Po výběru souboru vytvoříme worker a po jeho inicializaci povolíme tlačítko. Kliknutí na tlačítko načte CSV ve zvoleném kódování a předá jeho obsah ke zpracování. Následující výřez záměrně vynechává funkci render() a úplnou obsluhu chyb; obojí je v přiloženém projektu.
const fileInput = document.querySelector("#csv-file");
const encodingInput = document.querySelector("#encoding");
const analyzeButton = document.querySelector("#analyze");
const status = document.querySelector("#status");
let worker;
let workerReady = false;
function startWorker() {
if (worker) return;
status.textContent = "Načítám Python a pandas…";
worker = new Worker("./worker.mjs", { type: "module" });
worker.onmessage = ({ data }) => {
if (data.type === "ready") {
workerReady = true;
analyzeButton.disabled = !fileInput.files.length;
status.textContent = "Python je připraven. Soubor můžete analyzovat.";
} else if (data.type === "result") {
render(data.result);
analyzeButton.disabled = false;
status.textContent = "Hotovo. Data neopustila tento prohlížeč.";
} else if (data.type === "error") {
status.textContent = `Chyba: ${data.message}`;
}
};
}
fileInput.addEventListener("change", () => {
analyzeButton.disabled = !workerReady || !fileInput.files.length;
if (fileInput.files.length) startWorker();
});
analyzeButton.addEventListener("click", async () => {
const file = fileInput.files[0];
if (!file || file.size === 0) return;
analyzeButton.disabled = true;
status.textContent = "Čtu a analyzuji soubor…";
const buffer = await file.arrayBuffer();
const csv = new TextDecoder(encodingInput.value, { fatal: true })
.decode(buffer);
worker.postMessage({ type: "profile", csv });
});
Výsledky skládáme přes textContent, nikoli přes innerHTML. CSV je cizí vstup a název sloupce může stejně dobře obsahovat HTML značku nebo pokus o skript. WebAssembly tuto obyčejnou webovou bezpečnost neřeší za nás.
Pro podporu Windows-1250 máme dobrý důvod. České CSV exporty ze starších podnikových aplikací se s tímto kódováním stále objevují. TextDecoder soubor převede ještě před předáním Pythonu; díky volbě fatal: true dostane uživatel chybu místo tiše poškozených znaků.
Stránku spusťte přes lokální server
Protože worker načítá modul a processor.py přes fetch(), neotvírejte index.html přímo z disku. Ve složce projektu spusťte jednoduchý lokální server:
python -m http.server 8000
Pak otevřete http://localhost:8000. Stejnou složku můžete beze změny nasadit na GitHub Pages, Cloudflare Pages, Netlify, S3 nebo jakýkoli jiný statický hosting. Žádný Python na hostingu instalovat nemusíte. Pyodide se načte z URL uvedené ve workeru; stejný způsob popisuje také oficiální dokumentace.
Pro skutečné nasazení bych přidal Content Security Policy a podle požadavků projektu zvážil vlastní hostování Pyodide místo veřejného CDN. Verzi runtime ponechte připnutou. V produkci nepoužívejte neurčité latest, protože aktualizace může aplikaci změnit bez úpravy jejího kódu.
Kdy prohlížeč stačí - a kdy už ne
Architektura bez backendu se hodí na převodníky formátů, validátory JSON a XML, anonymizaci, malé statistické kalkulačky, výukové nástroje nebo jednorázové zpracování citlivých souborů. Uživatel nic neinstaluje a provozovatel neřeší frontu úloh ani úklid uploadů. Po prvním načtení navíc může aplikace s vhodně nastaveným service workerem fungovat i bez připojení.
Tento přístup má také omezení. Při první analýze musí uživatel počkat na stažení runtime a balíčků, výpočet spotřebovává paměť jeho zařízení a celý soubor v našem příkladu držíme jako text i jako DataFrame. U souborů o stovkách megabajtů může prohlížeči rychle dojít paměť a výpočet skončí chybou MemoryError. V takovém případě dává smysl streamování, úspornější knihovna nebo zpracování na backendu.
Také nelze čekat, že v Pyodide poběží libovolný balíček z PyPI. Čistě pythonové balíčky mají dobrou šanci; rozšíření s nativním kódem potřebují sestavení pro WebAssembly. Podporu potřebných knihoven proto ověřte v seznamu balíčků dodávaných s Pyodide ještě před návrhem řešení.
Náš profilovač není náhradou datové platformy. Je to statická stránka pro jednu přesně vymezenou úlohu, při které není nutné posílat soubor na server. Pyodide nenahrazuje backend obecně, u podobných menších nástrojů jej však může zcela odstranit. Před psaním dalšího API se proto vyplatí ověřit, zda je pro danou úlohu vůbec potřeba.