Files
coord-converter/file_io/txt_handler.py
T
mayihua e9f87fc693 feat: CoordConverter 坐标转换套件 v1.0.0
TXT / Excel(CSV) / SHP 三种格式任意互转
支持 WGS84 / CGCS2000 / Xian80 / Beijing54 / Web Mercator 坐标系转换

由 Mapo 🗺️ 自动生成
2026-07-29 12:49:08 +08:00

132 lines
4.6 KiB
Python

"""
TXT file reader/writer.
Auto-detects delimiters (space, tab, comma) and column names.
Handles optional header line and comment lines (#, //).
"""
import csv
import io
from pathlib import Path
from core.data_model import CoordData, CoordPoint
class TxtHandler:
EXTENSIONS = {".txt", ".csv"}
@classmethod
def read(cls, path: str) -> CoordData:
path = Path(path)
raw = path.read_text(encoding="utf-8-sig")
# Strip comment lines
lines = [l for l in raw.splitlines()
if l.strip() and not l.strip().startswith(("#", "//"))]
if not lines:
raise ValueError(f"文件为空或全为注释: {path}")
# Detect delimiter
delim = cls._detect_delimiter(lines)
reader = csv.reader(io.StringIO("\n".join(lines)), delimiter=delim)
all_rows = list(reader)
if not all_rows:
raise ValueError(f"无法解析文件: {path}")
# Check if first row looks like a header (contains non-numeric first field
# or known column names)
first_row = all_rows[0]
has_header = cls._looks_like_header(first_row)
if has_header:
col_names = first_row
data_rows = all_rows[1:]
else:
# No header — auto-generate column names
num_cols = len(first_row)
col_names = [f"COL{i+1}" for i in range(num_cols)]
data_rows = all_rows
# Detect X, Y, Z columns
x_col, y_col, z_col = cls._find_coord_cols(col_names)
records = []
for row in data_rows:
record = {}
for i, name in enumerate(col_names):
val = row[i].strip() if i < len(row) else None
record[name] = val
records.append(record)
return CoordData.from_records(records, x_col=x_col, y_col=y_col, z_col=z_col)
@classmethod
def write(cls, data: CoordData, path: str):
records = data.to_records()
if not records:
raise ValueError("无数据可写入")
delim = cls._detect_delimiter_from_ext(path)
fieldnames = list(records[0].keys())
with open(path, "w", newline="", encoding="utf-8-sig") as f:
writer = csv.DictWriter(f, fieldnames=fieldnames, delimiter=delim)
writer.writeheader()
writer.writerows(records)
@staticmethod
def _looks_like_header(row: list[str]) -> bool:
"""Heuristic: header if first few entries are non-numeric strings."""
if not row:
return False
# If any column name matches known coord patterns, it's a header
lower = {c.lower().strip() for c in row[:5]}
coord_keywords = {"x", "y", "lon", "lat", "经度", "纬度", "点号", "id"}
if lower & coord_keywords:
return True
# If the first entry is not a valid number, it's probably a header
try:
float(row[0].strip())
return False
except (ValueError, AttributeError):
return True
@staticmethod
def _detect_delimiter(lines: list[str]) -> str:
"""Auto-detect: tab > comma > space."""
sample = "\n".join(lines[:20])
tab_count = sample.count("\t")
comma_count = sample.count(",")
# Check if commas are between spaces (likely CSV) vs within text
if comma_count > 3 or comma_count > tab_count:
return ","
if tab_count > 0:
return "\t"
return " " # default: space-separated
@staticmethod
def _detect_delimiter_from_ext(path: str) -> str:
return "," if path.lower().endswith(".csv") else "\t"
@staticmethod
def _find_coord_cols(cols: list[str]) -> tuple[str, str, str | None]:
"""Find X, Y, Z columns."""
x_patterns = ["x", "lon", "经度", "lng", "easting"]
y_patterns = ["y", "lat", "纬度", "northing"]
z_patterns = ["z", "h", "高程", "高度", "alt", "elevation"]
x_col = y_col = z_col = None
for c in cols:
lc = c.lower().replace(" ", "").replace("_", "").replace("-", "")
if any(p == lc or lc.startswith(p) or lc.endswith(p) for p in x_patterns):
x_col = c
elif any(p == lc or lc.startswith(p) or lc.endswith(p) for p in y_patterns):
y_col = c
elif any(p == lc or lc.startswith(p) or lc.endswith(p) for p in z_patterns):
z_col = c
# Fallback: first two numeric-ish columns if no match
if not x_col and len(cols) >= 2:
x_col, y_col = cols[0], cols[1]
return x_col, y_col, z_col