简述
SAX(Simple API for XML)是用于 XML 解析器的基于事件的标准接口。XML 类的设计遵循 SAX2 Java interface,名称适合 Qt 的命名约定。对于任何使用 SAX2 的人来说,使用 Qt XML 类应该非常容易。
SAX 不同于 DOM 解析,它逐行扫描文档,一边扫描一边解析。由于应用程序只是在读取数据时检查数据,因此不需要将数据存储在内存中,这对于大型文档的解析是个巨大优势。
Qt XML 模块提供了一个抽象类 QXmlReader,它定义了潜在的 SAX2 读取器的接口,它有一个简单的 XML 读取器的实现 - QXmlSimpleReader(目前只有这一个,在将来的版本中,可能有更多的具有不同属性的读取器。例如:验证解析器),通过子类化,很容易适应。
QXmlSimpleReader
QXmlSimpleReader 类提供了一个简单的 XML 解析器的实现。
这种 XML reader(读取器)适用于广泛的应用,它能够解析格式良好的 XML,并可以将元素的命名空间报告给 ContentHandler。然而,它不解析任何外部实体。由于历史原因,不执行 XML 1.0 规范中描述的属性值规范化和结束处理。
此类的最简单的使用模式是创建一个读取器实例,定义一个输入源,指定读取器使用的 handler(处理程序),并解析数据。
例如,我们可以使用 QFile 来提供输入。在这里,创建一个读取器,并定义一个输入源供读取器使用:
QXmlSimpleReader xmlReader;
QXmlInputSource *source = new QXmlInputSource(file);
在读取器遇到某些类型的内容,或者输入发生错误时,handler 允许我们执行操作。必须告诉读取器哪种类型的事件使用哪个 handler 。对于许多常见的应用程序,可以通过对 QXmlDefaultHandler 进行子类化来创建自定义处理程序,并使用它来处理错误和内容事件:
Handler *handler = new Handler;
xmlReader.setContentHandler(handler);
xmlReader.setErrorHandler(handler);
如果既不设置 ContentHandler,又不设置 ErrorHandler,解析器将回退到其默认行为,并且什么也不做。
处理输入的最方便的方法是在单次传递中读取它,通过使用带有一个参数(指定输入源)的 parse() 函数:
bool ok = xmlReader.parse(source);
if (!ok)
std::cout << "Parsing failed." << std::endl;
如果不能一次性解析整个输入(例如:XML 比较巨大,或正在通过网络连接传递),数据可以被分片发送至到解析器。这个通过告诉 parse() 逐步工作来实现,并对 parseContinue() 函数进行后续调用,直到所有数据都被处理完成。
执行增量解析的常见方法是连接网络的 readyRead() 信号至一个从哦啊函数,并在那里处理传入的数据,可以参考:QNetworkAccessManager。
可以使用 setFeature() 和 setProperty() 来调整解析行为方面。
xmlReader.setFeature("http://xml.org/sax/features/namespace-prefixes", true);
QXmlSimpleReader 是不可重入的,如果要在线程代码中使用类,需要使用锁机制(例如:QMutex)来锁定使用 QXmlSimpleReader 的代码。
使用
为了便于演示,使用上节生成的格式化 XML(Blogs.xml):
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!--纯正开源之美,有趣、好玩、靠谱。。。-->
<?xml-stylesheet type="text/css" href="style.css"?>
<Blogs Version="1.0">
<Blog>
<作者>一去丶二三里</作者>
<主页>http://blog.csdn.net/liang19890820</主页>
<个人说明>青春不老,奋斗不止!</个人说明>
</Blog>
<Blog>
<作者>奇趣科技</作者>
<主页>https://www.qt.io</主页>
<个人说明>Qt 是由奇趣科技开发的跨平台 C++ 图形用户界面应用程序开发框架!</个人说明>
</Blog>
</Blogs>
详细说明见: Qt之生成XML(QXmlStreamWriter)
效果如下所示:
下面,一起来看下源码:
XbelHandler.h
#ifndef XBELHANDLER_H
#define XBELHANDLER_H
#include <QIcon>
#include <QXmlDefaultHandler>
QT_BEGIN_NAMESPACE
class QTreeWidget;
class QTreeWidgetItem;
QT_END_NAMESPACE
class XbelHandler : public QXmlDefaultHandler
{
public:
XbelHandler(QTreeWidget *treeWidget);
// 启动 XML 解析
bool readFile(const QString &fileName);
// 解析开始元素
bool startElement(const QString &namespaceURI, const QString &localName,
const QString &qName, const QXmlAttributes &attributes) Q_DECL_OVERRIDE;
// 解析结束元素
bool endElement(const QString &namespaceURI, const QString &localName,
const QString &qName) Q_DECL_OVERRIDE;
// 解析字符数据
bool characters(const QString &str) Q_DECL_OVERRIDE;
// 解析过程中的错误处理
bool fatalError(const QXmlParseException &exception) Q_DECL_OVERRIDE;
QString errorString() const Q_DECL_OVERRIDE;
private:
// 创建树节点
QTreeWidgetItem *createChildItem(const QString &tagName);
private:
QTreeWidget *treeWidget;
QTreeWidgetItem *item;
QString currentText;
QString errorStr;
QIcon folderIcon;
QIcon otherIcon;
};
#endif
XbelHandler.cpp
#include <QtWidgets>
#include "XbelHandler.h"
XbelHandler::XbelHandler(QTreeWidget *treeWidget)
: QXmlDefaultHandler(),
treeWidget(treeWidget)
{
item = 0;
QStyle *style = treeWidget->style();
folderIcon.addPixmap(style->standardPixmap(QStyle::SP_DirClosedIcon),
QIcon::Normal, QIcon::Off);
folderIcon.addPixmap(style->standardPixmap(QStyle::SP_DirOpenIcon),
QIcon::Normal, QIcon::On);
otherIcon.addPixmap(style->standardPixmap(QStyle::SP_FileIcon));
}
构造函数主要用于初始化变量。
来看 readFile() 函数:
// 启动 XML 解析
bool XbelHandler::readFile(const QString &fileName)
{
if (fileName.isEmpty())
return false;
treeWidget->clear();
item = 0;
QFile file(fileName);
QXmlInputSource inputSource(&file);
QXmlSimpleReader reader;
reader.setContentHandler(this);
reader.setErrorHandler(this);
return reader.parse(inputSource);
}
这个函数中,首先将成员变量清空,然后读取 XML 文档,用于加载新数据。注意:我们使用了QXmlSimpleReader,将 ContentHandler 和 ErrorHandler 设置为自身。因为我们仅重写了 ContentHandler 和 ErrorHandler 的函数。如果还需要另外的处理,还需要继续设置其它的 handler。设置完成之后,调用 QXmlSimpleReader 提供的 parse() 是函数,开始进行 XML 的解析。
// 解析开始元素
bool XbelHandler::startElement(const QString & /* namespaceURI */,
const QString & /* localName */,
const QString &qName,
const QXmlAttributes &attributes)
{
if (qName == "Blogs") {
QString version = attributes.value("Version");
if (!version.isEmpty() && version != "1.0") {
errorStr = QObject::tr("The file is not an XBEL version 1.0 file.");
return false;
}
item = createChildItem(qName);
item->setFlags(item->flags() | Qt::ItemIsEditable);
item->setIcon(0, folderIcon);
item->setText(0, qName);
treeWidget->setItemExpanded(item, true);
} else if (qName == "Blog") {
item = createChildItem(qName);
item->setFlags(item->flags() | Qt::ItemIsEditable);
item->setIcon(0, folderIcon);
item->setText(0, qName);
treeWidget->setItemExpanded(item, true);
} else if (qName == QString::fromLocal8Bit("作者")
|| qName == QString::fromLocal8Bit("主页")
|| qName == QString::fromLocal8Bit("个人说明")) {
item = createChildItem(qName);
item->setFlags(item->flags() | Qt::ItemIsEditable);
item->setIcon(0, otherIcon);
item->setText(0, qName);
}
currentText.clear();
return true;
}
在读取到一个新的开始标签时调用 startElement()。该函数有四个参数,这里主要关心第三和第四个参数:第三个参数是标签的名字(正式的名字是“限定名”qualified name,因此形参是 qName);第四个参数是属性列表。前两个参数主要用于带有命名空间的 XML 文档的处理,现在我们不关心命名空间。
函数开始,如果是 <Blogs>
标签,先判断标签中的属性值是否为“1.0”,如果不是,返回 false, 告诉 SAX 停止处理;否则创建一个新的 QTreeWidgetItem,设置图标、文本。然后将 currentText 清空,准备接下来的处理。最后,返回 true,告诉 SAX 继续处理文件。
// 解析字符数据
bool XbelHandler::characters(const QString &str)
{
currentText += str;
return true;
}
函数 characters() 将标签的内容保存至成员变量 currentText 中,保存到变量的 currentText 的值由 endElement() 设置为节点的文本。
**注意:**XML 文档中 characters() 在 <作者>
、<主页>
、<个人说明>
标签中出现。
// 解析结束元素
bool XbelHandler::endElement(const QString & /* namespaceURI */,
const QString & /* localName */,
const QString &qName)
{
if (qName == "Blog") {
if (item) {
item = item->parent();
}
} else if (qName == QString::fromLocal8Bit("作者")
|| qName == QString::fromLocal8Bit("主页")
|| qName == QString::fromLocal8Bit("个人说明")) {
if (item) {
item->setText(1, currentText);
item = item->parent();
}
}
return true;
}
在遇到结束标签时调用 endElement()。和 startElement() 类似,这个函数的第三个参数也是标签的名字。当检查如果是 </Blog>
,则将 item 指向其父节点,这保证了 item 恢复到处理 <Blog>
标签之前所指向的节点。如果是 </作者>
、</主页>
、</个人说明>
,需要把新读到的 currentText 追加到第二列,同时将 item 指向其父节点。
// 解析过程中的错误处理
bool XbelHandler::fatalError(const QXmlParseException &exception)
{
QMessageBox::information(treeWidget->window(), QObject::tr("SAX Parser"),
QObject::tr("Parse error at line %1, column %2:\n"
"%3")
.arg(exception.lineNumber())
.arg(exception.columnNumber())
.arg(exception.message()));
return false;
}
当遇到处理失败的时候,SAX 会回调 fatalError() 函数。这里仅仅向用户显示出来哪里遇到了错误。如果想看这个函数的运行,可以将 XML 文档修改为不合法的形式。
// 返回错误字符串
QString XbelHandler::errorString() const
{
return errorStr;
}
当 handler 的任何一个函数返回 false 时,errorString() 会得到一个错误的字符串。
// 创建树节点
QTreeWidgetItem *XbelHandler::createChildItem(const QString &tagName)
{
QTreeWidgetItem *childItem;
if (item) {
childItem = new QTreeWidgetItem(item);
} else {
childItem = new QTreeWidgetItem(treeWidget);
}
childItem->setData(0, Qt::UserRole, tagName);
return childItem;
}
在解析过程中,为了构建一颗树形控件,调用 createChildItem() 根据指定的标签名,来添加节点。
构建完 handler 后,就可以直接使用了。
// 加载 XML
void MainWindow::open()
{
QString fileName =
QFileDialog::getOpenFileName(this, tr("Open File"),
QDir::currentPath(),
tr("XBEL Files (*.xbel *.xml)"));
if (fileName.isEmpty())
return;
XbelHandler handler(m_pTreeWidget);
if (handler.readFile(fileName))
qDebug() << "File loaded";
}
打开文件选择对话框,选择文件,解析!