【发布时间】:2016-01-17 04:28:20
【问题描述】:
我想按结构而不是按内容来区分 html 文件。例如:b 和 a 与此 diff 相同,因为它们的结构相同。
任何人都知道工具(我更喜欢在 python 中)或实现吗?
【问题讨论】:
-
在问这个问题之前你尝试了什么?
-
我尝试通过beautifulSoup解析树,然后使用zhang shasha算法,但是太慢了……
我想按结构而不是按内容来区分 html 文件。例如:b 和 a 与此 diff 相同,因为它们的结构相同。
任何人都知道工具(我更喜欢在 python 中)或实现吗?
【问题讨论】:
您需要将 HTML/XML 解析为 DOM 树,然后比较这些树。为此,Python 中解析的首选解决方案是 lxml 库。为了比较,我不确定是否存在任何库,但以下是指导源代码。
这是来自 Ian Bicking 的一个 XML 比较函数(原始来源,在 Python 软件基金会许可下,https://bitbucket.org/ianb/formencode/src/tip/formencode/doctest_xml_compare.py?fileviewer=file-view-default#doctest_xml_compare.py-70)
try:
import doctest
doctest.OutputChecker
except AttributeError: # Python < 2.4
import util.doctest24 as doctest
try:
import xml.etree.ElementTree as ET
except ImportError:
import elementtree.ElementTree as ET
from xml.parsers.expat import ExpatError as XMLParseError
RealOutputChecker = doctest.OutputChecker
def debug(*msg):
import sys
print >> sys.stderr, ' '.join(map(str, msg))
class HTMLOutputChecker(RealOutputChecker):
def check_output(self, want, got, optionflags):
normal = RealOutputChecker.check_output(self, want, got, optionflags)
if normal or not got:
return normal
try:
want_xml = make_xml(want)
except XMLParseError:
pass
else:
try:
got_xml = make_xml(got)
except XMLParseError:
pass
else:
if xml_compare(want_xml, got_xml):
return True
return False
def output_difference(self, example, got, optionflags):
actual = RealOutputChecker.output_difference(
self, example, got, optionflags)
want_xml = got_xml = None
try:
want_xml = make_xml(example.want)
want_norm = make_string(want_xml)
except XMLParseError, e:
if example.want.startswith('<'):
want_norm = '(bad XML: %s)' % e
# '<xml>%s</xml>' % example.want
else:
return actual
try:
got_xml = make_xml(got)
got_norm = make_string(got_xml)
except XMLParseError, e:
if example.want.startswith('<'):
got_norm = '(bad XML: %s)' % e
else:
return actual
s = '%s\nXML Wanted: %s\nXML Got : %s\n' % (
actual, want_norm, got_norm)
if got_xml and want_xml:
result = []
xml_compare(want_xml, got_xml, result.append)
s += 'Difference report:\n%s\n' % '\n'.join(result)
return s
def xml_compare(x1, x2, reporter=None):
if x1.tag != x2.tag:
if reporter:
reporter('Tags do not match: %s and %s' % (x1.tag, x2.tag))
return False
for name, value in x1.attrib.items():
if x2.attrib.get(name) != value:
if reporter:
reporter('Attributes do not match: %s=%r, %s=%r'
% (name, value, name, x2.attrib.get(name)))
return False
for name in x2.attrib.keys():
if name not in x1.attrib:
if reporter:
reporter('x2 has an attribute x1 is missing: %s'
% name)
return False
if not text_compare(x1.text, x2.text):
if reporter:
reporter('text: %r != %r' % (x1.text, x2.text))
return False
if not text_compare(x1.tail, x2.tail):
if reporter:
reporter('tail: %r != %r' % (x1.tail, x2.tail))
return False
cl1 = x1.getchildren()
cl2 = x2.getchildren()
if len(cl1) != len(cl2):
if reporter:
reporter('children length differs, %i != %i'
% (len(cl1), len(cl2)))
return False
i = 0
for c1, c2 in zip(cl1, cl2):
i += 1
if not xml_compare(c1, c2, reporter=reporter):
if reporter:
reporter('children %i do not match: %s'
% (i, c1.tag))
return False
return True
def text_compare(t1, t2):
if not t1 and not t2:
return True
if t1 == '*' or t2 == '*':
return True
return (t1 or '').strip() == (t2 or '').strip()
def make_xml(s):
return ET.XML('<xml>%s</xml>' % s)
def make_string(xml):
if isinstance(xml, (str, unicode)):
xml = make_xml(xml)
s = ET.tostring(xml)
if s == '<xml />':
return ''
assert s.startswith('<xml>') and s.endswith('</xml>'), repr(s)
return s[5:-6]
def install():
doctest.OutputChecker = HTMLOutputChecker
【讨论】:
旁注:<\head> 不是有效的 HTML 标记,将被解释为文本。 HTML 关闭标签如下所示:</head>
正如其他回答者可能告诉您的那样,如果您要比较结构良好、完整的 HTML 文档或片段,那么使用真正知道 DOM 是什么的库可能是最可靠的选择。比使用 DOM 更简单的解决方案是使用正则表达式匹配 HTML 标签。
<pre> 或 <textarea> 元素中时,可能会产生意想不到的结果。</head>,而 DOM/解析库可能会抱怨缺少 <head> 标记。演示
以下是一些通过查找所有标签并连续打印它们来规范化 HTML 输入(实际上是该页面的 HTML)的代码。
import re, urllib
f = urllib.urlopen('http://stackoverflow.com/questions/33204018/html-structure-diff-in-python')
html = f.read()
for m in re.finditer(r'''</?\w+((\s+\w+(\s*=\s*(?:".*?"|'.*?'|[^'">\s]+))?)+\s*|\s*)/?>''', html):
print m.group(0)
您可以从上面获取输出并使用您喜欢的任何命令行差异工具来比较它们。
或者您可能想使用 Python 比较它们。您可能有兴趣将它们连接成一个字符串,而不是打印出所有行:
tags_as_string = ''
for m in re.finditer(r'''</?\w+((\s+\w+(\s*=\s*(?:".*?"|'.*?'|[^'">\s]+))?)+\s*|\s*)/?>''', html):
s += m.group(0) + '\n' # the newline makes diff output look nicer
或列表:
tags_as_list = []
for m in re.finditer(r'''</?(\w+)((\s+\w+(\s*=\s*(?:".*?"|'.*?'|[^'">\s]+))?)+\s*|\s*)/?>''', html):
s.append(m.group(0))
需要考虑的进一步步骤(可以在 for 循环内完成):
m.group(1)(括号中的第一个正则表达式组)访问。信用:实际的正则表达式来自http://haacked.com/archive/2004/10/25/usingregularexpressionstomatchhtml.aspx/
【讨论】: