HTML格式化的HTML片段比较
项目描述
HTML-Diff: HTML格式化的HTML片段比较
比较两个HTML片段字符串,并返回一个有效的HTML片段,其中的更改用<del>
和<ins>
标签包装。
依赖于BeautifulSoup4
和html.parser
后端进行HTML解析和输出。
HTML-Diff专注于提供有效的差异,即
- 返回的字符串代表有效的HTML片段;
- 假设输入的片段代表包含没有
<del>
或<ins>
标签的有效HTML片段,它们可以从差异输出中完全重建,通过移除<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
中的函数应接受一个bs4.element.Tag
作为输入并返回一个bool
;这些标签将与列表中的所有函数进行测试,如果任何调用返回True
,则被视为不可分割的块。
标签的分数
HTML标签有一个关联的基本分数,该分数将添加到其内容分数中。这个基本分数可以进行配置
>>> config.EMPTY_ELEMENT_SCORE # default: 2
>>> config.OTHER_ELEMENT_SCORE # default: 2
警告:一些结果被缓存,更改配置不会使缓存失效,因此结果可能在此之后是错误的。调用clear_cache()
以重置缓存。
从差异重建旧和新
>>> 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", [])'
-c
或--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
属性完全匹配- 如果标签被视为块(那些与
config.config.tags_fcts_as_blocks
中的函数测试为True
的标签):如果标签是空,则使用config.EMPTY_ELEMENT_SCORE
,否则使用config.OTHER_ELEMENT_SCORE
加上标签的字符串内容长度, - 否则,使用
config.OTHER_ELEMENT_SCORE
加上标签内容得分的总和(递归计算)。
- 如果标签被视为块(那些与
- 在最佳匹配子序列的左侧和右侧重复2。如果还有不可匹配的子序列,则将它们分配一个得分为0,并将它们视为完全删除/插入。
导出
- 有了这些匹配树结构,可以直接递归地导出,首先导出
node_left
,然后是匹配的子序列,最后是node_right
。匹配总是精确的,因此可以按原样导出,除非是不可匹配的子序列,这些子序列首先被完全删除,然后完全重新插入。 - 在
BeautifulSoup4
汤中导出,然后作为字符串输出。
旧实现
旧实现可在html_diff.legacy
中找到。
差异中的词分割
默认情况下,用于纯文本部分的差异算法不关心单词 - 如果单词部分被修改,则该部分被<del>
和<ins>
删除,而单词的其余部分保持不变。但是,将整个单词删除和重新插入可能更容易阅读。为了确保这一点,将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
(较慢,但使用早期单词分割以提高匹配度,尤其是如果输入的普通字符串部分在旧版和新版本之间被分割或合并时)
>>> 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
(根据可分割单词模式配置,匹配是在原始字符串、单词列表或原始字符串分割的子字符串上进行的); - 两个元素都是
bs4.element.Tag
,并且它们的name
和attrs
属性完全匹配。
- 两个元素都是
- 每个匹配项都临时存储,并附带一个“分数”
- 对于
bs4.element.NavigableString
匹配,按照difflib.SequenceMatcher
的匹配长度; - 对于作为块处理的
bs4.element.Tag
匹配(那些与config.config.tags_fcts_as_blocks
函数测试结果为True
的),不同的标签具有匹配长度为0
。相等的标签被分配以下匹配长度:空标签的长度(例如<br />
)(这是一个主要任意的选择),否则是标签内容的长度(tag.string
); - 对于其他“传统”的
bs4.element.Tag
匹配,匹配长度是使用相同算法递归计算其子元素的匹配长度之和。
- 对于
- 保留最高分的匹配项,并在匹配元素之前和之后的子可迭代对象上递归重复该算法。因此,每个匹配项(最大)被分配一个
match_before
和一个match_after
。 - 没有匹配的区域的存储作为“无匹配”。通过它们,两个可迭代对象都由匹配和无匹配完全覆盖。
导出
- 使用这些树匹配结构,可以通过递归直接转储,首先转储
match_before
,然后是匹配的元素本身,最后是match_after
。匹配的bs4.element.NavigableString
按difflib.SequenceMatcher
找到的块(单词或字符,取决于可分割单词模式配置)分部分转储。作为块处理的匹配的bs4.element.Tag
要么在不更改的情况下转储,要么在完全删除后完全重新插入。无匹配元素作为完全删除和完全插入转储。 - 在
BeautifulSoup4
汤中导出,然后作为字符串输出。
项目详情
下载文件
下载适用于您的平台文件。如果您不确定选择哪个,请了解更多关于安装包的信息。
源分布
构建分布
html-diff-0.4.1.tar.gz 的哈希值
算法 | 哈希摘要 | |
---|---|---|
SHA256 | 4260ad08d80a043f34a45721d429d3462ec2738b9002c7e5bd75040bdcd40528 |
|
MD5 | 4be7d2425fa8dbe48b0478984eb9ea4b |
|
BLAKE2b-256 | b3b253c5b08a6af8b90213f45aad08ef6dc02a50bffc30f27b6093fb9d4f1bb9 |