DOM4J 解析 XML 之忽略轉義字元

Youlou發表於2019-02-16

專案經驗,如需轉載,請註明作者:Yuloran (t.cn/EGU6c76)

背景

專案開發需要手動合入幾十種語言的翻譯到 string.xml 中,這是一件非常痛苦的事情:Copy、Paste,Copy、Paste,Copy、Paste… 人都快瘋了!被逼無奈寫了個自動替換翻譯的工具。原理很簡單:解析 Excel中的翻譯,替換到 Xml 中。Excel 解析用 jxl.jar,Xml 解析與修改用 DOM,一頓操作,一天就寫完了!正高興呢,趕緊使用 git diff 檢視修改對比,一看壞事了:“坑爹呢!一點也不完美啊!原字串中的轉義字元全被轉義了好嘛!難道還要手動還回去嘛!像我這樣優(懶)秀(惰)的人根本無法容忍好嘛!” 所以,本文記錄如何使用 DOM4J(上面不是說用 DOM 解析嗎?這裡怎麼又成 DOM4J 了?忽悠誰呢!) 解析 XML 並讓其忽略轉義字元。

為什麼不用 DOM

誰說我沒用 DOM,我一上來就用的 DOM 好嘛!畢竟 JDK 自帶的啊!但是用了後,使用者體驗賊差好嘛!稍微貼下使用方法:

package com.yuloran;

import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;

public class Main {

    public static void main(String[] args) throws ParserConfigurationException, IOException, SAXException, TransformerException {

        // 1. 解析
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        DocumentBuilder documentBuilder = factory.newDocumentBuilder();
        Document document = documentBuilder.parse(new InputSource(new InputStreamReader(new FileInputStream("strings.xml"), "UTF-8")));

        // 2. 遍歷
        NodeList strings = document.getElementsByTagName("string");
        for (int i = 0; i < strings.getLength(); i++) {
            Node item = strings.item(i);
            System.out.print(String.format("Element:[tag:%s, content:%s] ", item.getNodeName(), item.getTextContent()));
            NamedNodeMap attributes = item.getAttributes();
            for (int j = 0; j < attributes.getLength(); j++) {
                Node attr = attributes.item(j);
                System.out.println(String.format("Attr:[key:%s, value:%s]", attr.getNodeName(), attr.getNodeValue()));
            }
        }

        // 3. 儲存
        Transformer transformer = TransformerFactory.newInstance().newTransformer();
        transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
        transformer.setOutputProperty(OutputKeys.INDENT, "yes");
        transformer.transform(new DOMSource(document), new StreamResult("strings_copy.xml"));
    }

}
複製程式碼

strings.xml:

strings.xml

解析日誌:

解析日誌.png

strings_copy.xml:

strings_copy.png

DOM 解析儲存 XML 檔案的問題:

  • XML 文件宣告中自動新增 standalone=”no”,可以通過 document.setXmlStandalone(true); 去除,但是這樣的話,縮排就會失效!
  • 檔案換行符自動更換為作業系統所在的換行符!

所以,我不用 DOM,而是用 DOM4J。用了 DOM4J 這些問題都將成為浮雲!

DOM4J 解析

太簡單啦!直接閱讀官方指南即可!我從未見過如此簡潔明瞭的 API 文件!

DOM4J jar 包及依賴下載:

DOM4J 使用示例

package com.yuloran;

import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Node;
import org.dom4j.io.SAXReader;
import org.xml.sax.InputSource;

import java.io.*;
import java.util.List;

public class Main {

    public static void main(String[] args) throws DocumentException, IOException {
        // 1. 解析
        SAXReader reader = new SAXReader();
        Document document = reader.read(new InputSource(new InputStreamReader(new FileInputStream("strings.xml"), "UTF-8")));

        // 2. 遍歷
        List<Node> list = document.selectNodes("/resources/string[@name]");
        for (Node node : list) {
            System.out.print(String.format("Element:[tag:%s, content:%s] ", node.getName(), node.getText()));
            System.out.println(String.format("Attr:[name@%s]", node.valueOf("@name")));
        }

        // 3.儲存
        OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream("strings_dom4j.xml"), "UTF-8");
        XMLWriter xmlWriter = new XMLWriter(writer);
        // 忽略 Element 物件中的轉義字元
        xmlWriter.setEscapeText(false);
        xmlWriter.write(document);
        xmlWriter.close();
    }

}
複製程式碼

strings_dom4j.xml:

strings_dom4j.xml

怎麼樣?看看這輸出,一點毛病沒有!

忽略轉義字元

其實這是 SAX(Simple Application Interface For Xml) 解析的問題,SAX 解析 XML 時,會自動將元素文字中的轉義字元轉義,導致最後將 Document 物件儲存為檔案時,無法將原轉義字元寫回:

原檔案:

帶轉義字元的xml.png

DOM4J 解析再寫回:

DOM4J 解析再寫回.png

所以,我們需要實現一個過濾器,每當 SAX 解析一個轉義字元,我們就將其原樣寫回:

        reader.setXMLFilter(new XMLFilterImpl() {
            @Override
            public void characters(char[] ch, int start, int length) throws SAXException {
                String text = new String(ch, start, length);
                System.out.println("text is: " + text);

                if (length == 1) {
                    if ((int) ch[0] == 160) {
                        char[] escape = "&#160;".toCharArray();
                        super.characters(escape, 0, escape.length);
                        return;
                    }
                }

                super.characters(ch, start, length);
            }
        });
複製程式碼

再配合 xmlWriter.setEscapeText(false); 即可原樣輸出原 Xml 檔案中的轉義字元:

        // 3.儲存
        OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream("strings_dom4j.xml"), "UTF-8");
        XMLWriter xmlWriter = new XMLWriter(writer);
        xmlWriter.setEscapeText(false);
        xmlWriter.write(document);
        xmlWriter.close();
複製程式碼

測試結果:

原Xml.png

日誌:

日誌.png

strings_dom4j.xml:

strings_dom4j.xml

其它的轉義字元也是同樣的處理方法。可以將要忽略的轉義字元放到配置檔案中,做工具的時候從配置中讀取要忽略的轉義字元,這樣更靈活。

總結

本文只寫了最終的解決方案,實際上探索這個解決方案的過程還是比較複雜的。需求小眾,沒什麼資料,只能看原始碼,猜介面,反正我是不相信這樣的解析框架是沒有暴露使用者自行處理字串的介面的。果然還是可以通過 characters() 方法,只是 SAXReader 沒有暴露 ContentHandler 介面,內部封裝成了 SAXContentHandler,characters() 方法則暴露到了 XMLFilter 介面中,哈,一番好找。

相關文章