第16章 XML与JSON解析:数据交换的左右手

说到XML和JSON,这俩东西在Qt开发里几乎天天见。我个人习惯把XML当成「严谨的老学究」,JSON则是「随性的年轻人」。一个讲究格式规范,一个追求简洁高效。今天咱们就把Qt里解析它们的几种方式捋一遍。

核心知识点速览

  • XML解析:QXmlStreamReader(流式)、DOM(文档对象模型)、SAX(事件驱动)
  • XML写入:QXmlStreamWriter
  • JSON解析:QJsonDocument、QJsonObject、QJsonArray
  • JSON写入:QJsonDocument::toJson()
Qt数据解析体系 XML解析 QXmlStreamReader DOM解析 SAX解析 JSON解析 QJsonDocument QJsonObject QJsonArray 流式解析适合大文件,DOM适合小文档 JSON比XML更轻量,解析速度更快

16.1 XML解析:三种武器任你选

XML这东西,说它烦人吧,它确实啰嗦。但很多老系统、配置文件、SVG图形格式都在用。Qt给了我们三种解析方式,各有各的脾气。

16.1.1 QXmlStreamReader —— 我的首选

我个人最常用的是流式解析。为什么?因为它不占内存。你想想看,一个几百兆的XML文件,如果用DOM一次性加载到内存里,程序直接就崩了。流式解析就像读小说一样,一页一页翻,读过的就扔了。

// 流式读取XML
QFile file("config.xml");
if (!file.open(QIODevice::ReadOnly)) {
    qWarning() << "文件打开失败";
    return;
}

QXmlStreamReader reader(&file);
QString currentTag;

while (!reader.atEnd()) {
    reader.readNext();
    
    if (reader.isStartElement()) {
        currentTag = reader.name().toString();
        
        if (currentTag == "user") {
            QString id = reader.attributes().value("id").toString();
            qDebug() << "用户ID:" << id;
        }
    } 
    else if (reader.isCharacters() && !reader.isWhitespace()) {
        if (currentTag == "name") {
            qDebug() << "姓名:" << reader.text().toString();
        }
    }
}

if (reader.hasError()) {
    qWarning() << "解析错误:" << reader.errorString();
}
file.close();

小技巧:流式解析不能回头。如果你需要随机访问XML的某个节点,那就别用流式了。我当年做一个日志分析工具,一开始用了流式,后来发现需要反复查某个时间段的记录,果断换成了DOM。

16.1.2 DOM解析 —— 小文件的王者

DOM解析会把整个XML文档加载到内存里,构建成一棵树。好处是你可以随便在树上爬来爬去,想查哪个节点就查哪个节点。

// DOM方式解析
QDomDocument doc;
QFile file("config.xml");
if (!file.open(QIODevice::ReadOnly)) return;

if (!doc.setContent(&file)) {
    qWarning() << "文档格式错误";
    file.close();
    return;
}

QDomElement root = doc.documentElement();
QDomNodeList users = root.elementsByTagName("user");

for (int i = 0; i < users.size(); ++i) {
    QDomElement user = users.at(i).toElement();
    QString id = user.attribute("id");
    QString name = user.firstChildElement("name").text();
    qDebug() << "ID:" << id << "Name:" << name;
}
file.close();

注意:DOM解析有个坑——它会把整个文档都吃掉。我曾经接手过一个项目,用DOM解析一个50MB的XML,结果程序启动要等10秒。后来改成流式解析,瞬间就快了。所以记住:小文件用DOM,大文件用流式。

16.1.3 SAX解析 —— 老古董但依然能打

SAX解析是事件驱动的。说白了就是:你告诉解析器「遇到开始标签叫我,遇到结束标签也叫我」,然后它一边读一边通知你。这种方式比流式更底层一些,但控制力更强。

// SAX解析需要继承QXmlDefaultHandler
class MyHandler : public QXmlDefaultHandler {
public:
    bool startElement(const QString &namespaceURI,
                      const QString &localName,
                      const QString &qName,
                      const QXmlAttributes &atts) override {
        if (qName == "user") {
            qDebug() << "发现用户:" << atts.value("id");
        }
        return true;
    }
    
