跳转到内容
新建笔记

Qt XML:命名空间、任务模板、DOM 修改与 Python 读取

Qt 中的 XML 处理首先要区分顺序读取和文档修改。QXmlStreamReader 逐个读取标记,适合扫描或提取字段;QDomDocument 在内存中保留整棵树,便于查找、创建和修改节点。

本篇保留原笔记的流式读取、DOM、.ui 检查、配置创建与修改、Python 解析,以及频谱任务模板示例。C++ 程序采用 C++17;DOM 的 ParseResult 接口要求 Qt 6.5 或更新版本。

需求当前接口边界
顺序检查开始/结束元素、属性和文本QXmlStreamReader,属于 Qt Core流式读取不代表程序积累的结果也没有内存开销
随机访问或修改小型文档QDomDocument,链接 Qt6::Xml整棵 DOM 占用内存,复制句柄也不等于深复制树
创建顺序输出文档QXmlStreamWriter适合按既定结构写出,仍要检查设备错误
维护旧 SAX 代码旧 QXmlSimpleReader 等已从 Qt 6 的 Qt XML 移除;过渡代码可使用 Core5Compat

不要将旧 SAX 类作为新 Qt 6 项目的默认选择。官方迁移建议使用 QXmlStreamReader;兼容模块只是帮助维护旧代码。Qt 6 XML 变化

流式阅读的基本循环是反复调用 readNext():isStartElement() 时读名称和属性,isEndElement() 时处理闭合,isCharacters() 时读取文本。文本可能被分成多个标记;trimmed() 只适合你已经决定忽略首尾空白的字段,不能对任意 XML 文本都这样处理。QXmlStreamReader

任务模板:名字相同还不够

跳转到“任务模板:名字相同还不够”

原示例中的默认命名空间是 http://www.xxxx.com/cm,Python 却查找 http://www.pruftechnik.com/cm,因此找不到对应元素。URI 不一致就是不同的命名空间;它在这里是标识符,不是要求解析器访问的网页。

下面继续使用原 XML 的示例 URI,并明确把它当作教学格式,不声称这是厂商实际文件的完整规范。真实文件应按它自己的 URI、层级和版本约定解析。

层级保留的字段与示例值
task_templateversion=4.0.0、task_id 末尾为 003、name=Spec (a) 10Hz - 12800Hz、source=factory
meas_setupkind=trending_spectrum_amplitude
spectrumname=Spectrum Velocity、res_id 末尾为 002、quant=acc、lines=51200、window=hanning
time_waveform_configname=Time Waveform Velocity、res_id 末尾为 001、quant=acc
averagenum=3、overlap=60、factor=0.5、type=lin
freq_spectrummax=12800、max_type=time_based、min=10

旧 C++ 代码只比较 xml.name(),会把错误命名空间或错误层级中的同名元素也当成目标。本例同时检查 URI 和完整元素路径,要求六个节点各出现一次、必需属性非空。它只验证这个明确的教学子集,尚未验证字段间的全部业务关系。

完整例子一:按命名空间与层级读取

跳转到“完整例子一:按命名空间与层级读取”

此程序只需 Qt6::Core。它拒绝超过 1 MiB 的输入、超过 16 层的结构、DTD、未知层级和非空文本;这些是本示例格式的边界,不是 XML 标准的一般限制。用 Debug 配置运行断言。

