我怎样才能建立一个HTML org.w3c.dom.Document?

How can I build an HTML org.w3c.dom.Document?

documentation of the Document interface 将接口描述为:

The Document interface represents the entire HTML or XML document.

javax.xml.parsers.DocumentBuilder 构建 XML Documents。但是,我找不到构建 HTML DocumentDocument 的方法!

我想要一个 HTML Document,因为我正在尝试构建一个文档,然后将其传递给一个需要 HTML Document 的库。此库以不区分大小写的方式使用 Document#getElementsByTagName(String tagname),这适用于 HTML,但不适用于 XML.

我环顾四周,没有找到任何东西。 How to convert an Html source of a webpage into org.w3c.dom.Document in java? 之类的项目实际上没有答案。

您似乎有两个明确的要求:

  1. 您需要将 HTML 表示为 org.w3c.dom.Document
  2. 您需要 Document#getElementsByTagName(String tagname) 以不区分大小写的方式进行操作。

如果您尝试使用 org.w3c.dom.Document, then I assume you are working with some flavor of XHTML. Because an XML API, such as DOM, is going to expect well-formed XML. HTML isn't necessarily well-formed XML, but XHTML is well-formed XML. Even if you were working with HTML, you would have to do some pre-processing to ensure it is well-formed XML before trying to run it through an XML parser. It might just be easier to parse the HTML first with an HTML parser, such as jsoup 来处理 HTML,然后通过遍历 HTML 解析器生成的树(org.jsoup.nodes.Document 在 jsoup 的情况下)。


有一个org.w3c.dom.html.HTMLDocument interface, which extends org.w3c.dom.Document. The only implementation I found was in Xerces-j (2.11.0) in the form of org.apache.html.dom.HTMLDocumentImpl。起初这似乎很有希望,但仔细检查后,我们发现存在一些问题。

1.没有明确的 "clean" 方法来获取实现 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");

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

(注意:我还看到 Xerces 也有一个内部 HTMLBuilder(实现了已弃用的 DocumentHandler),据说可以生成 HTMLDocument using a SAX parser, but I didn't bother looking into it.

2。 org.w3c.dom.html.HTMLDocument 没有生成正确的 XHTML.

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

此外,XHTML 文档应该有一个正确的 DOCTYPE declaration and xmlns declaration for the XHTML namespace 。似乎没有一种直接的方法可以在 HTMLDocument 中设置它们(除非您对内部 Xerces 实现进行一些摆弄)。

3. org.w3c.dom.html.HTMLDocument 文档很少,接口的 Xerces 实现似乎不完整。

我没有搜索整个 Internet,但我为 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)

MyHTMLDocumentImpl:

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 接口本身。这需要比 "extending DocumentImpl" 方法更多的代码,但这种方法是 "cleaner",因为我们不必关心特定的 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 连接的实体管理 class 中挂起。为什么我不知道?特别是因为我在没有实体的本地 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>