1639f05e6e
Programma dat CSV, Excel, JSON, vaste-breedte tekst, pickle en Python-snippets onderling converteert via een Pandas DataFrame. Demonstreert dtype-afleiding, datetime-parsing met tijdzonebeheer (UTC-normalisatie) en het omzetten naar een lijst-van-dicts. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
599 lines
23 KiB
Python
599 lines
23 KiB
Python
"""
|
|
Pandas Bestandsformaat-Converter — Demo voor Python developers
|
|
=====================================================================
|
|
|
|
Dit programma leest een databestand in één van vier formaten (vaste-breedte
|
|
tekst, Excel, JSON, CSV) in een Pandas DataFrame en schrijft het terug naar
|
|
één van diezelfde vier formaten. Het invoerformaat wordt afgeleid uit de
|
|
extensie van het invoerbestand, en het uitvoerformaat uit de extensie van
|
|
het uitvoerbestand.
|
|
|
|
Gebruik:
|
|
uv run main.py <invoerbestand> <uitvoerbestand>
|
|
|
|
Voorbeelden:
|
|
uv run main.py sample.txt sample.csv
|
|
uv run main.py sample.csv sample.xlsx
|
|
uv run main.py sample.xlsx sample.json
|
|
uv run main.py sample.json sample2.txt # schrijft vaste breedte
|
|
uv run main.py sample.csv sample.pkl # pickle van lijst-van-dicts
|
|
uv run main.py sample.csv sample.py # Python-snippet met ingebedde data
|
|
|
|
Ondersteunde extensies invoer:
|
|
.txt -> vaste-breedte tekst
|
|
.csv -> kommagescheiden waarden
|
|
.xlsx -> Excel-werkmap
|
|
.json -> JSON (records-oriëntatie)
|
|
.pkl -> pickle van een lijst-van-dicts of een DataFrame
|
|
|
|
Ondersteunde extensies uitvoer:
|
|
.txt -> vaste-breedte tekst
|
|
.csv -> kommagescheiden waarden
|
|
.xlsx -> Excel-werkmap
|
|
.json -> JSON (records-oriëntatie)
|
|
.pkl -> pickle van een lijst-van-dicts
|
|
.py -> plakbaar Python-fragment met DATA = [...]
|
|
|
|
|
|
Hoe Pandas de conversie afhandelt
|
|
---------------------------------
|
|
Een Pandas DataFrame is een 2-dimensionale, in-memory, gelabelde tabel —
|
|
zie het als een spreadsheet die in Python leeft. De truc die formaat-
|
|
conversie eenvoudig maakt, is dat Pandas I/O ontkoppelt van het datamodel:
|
|
|
|
bestand op schijf --[lezer]--> DataFrame --[schrijver]--> bestand op schijf
|
|
|
|
Zodra de data in een DataFrame staat, maakt het niet meer uit waar ze
|
|
vandaan komt. Elke lezer produceert hetzelfde soort object, en elke
|
|
schrijver accepteert datzelfde object. Conversie is dus gewoon: "lees
|
|
in een DataFrame, schrijf het DataFrame uit in het nieuwe formaat."
|
|
|
|
Hier gebruikte Pandas-lezers:
|
|
|
|
* pd.read_fwf(pad, colspecs=..., names=..., dtype=...)
|
|
Vaste-breedte bestanden hebben geen scheidingsteken; elke kolom
|
|
bezet een specifiek karakterbereik. `colspecs` is een lijst van
|
|
(start, eind) tuples (eind-exclusief), `names` levert de
|
|
kolomkoppen (omdat het bestand er geen heeft), en `dtype` vertelt
|
|
Pandas welk Python/NumPy-type elke kolom moet krijgen.
|
|
|
|
* pd.read_csv(pad)
|
|
CSV is het eenvoudigste formaat. Pandas leidt de types (int,
|
|
float, string) automatisch per kolom af.
|
|
|
|
* pd.read_excel(pad)
|
|
Leest .xlsx via de `openpyxl`-engine (een extra dependency).
|
|
Pandas behandelt types vergelijkbaar met CSV.
|
|
|
|
* pd.read_json(pad, orient="records")
|
|
JSON kan veel verschillende vormen hebben; `orient="records"`
|
|
verwacht een lijst van objecten (`[{"kolom": waarde, ...}, ...]`),
|
|
wat de meest natuurlijke vertaling naar een DataFrame is.
|
|
|
|
Hier gebruikte Pandas-schrijvers:
|
|
|
|
* df.to_csv(pad, index=False, date_format=...)
|
|
`index=False` voorkomt dat Pandas de rijnummer-index als
|
|
extra kolom wegschrijft. `date_format` regelt hoe Timestamps
|
|
als tekst worden geserialiseerd.
|
|
|
|
* df.to_excel(pad, index=False)
|
|
Gebruikt onder de motorkap eveneens openpyxl voor .xlsx-uitvoer.
|
|
Datetimes worden als echte Excel-datumcellen weggeschreven.
|
|
|
|
* df.to_json(pad, orient="records", indent=2, date_format="iso")
|
|
`indent=2` maakt het bestand leesbaar voor mensen.
|
|
`date_format="iso"` voorkomt de standaard Unix-milliseconden.
|
|
|
|
* Vaste-breedte UITVOER heeft geen ingebouwde `to_fwf`. We bouwen het
|
|
handmatig: per kolom de maximale benodigde breedte bepalen
|
|
(kop versus waarden), en daarna elke cel opvullen met
|
|
str.ljust / str.rjust tot die breedte. Numerieke kolommen
|
|
worden rechts uitgelijnd; tekstkolommen links.
|
|
|
|
|
|
Waarom dtypes ertoe doen
|
|
------------------------
|
|
Wanneer je een vaste-breedte bestand inleest, is elke "cel" aanvankelijk
|
|
gewoon een stuk tekst. Als je Pandas niet vertelt welk type bedoeld is,
|
|
kan een kolom met "42" als string eindigen en later rekenkundige bewerkingen
|
|
breken. Daarom wordt het `dtype`-argument hieronder expliciet meegegeven
|
|
aan `read_fwf`. CSV/Excel/JSON-lezers doen redelijke inferentie zelf,
|
|
maar vaste breedte is dom over types — je moet het sturen.
|
|
|
|
|
|
Datums en tijden — verschillende formaten
|
|
-----------------------------------------
|
|
Datums leven in bestanden als gewone tekst, maar in een DataFrame willen we
|
|
ze als een echt `datetime64`-type zodat we erop kunnen sorteren, filteren,
|
|
optellen ("+1 dag"), enz. De brugfunctie is `pd.to_datetime(...)`.
|
|
|
|
Veelvoorkomende formaten in de praktijk:
|
|
|
|
* ISO 8601: "2026-05-08T14:30:00+02:00" (machine-vriendelijk)
|
|
* Dag eerst (NL/EU): "08/05/2026"
|
|
* Maand eerst (US): "05/08/2026"
|
|
* Met of zonder tijdzone-offset (`+02:00`, `Z`, of niets)
|
|
|
|
Belangrijkste argumenten van `pd.to_datetime`:
|
|
|
|
* `format=...` Expliciete strftime-string, bv. "%d/%m/%Y".
|
|
Snelste en betrouwbaarste optie.
|
|
* `format="mixed"` Pandas herkent het formaat per rij. Handig
|
|
wanneer dezelfde kolom meerdere formaten
|
|
bevat (bv. ISO én dd/mm/yyyy door elkaar).
|
|
* `dayfirst=True` Voor ambigue waarden zoals "01/02/2026"
|
|
("1 februari" i.p.v. "2 januari").
|
|
* `utc=True` Zet alles om naar UTC. Dit is cruciaal als
|
|
sommige rijen wél en andere géén tijdzone-
|
|
offset hebben — anders krijg je een mengsel
|
|
van naïeve en bewuste timestamps en kan je
|
|
niet vergelijken.
|
|
* `errors="coerce"` Onparseerbare waarden worden `NaT` (Not-a-
|
|
Time) i.p.v. een crash.
|
|
|
|
Tijdzones in 30 seconden
|
|
------------------------
|
|
Pandas onderscheidt **naïeve** en **tijdzone-bewuste** timestamps:
|
|
|
|
- Naïef: "2026-05-08 14:30:00" (geen offset, geen tz)
|
|
- Bewust: "2026-05-08 14:30:00+02:00" (heeft offset / zone)
|
|
|
|
Drie operaties dekken 95% van alle gebruik:
|
|
|
|
1. `pd.to_datetime(serie, utc=True)`
|
|
Eén stap: parse en zet om naar UTC. Inputs met offset worden
|
|
omgerekend; inputs zonder offset worden als UTC geïnterpreteerd.
|
|
Als je niets weet over het bronbestand, is dit altijd veilig.
|
|
|
|
2. `serie.dt.tz_localize("Europe/Brussels")`
|
|
Plak een tijdzone op een naïeve timestamp. Gebruik dit als je
|
|
weet dat lokale tijd bedoeld was maar de offset niet meegeschreven
|
|
is. Werkt enkel op naïeve timestamps.
|
|
|
|
3. `serie.dt.tz_convert("Europe/Brussels")`
|
|
Reken een bewuste timestamp om naar een andere tijdzone (de
|
|
absolute tijd verandert niet, alleen de weergave).
|
|
|
|
De gouden regel: **opslaan in UTC, weergeven in lokale tijd**. Daarom
|
|
roept dit programma `pd.to_datetime(..., utc=True)` aan in
|
|
`parse_datetimes()`. Wil je het uitvoerbestand in lokale tijd? Pas
|
|
`.dt.tz_convert("Europe/Brussels")` toe voordat je schrijft.
|
|
|
|
Datums zonder tijd (zoals `delivery_date` in dd/mm/yyyy) hebben in theorie
|
|
geen tijdzone nodig, maar Pandas heeft geen apart "date"-type: het wordt
|
|
een Timestamp op middernacht. Met `utc=True` is dat middernacht UTC. Voor
|
|
pure datums maakt dat verder weinig uit.
|
|
|
|
|
|
Van DataFrame naar lijst-van-dicts
|
|
-----------------------------------
|
|
Een DataFrame is handig voor berekeningen, maar soms wil je de data als
|
|
gewone Python-structuren: een lijst van dictionaries, waarbij elke dict
|
|
één rij voorstelt. Dit is de standaardvorm die je doorgeeft aan REST-APIs,
|
|
Jinja2-templates, unittest-fixtures, enz.
|
|
|
|
De sleutel is:
|
|
|
|
records = df.to_dict(orient="records")
|
|
|
|
Het resultaat is een list[dict], bv.:
|
|
[
|
|
{"id": 1, "name": "Widget", "price": 9.99, ...},
|
|
{"id": 2, "name": "Gadget", "price": 19.95, ...},
|
|
]
|
|
|
|
Terug de andere kant op — van lijst naar DataFrame — werkt via:
|
|
|
|
df = pd.DataFrame(records)
|
|
|
|
Hierbij leidt Pandas de kolomnamen en -types opnieuw af uit de dicts.
|
|
|
|
De `orient`-parameter bepaalt de structuur van de output:
|
|
|
|
orient="records" -> [{"col": val, ...}, ...] (rij per dict) *meest gebruikt*
|
|
orient="dict" -> {"col": {0: val, 1: val}} (kolom per dict, rij-index als sleutel)
|
|
orient="list" -> {"col": [val, val, ...]} (kolom per lijst)
|
|
orient="index" -> {0: {"col": val}, ...} (rij-index als sleutel)
|
|
orient="split" -> {"columns": [...], "data": [[...], ...]} (compacte vorm)
|
|
|
|
Voor de meeste dagelijkse situaties is `orient="records"` de juiste keuze:
|
|
het sluit het beste aan bij hoe mensen over rijen denken.
|
|
|
|
Aandacht voor Pandas-types in dicts
|
|
------------------------------------
|
|
`to_dict()` converteert waarden naar Python-types waar het kan, maar
|
|
sommige types blijven Pandas/NumPy-specifiek:
|
|
|
|
- int-kolommen -> numpy.int64 (werkt als int, maar type is anders)
|
|
- float-kolommen -> numpy.float64
|
|
- datetime-kolommen -> pandas.Timestamp (subklasse van datetime.datetime)
|
|
|
|
Dit kan problemen geven bij:
|
|
* json.dumps(records) — NumPy-types zijn niet JSON-serialiseerbaar.
|
|
* pickle — werkt prima, Timestamps zijn picklable.
|
|
* vergelijking met isinstance(v, int) — np.int64 is géén int.
|
|
|
|
Oplossing: converteer de records expliciet naar standaard Python-types:
|
|
|
|
import numpy as np
|
|
|
|
def to_plain_python(record: dict) -> dict:
|
|
result = {}
|
|
for k, v in record.items():
|
|
if isinstance(v, pd.Timestamp):
|
|
result[k] = v.to_pydatetime() # -> datetime.datetime
|
|
elif isinstance(v, (np.integer,)):
|
|
result[k] = int(v) # -> int
|
|
elif isinstance(v, (np.floating,)):
|
|
result[k] = float(v) # -> float
|
|
else:
|
|
result[k] = v
|
|
return result
|
|
|
|
records = [to_plain_python(r) for r in df.to_dict(orient="records")]
|
|
|
|
Dit programma past die conversie toe in `df_to_records()` — zodat zowel
|
|
pickle als het Python-snippet correcte standaard-types bevatten.
|
|
|
|
Pickle — binaire opslag van Python-objecten
|
|
--------------------------------------------
|
|
Pickle (.pkl) is Python's ingebouwde binaire serialisatieformaat. Het kan
|
|
vrijwel elk Python-object opslaan: lijsten, dicts, datetime-objecten, enz.
|
|
|
|
import pickle
|
|
|
|
# Wegschrijven:
|
|
with open("data.pkl", "wb") as f: # "wb" = write binary
|
|
pickle.dump(records, f)
|
|
|
|
# Inlezen:
|
|
with open("data.pkl", "rb") as f: # "rb" = read binary
|
|
records = pickle.load(f)
|
|
|
|
Voordelen:
|
|
+ Razendsnel (binair formaat, geen tekst-parsing)
|
|
+ Behoudt exacte Python-types inclusief datetime met tijdzone
|
|
+ Geschikt voor caching van tussenresultaten
|
|
|
|
Nadelen:
|
|
- Niet leesbaar voor mensen of andere programmeertalen
|
|
- Pickle-bestanden van de ene Python-versie werken mogelijk niet in
|
|
een andere (gebruik het dus niet als uitwisselingsformaat)
|
|
- Nooit een pickle inlezen van een onbekende bron — het kan
|
|
arbitraire code uitvoeren bij het laden (veiligheidsrisico)
|
|
|
|
Python-snippet (.py) — data direct in code
|
|
-------------------------------------------
|
|
Soms wil je testdata of fixtures rechtstreeks in een Python-bestand
|
|
inbedden, zodat je ze kunt importeren zonder een extern bestand nodig te
|
|
hebben. Dit programma genereert een `.py`-bestand met een `DATA`-variabele:
|
|
|
|
from datetime import datetime, timezone
|
|
|
|
DATA = [
|
|
{
|
|
"id": 1,
|
|
"name": "Widget",
|
|
"price": 9.99,
|
|
"quantity": 10,
|
|
"created_at": datetime(2026, 5, 8, 12, 30, 0, tzinfo=timezone.utc),
|
|
"delivery_date": datetime(2026, 5, 15, 0, 0, 0, tzinfo=timezone.utc),
|
|
},
|
|
...
|
|
]
|
|
|
|
Dit bestand kan je kopiëren in een project of importeren:
|
|
|
|
from sample import DATA
|
|
for row in DATA:
|
|
print(row["name"], row["created_at"])
|
|
|
|
Datetimes worden als `datetime(...)` uitgedrukt (niet als strings) zodat
|
|
de types meteen correct zijn bij importeren.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pickle
|
|
import sys
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
|
|
import numpy as np
|
|
import pandas as pd
|
|
|
|
|
|
# Schema voor het voorbeeld-bestand met vaste breedte. Elke tuple is
|
|
# (start, eind_exclusief). Deze bereiken definiëren de karakterposities
|
|
# die elk veld inneemt.
|
|
FWF_COLSPECS = [(0, 5), (5, 25), (25, 35), (35, 45), (45, 72), (72, 85)]
|
|
FWF_NAMES = ["id", "name", "price", "quantity", "created_at", "delivery_date"]
|
|
FWF_DTYPES = {
|
|
"id": "int64",
|
|
"name": "string",
|
|
"price": "float64",
|
|
"quantity": "int64",
|
|
# Datetime-kolommen lezen we eerst als string en parsen we daarna
|
|
# uniform met pd.to_datetime — zo werkt dezelfde code voor alle formaten.
|
|
"created_at": "string",
|
|
"delivery_date": "string",
|
|
}
|
|
|
|
# Datetime-kolommen + hoe we ze terug naar tekst formatteren bij het
|
|
# wegschrijven naar vaste breedte. Bij het inlezen herkennen we het
|
|
# formaat automatisch (ISO of dd/mm/yyyy) via format="mixed".
|
|
DATETIME_COLUMNS: dict[str, str] = {
|
|
"created_at": "%Y-%m-%dT%H:%M:%S%z", # ISO 8601 met offset
|
|
"delivery_date": "%d/%m/%Y", # dag eerst (NL/EU)
|
|
}
|
|
|
|
|
|
def parse_datetimes(df: pd.DataFrame) -> pd.DataFrame:
|
|
"""Zet datetime-kolommen om naar tijdzone-bewuste UTC Timestamps.
|
|
|
|
Werkt voor alle invoerformaten:
|
|
- txt/csv/json: kolommen komen binnen als string en worden geparsed.
|
|
- xlsx: Excel parseert datums al zelf; we zorgen alleen dat
|
|
ze tijdzone-bewust (UTC) zijn.
|
|
|
|
`format="mixed"` laat Pandas per rij het formaat raden, zodat ISO
|
|
("2026-05-08T14:30:00+02:00") én dd/mm/yyyy ("15/05/2026") in dezelfde
|
|
kolom kunnen voorkomen — handig wanneer data uit verschillende bronnen
|
|
samengevoegd wordt.
|
|
|
|
`utc=True` is de sleutel voor tijdzone-veiligheid: alles wordt
|
|
genormaliseerd naar UTC, ongeacht of de input een offset had of niet.
|
|
"""
|
|
for col in DATETIME_COLUMNS:
|
|
if col not in df.columns:
|
|
continue
|
|
series = df[col]
|
|
|
|
# Als de kolom al datetime is (bv. uit Excel), alleen de tijdzone fixen.
|
|
if pd.api.types.is_datetime64_any_dtype(series):
|
|
if series.dt.tz is None:
|
|
df[col] = series.dt.tz_localize("UTC")
|
|
else:
|
|
df[col] = series.dt.tz_convert("UTC")
|
|
continue
|
|
|
|
# Anders: parse de strings. We "snuiven" of de waarden in
|
|
# dd/mm/yyyy-stijl staan (bevatten een schuine streep) of in
|
|
# ISO-stijl (yyyy-mm-dd). dayfirst=True is enkel veilig voor de
|
|
# eerste vorm — op ISO-strings zou het maand en dag verwisselen.
|
|
sample_values = series.dropna().astype(str)
|
|
looks_ddmm = len(sample_values) > 0 and sample_values.str.contains("/").any()
|
|
df[col] = pd.to_datetime(
|
|
series,
|
|
format="mixed",
|
|
dayfirst=looks_ddmm,
|
|
utc=True,
|
|
errors="coerce",
|
|
)
|
|
return df
|
|
|
|
|
|
def format_value(value: object, col: str) -> str:
|
|
"""Formatteer één celwaarde voor tekstuitvoer (vaste breedte)."""
|
|
if pd.isna(value):
|
|
return ""
|
|
if col in DATETIME_COLUMNS and isinstance(value, pd.Timestamp):
|
|
return value.strftime(DATETIME_COLUMNS[col])
|
|
return str(value)
|
|
|
|
|
|
def _to_plain_python(value: object) -> object:
|
|
"""Zet een Pandas/NumPy-waarde om naar een standaard Python-type.
|
|
|
|
`df.to_dict()` geeft soms numpy.int64, numpy.float64 of pd.Timestamp
|
|
terug. Die zijn niet JSON-serialiseerbaar en gedragen zich subtiel anders
|
|
dan de ingebouwde int/float/datetime. Deze functie normaliseert ze.
|
|
"""
|
|
if isinstance(value, pd.Timestamp):
|
|
return value.to_pydatetime() # pd.Timestamp -> datetime.datetime (met tz)
|
|
if isinstance(value, np.integer):
|
|
return int(value) # numpy.int64 -> int
|
|
if isinstance(value, np.floating):
|
|
return float(value) # numpy.float64 -> float
|
|
if isinstance(value, float) and np.isnan(value):
|
|
return None # NaN -> None (JSON-vriendelijk)
|
|
return value
|
|
|
|
|
|
def df_to_records(df: pd.DataFrame) -> list[dict]:
|
|
"""Zet een DataFrame om naar een lijst van gewone Python-dicts.
|
|
|
|
Stap 1: df.to_dict(orient="records") geeft [{"col": val, ...}, ...]
|
|
Stap 2: _to_plain_python() vervangt NumPy/Pandas-types door standaard
|
|
Python-types zodat pickle, json.dumps en isinstance() correct werken.
|
|
"""
|
|
raw = df.to_dict(orient="records")
|
|
return [{k: _to_plain_python(v) for k, v in row.items()} for row in raw]
|
|
|
|
|
|
def _datetime_repr(dt: datetime) -> str:
|
|
"""Genereer een leesbare Python-expressie voor een datetime-object."""
|
|
if dt.tzinfo is not None:
|
|
return (
|
|
f"datetime({dt.year}, {dt.month}, {dt.day}, "
|
|
f"{dt.hour}, {dt.minute}, {dt.second}, tzinfo=timezone.utc)"
|
|
)
|
|
return (
|
|
f"datetime({dt.year}, {dt.month}, {dt.day}, "
|
|
f"{dt.hour}, {dt.minute}, {dt.second})"
|
|
)
|
|
|
|
|
|
def _value_repr(value: object) -> str:
|
|
"""Genereer een Python-literal voor één waarde in het .py-snippet."""
|
|
if isinstance(value, datetime):
|
|
return _datetime_repr(value)
|
|
return repr(value)
|
|
|
|
|
|
def read_any(path: Path) -> pd.DataFrame:
|
|
"""Stuur door naar de juiste Pandas-lezer op basis van de bestandsextensie."""
|
|
ext = path.suffix.lower()
|
|
if ext == ".txt":
|
|
# Vaste breedte: kolomgrenzen, namen en dtypes moeten meegegeven worden.
|
|
# `skiprows=1` slaat de leesbare kopregel in het voorbeeldbestand over.
|
|
df = pd.read_fwf(
|
|
path,
|
|
colspecs=FWF_COLSPECS,
|
|
names=FWF_NAMES,
|
|
dtype=FWF_DTYPES,
|
|
skiprows=1,
|
|
)
|
|
elif ext == ".csv":
|
|
df = pd.read_csv(path)
|
|
elif ext == ".xlsx":
|
|
df = pd.read_excel(path)
|
|
elif ext == ".json":
|
|
df = pd.read_json(path, orient="records")
|
|
elif ext == ".pkl":
|
|
# Een pickle kan een lijst-van-dicts of een DataFrame zijn.
|
|
data = pickle.loads(path.read_bytes())
|
|
df = pd.DataFrame(data) if isinstance(data, list) else data
|
|
else:
|
|
raise ValueError(f"Niet-ondersteunde invoerextensie: {ext}")
|
|
return parse_datetimes(df)
|
|
|
|
|
|
def write_fwf(df: pd.DataFrame, path: Path) -> None:
|
|
"""Schrijf een DataFrame weg als een tekstbestand met vaste breedte.
|
|
|
|
Pandas heeft geen ingebouwde `to_fwf`, dus we doen het zelf: kies een
|
|
breedte per kolom (max van de kopbreedte en de langste waarde) en vul
|
|
daarna elke cel op. Numerieke kolommen worden rechts uitgelijnd,
|
|
tekstkolommen en datums links — dat is de gangbare opmaak voor data
|
|
met vaste breedte.
|
|
"""
|
|
widths: dict[str, int] = {}
|
|
for col in df.columns:
|
|
header_w = len(str(col))
|
|
formatted = df[col].apply(lambda v, c=col: format_value(v, c))
|
|
values_w = int(formatted.map(len).max() or 0)
|
|
widths[col] = max(header_w, values_w) + 2 # +2 voor wat ademruimte
|
|
|
|
def fmt_cell(value: object, col: str) -> str:
|
|
s = format_value(value, col)
|
|
if pd.api.types.is_numeric_dtype(df[col]):
|
|
return s.rjust(widths[col])
|
|
return s.ljust(widths[col])
|
|
|
|
lines: list[str] = []
|
|
header = "".join(
|
|
str(col).rjust(widths[col]) if pd.api.types.is_numeric_dtype(df[col])
|
|
else str(col).ljust(widths[col])
|
|
for col in df.columns
|
|
)
|
|
lines.append(header)
|
|
for _, row in df.iterrows():
|
|
lines.append("".join(fmt_cell(row[col], col) for col in df.columns))
|
|
|
|
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
|
|
|
|
def write_pkl(df: pd.DataFrame, path: Path) -> None:
|
|
"""Schrijf de DataFrame weg als een pickle van een lijst-van-dicts.
|
|
|
|
We gebruiken df_to_records() zodat de pickle standaard Python-types
|
|
bevat (int, float, datetime) in plaats van NumPy/Pandas-specifieke types.
|
|
"""
|
|
records = df_to_records(df)
|
|
path.write_bytes(pickle.dumps(records))
|
|
|
|
|
|
def write_py(df: pd.DataFrame, path: Path) -> None:
|
|
"""Genereer een plakbaar Python-bestand met DATA = [...].
|
|
|
|
Het resultaat is een geldig .py-bestand dat je rechtstreeks kunt
|
|
importeren:
|
|
|
|
from sample import DATA
|
|
for row in DATA:
|
|
print(row["name"])
|
|
|
|
Datetimes worden als `datetime(...)` uitgedrukt zodat je ze kunt
|
|
gebruiken zonder extra parsing.
|
|
"""
|
|
records = df_to_records(df)
|
|
|
|
needs_datetime = any(
|
|
isinstance(v, datetime)
|
|
for row in records
|
|
for v in row.values()
|
|
)
|
|
|
|
lines: list[str] = ["# Auto-gegenereerd door main.py\n"]
|
|
if needs_datetime:
|
|
lines.append("from datetime import datetime, timezone\n")
|
|
lines.append("\nDATA = [\n")
|
|
|
|
for row in records:
|
|
lines.append(" {\n")
|
|
for key, value in row.items():
|
|
lines.append(f" {key!r}: {_value_repr(value)},\n")
|
|
lines.append(" },\n")
|
|
|
|
lines.append("]\n")
|
|
path.write_text("".join(lines), encoding="utf-8")
|
|
|
|
|
|
def write_any(df: pd.DataFrame, path: Path) -> None:
|
|
"""Stuur door naar de juiste Pandas-schrijver op basis van de bestandsextensie."""
|
|
ext = path.suffix.lower()
|
|
if ext == ".txt":
|
|
write_fwf(df, path)
|
|
elif ext == ".csv":
|
|
# ISO 8601 met offset is de veiligste "tekstvorm" voor datetimes.
|
|
df.to_csv(path, index=False, date_format="%Y-%m-%dT%H:%M:%S%z")
|
|
elif ext == ".xlsx":
|
|
# Excel-cellen kunnen geen tijdzone bevatten — drop tz vlak voor
|
|
# het schrijven, zodat openpyxl niet struikelt.
|
|
df_out = df.copy()
|
|
for col in DATETIME_COLUMNS:
|
|
if col in df_out.columns and pd.api.types.is_datetime64_any_dtype(df_out[col]):
|
|
if df_out[col].dt.tz is not None:
|
|
df_out[col] = df_out[col].dt.tz_convert("UTC").dt.tz_localize(None)
|
|
df_out.to_excel(path, index=False)
|
|
elif ext == ".json":
|
|
# date_format="iso" voorkomt Unix-milliseconden in de JSON.
|
|
df.to_json(path, orient="records", indent=2, date_format="iso")
|
|
elif ext == ".pkl":
|
|
write_pkl(df, path)
|
|
elif ext == ".py":
|
|
write_py(df, path)
|
|
else:
|
|
raise ValueError(f"Niet-ondersteunde uitvoerextensie: {ext}")
|
|
|
|
|
|
def main(argv: list[str]) -> int:
|
|
if len(argv) != 3:
|
|
print(__doc__)
|
|
print("FOUT: precies twee argumenten verwacht: <invoerbestand> <uitvoerbestand>")
|
|
return 1
|
|
|
|
in_path = Path(argv[1])
|
|
out_path = Path(argv[2])
|
|
|
|
if not in_path.exists():
|
|
print(f"FOUT: invoerbestand bestaat niet: {in_path}")
|
|
return 1
|
|
|
|
print(f"Bezig met lezen van {in_path} ({in_path.suffix}) ...")
|
|
df = read_any(in_path)
|
|
print(f"{len(df)} rijen geladen, {len(df.columns)} kolommen.")
|
|
print("Voorbeeld:")
|
|
print(df.head().to_string(index=False))
|
|
|
|
print(f"Bezig met schrijven van {out_path} ({out_path.suffix}) ...")
|
|
write_any(df, out_path)
|
|
print("Klaar.")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main(sys.argv))
|