#include <QByteArray>
#include <QDebug>
#include <QMap>
#include <QStringList>
#include <QXmlStreamReader>
#include <cassert>
#include <stdexcept>
using Attributes = QMap<QString, QString>;
using TaskFields = QMap<QString, Attributes>;
const QString taskNamespace = QStringLiteral("http://www.xxxx.com/cm");
TaskFields readTask(const QByteArray& bytes) {
if (bytes.size() > 1024 * 1024)
throw std::runtime_error("Task input exceeds 1 MiB");
const QString root = QStringLiteral("task_template");
const QString setup = root + "/meas_setup";
const QString spectrum = setup + "/spectrum";
const QMap<QString, QStringList> required{
{root, {"version", "task_id", "name", "source"}},
{setup, {"kind"}},
{spectrum, {"name", "res_id", "quant", "lines", "window"}},
{spectrum + "/time_waveform_config", {"name", "res_id", "quant"}},
{spectrum + "/average", {"num", "overlap", "factor", "type"}},
{spectrum + "/freq_spectrum", {"max", "max_type", "min"}}
};
QXmlStreamReader xml(bytes);
QStringList stack;
TaskFields fields;
while (!xml.atEnd()) {
xml.readNext();
if (xml.isDTD()) {
xml.raiseError("DTD is not part of this task format");
} else if (xml.isStartElement()) {
stack.append(xml.name().toString());
const QString key = stack.join('/');
if (stack.size() > 16 || xml.namespaceUri() != taskNamespace ||
!required.contains(key) || fields.contains(key)) {
xml.raiseError("Unexpected namespace, path, depth or duplicate element");
break;
}
Attributes values;
for (const QString& attribute : required.value(key)) {
// 这些属性不带命名空间,按本格式的明确约定读取。
const QString value = xml.attributes().value(QString{}, attribute).toString();
if (value.isEmpty()) {
xml.raiseError("Missing required attribute");
break;
}
values.insert(attribute, value);
}
if (xml.hasError())
break;
fields.insert(key, values);
} else if (xml.isEndElement()) {
if (!stack.isEmpty())
stack.removeLast();
} else if (xml.isCharacters() && !xml.isWhitespace()) {
xml.raiseError("This task format stores values in attributes");
}
}
if (xml.hasError()) {
const QString message = QStringLiteral("%1 at %2:%3")
.arg(xml.errorString()).arg(xml.lineNumber()).arg(xml.columnNumber());
throw std::runtime_error(message.toUtf8().constData());
}
if (fields.size() != required.size())
throw std::runtime_error("Missing required task element");
bool ok = false;
const int lines = fields.value(spectrum).value("lines").toInt(&ok);
if (!ok || lines <= 0)
throw std::runtime_error("lines must be a positive int");
return fields;
}
bool rejected(const QByteArray& bytes) {
try { (void)readTask(bytes); }
catch (const std::runtime_error&) { return true; }
return false;
}
int main() {
const QByteArray sample = R"xml(<?xml version="1.0" encoding="UTF-8"?>
<task_template xmlns="http://www.xxxx.com/cm" version="4.0.0" task_id="00000000-0000-0000-0000-000000000003" name="Spec (a) 10Hz - 12800Hz" source="factory">
<meas_setup kind="trending_spectrum_amplitude">
<spectrum name="Spectrum Velocity" res_id="00000000-0000-0000-0000-000000000002" quant="acc" lines="51200" window="hanning">
<time_waveform_config name="Time Waveform Velocity" res_id="00000000-0000-0000-0000-000000000001" quant="acc"/>
<average num="3" overlap="60" factor="0.5" type="lin"/>
<freq_spectrum max="12800" max_type="time_based" min="10"/>
</spectrum>
</meas_setup>
</task_template>)xml";
const TaskFields fields = readTask(sample);
assert(fields.size() == 6);
const QString spectrum = QStringLiteral("task_template/meas_setup/spectrum");
assert(fields.value(spectrum).value("lines") == "51200");
assert(fields.value(spectrum + "/freq_spectrum").value("max") == "12800");
for (auto node = fields.cbegin(); node != fields.cend(); ++node)
qDebug().noquote() << node.key() << node.value();
QByteArray wrongNamespace = sample;
wrongNamespace.replace("http://www.xxxx.com/cm", "urn:wrong");
assert(rejected(wrongNamespace));
QByteArray missingAttribute = sample;
missingAttribute.replace(" lines=\"51200\"", "");
assert(rejected(missingAttribute));
QByteArray wrongType = sample;
wrongType.replace("lines=\"51200\"", "lines=\"abc\"");
assert(rejected(wrongType));
assert(rejected(sample.left(sample.size() - 16)));
}

读取实际文件时,先检查 QFile::open(),按上限读取并检查 QFile::error(),再交给解析器;不要先对任意文件 readAll(),然后才认为内存上限已受到保护。无事件的同步解析程序完成后直接退出,不能因为创建了 QCoreApplication 就无条件进入 exec()。

DOM:每个节点只访问一次

跳转到“DOM:每个节点只访问一次”

原遍历函数内部会走完整条兄弟链,外部又逐个兄弟调用它,造成重复。以下约定递归函数只负责当前节点及其子树,兄弟节点只由父循环推进一次。

