如何构建一个HTML的org.w3c.dom.Document?

14

文档接口的文档将接口描述为:

文档接口表示整个HTML或XML文档。

javax.xml.parsers.DocumentBuilder 构建 XML Document。然而,我找不到一种方法来构建一个HTML Document

我想要一个HTML Document,因为我试图构建一个文档,然后将其传递给一个期望HTML Document的库。这个库以非区分大小写的方式使用了Document#getElementsByTagName(String tagname),这对于HTML来说是可以的,但对于XML则不行。

我已经找了一些资料,但没有找到任何有用的信息。像如何在java中将网页的Html源代码转换为org.w3c.dom.Document?这样的问题并没有一个明确的答案。


你可能已经可以使用XMLSerializer了。https://xerces.apache.org/xerces-j/apiDocs/org/apache/xml/serialize/XMLSerializer.html - ThW
我认为我正在寻找的是https://xerces.apache.org/xerces-j/apiDocs/org/apache/html/dom/HTMLDocumentImpl.html。不过还不确定。 - Dmitry Minkovsky
@dimadima 我一开始也是这么想的,但现在不太确定了。稍后我会尝试写出一个答案来解释为什么以及可能的替代方案。 - dbank
@dimadima 我已经发布了我目前所发现的答案。如果我发现更多或更正,我将编辑我的答案。 - dbank
1个回答

14
您似乎有两个明确的要求:
  1. 您需要将HTML表示为org.w3c.dom.Document
  2. 您需要使Document#getElementsByTagName(String tagname)在不区分大小写的情况下操作。
如果您正在使用org.w3c.dom.Document处理HTML,则我假设您正在使用某种XHTML版本。因为XML API(如DOM)将期望格式良好的XML。HTML并不一定是格式良好的XML,但XHTML是格式良好的XML。即使您使用HTML工作,也必须对其进行一些预处理,以确保它是格式良好的XML,然后再尝试通过XML解析器运行它。可能更容易首先使用HTML解析器(例如jsoup)解析HTML,然后通过遍历HTML解析器生成的树(在jsoup的情况下是org.jsoup.nodes.Document)构建您的org.w3c.dom.Document

有一个org.w3c.dom.html.HTMLDocument接口,它扩展了org.w3c.dom.Document。我找到的唯一实现是在Xerces-j(2.11.0)中以org.apache.html.dom.HTMLDocumentImpl的形式出现。乍一看这似乎很有前途,但仔细检查后,我们发现存在一些问题。

1. 没有明确的“清晰”的方法来获取实现org.w3c.dom.html.HTMLDocument接口的对象实例。

使用Xerces,我们通常会使用DocumentBuilder以以下方式获取Document对象:

DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.newDocument();
//or doc = builder.parse(xmlFile) if parsing from a file

或者使用 DOMImplementation 类型:

DOMImplementationRegistry registry = DOMImplementationRegistry.newInstance();
DOMImplementationLS impl = (DOMImplementationLS)registry.getDOMImplementation("LS");
LSParser lsParser = impl.createLSParser(DOMImplementationLS.MODE_SYNCHRONOUS, null);
Document document = lsParser.parseURI("myFile.xml");

在这两种情况下,我们纯粹使用org.w3c.dom.*接口来获取Document对象。
我找到的与HTMLDocument最接近的等效物是这样的:
HTMLDOMImplementation htmlDocImpl = HTMLDOMImplementationImpl.getHTMLDOMImplementation();
HTMLDocument htmlDoc = htmlDocImpl.createHTMLDocument("My Title");

这需要我们直接实例化内部实现类,使我们的实现依赖于Xerces。

(注意:我还看到Xerces也有一个内部的HTMLBuilder(它实现了已弃用的DocumentHandler),可以使用SAX解析器生成HTMLDocument,但我没有深入研究。)

2. org.w3c.dom.html.HTMLDocument不能生成正确的XHTML。

虽然你可以使用getElementsByTagName(String tagname)以不区分大小写的方式搜索HTMLDocument树,但所有元素名称都保存在内部的大写字母中。但是XHTML元素和属性名称应该是全部小写。(这可以通过遍历整个文档树并使用DocumentrenameNode()方法将所有元素的名称更改为小写来解决。)

