HTML 格式的 HTML 片段差异
项目描述
HTML-Diff:HTML 格式的 HTML 片段差异
比较两个 HTML 片段字符串并将差异作为有效的 HTML 片段返回,其中包含更改的<del>和<ins>标记。
依赖后端进行 HTML 解析和转储BeautifulSoup4。html.parser
HTML-Diff 专注于提供有效的差异,即:
- 返回的字符串代表有效的 HTML 片段;
- 如果输入片段代表不包含
<del>或<ins>标记的有效 HTML 片段,则可以从 diff 输出中相同地重建它们,方法是删除<ins>标记及其内容并用旧<del>片段的内容替换标记,而新片段则相反。模块中提供了用于这些重建的功能,并检查重建是否与输入匹配。check
用法
基本用法
>>> from html_diff import diff
>>> diff("<em>ABC</em>", "<em>AB</em>C")
'<em><del>ABC</del><ins>AB</ins></em><ins>C</ins>'
添加自定义标签以被视为不可分割的块
示例用例:将 MathJax 元素包裹在其中并<span class="math-tex">\(...\)</span>希望避免在其中使用标签(这将被严重渲染):<del><ins>\(...\)
>>> from html_diff.config import config
>>> config.tags_fcts_as_blocks.append(lambda tag: tag.name == "span" and "math-tex" in tag.attrs.get("class", []))
没有它(不能用 MathJax 正确渲染):
>>> diff(r'<span class="math-tex">\(\vec{v}\)</span>', r'<span class="math-tex">\(\vec{w}\)</span>')
'<span class="math-tex">\\(\\vec{<del>v</del><ins>w</ins>}\\)</span>'
用它:
>>> from html_diff import clear_cache
>>> clear_cache()
>>> diff(r'<span class="math-tex">\(\vec{v}\)</span>', r'<span class="math-tex">\(\vec{w}\)</span>')
'<del><span class="math-tex">\\(\\vec{v}\\)</span></del><ins><span class="math-tex">\\(\\vec{w}\\)</span></ins>'
中的函数config.config.tags_fcts_as_blocks应将 abs4.element.Tag作为输入并返回 a bool; 这些标签针对列表中的所有函数进行了测试,如果有任何调用返回,则将其视为不可分割的块True。
标签得分
HTML 标记具有关联的基本分数,该分数被添加到内容分数中。可以配置此基本分数:
>>> config.EMPTY_ELEMENT_SCORE # default: 2
>>> config.OTHER_ELEMENT_SCORE # default: 2
警告:一些结果被缓存并且更改配置不会使缓存无效,因此之后的结果可能是错误的。调用clear_cache()以重置缓存。
从diff重构新旧_ _
>>> old = "old_string"
>>> new = "new_string"
>>> d = diff(old, new)
>>> from html_diff.check import new_from_diff
>>> new == new_from_diff(d)
True
>>> from html_diff.check import old_from_diff
>>> old == old_from_diff(d)
True
>>> from html_diff.check import is_diff_valid
>>> is_diff_valid(old, new, d)
True
测试
自动化测试
跑
python -m unittest
在项目的根目录。
手动测试
跑
python -m html_diff
并使用浏览器导航到 127.0.0.1:8080。
您可以指定更多选项:
-a或--address:服务器的自定义地址(默认:127.0.0.1)-p或--port:服务器的自定义端口(默认:8080)-b或--blocks:要添加到的函数的定义config.config.tags_fcts_as_blocks,例如:
python -m html_diff -b 'lambda tag: tag.name == "span" and "math-tex" in tag.attrs.get("class", [])'
-cor--cuttable-words-mode: 切字处理方式,详见上文;值之一config.Config.CuttableWordsMode(默认值:CUTTABLE)
算法
新的实现使用了更接近 的算法difflib.SequenceMatcher,尽管讽刺的是它不再使用它了。
该算法与UNCUTTABLE_PRECISE配置的传统实现类似,不同之处在于它在所有级别上使用类似 Ratcliff-Obershelp 的过程(最佳匹配子序列),而不是测试所有组合以找到最佳值。因此速度更快。
匹配
BeautifulSoup4用;解析输入 这会产生两个可迭代的元素,要么bs4.element.NavigableString要么bs4.element.Tag。- 在 HTML 结构的顶层,将
bs4.element.NavigableString' 拆分为单词(使用re'\W模式),然后使用分数找到最佳匹配的子序列:- 相同的词:词的长度,
bs4.element.Tag的name和attrs属性完全匹配的地方:- 如果标签被认为是块(那些
True用函数测试的config.config.tags_fcts_as_blocks):config.EMPTY_ELEMENT_SCORE如果标签是空的,否则config.OTHER_ELEMENT_SCORE加上标签的字符串内容的长度, - 否则,
config.OTHER_ELEMENT_SCORE加上标签内容的分数总和(递归计算)。
- 如果标签被认为是块(那些
- 在最佳匹配的子序列的左右,重复2。如果仍然存在不可匹配的子序列,则将其分配为0,并将其视为完全删除/插入。
倾倒
- 使用这些树匹配结构,可以直接递归地进行转储,通过转储第
node_left一个,然后是匹配的子序列,最后是node_right. 匹配总是准确的,因此可以按原样转储,但首先完全删除然后完全重新插入的不可匹配子序列除外。 - 倾倒在
BeautifulSoup4汤中完成,然后作为字符串输出。
旧版实施
旧版实现在html_diff.legacy.
差异中的单词切割
默认情况下,纯文本部分的 diff'ing 算法不关心单词 - 如果修改了单词部分,则该部分得到<del>'ed 和<ins>'ed,而单词的其余部分保持不变。然而,删除并重新插入完整的单词可能更具可读性。为确保这一点,请切换config.config.cuttable_words_mode到config.Config.CuttableWordsMode.UNCUTTABLE_SIMPLE或config.Config.CuttableWordsMode.UNCUTTABLE_PRECISE:
config.config.cuttable_words_mode == config.Config.CuttableWordsMode.CUTTABLE(默认):
>>> from html_diff.legacy import diff as ldiff
>>> ldiff("OlyExams", "ExamTools")
'<del>Oly</del>Exam<ins>Tool</ins>s'
>>> ldiff("abcdef<br/>ghifjk", "abcdef ghifjk")
'abcdef<ins> ghifjk</ins><del><br/>ghifjk</del>'
config.config.cuttable_words_mode == config.Config.CuttableWordsMode.UNCUTTABLE_SIMPLE(快速并给出可接受的结果):
>>> ldiff("OlyExams", "ExamTools")
'<del>OlyExams</del><ins>ExamTools</ins>'
>>> ldiff("abcdef<br/>ghifjk", "abcdef ghifjk")
'abcdef<ins> ghifjk</ins><del><br/>ghifjk</del>'
config.config.cuttable_words_mode == config.Config.CuttableWordsMode.UNCUTTABLE_PRECISE(相当慢,但使用早期分词以获得更好的匹配,特别是如果输入的纯字符串部分在old和new之间拆分或合并):
>>> ldiff("OlyExams", "ExamTools")
'<del>OlyExams</del><ins>ExamTools</ins>'
>>> ldiff("abcdef<br/>ghifjk", "abcdef ghifjk")
'abcdef<del><br/></del><ins> </ins>ghifjk'
在不可切割的单词模式中,非单词字符对应于re'\W模式。
算法
匹配
BeautifulSoup4用;解析输入 这会产生两个可迭代的元素,要么bs4.element.NavigableString要么bs4.element.Tag。- 将第一个迭代的每个元素与第二个迭代的每个元素进行比较。只有在两种情况下才允许匹配:
- 两个元素都是
bs4.element.NavigableString's(取决于可切割词模式配置,匹配是在原始字符串、单词列表或拆分为子字符串的原始字符串上完成); - 两个元素都是
bs4.element.Tag's 并且它们的name和attrs属性完全匹配。
- 两个元素都是
- 每场比赛都被临时存储,连同一个“分数”:
- 对于
bs4.element.NavigableString匹配项,它们的匹配长度按照difflib.SequenceMatcher; - 对于
bs4.element.Tag被视为块的匹配项(True使用 函数进行测试的匹配项config.config.tags_fcts_as_blocks),不同的标签的匹配长度为0. 比较相等的标签被分配以下匹配长度:空标签的标签本身的长度(例如<br />)(这主要是任意选择),否则标签内容的长度(tag.string); - 对于其他“常规”
bs4.element.Tag匹配,匹配长度是递归使用相同算法的孩子的匹配长度之和。
- 对于
- 得分最高的匹配被保留,并且算法在匹配元素之前和之后的子迭代上递归地重复。因此,每个匹配都(最大)得到 a
match_before和 amatch_after分配。 - 没有匹配的区域被存储为“不匹配”。有了它们,两个可迭代对象都完全被匹配和不匹配所覆盖。
倾倒
- 使用这些树匹配结构,可以直接递归地进行转储,通过转储第
match_before一个,然后是匹配的元素本身,最后是match_after. 匹配bs4.element.NavigableString的被. _difflib.SequenceMatcher如果完全匹配bs4.element.Tag,将被视为块的匹配的被转储而不更改,否则首先完全删除,然后完全重新插入。不匹配的元素被转储为完全删除并完全插入。 - 倾倒在
BeautifulSoup4汤中完成,然后作为字符串输出。
项目详情
下载文件
下载适用于您平台的文件。如果您不确定要选择哪个,请了解有关安装包的更多信息。