|
- #!/usr/bin/env python3
- """解析正点原子逻辑分析仪导出的 CSV 文件。
-
- 文件开头以分号开头的说明行会被忽略。输出文件默认不写表头,
- 每行格式为:时间,电平
-
- 示例:
- python parse_logic_csv.py input.csv output.csv --channel CH1 --mode changes
- python parse_logic_csv.py input.csv output.csv --channel 2 --mode all
- """
-
- from __future__ import annotations
-
- import argparse
- import csv
- import io
- import sys
- from pathlib import Path
- from typing import Iterable, Sequence
-
-
- TIME_NAMES = {"time", "timestamp", "时间", "时刻", "t"}
- LEVEL_NAMES = {
- "0": "0",
- "1": "1",
- "low": "0",
- "l": "0",
- "false": "0",
- "off": "0",
- "低": "0",
- "低电平": "0",
- "high": "1",
- "h": "1",
- "true": "1",
- "on": "1",
- "高": "1",
- "高电平": "1",
- }
-
-
- class CsvParseError(ValueError):
- """CSV 格式或参数不符合预期。"""
-
-
- def read_text(path: Path, encoding: str) -> str:
- """读取文本,utf-8 失败时自动尝试常见的 GBK 编码。"""
- if encoding != "auto":
- return path.read_text(encoding=encoding)
- data = path.read_bytes()
- for candidate in ("utf-8-sig", "gbk", "utf-16"):
- try:
- return data.decode(candidate)
- except UnicodeDecodeError:
- continue
- raise CsvParseError(f"无法识别文件编码:{path}")
-
-
- def remove_comment_lines(text: str) -> str:
- """删除空行和以分号开头的逻辑分析仪说明行。"""
- lines = []
- for line in text.splitlines():
- if not line.strip() or line.lstrip().startswith(";"):
- continue
- lines.append(line)
- return "\n".join(lines)
-
-
- def detect_delimiter(text: str) -> str:
- """识别逗号、制表符或分号分隔格式。"""
- sample_lines = text.splitlines()[:20]
- sample = "\n".join(sample_lines)
- try:
- dialect = csv.Sniffer().sniff(sample, delimiters=",\t;")
- return dialect.delimiter
- except csv.Error:
- counts = {delimiter: sample.count(delimiter) for delimiter in (",", "\t", ";")}
- delimiter = max(counts, key=counts.get)
- if counts[delimiter] == 0:
- raise CsvParseError("无法识别 CSV 分隔符,请使用逗号、制表符或分号")
- return delimiter
-
-
- def parse_level(value: str) -> str:
- """把常见的高低电平写法统一为 0 或 1。"""
- text = value.strip().lower()
- if text in LEVEL_NAMES:
- return LEVEL_NAMES[text]
- try:
- number = float(text)
- except ValueError as exc:
- raise CsvParseError(f"无法识别电平值:{value!r}") from exc
- if number == 0:
- return "0"
- if number == 1:
- return "1"
- raise CsvParseError(f"电平必须为0或1,实际得到:{value!r}")
-
-
- def find_column(header: Sequence[str], name: str, description: str) -> int:
- """按列名或列序号查找列,列序号从0开始。"""
- name = name.strip()
- if name.isdigit():
- index = int(name)
- if 0 <= index < len(header):
- return index
- raise CsvParseError(f"{description}列序号超出范围:{index}")
- lowered = [item.strip().lower() for item in header]
- target = name.lower()
- if target in lowered:
- return lowered.index(target)
- choices = ", ".join(header)
- raise CsvParseError(f"找不到{description}列 {name!r},可用列:{choices}")
-
-
- def find_time_column(header: Sequence[str]) -> int:
- """优先查找常见时间列,找不到时默认使用第0列。"""
- for index, name in enumerate(header):
- if name.strip().lower() in TIME_NAMES:
- return index
- return 0
-
-
- def parse_rows(text: str, channel: str, time_column: str | None) -> list[tuple[str, str]]:
- """读取 CSV 并返回有效的(时间,电平)采样点。"""
- clean_text = remove_comment_lines(text)
- if not clean_text:
- raise CsvParseError("文件中没有有效数据")
- delimiter = detect_delimiter(clean_text)
- rows = list(csv.reader(io.StringIO(clean_text), delimiter=delimiter))
- if not rows or len(rows[0]) < 2:
- raise CsvParseError("CSV 至少需要时间列和一个通道列")
-
- header = [item.strip() for item in rows[0]]
- channel_column = find_column(header, channel, "通道")
- time_index = (
- find_column(header, time_column, "时间")
- if time_column is not None
- else find_time_column(header)
- )
- points: list[tuple[str, str]] = []
- for line_number, row in enumerate(rows[1:], start=2):
- if len(row) <= max(channel_column, time_index):
- raise CsvParseError(f"第{line_number}行列数不足")
- time_value = row[time_index].strip()
- if not time_value:
- raise CsvParseError(f"第{line_number}行时间为空")
- points.append((time_value, parse_level(row[channel_column])))
- if not points:
- raise CsvParseError("CSV 表头后没有采样数据")
- return points
-
-
- def compress_level_changes(points: Iterable[tuple[str, str]]) -> list[tuple[str, str]]:
- """只保留首个采样点和电平发生变化的采样点。"""
- output: list[tuple[str, str]] = []
- previous_level: str | None = None
- for point in points:
- if previous_level is None or point[1] != previous_level:
- output.append(point)
- previous_level = point[1]
- return output
-
-
- def write_points(path: Path, points: Iterable[tuple[str, str]]) -> int:
- """写出时间、电平两列,不写表头。"""
- count = 0
- with path.open("w", encoding="utf-8", newline="") as file:
- writer = csv.writer(file, lineterminator="\n")
- for time_value, level in points:
- writer.writerow((time_value, level))
- count += 1
- return count
-
-
- def build_argument_parser() -> argparse.ArgumentParser:
- """创建命令行参数。"""
- parser = argparse.ArgumentParser(
- description="解析正点原子逻辑分析仪 CSV,输出时间和电平两列"
- )
- parser.add_argument("input", type=Path, help="逻辑分析仪导出的 CSV 文件")
- parser.add_argument("output", type=Path, help="输出 CSV 文件")
- parser.add_argument(
- "--channel",
- required=True,
- help="通道名称,例如 CH1、D0;也可以填写列序号,序号从0开始",
- )
- parser.add_argument(
- "--mode",
- choices=("all", "changes"),
- default="changes",
- help="all输出全部采样点,changes只输出电平跳变坐标点(默认)",
- )
- parser.add_argument(
- "--time-column",
- help="时间列名称或列序号,默认自动识别,找不到时使用第0列",
- )
- parser.add_argument(
- "--encoding",
- default="auto",
- choices=("auto", "utf-8-sig", "gbk", "utf-16"),
- help="输入文件编码,默认自动识别",
- )
- return parser
-
-
- def main(argv: Sequence[str] | None = None) -> int:
- """命令行入口。"""
- parser = build_argument_parser()
- args = parser.parse_args(argv)
- try:
- points = parse_rows(read_text(args.input, args.encoding), args.channel, args.time_column)
- output_points = points if args.mode == "all" else compress_level_changes(points)
- output_count = write_points(args.output, output_points)
- except (OSError, CsvParseError) as exc:
- parser.error(str(exc))
- return 2
-
- compression = 0.0 if not points else (1.0 - output_count / len(points)) * 100.0
- print(f"输入采样点数:{len(points)}")
- print(f"输出点数:{output_count}")
- print(f"点数压缩率:{compression:.2f}%")
- print(f"输出文件:{args.output}")
- return 0
-
-
- if __name__ == "__main__":
- sys.exit(main())
|