此外,XHTML文档应该有适当的DOCTYPE声明XHTML命名空间的xmlns声明。在HTMLDocument中似乎没有简单的方法来设置这些(除非你对内部Xerces实现进行一些调整)。 org.w3c.dom.html.HTMLDocument文档很少,接口的Xerces实现似乎不完整。我没有搜遍整个互联网,但我找到的唯一关于HTMLDocument的文档是先前链接的JavaDocs,以及Xerces内部实现的源代码注释。在这些注释中,我还发现了几个接口的不同部分没有实现。(旁注:我真的觉得org.w3c.dom.html.HTMLDocument接口本身并没有被任何人使用,并且可能本身也是不完整的。)
由于这些原因,我认为最好避免使用org.w3c.dom.html.HTMLDocument,而是尽可能利用org.w3c.dom.Document。我们能做什么呢?
一种方法是扩展org.apache.xerces.dom.DocumentImpl(它扩展了org.apache.xerces.dom.CoreDocumentImpl,实现了org.w3c.dom.Document)。这种方法不需要太多的代码,但它仍然使我们依赖于Xerces的实现,因为我们正在扩展DocumentImpl。在我们的MyHTMLDocumentImpl中,我们只是在元素创建和搜索时将所有标签名称转换为小写。这将允许以不区分大小写的方式使用Document#getElementsByTagName(String tagname)
import org.apache.xerces.dom.DocumentImpl;
import org.apache.xerces.dom.DocumentTypeImpl;
import org.w3c.dom.DOMException;
import org.w3c.dom.Document;
import org.w3c.dom.DocumentType;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

//a base class somewhere in the hierarchy implements org.w3c.dom.Document
public class MyHTMLDocumentImpl extends DocumentImpl {

    private static final long serialVersionUID = 1658286253541962623L;


    /**
     * Creates an Document with basic elements required to meet
     * the <a href="http://www.w3.org/TR/xhtml1/#strict">XHTML standards</a>.
     * <pre>
     * {@code
     * <?xml version="1.0" encoding="UTF-8"?>
     * <!DOCTYPE html 
     *     PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" 
     *     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
     * <html xmlns="http://www.w3.org/1999/xhtml">
     *     <head>
     *         <title>My Title</title>
     *     </head>
     *     <body/>
     * </html>
     * }
     * </pre>
     * 
     * @param title desired text content for title tag. If null, no text will be added.
     * @return basic HTML Document. 
     */
    public static Document makeBasicHtmlDoc(String title) {
        Document htmlDoc = new MyHTMLDocumentImpl();
        DocumentType docType = new DocumentTypeImpl(null, "html",
                "-//W3C//DTD XHTML 1.0 Strict//EN",
                "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd");
        htmlDoc.appendChild(docType);
        Element htmlElement = htmlDoc.createElementNS("http://www.w3.org/1999/xhtml", "html");
        htmlDoc.appendChild(htmlElement);
        Element headElement = htmlDoc.createElement("head");
        htmlElement.appendChild(headElement);
        Element titleElement = htmlDoc.createElement("title");
        if(title != null)
            titleElement.setTextContent(title);
        headElement.appendChild(titleElement);
        Element bodyElement = htmlDoc.createElement("body");
        htmlElement.appendChild(bodyElement);

        return htmlDoc;
    }

    /**
     * This method will allow us to create a our
     * MyHTMLDocumentImpl from an existing Document.
     */
    public static Document createFrom(Document doc) {
        Document htmlDoc = new MyHTMLDocumentImpl();
        DocumentType originDocType = doc.getDoctype();
        if(originDocType != null) {
            DocumentType docType = new DocumentTypeImpl(null, originDocType.getName(),
                    originDocType.getPublicId(),
                    originDocType.getSystemId());
            htmlDoc.appendChild(docType);
        }
        Node docElement = doc.getDocumentElement();
        if(docElement != null) {
            Node copiedDocElement = docElement.cloneNode(true);
            htmlDoc.adoptNode(copiedDocElement);
            htmlDoc.appendChild(copiedDocElement);
        }
        return htmlDoc;
    }

    private MyHTMLDocumentImpl() {
        super();
    }

    @Override
    public Element createElement(String tagName) throws DOMException {
        return super.createElement(tagName.toLowerCase());
    }

    @Override
    public Element createElementNS(String namespaceURI, String qualifiedName) throws DOMException {
        return super.createElementNS(namespaceURI, qualifiedName.toLowerCase());
    }

    @Override
    public NodeList getElementsByTagName(String tagname) {
        return super.getElementsByTagName(tagname.toLowerCase());
    }

    @Override
    public NodeList getElementsByTagNameNS(String namespaceURI, String localName) {
        return super.getElementsByTagNameNS(namespaceURI, localName.toLowerCase());
    }

    @Override
    public Node renameNode(Node n, String namespaceURI, String qualifiedName) throws DOMException {
        return super.renameNode(n, namespaceURI, qualifiedName.toLowerCase());
    }
}

测试人员:

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;

import org.w3c.dom.DOMConfiguration;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.w3c.dom.bootstrap.DOMImplementationRegistry;
import org.w3c.dom.ls.DOMImplementationLS;
import org.w3c.dom.ls.LSOutput;
import org.w3c.dom.ls.LSSerializer;


public class HTMLDocumentTest {

    private final static int P_ELEMENT_NUM = 3;

