Source document

scripts/loaders/miami/_zip.ts

Served verbatim from the project repository. Internal working document conventions apply: documents may reference file paths, branch names, and findings-ledger anchors from the repo.

// Stream one member of a zip through a CSV parser, keeping only rows that
// pass `keep`. Used for the NCES CCD files (a 200 MB zip expands to a ~1.5 GB
// CSV; we only want the Miami-Dade rows). Relies on the system `unzip`.

import { spawn } from "node:child_process";
import { parse } from "csv-parse";

export async function streamZipCsv<T = Record<string, string>>(
  zipPath: string,
  memberPattern: RegExp,
  keep: (row: Record<string, string>) => boolean,
): Promise<T[]> {
  const list = spawn("unzip", ["-Z1", zipPath]);
  let names = "";
  for await (const chunk of list.stdout) names += chunk;
  await new Promise((res) => list.on("close", res));
  const member = names.split("\n").find((n) => memberPattern.test(n));
  if (!member) throw new Error(`no zip member matching ${memberPattern} in ${zipPath}`);

  const proc = spawn("unzip", ["-p", zipPath, member]);
  const parser = parse({ columns: true, bom: true, relax_column_count: true, skip_empty_lines: true });
  const out: T[] = [];
  proc.stdout.pipe(parser);
  for await (const row of parser as AsyncIterable<Record<string, string>>) {
    if (keep(row)) out.push(row as unknown as T);
  }
  return out;
}