在 Python 3 中使用开放任意标签解析 SGML

问题描述 投票:0回答:2

我正在尝试解析一个文件,例如: http://www.sec.gov/Archives/edgar/data/1409896/000118143112051484/0001181431-12-051484.hdr.sgml

我正在使用 Python 3,并且无法找到现有库的解决方案来解析带有开放标签的 SGML 文件。 SGML 允许隐式闭合标签。当尝试使用 LXML、XML 或 beautiful soup 解析示例文件时,我最终得到的结果是隐式关闭标签在文件末尾而不是在行末尾关闭。

例如:

<COMPANY>Awesome Corp
<FORM> 24-7
<ADDRESS>
<STREET>101 PARSNIP LN
<ZIP>31337
</ADDRESS>

这最终被解释为:

<COMPANY>Awesome Corp
<FORM> 24-7
<ADDRESS>
<STREET>101 PARSNIP LN
<ZIP>31337
</ADDRESS>
</ZIP>
</STREET>
</FORM>
</COMPANY>

但是,我需要将其解释为:

<COMPANY>Awesome Corp</COMPANY>  
<FORM> 24-7</FORM>
<ADDRESS>
<STREET>101 PARSNIP LN</STREET>
<ZIP>31337</ZIP>
</ADDRESS>

如果有一个非默认解析器传递给 LXML/BS4 可以处理这个问题,我就错过了。

python xml python-3.x lxml sgml
2个回答
7
投票

如果您可以找到所使用文档的 SGML DTD,解决方案可能是使用 OpenSP SGML 工具包 中的 osx SGML 到 XML 转换器将文档转换为 XML。

这是一个简单的例子。假设我们有以下 SGML 文档(company.sgml;带有根元素):

<!DOCTYPE ROOT SYSTEM "company.dtd">
<ROOT>
<COMPANY>Awesome Corp
<FORM> 24-7
<ADDRESS>
<STREET>101 PARSNIP LN
<ZIP>31337
</ADDRESS>

DTD (company.dtd) 如下所示:

<!ELEMENT ROOT       -  o (COMPANY, FORM, ADDRESS) >
<!ELEMENT COMPANY    -  o (#PCDATA) >
<!ELEMENT FORM       -  o (#PCDATA) >
<!ELEMENT ADDRESS    -  - (STREET, ZIP) >
<!ELEMENT STREET     -  o (#PCDATA) >
<!ELEMENT ZIP        -  o (#PCDATA) >

-  o
位表示结束标记可以省略。

SGML文档可以使用osx进行解析,输出可以使用xmllint进行格式化,如下:

osx company.sgml | xmllint --format -

上述命令的输出:

<?xml version="1.0"?>
<ROOT>
  <COMPANY>Awesome Corp</COMPANY>
  <FORM> 24-7</FORM>
  <ADDRESS>
    <STREET>101 PARSNIP LN</STREET>
    <ZIP>31337</ZIP>
  </ADDRESS>
</ROOT>

现在我们有了格式良好的 XML,可以使用 lxml 或其他 XML 工具进行处理。

我不知道您链接到的文档是否有完整的 DTD。以下 PDF 文件包含有关 EDGAR 的相关信息,包括可能有用的 DTD:http://www.sec.gov/info/edgar/pdsdissemspec910.pdf(我通过这个答案找到它)。但链接的 SGML 文档包含 PDF 文件中未提及的元素(例如,

SEC-HEADER
)。


0
投票

我使用 OpenSP 的 osx 进行 Edgar 解析工作,但我还构建了一个仅 Python 的 hack,它似乎也有效。

from html.parser import HTMLParser


class EndTagFinder(HTMLParser):
    
    def __init__(self,):
        super().__init__()
        self.end_tags = set()

    def handle_endtag(self, tag):
        self.end_tags.add(tag)


def end_tags(sgml: str) -> set[str]:
    f = EndTagFinder()
    f.feed(sgml)
    return f.end_tags


class SGMLFixer(HTMLParser):

    def __init__(self, end_tags: set[str]=None):
        super().__init__()
        if end_tags is None:
            self.end_tags = set()
        else:
            self.end_tags = end_tags
        self.pieces = []
        self.open_starttag = None

    def handle_starttag(self, tag, attrs):
        if self.open_starttag is not None:
            if self.open_starttag not in self.end_tags:
                self.pieces.append(('end', self.open_starttag))
                self.open_starttag = None
        self.pieces.append(('start', (tag, attrs)))
        self.open_starttag = tag
    
    def handle_data(self, data):
        cleaned_data = data.strip()
        if cleaned_data:
            self.pieces.append(('data', cleaned_data))

    def handle_endtag(self, tag):
        if self.open_starttag is not None:
            if self.open_starttag not in self.end_tags:
                self.pieces.append(('end', self.open_starttag))
                self.open_starttag = None
        self.pieces.append(('end', tag))
        

def pieces(sgml: str, endtags: set[str]) -> list[tuple]:
    f = SGMLFixer(endtags)
    f.feed(sgml)
    return f.pieces


def piece_to_str(piece: tuple) -> str:
    _type, value = piece
    if _type == 'start':
        tag, attrs = value
        return f'<{tag}>'
    elif _type == 'end':
        return f'</{value}>'
    elif _type == 'data':
        return value
    else:
        raise


def sgml_to_xml(sgml: str) -> str:
    return ''.join(piece_to_str(p) for p in pieces(sgml, end_tags(sgml)))


if __name__ == '__main__':
    sgml = '''<COMPANY>Awesome Corp
    <FORM> 24-7
    <ADDRESS>
    <STREET>101 PARSNIP LN
    <ZIP>31337
    </ADDRESS>
    '''

    print(sgml_to_xml(sgml))

<company>Awesome Corp</company><form>24-7</form><address><street>101 PARSNIP LN</street><zip>31337</zip></address>
© www.soinside.com 2019 - 2024. All rights reserved.