【问题标题】:HTML structure diff in PythonPython中的HTML结构差异
【发布时间】:2016-01-17 04:28:20
【问题描述】:

我想按结构而不是按内容来区分 html 文件。例如:b 和 a 与此 diff 相同,因为它们的结构相同。

任何人都知道工具(我更喜欢在 python 中)或实现吗?

【问题讨论】:

  • 在问这个问题之前你尝试了什么?
  • 我尝试通过beautifulSoup解析树,然后使用zhang shasha算法,但是太慢了……

标签: python html dom diff lxml


【解决方案1】:

您需要将 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

【讨论】:

  • 这很不错,但是有可能让它更短吗?
  • 缩短它是留给读者的练习。你在问一个不平凡的问题的答案:)
【解决方案2】:

旁注:&lt;\head&gt; 不是有效的 HTML 标记,将被解释为文本。 HTML 关闭标签如下所示:&lt;/head&gt;

正如其他回答者可能告诉您的那样,如果您要比较结构良好、完整的 HTML 文档或片段,那么使用真正知道 DOM 是什么的库可能是最可靠的选择。比使用 DOM 更简单的解决方案是使用正则表达式匹配 HTML 标签

  • 很简单(两行即可完成)。
  • 到目前为止,在我测试过的所有内容中,它都是可靠的,但是当 HTML 标记出现在 &lt;pre&gt;&lt;textarea&gt; 元素中时,可能会产生意想不到的结果。
  • 可以处理部分 HTML 片段,例如 &lt;/head&gt;,而 DOM/解析库可能会抱怨缺少 &lt;head&gt; 标记。

演示
以下是一些通过查找所有标签并连续打印它们来规范化 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 循环内完成):

  • 也许您只对标签名称感兴趣,而不对属性感兴趣。标签名称可以通过 for 循环中的m.group(1)(括号中的第一个正则表达式组)访问。
  • 表示相同内容的标记仍可能因空格而有所不同。您可能希望使用类似的技术规范化每个标签中的空白。

信用:实际的正则表达式来自http://haacked.com/archive/2004/10/25/usingregularexpressionstomatchhtml.aspx/

【讨论】:

  • 强烈建议不要在 HTML 上执行 any 正则表达式。有关更多信息,请参阅此答案stackoverflow.com/questions/1732348/…
  • 谢谢,我已经多次看到这个答案,但它并没有告诉我原因。这个答案stackoverflow.com/a/590789/782045 告诉我们原因:“它取决于匹配开始和结束标记,而这在正则表达式中是不可能的。”事实证明,这个问题不需要匹配开始和结束标签。我相信标签本身(不匹配)确实形成了一种自然语言,正则表达式非常适合。
  • 因为 HTML 在继承上是复杂的,而正则表达式不足以满足它。陷阱案例包括例如内联 JavaScript 中的类标签字符串、内联 CSS 中的类标签字符串、CDATA 部分、HTML 实体编码。
  • 我开始明白你的意思了——也许正则表达式不适合这个,因为它会很长。但是,您描述的每一件事都不需要我们理解嵌套级别(这些东西都不能嵌套在它们自己内部),这意味着该语言仍然是常规的并且可以使用正则表达式进行处理。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-02-06
  • 2018-08-30
  • 1970-01-01
  • 1970-01-01
  • 2020-03-19
  • 1970-01-01
  • 2012-11-28
相关资源
最近更新 更多