    public static void main(String[] args) //I'm throwing all my exceptions here to shorten the example, but obviously you should handle them appropriately.
            throws ClassNotFoundException, InstantiationException, IllegalAccessException, ClassCastException, IOException {

        Document htmlDoc = MyHTMLDocumentImpl.makeBasicHtmlDoc("My Title");

        //populate the html doc with some example content
        Element bodyElement = (Element) htmlDoc.getElementsByTagName("body").item(0);
        for(int i = 0; i < P_ELEMENT_NUM; ++i) {
            Element pElement = htmlDoc.createElement("p");
            String id = Integer.toString(i+1);
            pElement.setAttribute("id", "anId"+id);
            pElement.setTextContent("Here is some text"+id+".");
            bodyElement.appendChild(pElement);
        }

        //get the title element in a case insensitive manner.
        NodeList titleNodeList = htmlDoc.getElementsByTagName("tItLe");
        for(int i = 0; i < titleNodeList.getLength(); ++i)
            System.out.println(titleNodeList.item(i).getTextContent());

        System.out.println();

        {//get all p elements searching with lowercase
            NodeList pNodeList = htmlDoc.getElementsByTagName("p");
            for(int i = 0; i < pNodeList.getLength(); ++i) {
                System.out.println(pNodeList.item(i).getTextContent());
            }
        }

        System.out.println();

        {//get all p elements searching with uppercase
            NodeList pNodeList = htmlDoc.getElementsByTagName("P");
            for(int i = 0; i < pNodeList.getLength(); ++i) {
                System.out.println(pNodeList.item(i).getTextContent());
            }
        }

        System.out.println();

        //to serialize
        DOMImplementationRegistry registry = DOMImplementationRegistry.newInstance();
        DOMImplementationLS domImplLS = (DOMImplementationLS) registry.getDOMImplementation("LS");

        LSSerializer lsSerializer = domImplLS.createLSSerializer();
        DOMConfiguration domConfig = lsSerializer.getDomConfig();
        domConfig.setParameter("format-pretty-print", true);  //if you want it pretty and indented

        LSOutput lsOutput = domImplLS.createLSOutput();
        lsOutput.setEncoding("UTF-8");

        //to write to file
        try (OutputStream os = new FileOutputStream(new File("myFile.html"))) {
            lsOutput.setByteStream(os);
            lsSerializer.write(htmlDoc, lsOutput);
        }

        //to print to screen
        System.out.println(lsSerializer.writeToString(htmlDoc)); 
    }

}

输出:

My Title

Here is some text1.
Here is some text2.
Here is some text3.

Here is some text1.
Here is some text2.
Here is some text3.

<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <title>My Title</title>
    </head>
    <body>
        <p id="anId1">Here is some text1.</p>
        <p id="anId2">Here is some text2.</p>
        <p id="anId3">Here is some text3.</p>
    </body>
</html>

另一种类似于上述方法的方法是创建一个Document包装器,它包装了一个Document对象并实现了Document接口。这比“扩展DocumentImpl”方法需要更多的代码,但这种方法更加“清洁”,因为我们不必关心特定的Document实现。这种方法的额外代码并不难写;只是需要提供所有那些包装器实现Document方法的代码有点繁琐。我还没有完全解决这个问题,可能会有一些问题,但如果可以工作,这就是一般的想法:
public class MyHTMLDocumentWrapper implements Document {

    private Document doc;

    public MyHTMLDocumentWrapper(Document doc) {
        //...
        this.doc = doc;
        //...
    }

    //...
}

无论是org.w3c.dom.html.HTMLDocument,我上面提到的方法之一,还是其他什么东西,也许这些建议可以帮助您了解如何继续进行。

编辑:

在尝试解析以下XHTML文件时,我的解析测试中,Xerces会在实体管理类中尝试打开http连接而挂起。我不知道为什么?特别是因为我在本地测试了一个没有实体的html文件。(可能与DOCTYPE或命名空间有关吗?)这是该文档:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC 
    "-//W3C//DTD XHTML 1.0 Strict//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <title>My Title</title>
    </head>
    <body>
        <p id="anId1">Here is some text1.</p>
        <p id="anId2">Here is some text2.</p>
        <p id="anId3">Here is some text3.</p>
    </body>
</html>

嗨,dbank,感谢您的回答!我确实先通过jsoup运行原始html来构建一个org.jsoup.nodes.Document。然后,我通过遍历jsoup Document的节点并为xerces 2 HTMLDocumentImplementation创建类似的节点将其转换为org.w3c.dom.Document。无论如何,在那一刻,这一切都对我来说太不愉快了,我甚至从未测试过它是否在区分大小写的查询方面起作用:)。谢谢您的回答!非常感激。 - Dmitry Minkovsky
@dimadima:我刚刚进行了一次编辑。MyHTMLDocumentImpl.createFrom(Document doc)似乎实际上运行良好。但是,Xerces DOM解析器在解析示例XHTML文件时似乎会挂起。 - dbank
@dimadima:无论如何,使用风险自负。但愿能有所帮助。 :-) - dbank
我现在想我知道为什么Xerces DOM解析器似乎在解析示例XHTML文件时会挂起。实际上,它最终会在长时间的挂起后解析。当我有时间时,我将尝试编辑答案并提供解释和可能的解决方案。 - dbank

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接