Trailing whitespace strip in <pre> is quadratic on a long whitespace run
評価
調査の方向性
処理が遅いのは markdownify/init.py の 78 行目付近にある strip_pre 内の re_pre_rstrip.sub で、issue では str.rstrip の呼び出しに置き換えることが提案されています。re_pre_lstrip と re_pre_rstrip の定義を読んでから、末尾の空白を含む 64 KB の <pre> ブロックを markdownify() に渡して再現してください。このケースが 1 秒よりずっと短い時間で完了し、既存の 83 件のテストスイートが引き続き通り、issue に挙げられた <pre> の表記を新しいタイミングテストで網羅できれば完了です。
索引モデルが issue の本文から書いたものです。
説明
strip_pre() takes quadratic time on a <pre> block whose trailing whitespace run is a run of spaces not ended by a newline. Measured through markdownify() on tag 1.2.3, both trees in one isolated run:
| HTML | before | after |
|---|---|---|
| 16,000 B | 0.6881 s | 0.0003 s |
| 32,000 B | 2.6570 s | 0.0004 s |
| 65,536 B | 9.1117 s | 0.0007 s |
| 131,072 B | 35.5100 s | 0.0014 s |
| 16,000 B of ordinary words | 0.0011 s | 0.0009 s |
Each doubling of the block quadruples the time, so the cost belongs to the shape rather than the size - the last row is a document of the same size that converts in a millisecond. The input is an HTML document, so nothing bounds it, and this is the default path: strip_pre defaults to STRIP, while STRIP_ONE uses the separate re_pre_rstrip1 = r'\n *$' and is unaffected.
Cause. re_pre_rstrip = re.compile(r'[ \n]*$') applied with .sub() at __init__.py:78. .sub restarts at every position in the string, and at each one the unbounded [ \n]* run expands and then backtracks looking for the $ that the following character denies.
It is worth saying why the costly shape is spaces and not newlines, because it is not obvious from the pattern alone: re_pre_lstrip = r'^[ \n]*\n' runs on the line before, so a leading newline run is consumed there and re_pre_rstrip is handed a one-character string. With no newline in the block, lstrip does not fire and rstrip receives the whole thing. On the same 65,536-byte input a newline run costs 0.0001 s and a space run 9.3704 s.
Fix. The docstring already describes a strip, so use one:
- text = re_pre_rstrip.sub('', text)
+ text = text.rstrip(' \n')
Verification.
- Test suite on both trees at tag
1.2.3: 83 passed, unchanged, with each run asserting whichmarkdownify/__init__.pywas loaded and whether it still contains the regex call. - Differential: 0 differences from current behaviour across all 29,524 strings of length 0 to 9 over
{space, newline, x}- the whole alphabet the two patterns act on. - A new test converts a 64 KB
<pre>block in under 0.5 s and checks seven<pre>spellings (blank lines, leading and trailing spaces, indentation, empty, whitespace-only). On1.2.3the timing test fails and the behaviour test passes. - Shapes aimed at the fix rather than the original (pure space run with no terminator, a leading
xthen a run, many short runs split byx) are all flat: worst 0.0003 s at 65,536 B.
re_pre_rstrip is now unused. I have deliberately left the definition in place to keep the diff to one line, since it is a module-level name that something downstream may import; say the word and I will remove it.
I am happy to open a pull request with the patch if that is easier.
Found with AI assistance (Claude) and verified by hand.
Patch: full patch
--- a/markdownify/__init__.py
+++ b/markdownify/__init__.py
@@ -75,7 +75,7 @@
def strip_pre(text):
"""Strip all leading and trailing newlines from a <pre> string."""
text = re_pre_lstrip.sub('', text)
- text = re_pre_rstrip.sub('', text)
+ text = text.rstrip(' \n')
return text
- 主要言語
- Python
- スター
- 2.3k
- フォーク
- 210
- PR マージ指標
- 30日以内にマージされた PR はありません
環境構築
このプロジェクトには開発コンテナ、Dockerfile、コントリビューションガイドがありません。まず README を読み、一般的な手順ははじめてのコントリビューションガイドを参照してください。
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
matthewwithanm/python-markdownify のほかの issue
-
RecursionError on deeply nested HTML (about 330 levels), separate from the cyclic-tree case in #256対応中かも @HardMax71 が 42 日前に担当しました。 オープン
難易度 3/5 1〜2日 初心者へのやさしさ 68/100
-
Image/link attributes containing `]`, `)`, or spaces produce broken Markdown output対応中かも @assinscreedFC が 132 日前に担当しました。 オープン
難易度 4/5 3〜5日 初心者へのやさしさ 52/100
matthewwithanm/python-markdownify#261 · コメント 1 件 · リアクション 1 件 ·
-
難易度 3/5 1〜2日 初心者へのやさしさ 65/100
matthewwithanm/python-markdownify#259 · リアクション 2 件 ·
-
Recursion Error: Process_element / process_tag mutual recursion (infinite loop)対応中かも @chiliec が 49 日前に担当しました。 オープン
難易度 3/5 1〜2日 初心者へのやさしさ 50/100
-
Behavior with strip_pre=mdfy.STRIP_ONE seems incorrect with trailing newlines within the pre対応中かも @oiahoon が 95 日前に担当しました。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 50/100
matthewwithanm/python-markdownify の issue をすべて見る
似ている issue
-
bug
難易度 2/5 1〜3時間 初心者へのやさしさ 68/100
jbacalso24/mimry#291 ·
メンテナーはふだん 1 日以内に返信
-
good first issue
難易度 2/5 1時間未満 初心者へのやさしさ 82/100
eli-alvarado/us-watchlist-alerts#14 ·
メンテナーはふだん 1 日以内に返信
-
from-review-extraction python severity:nit
難易度 2/5 1〜3時間 初心者へのやさしさ 80/100
LearningCircuit/local-deep-research#7462 ·
メンテナーはふだん 2 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 82/100
cathrynlavery/diagram-design#441 ·
メンテナーはふだん 2 日以内に返信
-
[Cleanup] Include the public header in work_split.cpp対応中かも @blozano-tt が今日担当しました。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 82/100
tenstorrent/tt-metal#60850 ·
メンテナーはふだん 1 日以内に返信