Spanien (Renfe + Cercanías), Polen, Norwegen, Dänemark, Österreich (ÖBB + Wiener Linien), Schweden (Key)
- mehrere Quellen je Land (country), Grenzen aus Natural Earth, OSM und Kachelausschnitt erweitert - Fahrpläne glätten: Unterordner im Zip, aufgefüllte Felder, fehlende parent_station, level_id - Gattung aus der Zugnummer (ÖBB 'RJ 658'), Fernverkehr auch über Routentyp 101/102/105 - Echtzeit: Verspätung vor Uhrzeit (Renfe), Fahrten ohne Datum, Key in der Adresse (Trafiklab) - httpx-Logs aus (würden Keys zeigen) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X48pE4bzjHej8w9HyZXCjb
This commit is contained in:
+43
-7
@@ -43,7 +43,9 @@ def timetable_year(today: dt.date) -> int:
|
||||
|
||||
|
||||
def gtfs_url(src: str) -> str:
|
||||
url = C.SOURCES[src]["gtfs_url"]
|
||||
cfg = C.SOURCES[src]
|
||||
# {year}: ÖBB (Datei je Fahrplanjahr), {key}: Schweden (Key in der Adresse)
|
||||
url = cfg["gtfs_url"].replace("{year}", str(timetable_year(dt.date.today()))).replace("{key}", cfg["rt_key"])
|
||||
if src == "ch" and not url:
|
||||
# shortcut: in der Nacht des Fahrplanwechsels fehlen bis zum nächsten Stundenlauf die Züge
|
||||
url = f"https://data.opentransportdata.swiss/dataset/timetable-{timetable_year(dt.date.today())}-gtfs2020/permalink"
|
||||
@@ -132,6 +134,28 @@ def filter_by_col(z: zipfile.ZipFile, name: str, col: str, keys: set, out: Path,
|
||||
return n, seen
|
||||
|
||||
|
||||
def normalize_zip(zip_path: Path, work: Path) -> Path:
|
||||
"""Eigenheiten mancher Fahrpläne glätten: Dateien in einem Unterordner (ÖBB) und mit Leerzeichen
|
||||
aufgefüllte Felder ("10T0001C1 ", Renfe Cercanías). Sonst die Datei unverändert."""
|
||||
z = zipfile.ZipFile(zip_path)
|
||||
members = {Path(n).name: n for n in z.namelist() if n.endswith(".txt")}
|
||||
padded = any(c != c.strip() for n in members.values() for c in rows(z, n)[0])
|
||||
if not padded and all(members[k] == k for k in members):
|
||||
return zip_path
|
||||
out = work / "normalized.zip"
|
||||
with zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED, compresslevel=1) as o:
|
||||
for base, name in members.items():
|
||||
with z.open(name) as src, o.open(base, "w") as dst:
|
||||
if not padded:
|
||||
shutil.copyfileobj(src, dst, 1 << 20)
|
||||
continue
|
||||
with io.TextIOWrapper(dst, encoding="utf-8", newline="") as t: # schliessen = alles schreiben
|
||||
w = csv.writer(t)
|
||||
for row in csv.reader(io.TextIOWrapper(src, encoding="utf-8-sig", newline="")):
|
||||
w.writerow([c.strip() for c in row])
|
||||
return out
|
||||
|
||||
|
||||
def filter_rail(zip_path: Path, out: Path, version: str | None = None) -> dict:
|
||||
out.mkdir(parents=True)
|
||||
z = zipfile.ZipFile(zip_path)
|
||||
@@ -159,9 +183,12 @@ def filter_rail(zip_path: Path, out: Path, version: str | None = None) -> dict:
|
||||
n_st, used_stops = filter_by_col(z, "stop_times.txt", "trip_id", trip_ids, out / "stop_times.txt", "stop_id")
|
||||
|
||||
h, rd = rows(z, "stops.txt")
|
||||
sid, par = h.index("stop_id"), h.index("parent_station")
|
||||
sid = h.index("stop_id")
|
||||
stops = list(rd)
|
||||
parents = {r[par] for r in stops if r[sid] in used_stops}
|
||||
parents = {r[h.index("parent_station")] for r in stops if r[sid] in used_stops} if "parent_station" in h else set()
|
||||
if "level_id" in h: # levels.txt kopieren wir nicht (ÖBB) -> Verweise leeren, sonst bricht pfaedle ab
|
||||
for r in stops:
|
||||
r[h.index("level_id")] = ""
|
||||
write_csv(out / "stops.txt", h, (r for r in stops if r[sid] in used_stops or r[sid] in parents))
|
||||
|
||||
for name in ("calendar.txt", "calendar_dates.txt"):
|
||||
@@ -223,7 +250,7 @@ def run_pfaedle(work: Path, rail: Path, source: str) -> Path | None:
|
||||
# Netz aufs Land (+ Rand) zuschneiden: die Fahrpläne enthalten Züge bis Paris oder Hamburg,
|
||||
# pfaedle würde sonst das Netz aller Länder laden. Ausserhalb zeigt ohnehin der Nachbar-Fahrplan.
|
||||
osm = work / "region.osm.pbf"
|
||||
subprocess.run([C.OSMIUM, "extract", "-b", border.bbox(source), str(ensure_osm()), "-o", str(osm),
|
||||
subprocess.run([C.OSMIUM, "extract", "-b", border.bbox(C.country(source)), str(ensure_osm()), "-o", str(osm),
|
||||
"--overwrite"], check=True)
|
||||
log.info("[%s] pfaedle rechnet Streckenverläufe …", source)
|
||||
t = time.time()
|
||||
@@ -308,10 +335,16 @@ def prepare(source: str, state: dict, zip_path: Path | None = None, force: bool
|
||||
shutil.rmtree(work, ignore_errors=True)
|
||||
work.mkdir()
|
||||
|
||||
version = None
|
||||
cfg, version = C.SOURCES[source], None
|
||||
if zip_path:
|
||||
name = zip_path.name
|
||||
else:
|
||||
if cfg.get("static_needs_key") and not cfg["rt_key"]:
|
||||
log.info("[%s] Kein Key eingetragen – übersprungen", source)
|
||||
return
|
||||
if time.time() - state.get(source + "@checked", 0) < cfg.get("check_hours", 0) * 3600 and not force:
|
||||
return # Anbieter mit knappem Abruf-Kontingent (Schweden) nicht stündlich fragen
|
||||
state[source + "@checked"] = time.time()
|
||||
url = gtfs_url(source)
|
||||
name, version = remote_id(source, url)
|
||||
if f"{name} #{FORMAT}" == state.get(source) and not force:
|
||||
@@ -322,10 +355,12 @@ def prepare(source: str, state: dict, zip_path: Path | None = None, force: bool
|
||||
download(url, zip_path)
|
||||
|
||||
t = time.time()
|
||||
version = version or zip_path.stem # (nach dem Glätten hiesse die Datei sonst "normalized")
|
||||
zip_path = normalize_zip(zip_path, work)
|
||||
meta = filter_rail(zip_path, work / "rail", version)
|
||||
log.info("[%s] Bahn-Filter: %s in %.0f s", source, meta, time.time() - t)
|
||||
if zip_path.parent == work:
|
||||
zip_path.unlink()
|
||||
for p in (work / "gtfs.zip", work / "normalized.zip"):
|
||||
p.unlink(missing_ok=True)
|
||||
|
||||
shaped = run_pfaedle(work, work / "rail", source)
|
||||
meta["shapes"] = shaped is not None
|
||||
@@ -347,6 +382,7 @@ def main() -> None:
|
||||
ap.add_argument("--force", action="store_true")
|
||||
args = ap.parse_args()
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
|
||||
logging.getLogger("httpx").setLevel(logging.WARNING) # würde Adressen samt Key (Schweden) loggen
|
||||
if args.zip and not args.source:
|
||||
ap.error("--zip braucht --source")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user