    bool characters(const QString &ch) override {
        // 处理文本内容
        return true;
    }
};

// 使用SAX解析
QXmlSimpleReader reader;
MyHandler handler;
reader.setContentHandler(&handler);

QXmlInputSource source(&file);
reader.parse(source);

16.2 写入XML:QXmlStreamWriter

写XML比读XML简单多了。QXmlStreamWriter就像一支笔,你告诉它写什么,它就写什么。我习惯用它来生成配置文件。

QFile file("output.xml");
if (!file.open(QIODevice::WriteOnly)) return;

QXmlStreamWriter writer(&file);
writer.setAutoFormatting(true);  // 自动缩进,好看!

writer.writeStartDocument();
writer.writeStartElement("config");

writer.writeStartElement("database");
writer.writeAttribute("type", "mysql");
writer.writeTextElement("host", "localhost");
writer.writeTextElement("port", "3306");
writer.writeEndElement();  // 结束database

writer.writeEndElement();  // 结束config
writer.writeEndDocument();
file.close();

经验之谈:setAutoFormatting(true)一定要开。不然生成的XML全挤在一行,你自己都不想看。我刚开始写Qt的时候就没开这个,结果被同事吐槽了好久。

16.3 JSON解析:现代人的选择

JSON现在太流行了。Web API返回的基本都是JSON,Qt也提供了很舒服的接口。说实话,我现在写新项目,能用JSON绝不用XML。

16.3.1 解析JSON字符串

QString jsonStr = R"({
    "name": "张三",
    "age": 28,
    "skills": ["C++", "Qt", "Python"],
    "address": {
        "city": "北京",
        "street": "长安街"
    }
})";

QJsonDocument doc = QJsonDocument::fromJson(jsonStr.toUtf8());
if (doc.isNull()) {
    qWarning() << "JSON格式错误";
    return;
}

QJsonObject obj = doc.object();
QString name = obj["name"].toString();
int age = obj["age"].toInt();

// 处理数组
QJsonArray skills = obj["skills"].toArray();
for (const auto &skill : skills) {
    qDebug() << "技能:" << skill.toString();
}

// 处理嵌套对象
QJsonObject addr = obj["address"].toObject();
QString city = addr["city"].toString();

16.3.2 构建JSON数据

QJsonObject user;
user["name"] = "李四";
user["age"] = 32;

QJsonArray tags;
tags.append("工程师");
tags.append("项目经理");
user["tags"] = tags;

QJsonObject config;
config["user"] = user;
config["version"] = "1.0.0";

QJsonDocument doc(config);
QString jsonOutput = doc.toJson(QJsonDocument::Indented);
// 保存到文件
QFile file("user.json");
file.open(QIODevice::WriteOnly);
file.write(jsonOutput.toUtf8());
file.close();

关键区别:JSON解析比XML快得多,代码也更简洁。但XML有Schema验证、命名空间等高级功能,JSON没有。选哪个?看场景。如果是配置文件,我推荐JSON。如果是需要严格数据验证的场合,XML更合适。

16.4 实战对比:XML vs JSON

特性 XML JSON
可读性 标签多,略显臃肿 简洁明了
解析速度 较慢(尤其DOM)
内存占用 流式低,DOM高 整体较低
数据类型 纯文本 支持数字、布尔、数组
验证机制 有DTD/Schema 无内置验证
适用场景 配置文件、文档存储 Web API、数据交换

避坑指南:我曾经在解析JSON时犯过一个低级错误——用QJsonDocument::fromJson()后没检查isNull()。结果程序跑起来一直崩溃,查了半天才发现是JSON字符串里多了一个逗号。记住:任何解析操作都要做错误检查!

嗯,XML和JSON的解析就说这么多。实际开发中,你八成会遇到混合使用的情况——比如从JSON接口拿数据,然后转成XML存到本地。Qt的这套API设计得挺顺手,多用几次就熟练了。

我的建议:新手先从JSON入手,它更直观。等需要处理遗留系统或者做文档格式转换时,再深入研究XML的三种解析方式。别一口气全学,容易消化不良。


公众号:蓝海资料掘金营,微信deep3321