Skip to main content

HTML 格式的 HTML 片段差异

项目描述

HTML-Diff:HTML 格式的 HTML 片段差异

比较两个 HTML 片段字符串并将差异作为有效的 HTML 片段返回,其中包含更改的<del><ins>标记。

依赖后端进行 HTML 解析和转储BeautifulSoup4html.parser

HTML-Diff 专注于提供有效的差异,即:

  1. 返回的字符串代表有效的 HTML 片段;
  2. 如果输入片段代表不包含<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 的过程(最佳匹配子序列),而不是测试所有组合以找到最佳值。因此速度更快。

匹配

  1. BeautifulSoup4用;解析输入 这会产生两个可迭代的元素,要么bs4.element.NavigableString要么bs4.element.Tag
  2. 在 HTML 结构的顶层,将bs4.element.NavigableString' 拆分为单词(使用re'\W模式),然后使用分数找到最佳匹配的子序列:
    • 相同的词:词的长度,
    • bs4.element.Tagnameattrs属性完全匹配的地方:
      • 如果标签被认为是块(那些True用函数测试的config.config.tags_fcts_as_blocks):config.EMPTY_ELEMENT_SCORE如果标签是的,否则config.OTHER_ELEMENT_SCORE加上标签的字符串内容的长度,
      • 否则,config.OTHER_ELEMENT_SCORE加上标签内容的分数总和(递归计算)。
  3. 在最佳匹配的子序列的左右,重复2。如果仍然存在不可匹配的子序列,则将其分配为0,并将其视为完全删除/插入。

倾倒

  1. 使用这些树匹配结构,可以直接递归地进行转储,通过转储第node_left一个,然后是匹配的子序列,最后是node_right. 匹配总是准确的,因此可以按原样转储,但首先完全删除然后完全重新插入的不可匹配子序列除外。
  2. 倾倒在BeautifulSoup4汤中完成,然后作为字符串输出。

旧版实施

旧版实现在html_diff.legacy.

差异中的单词切割

默认情况下,纯文本部分的 diff'ing 算法不关心单词 - 如果修改了单词部分,则该部分得到<del>'ed 和<ins>'ed,而单词的其余部分保持不变。然而,删除并重新插入完整的单词可能更具可读性。为确保这一点,请切换config.config.cuttable_words_modeconfig.Config.CuttableWordsMode.UNCUTTABLE_SIMPLEconfig.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(相当慢,但使用早期分词以获得更好的匹配,特别是如果输入的纯字符串部分在oldnew之间拆分或合并):

>>> ldiff("OlyExams", "ExamTools")
'<del>OlyExams</del><ins>ExamTools</ins>'
>>> ldiff("abcdef<br/>ghifjk", "abcdef ghifjk")
'abcdef<del><br/></del><ins> </ins>ghifjk'

在不可切割的单词模式中,非单词字符对应于re'\W模式。

算法

匹配

  1. BeautifulSoup4用;解析输入 这会产生两个可迭代的元素,要么bs4.element.NavigableString要么bs4.element.Tag
  2. 将第一个迭代的每个元素与第二个迭代的每个元素进行比较。只有在两种情况下才允许匹配:
    • 两个元素都是bs4.element.NavigableString's(取决于可切割词模式配置,匹配是在原始字符串、单词列表或拆分​​为子字符串的原始字符串上完成);
    • 两个元素都是bs4.element.Tag's 并且它们的nameattrs属性完全匹配。
  3. 每场比赛都被临时存储,连同一个“分数”:
    • 对于bs4.element.NavigableString匹配项,它们的匹配长度按照difflib.SequenceMatcher;
    • 对于bs4.element.Tag被视为块的匹配项(True使用 函数进行测试的匹配项config.config.tags_fcts_as_blocks),不同的标签的匹配长度为0. 比较相等的标签被分配以下匹配长度:标签的标签本身的长度(例如<br />)(这主要是任意选择),否则标签内容的长度(tag.string);
    • 对于其他“常规”bs4.element.Tag匹配,匹配长度是递归使用相同算法的孩子的匹配长度之和。
  4. 得分最高的匹配被保留,并且算法在匹配元素之前和之后的子迭代上递归地重复。因此,每个匹配都(最大)得到 amatch_before和 amatch_after分配。
  5. 没有匹配的区域被存储为“不匹配”。有了它们,两个可迭代对象都完全被匹配和不匹配所覆盖。

倾倒

  1. 使用这些树匹配结构,可以直接递归地进行转储,通过转储第match_before一个,然后是匹配的元素本身,最后是match_after. 匹配bs4.element.NavigableString的被. _ difflib.SequenceMatcher如果完全匹配bs4.element.Tag,将被视为块的匹配的被转储而不更改,否则首先完全删除,然后完全重新插入。不匹配的元素被转储为完全删除并完全插入。
  2. 倾倒在BeautifulSoup4汤中完成,然后作为字符串输出。

项目详情


下载文件

下载适用于您平台的文件。如果您不确定要选择哪个,请了解有关安装包的更多信息。

源分布

html-diff-0.4.1.tar.gz (24.5 kB 查看哈希)

已上传 source

内置分布

html_diff-0.4.1-py3-none-any.whl (24.2 kB 查看哈希)

已上传 py3