From 9c6958e7cea167b04b4a1d342516a6c607c9b4bd Mon Sep 17 00:00:00 2001 From: ROTl24 <119035617+ROTl24@users.noreply.github.com> Date: Fri, 11 Sep 2026 02:47:20 +0800 Subject: [PATCH] =?UTF-8?q?perf(csv):=20=E6=89=B9=E9=87=8F=E7=A7=BB?= =?UTF-8?q?=E9=99=A4=E7=A9=BA=E8=A1=8C=E4=BB=A5=E6=B6=88=E9=99=A4=E8=A1=A8?= =?UTF-8?q?=E6=A0=BC=E8=BD=AC=E6=8D=A2=E7=9A=84=E5=B9=B3=E6=96=B9=E7=BA=A7?= =?UTF-8?q?=E5=BC=80=E9=94=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CSV 清理函数逐次 pop(0) 或 pop(1),每次都移动剩余元素,导致大量前导空行或表头后空行的转换耗时呈平方级增长。 改为先定位表头、数据起点和末尾,再逆序删除空行区间;保留内部空行、表头及行对象,不改变列宽或转义逻辑。新增四个十万空行场景,覆盖前导、表头后、尾部与全空输入,同时检查内部空行和宽行保留行为。 验证:37 项 CSV 测试通过;511 种空行排列与基线函数输出和对象身份一致;pre-commit 全文件检查及 diff 空白检查通过。Windows / Python 3.13.15 的合成输入测量中,十万前导空行的真实转换入口三次中位数从 0.6953 秒降至 0.0084 秒。 未运行整个项目的可选格式、远程服务或付费模型测试。性能测量针对合成空行输入,不代表普通文档的整体提速。 --- .../markitdown/converters/_csv_converter.py | 32 +++++++++++------ .../markitdown/tests/test_csv_blank_runs.py | 35 +++++++++++++++++++ 2 files changed, 56 insertions(+), 11 deletions(-) create mode 100644 packages/markitdown/tests/test_csv_blank_runs.py diff --git a/packages/markitdown/src/markitdown/converters/_csv_converter.py b/packages/markitdown/src/markitdown/converters/_csv_converter.py index 82fd29747..1aee0ffb8 100644 --- a/packages/markitdown/src/markitdown/converters/_csv_converter.py +++ b/packages/markitdown/src/markitdown/converters/_csv_converter.py @@ -30,17 +30,27 @@ def _escape_table_cell(value: str) -> str: def _trim_outer_blank_rows(rows: list[list[str]]) -> None: """Remove empty rows from the beginning and end, and immediately after the header. This operation is performed in-place.""" - # Pop empty rows from the beginning - while len(rows) > 0 and not rows[0]: - rows.pop(0) - - # Pop empty rows after the header - while len(rows) > 1 and not rows[1]: - rows.pop(1) - - # Pop empty rows from the end - while len(rows) > 0 and not rows[-1]: - rows.pop(-1) + start = 0 + while start < len(rows) and not rows[start]: + start += 1 + + if start == len(rows): + rows.clear() + return + + header_index = start + start += 1 + while start < len(rows) and not rows[start]: + start += 1 + + end = len(rows) + while end > start and not rows[end - 1]: + end -= 1 + + # Remove each blank run at once, rather than shifting the list per row. + del rows[end:] + del rows[header_index + 1 : start] + del rows[:header_index] class CsvConverter(DocumentConverter): diff --git a/packages/markitdown/tests/test_csv_blank_runs.py b/packages/markitdown/tests/test_csv_blank_runs.py new file mode 100644 index 000000000..357af591e --- /dev/null +++ b/packages/markitdown/tests/test_csv_blank_runs.py @@ -0,0 +1,35 @@ +"""CSV conversion preserves table contents when trimming long blank runs.""" + +import io + +import pytest + +from markitdown import MarkItDown, StreamInfo + + +@pytest.mark.parametrize("position", ["leading", "after_header", "trailing", "all"]) +def test_csv_long_blank_runs(position: str) -> None: + blank = b"\n" * 100_000 + header = b"name,value\n" + # An internal blank row and a wider data row must survive trimming. + data = b"Alice,1\n\nBob,2,extra\n" + content = { + "leading": blank + header + data, + "after_header": header + blank + data, + "trailing": header + data + blank, + "all": blank, + }[position] + + result = MarkItDown(enable_plugins=False).convert_stream( + io.BytesIO(content), + stream_info=StreamInfo(extension=".csv", charset="utf-8"), + ) + + expected = ( + "| name | value | |\n" + "| --- | --- | --- |\n" + "| Alice | 1 | |\n" + "| | | |\n" + "| Bob | 2 | extra |" + ) + assert result.markdown == ("" if position == "all" else expected)