Files
my-v0-server/app/datafiles/parse.py
T

137 lines
4.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""解析入口:识别格式 → 读取 → 拆表 → 推断类型 → 每张表写一个 Parquet → 列概况
只读写本地文件,不碰数据库;上传流程在子进程里调用(worker.py),超时直接终止子进程
"""
from pathlib import Path
from typing import Any
import pyarrow.parquet as pq
from pydantic import BaseModel
from app.datafiles.base import DataFileError
from app.datafiles.columns import (
TypedColumns,
arrow_columns,
build_columns,
clean_names,
)
from app.datafiles.detect import detect
from app.datafiles.profile import profile_table
from app.datafiles.readers import (
Grid,
Records,
read_delimited,
read_excel,
read_json,
read_jsonl,
read_parquet,
)
from app.datafiles.tables import split_tables
from app.schemas.data_file import DataColumn, DataFormat, ParseWarning, TableProfile
# 子进程把解析结果转成 JSON 交回上传流程,所以用 pydantic 模型
class ParsedTable(BaseModel):
sheet: str | None
# 同一 sheet 切出多张表时的序号,从 1 开始
part: int | None
rows: int
columns: list[DataColumn]
# out_dir 下的 Parquet 文件名
parquet: str
profile: TableProfile
preview: list[list[Any]]
warnings: list[ParseWarning]
class ParsedFile(BaseModel):
format: DataFormat
encoding: str | None
tables: list[ParsedTable]
# 不属于某张表的警告,例如跳过的 sheet
warnings: list[ParseWarning]
def parse_file(path: Path, out_dir: Path) -> ParsedFile:
"""解析 path,每张表在 out_dir 下写一个 Parquet:1.parquet、2.parquet……"""
detected = detect(path)
out_dir.mkdir(parents=True, exist_ok=True)
warnings: list[ParseWarning] = []
tables: list[ParsedTable] = []
def add_table(
sheet: str | None,
part: int | None,
typed: TypedColumns,
table_warnings: list[ParseWarning],
) -> None:
target = out_dir / f"{len(tables) + 1}.parquet"
pq.write_table(typed.table, target)
profile, preview = profile_table(target, typed.columns, typed.notes)
tables.append(
ParsedTable(
sheet=sheet,
part=part,
rows=typed.table.num_rows,
columns=typed.columns,
parquet=target.name,
profile=profile,
preview=preview,
warnings=table_warnings,
)
)
def add_grid(grid: Grid) -> None:
found, dropped = split_tables(grid)
warnings.extend(dropped)
for table in found:
names, renamed = clean_names(table.header, table.sheet)
typed, kept = build_columns(names, list(zip(*table.rows)), table.sheet)
add_table(
table.sheet,
table.part,
typed,
[*table.warnings, *_present(renamed, kept)],
)
def add_records(records: Records) -> None:
warnings.extend(records.warnings)
names, renamed = clean_names(records.names, None)
typed, kept = build_columns(names, records.columns, None)
add_table(None, None, typed, _present(renamed, kept))
match detected.format:
case "csv" | "tsv":
add_grid(read_delimited(detected.text, detected.delimiter))
case "xlsx" | "xls" | "xlsb":
grids, sheet_warnings = read_excel(path)
warnings.extend(sheet_warnings)
for grid in grids:
add_grid(grid)
case "json":
result = read_json(detected.value)
if isinstance(result, Grid):
add_grid(result)
else:
add_records(result)
case "jsonl":
add_records(read_jsonl(detected.text))
case "parquet":
typed, renamed = arrow_columns(read_parquet(path))
if typed.table.num_rows:
add_table(None, None, typed, _present(renamed))
if not tables:
raise DataFileError(422, "文件里没有找到数据")
return ParsedFile(
format=detected.format,
encoding=detected.encoding,
tables=tables,
warnings=warnings,
)
def _present(*warnings: ParseWarning | None) -> list[ParseWarning]:
return [warning for warning in warnings if warning is not None]