您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
 
 
 
 
 
 

228 行
7.5 KiB

  1. #!/usr/bin/env python3
  2. """解析正点原子逻辑分析仪导出的 CSV 文件。
  3. 文件开头以分号开头的说明行会被忽略。输出文件默认不写表头,
  4. 每行格式为:时间,电平
  5. 示例:
  6. python parse_logic_csv.py input.csv output.csv --channel CH1 --mode changes
  7. python parse_logic_csv.py input.csv output.csv --channel 2 --mode all
  8. """
  9. from __future__ import annotations
  10. import argparse
  11. import csv
  12. import io
  13. import sys
  14. from pathlib import Path
  15. from typing import Iterable, Sequence
  16. TIME_NAMES = {"time", "timestamp", "时间", "时刻", "t"}
  17. LEVEL_NAMES = {
  18. "0": "0",
  19. "1": "1",
  20. "low": "0",
  21. "l": "0",
  22. "false": "0",
  23. "off": "0",
  24. "低": "0",
  25. "低电平": "0",
  26. "high": "1",
  27. "h": "1",
  28. "true": "1",
  29. "on": "1",
  30. "高": "1",
  31. "高电平": "1",
  32. }
  33. class CsvParseError(ValueError):
  34. """CSV 格式或参数不符合预期。"""
  35. def read_text(path: Path, encoding: str) -> str:
  36. """读取文本,utf-8 失败时自动尝试常见的 GBK 编码。"""
  37. if encoding != "auto":
  38. return path.read_text(encoding=encoding)
  39. data = path.read_bytes()
  40. for candidate in ("utf-8-sig", "gbk", "utf-16"):
  41. try:
  42. return data.decode(candidate)
  43. except UnicodeDecodeError:
  44. continue
  45. raise CsvParseError(f"无法识别文件编码:{path}")
  46. def remove_comment_lines(text: str) -> str:
  47. """删除空行和以分号开头的逻辑分析仪说明行。"""
  48. lines = []
  49. for line in text.splitlines():
  50. if not line.strip() or line.lstrip().startswith(";"):
  51. continue
  52. lines.append(line)
  53. return "\n".join(lines)
  54. def detect_delimiter(text: str) -> str:
  55. """识别逗号、制表符或分号分隔格式。"""
  56. sample_lines = text.splitlines()[:20]
  57. sample = "\n".join(sample_lines)
  58. try:
  59. dialect = csv.Sniffer().sniff(sample, delimiters=",\t;")
  60. return dialect.delimiter
  61. except csv.Error:
  62. counts = {delimiter: sample.count(delimiter) for delimiter in (",", "\t", ";")}
  63. delimiter = max(counts, key=counts.get)
  64. if counts[delimiter] == 0:
  65. raise CsvParseError("无法识别 CSV 分隔符,请使用逗号、制表符或分号")
  66. return delimiter
  67. def parse_level(value: str) -> str:
  68. """把常见的高低电平写法统一为 0 或 1。"""
  69. text = value.strip().lower()
  70. if text in LEVEL_NAMES:
  71. return LEVEL_NAMES[text]
  72. try:
  73. number = float(text)
  74. except ValueError as exc:
  75. raise CsvParseError(f"无法识别电平值:{value!r}") from exc
  76. if number == 0:
  77. return "0"
  78. if number == 1:
  79. return "1"
  80. raise CsvParseError(f"电平必须为0或1,实际得到:{value!r}")
  81. def find_column(header: Sequence[str], name: str, description: str) -> int:
  82. """按列名或列序号查找列,列序号从0开始。"""
  83. name = name.strip()
  84. if name.isdigit():
  85. index = int(name)
  86. if 0 <= index < len(header):
  87. return index
  88. raise CsvParseError(f"{description}列序号超出范围:{index}")
  89. lowered = [item.strip().lower() for item in header]
  90. target = name.lower()
  91. if target in lowered:
  92. return lowered.index(target)
  93. choices = ", ".join(header)
  94. raise CsvParseError(f"找不到{description}列 {name!r},可用列:{choices}")
  95. def find_time_column(header: Sequence[str]) -> int:
  96. """优先查找常见时间列,找不到时默认使用第0列。"""
  97. for index, name in enumerate(header):
  98. if name.strip().lower() in TIME_NAMES:
  99. return index
  100. return 0
  101. def parse_rows(text: str, channel: str, time_column: str | None) -> list[tuple[str, str]]:
  102. """读取 CSV 并返回有效的(时间,电平)采样点。"""
  103. clean_text = remove_comment_lines(text)
  104. if not clean_text:
  105. raise CsvParseError("文件中没有有效数据")
  106. delimiter = detect_delimiter(clean_text)
  107. rows = list(csv.reader(io.StringIO(clean_text), delimiter=delimiter))
  108. if not rows or len(rows[0]) < 2:
  109. raise CsvParseError("CSV 至少需要时间列和一个通道列")
  110. header = [item.strip() for item in rows[0]]
  111. channel_column = find_column(header, channel, "通道")
  112. time_index = (
  113. find_column(header, time_column, "时间")
  114. if time_column is not None
  115. else find_time_column(header)
  116. )
  117. points: list[tuple[str, str]] = []
  118. for line_number, row in enumerate(rows[1:], start=2):
  119. if len(row) <= max(channel_column, time_index):
  120. raise CsvParseError(f"第{line_number}行列数不足")
  121. time_value = row[time_index].strip()
  122. if not time_value:
  123. raise CsvParseError(f"第{line_number}行时间为空")
  124. points.append((time_value, parse_level(row[channel_column])))
  125. if not points:
  126. raise CsvParseError("CSV 表头后没有采样数据")
  127. return points
  128. def compress_level_changes(points: Iterable[tuple[str, str]]) -> list[tuple[str, str]]:
  129. """只保留首个采样点和电平发生变化的采样点。"""
  130. output: list[tuple[str, str]] = []
  131. previous_level: str | None = None
  132. for point in points:
  133. if previous_level is None or point[1] != previous_level:
  134. output.append(point)
  135. previous_level = point[1]
  136. return output
  137. def write_points(path: Path, points: Iterable[tuple[str, str]]) -> int:
  138. """写出时间、电平两列,不写表头。"""
  139. count = 0
  140. with path.open("w", encoding="utf-8", newline="") as file:
  141. writer = csv.writer(file, lineterminator="\n")
  142. for time_value, level in points:
  143. writer.writerow((time_value, level))
  144. count += 1
  145. return count
  146. def build_argument_parser() -> argparse.ArgumentParser:
  147. """创建命令行参数。"""
  148. parser = argparse.ArgumentParser(
  149. description="解析正点原子逻辑分析仪 CSV,输出时间和电平两列"
  150. )
  151. parser.add_argument("input", type=Path, help="逻辑分析仪导出的 CSV 文件")
  152. parser.add_argument("output", type=Path, help="输出 CSV 文件")
  153. parser.add_argument(
  154. "--channel",
  155. required=True,
  156. help="通道名称,例如 CH1、D0;也可以填写列序号,序号从0开始",
  157. )
  158. parser.add_argument(
  159. "--mode",
  160. choices=("all", "changes"),
  161. default="changes",
  162. help="all输出全部采样点,changes只输出电平跳变坐标点(默认)",
  163. )
  164. parser.add_argument(
  165. "--time-column",
  166. help="时间列名称或列序号,默认自动识别,找不到时使用第0列",
  167. )
  168. parser.add_argument(
  169. "--encoding",
  170. default="auto",
  171. choices=("auto", "utf-8-sig", "gbk", "utf-16"),
  172. help="输入文件编码,默认自动识别",
  173. )
  174. return parser
  175. def main(argv: Sequence[str] | None = None) -> int:
  176. """命令行入口。"""
  177. parser = build_argument_parser()
  178. args = parser.parse_args(argv)
  179. try:
  180. points = parse_rows(read_text(args.input, args.encoding), args.channel, args.time_column)
  181. output_points = points if args.mode == "all" else compress_level_changes(points)
  182. output_count = write_points(args.output, output_points)
  183. except (OSError, CsvParseError) as exc:
  184. parser.error(str(exc))
  185. return 2
  186. compression = 0.0 if not points else (1.0 - output_count / len(points)) * 100.0
  187. print(f"输入采样点数:{len(points)}")
  188. print(f"输出点数:{output_count}")
  189. print(f"点数压缩率:{compression:.2f}%")
  190. print(f"输出文件:{args.output}")
  191. return 0
  192. if __name__ == "__main__":
  193. sys.exit(main())