Salta el contingut

Formats tabulars i semiestructurats

Els formats de dades tabulars com CSV i TSV son els més universals i simples d'intercanviar, però presenten limitacions importants a escala de Big Data: manca de tipus de dades, problemes d'encoding i absència de compressió nativa. JSON i NDJSON permeten representar dades jeràrquiques i semiestructurades amb suport de tipus, mentre que XML continua present en sistemes legacy empresarials. Conèixer les fortaleses i debilitats de cadascun permet escollir el format adequat a cada situació.


CSV i variants

Com funciona el CSV

CSV (Comma-Separated Values) és un format de text pla on cada línia representa un registre i els camps estan separats per comes. Malgrat la seva aparent simplicitat, amaguen una quantitat notable de decisions de disseny que poden provocar problemes d'interoperabilitat.

Els elements bàsics d'un CSV son:

  • Separador: la coma per defecte, però pot ser qualsevol caràcter.
  • Delimitador de text: les cometes dobles (") s'usen per encapsular camps que contenen el separador o salts de línia.
  • Capçalera: la primera fila pot contenir els noms de les columnes (no obligatori).
  • Encoding: UTF-8 és el recomanat, però sovint es troben fitxers en ISO-8859-1 o Windows-1252.
id,nom,import,moneda
1,"Empresa, SL",1500.00,EUR
2,Acme Inc,200.50,USD

El camp "Empresa, SL" necessita cometes perquè conté una coma. Si el camp mateix contingués cometes, se n'escapa doblant-les: "Ell digué ""hola""".

Variants del CSV

TSV (Tab-Separated Values): usa el tabulador (\t) com a separador. Redueix el problema dels camps amb comes, però els camps no poden contenir tabuladors (poc freqüent). Molt usat en exportacions de fulls de càlcul.

PSV (Pipe-Separated Values): usa la barra vertical (|). Habitual en sistemes bancaris i de salut on les dades contenen moltes comes.

CSV sense capçalera: alguns sistemes (EDI, legacy) envien CSV sense capçalera. Cal indicar els noms de columna manualment en llegir el fitxer.

RFC 4180: l'estàndard que ningú segueix del tot

L'RFC 4180 de 2005 defineix el format CSV "oficial", però en la pràctica:

  • Molts productors no encapsulen els camps amb cometes (quan no és necessari).
  • Alguns sistemes usen salts de línia \r\n (Windows), d'altres \n (Unix).
  • L'encoding no s'especifica al RFC, cosa que genera problemes constants.
  • Alguns exportadors afegeixen una marca BOM (Byte Order Mark) al principi del fitxer UTF-8 que trenca parsers desprevenits.

Regla pràctica

Mai assumeixis que un CSV compleix l'RFC 4180. Sempre inspeccioneu els primers bytes del fitxer (encoding, BOM, separador real) abans de processar-lo de manera automàtica.

Llegir i escriure CSV amb Python

Mòdul csv estàndard — per a fitxers petits i control precís:

import csv

# Lectura bàsica
with open('vendes.csv', newline='', encoding='utf-8') as f:
    reader = csv.DictReader(f)
    for fila in reader:
        print(fila['nom'], fila['import'])

# Escriptura
camps = ['id', 'nom', 'import', 'moneda']
with open('sortida.csv', 'w', newline='', encoding='utf-8') as f:
    writer = csv.DictWriter(f, fieldnames=camps)
    writer.writeheader()
    writer.writerow({'id': 1, 'nom': 'Empresa SL', 'import': 1500.0, 'moneda': 'EUR'})

pandas — per a anàlisi i transformació de dades:

import pandas as pd

# Lectura amb opcions habituals
df = pd.read_csv(
    'vendes.csv',
    sep=',',                          # separador
    encoding='utf-8',                 # encoding
    dtype={'id': int, 'import': float},  # tipus explícits
    parse_dates=['data'],             # convertir columna a datetime
    na_values=['N/A', 'null', ''],    # valors nuls
)

# Escriptura sense l'índex de pandas
df.to_csv('sortida.csv', index=False, encoding='utf-8')

# TSV
df.to_csv('sortida.tsv', sep='\t', index=False)

Consell de rendiment

Especificar dtype explícitament evita que pandas infereixi els tipus llegint tot el fitxer dues vegades. En fitxers grans, pot reduir el consum de memòria fins a un 50%.

Chunking: processar fitxers grans sense explotar la RAM

Un CSV de 10 GB no cap en memòria d'un ordinador de 16 GB (el fitxer descomprimit + el DataFrame de pandas ocupa aproximadament el doble de la mida original). La solució és el chunking: llegir i processar el fitxer en blocs de N files.

import pandas as pd

CHUNK_SIZE = 100_000  # 100.000 files per bloc
totals_per_moneda = {}

for chunk in pd.read_csv('transaccions_grans.csv', chunksize=CHUNK_SIZE):
    # Cada 'chunk' és un DataFrame de 100.000 files màxim
    parcial = chunk.groupby('moneda')['import'].sum()
    for moneda, total in parcial.items():
        totals_per_moneda[moneda] = totals_per_moneda.get(moneda, 0) + total

# Mostrar resultats finals
for moneda, total in sorted(totals_per_moneda.items()):
    print(f"{moneda}: {total:,.2f}")

Per que funciona: en lloc de carregar totes les files a memòria de cop, pandas llegeix un bloc, el processa, l'allibera, i llegeix el bloc següent. La memòria usada és proporcional a CHUNK_SIZE, no a la mida total del fitxer.

Quan ajustar chunksize:

  • Massa gran: risca d'esgotar la memòria.
  • Massa petit: l'overhead d'I/O el fa lent.
  • Valor recomanat de partida: entre 50.000 i 500.000 files, depenent de la RAM disponible i el nombre de columnes.

Limitacions del CSV a escala Big Data

  • Sense tipus natius: tots els valors son text. Cal inferir o declarar els tipus en cada lectura, cosa que pot provocar errors.
  • Sense compressió nativa: un CSV de 10 GB continua ocupant 10 GB al disc. Cal comprimir-lo externament (gzip, bzip2) però llavors no és seekable (no es pot anar a una posició arbitrària sense descomprimir tot).
  • Sense indexació: per trobar files específiques cal llegir tot el fitxer o mantenir un índex separat.
  • Lent per a consultes columnar: per sumar una sola columna, cal llegir totes les files amb tots els camps.
  • Problemes d'encoding: la font de la majoria d'errors en pipelines ETL reals.

Quan usar CSV

El CSV és la millor opció quan:

  • Es necessita interoperabilitat màxima amb sistemes externs (proveïdors, partners, administració pública).
  • El volum de dades és petit (< 1 GB).
  • El destinatari final és un full de càlcul o un sistema legacy.
  • La simplicitat de depuració és prioritària (és text pla llegible).

JSON i NDJSON

JSON estàndard

JSON (JavaScript Object Notation) és un format de text basat en parelles clau-valor que suporta anidament arbitrari. A diferència del CSV, JSON sí que té tipus natius: null, boolean, number (sense distinció int/float), string, array i object.

{
  "id": 1,
  "nom": "Empresa SL",
  "import": 1500.0,
  "moneda": "EUR",
  "actiu": true,
  "adreca": {
    "carrer": "Avda. Principal, 10",
    "codi_postal": "17300"
  },
  "etiquetes": ["client", "preferent"]
}

JSON és ideal per a dades jeràrquiques, però no és un format tabular natiu. Un fitxer JSON d'un milió de registres és tot un array enorme que cal llegir complet a memòria abans de poder processar res.

NDJSON (Newline Delimited JSON) / JSON Lines

NDJSON resol el problema de streaming: cada línia és un document JSON complet i independent. No hi ha cap array extern que emboliqui tot el contingut.

{"id": 1, "nom": "Empresa SL", "import": 1500.0, "moneda": "EUR"}
{"id": 2, "nom": "Acme Inc", "import": 200.5, "moneda": "USD"}
{"id": 3, "nom": "Beta Corp", "import": 750.0, "moneda": "EUR"}

Avantatges de NDJSON sobre JSON estàndard:

  • Streaming friendly: es pot processar línia per línia sense carregar tot el fitxer.
  • Fàcil d'afegir dades (append): s'afegeix una línia al final sense reescriure res.
  • Tolerant a errors: si una línia és invàlida, les altres segueixen sent llegibles.
  • Compatible amb wc -l: el nombre de línies és el nombre de registres.

Avantatges sobre CSV

Característica CSV NDJSON
Tipus de dades Cap (tot text) Natiu (null, bool, int, float, string)
Anidament No Si (objectes i arrays)
Valors nuls Ambiguous ("" vs ,) Explícit (null)
Streaming Possible Natural
Compressibilitat Alta Alta

Lectura de NDJSON gran amb Python

Opció 1: pandas (per a fitxers que caben a memòria o en chunks):

import pandas as pd

# Lectura completa
df = pd.read_json('logs.ndjson', lines=True)

# Lectura en chunks per a fitxers grans
for chunk in pd.read_json('logs_grans.ndjson', lines=True, chunksize=50_000):
    # processar chunk
    pass

Opció 2: ijson (streaming parser, per a fitxers enormes):

import ijson

# Processar un fitxer NDJSON de 50 GB sense carregar-lo a memòria
with open('events_grans.ndjson', 'rb') as f:
    for linia in f:
        import json
        event = json.loads(linia)
        # processar event individualment
        if event.get('tipus') == 'pagament':
            print(event['import'])

ijson per a JSON jeràrquic

La biblioteca ijson brilla especialment quan el fitxer és un JSON estàndard (no NDJSON) amb un array de milers d'objectes. Permet iterar sobre els elements del array sense descomprimir-lo tot a memòria, usant un parser SAX-like.

import ijson

with open('dades_grans.json', 'rb') as f:
    for item in ijson.items(f, 'item'):
        # 'item' itera sobre cada element de l'array arrel
        print(item['nom'])

Quan usar NDJSON

NDJSON és la millor opció per a:

  • Logs d'API i events: cada event és un registre independent.
  • Dades semiestructurades: registres amb esquemes variables (no tots els camps sempre presents).
  • Pipelines de streaming: Kafka, Kinesis, etc. on cada missatge és un JSON.
  • Ingestió incremental: afegir nous registres al final del fitxer sense reescriure-ho.

XML

XML (eXtensible Markup Language) és un format de text jeràrquic basat en etiquetes. Malgrat la seva verbositat (un fitxer XML pot ocupar el triple que el JSON equivalent), segueix present en molts contextos empresarials:

  • Intercanvis EDI (Electronic Data Interchange) en el sector logístic i comercial.
  • Serveis SOAP en integracions empresarials antigues.
  • Configuració de sistemes: Maven, Spring, Android Manifest.
  • Formats de documents: OOXML (Excel .xlsx), OpenDocument.
<transaccions>
  <transaccio id="1">
    <nom>Empresa SL</nom>
    <import moneda="EUR">1500.00</import>
    <data>2024-03-15</data>
  </transaccio>
</transaccions>

Lectura amb Python:

import xml.etree.ElementTree as ET

arbre = ET.parse('transaccions.xml')
arrel = arbre.getroot()

for trans in arrel.findall('transaccio'):
    nom = trans.find('nom').text
    import_ = trans.find('import').text
    moneda = trans.find('import').get('moneda')
    print(f"{nom}: {import_} {moneda}")

# Per a XML complex, lxml és més potent i ràpid:
from lxml import etree

arbre = etree.parse('transaccions.xml')
# XPath complet disponible
resultats = arbre.xpath('//transaccio[@id="1"]/nom/text()')

Consell sobre XML

Si reps XML d'un sistema extern i has de transformar-lo a un format analític, converteix-lo a Parquet o NDJSON com a primer pas del pipeline. No intentes fer analítica directament sobre XML.


Taula comparativa de formats tabulars

Característica CSV TSV JSON NDJSON XML
Mida relativa 1x 1x 1.3x 1.2x 2-3x
Velocitat de parse Alta Alta Mitjana Alta Baixa
Tipus de dades Cap Cap Parcial Parcial Cap
Anidament No No Si Si Si
Streaming Possible Possible Dificil Natural Possible
Compressio nativa No No No No No
Llegibilitat humana Alta Alta Alta Alta Mitjana
Suport d'eines Universal Alt Alt Alt Alt
Casos d'us Intercanvi Exports APIs, configs Logs, events Legacy, EDI

AC5074/06/01 — Miniactivitat

Donada la URL d'un CSV de 500 MB amb transaccions financeres (pots generar-ne un de sintètic amb Faker), escriu un script Python que:

  1. El processi en chunks de 50.000 files usant pd.read_csv(chunksize=50_000).
  2. Calculi, sense carregar-lo tot a memòria, les estadístiques següents:
  3. Nombre total de files.
  4. Suma total d'imports per moneda.
  5. Import màxim i mínim global.
  6. Nombre de valors nuls per columna.
  7. Mostri un resum final un cop processat tot el fitxer.
  8. Mesuri el temps total de processament amb time.perf_counter().

Compara el temps i el consum de RAM (amb tracemalloc) entre el processament per chunks i el pd.read_csv() directe (si la RAM ho permet).

Lliurament: script .py amb comentaris que expliquin cada decisió de disseny.