DOM 的节点是共享句柄;要独立修改一份文档,应使用深度 cloneNode(true) 等方式,而不是假定普通复制隔离了内容。setContent 的结果还应保留错误信息及行列号。QDomDocument

下例中的 QtSettings 和 .ui XML 都没有命名空间。setContent(bytes) 默认关闭命名空间处理;解析带命名空间的文档时,应传入 QDomDocument::ParseOption::UseNamespaceProcessing,再按 URI 与本地名称检查节点,不能把这里按 tagName() 查找的代码直接当成前半篇任务模板的校验器。DOM 解析选项

完整例子二:创建、修改、保存和检查 UI XML

跳转到“完整例子二:创建、修改、保存和检查 UI XML”

程序需要 Qt6::Core 与 Qt6::Xml。它创建 QtSettings、800×600 的 MainWindow 和 btnOK,把文本改为 New Text,添加 btnCancel,随后保存并重新解析。setText 能处理空 Text 元素,不假设它一定已经有第一个文本子节点。

#include <QByteArray>
#include <QDomDocument>
#include <QFile>
#include <QMap>
#include <QSaveFile>
#include <QStringList>
#include <QTemporaryDir>
#include <cassert>
#include <stdexcept>
void require(bool success, const QString& message) {
if (!success)
throw std::runtime_error(message.toUtf8().constData());
}
QDomDocument parse(const QByteArray& bytes) {
QDomDocument document;
const auto result = document.setContent(bytes);
require(static_cast<bool>(result), QStringLiteral("%1 at %2:%3")
.arg(result.errorMessage).arg(result.errorLine).arg(result.errorColumn));
return document;
}
QDomElement add(QDomDocument& doc, QDomNode parent, const QString& tag,
const QString& text = {}) {
QDomElement element = doc.createElement(tag);
parent.appendChild(element);
if (!text.isNull())
element.appendChild(doc.createTextNode(text));
return element;
}
void setText(QDomDocument& doc, QDomElement element, const QString& text) {
while (!element.firstChild().isNull())
element.removeChild(element.firstChild());
element.appendChild(doc.createTextNode(text));
}
void visit(const QDomNode& node, QMap<QString, int>& counts) {
if (node.isElement())
++counts[node.toElement().tagName()];
for (QDomNode child = node.firstChild(); !child.isNull(); child = child.nextSibling())
visit(child, counts);
}
int main() {
QDomDocument doc;
doc.appendChild(doc.createProcessingInstruction("xml", "version=\"1.0\" encoding=\"UTF-8\""));
QDomElement root = add(doc, doc, "QtSettings");
root.setAttribute("version", "1.0");
QDomElement window = add(doc, root, "Window");
window.setAttribute("name", "MainWindow");
add(doc, window, "Width", "800");
add(doc, window, "Height", "600");
QDomElement ok = add(doc, root, "Button");
ok.setAttribute("name", "btnOK");
QDomElement okText = add(doc, ok, "Text", "OK");
setText(doc, okText, "New Text");
QDomElement cancel = add(doc, root, "Button");
cancel.setAttribute("name", "btnCancel");
QDomElement cancelText = add(doc, cancel, "Text");
setText(doc, cancelText, "Cancel");
QMap<QString, int> counts;
visit(doc.documentElement(), counts);
assert(counts.value("Button") == 2 && counts.value("Text") == 2);
assert(okText.text() == "New Text" && cancelText.text() == "Cancel");
QTemporaryDir temporary;
require(temporary.isValid(), QStringLiteral("Cannot create temporary directory"));
const QString filename = temporary.filePath("qt_config_modified.xml");
QSaveFile output(filename);
const bool opened = output.open(QIODevice::WriteOnly);
require(opened, output.errorString());
const QByteArray bytes = doc.toByteArray(2);
const qint64 written = output.write(bytes);
require(written == bytes.size(), output.errorString());
const bool committed = output.commit();
require(committed, output.errorString());
QFile input(filename);
const bool inputOpened = input.open(QIODevice::ReadOnly);
require(inputOpened, input.errorString());
const QByteArray loaded = input.readAll(); // 文件由本程序刚生成,尺寸已知很小
require(input.error() == QFileDevice::NoError, input.errorString());
const QDomDocument roundTrip = parse(loaded);
assert(roundTrip.documentElement().firstChildElement("Button")
.firstChildElement("Text").text() == "New Text");
const QDomDocument ui = parse(R"xml(<ui version="4.0"><class>Window</class>
<widget class="QWidget" name="Window"><widget class="QPushButton" name="btnOK"/></widget>
<connections><connection><sender>btnOK</sender><signal>clicked()</signal>
<receiver>Window</receiver><slot>close()</slot></connection></connections></ui>)xml");
const QDomNodeList widgets = ui.documentElement().elementsByTagName("widget");
assert(widgets.size() == 2);
QStringList widgetDescriptions;
for (int i = 0; i < widgets.size(); ++i) {
const QDomElement element = widgets.at(i).toElement();
widgetDescriptions.append(element.attribute("name") + ":" + element.attribute("class"));
}
assert(widgetDescriptions.contains("btnOK:QPushButton"));
const QDomNodeList connections = ui.documentElement().elementsByTagName("connection");
assert(connections.size() == 1);
const QDomElement connection = connections.at(0).toElement();
assert(connection.firstChildElement("sender").text() == "btnOK");
assert(connection.firstChildElement("signal").text() == "clicked()");
assert(connection.firstChildElement("receiver").text() == "Window");
assert(connection.firstChildElement("slot").text() == "close()");
}

示例对 .ui 只读取控件和连接声明,没有据此创建窗口或执行连接。真正的 Designer UI 加载见 .ui 文件与所有权。QSaveFile 在提交前使用临时文件写入,仍须检查 write() 与 commit(),不能把“打开成功”当成“保存成功”。QSaveFile

Python 解析同一份任务模板

跳转到“Python 解析同一份任务模板”

原 PyQt5 版本的 QFile/QXmlStreamReader 循环与前面的流式思路相同;维护旧 PyQt5 项目时按对应绑定版本使用这些类,不要将它的包名混入 Qt 6 C++ 构建要求。仅需离线处理这类小文件时,Python 标准库的 ElementTree 就能按完整命名空间查找元素。ElementTree 命名空间

下面是独立脚本,运行时传入 XML 路径。它提取上表中的全部字段,并明确处理文件错误、XML 语法错误以及缺失字段。它与前面的 C++ 格式检查不是完整等价校验器:此脚本只查必需的直接子节点,其他扩展节点没有被一律拒绝。

import argparse
import xml.etree.ElementTree as ET
NAMESPACE = "http://www.xxxx.com/cm"
def read_task(filename):
with open(filename, "rb") as source:
data = source.read(1024 * 1024 + 1)
if len(data) > 1024 * 1024:
raise ValueError("task input exceeds 1 MiB")
root = ET.fromstring(data)
if root.tag != f"{{{NAMESPACE}}}task_template":
raise ValueError("unexpected root or namespace")
def child(parent, name):
matches = parent.findall(f"{{{NAMESPACE}}}{name}")
if len(matches) != 1:
raise ValueError(f"expected exactly one {name}")
return matches[0]
setup = child(root, "meas_setup")
spectrum = child(setup, "spectrum")
nodes = {
"task_template": (root, ("version", "task_id", "name", "source")),
"meas_setup": (setup, ("kind",)),
"spectrum": (spectrum, ("name", "res_id", "quant", "lines", "window")),
"time_waveform_config": (child(spectrum, "time_waveform_config"),
("name", "res_id", "quant")),
"average": (child(spectrum, "average"), ("num", "overlap", "factor", "type")),
"freq_spectrum": (child(spectrum, "freq_spectrum"), ("max", "max_type", "min")),
}
result = {}
for name, (element, attributes) in nodes.items():
result[name] = {}
for attribute in attributes:
value = element.get(attribute)
if value is None or value == "":
raise ValueError(f"missing {name}.{attribute}")
result[name][attribute] = value
if int(result["spectrum"]["lines"]) <= 0:
raise ValueError("lines must be positive")
return result
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("filename")
arguments = parser.parse_args()
try:
for name, fields in read_task(arguments.filename).items():
print(name, fields)
except (OSError, ET.ParseError, ValueError) as error:
parser.exit(1, f"Cannot read task template: {error}\n")

历史示例使用的 D:/shawei/temp/jpbalance/temp/TaskTemplates/Shock Pulse.xml 只是当时的本机路径。脚本通过参数接收路径,避免把这一路径写死为其他人的运行前提。真实文件的频率单位、有效范围、量值代码和兼容版本,需要按实际格式另行验证。