SAX2
Qt 的 SAX 类基于 SAX2 的 Java 实现,不过具有一些必要的名称上的转换。相比 DOM,SAX 的实现更底层因而处理起来通常更快。但是,我们前面介绍的QXmlStreamReader类更偏向 Qt 风格的 API,并且比 SAX 处理器更快,所以,现在我们之所以使用 SAX API,更主要的是为了把 SAX API 引入 Qt。在我们通常的项目中,并不需要真的使用 SAX。
Qt 提供了QXmlSimpleReader类,提供基于 SAX 的 XML 处理。同前面所说的 DOM 方式类似,这个类也不会对 XML 文档进行有效性验证。QXmlSimpleReader可以识别良格式的 XML 文档,支持 XML 命名空间。当这个处理器读取 XML 文档时,每当到达一个特定位置,都会调用一个用于处理解析事件的处理类。注意,这里所说的“事件”,不同于 Qt 提供的鼠标键盘事件,这仅是处理器在到达预定位置时发出的一种通知。例如,当处理器遇到一个标签的开始时,会发出“新开始一个标签”这个通知,也就是一个事件。
准备工作
需要定义好用于表示一个XML节点的结构体,我把这个结构体命名为XMLNode,每个节点包含若干个属性(attributes),若干个子节点(children), 节点的名字(name),节点的内容文本(characters)和一个父节点(parent)。这样一来可以得到一个结构体:
typedef struct xml_node_t{
XMLAttributes attributes; //属性
QVector<xml_node_t*> children; //子节点
QString name; //节点名称
QString characters; //节点文本内容
xml_node_t* parent; //父节点
}XMLNode;
这个结构体即为解析XML所需的节点结构体(XMLNode)。
XMLNode中的属性(XMLAttributes)这样定义:
typedef struct xml_attribute_t{
QString name; //名字
QString value; //值
}XMLAttribute;
一个属性由一个名字(name)和一个值(value)组成。
PS: 为了方便使用,我往XMLNode这个结构体中加入了若干个成员函数,这几个函数分别是:
获取某一个属性的值:
QString getAttribute(const QString& name){
QString value = "";
for(int i = 0; i < attributes.count(); i++){
if(attributes[i].name == name){
value = attributes[i].value;
break;
}
}
return value;
}
设置某一个属性的值:
void setAttribute(const QString& name, const QString& value){
for(int i = 0; i < attributes.count(); i++){
if(attributes[i].name == name){
attributes[i].value = value;
return;
}
}
XMLAttribute attr;
attr.name = name;
attr.value = value;
attributes << attr;
}
通过节点名称获取XMLNode的某个子节点:
QVector<xml_node_t*> getChildrenByName(QString name){
QVector<xml_node_t*> result;
for(int i = 0; i < children.count(); i++){
if(children[i]->name == name){
result << children[i];
}
}
return result;
}
为了方便清理节点,在析构的时候将其子节点也一并析构:
~xml_node_t(){
xml_node_t* p = this->parent;
if(p)
{
for(int i = p->children.count() - 1; i >=0; i-- )
{
if(p->children[i] == this)
p->children.remove(i);
}
}
for(int i = 0; i < this->children.count(); i++){
xml_node_t* child = this->children[i];
if(child)
{
child->parent = 0;
delete child;
}
}
}
将节点列表定义成XMLNodeVector
typedef QVector<XMLNode*> XMLNodeVector;
实现Sax2解析处理对象
我把这个对象命名为EO_XmlSax2Parser,这个对象就是用来当系统在扫描XML文件时所发生的各种事件的。
[注]:这里仅实现了比较常用的事件,小伙伴们如果有特别需要可以研究一下所需要的其他事件。
1)首先,EO_XmlSax2Parse需要一个保存当前解析到的节点数量的变量(currentNodeCount),需要一个XML文件的根节点(root,类型为XMLNode),以及一个用于辅助解析的栈(rootStack,这个栈的最顶层一定是下一层的子节点,最底层一定是root)。
2)得定义Sax2解析的事件处理函数
[注]: 我没有描述到的参数是我这个示例中没有用到的(实际上也不常用),如果有需要的小朋友可以自行去查阅。
① 信号处理函数: bool startElement(namespaceUrl, localName, qName, atts)这个函数为了处理当系统扫描到一个新节点的事件,其中localName为节点名称(即为XML标签名),atts即为属性列表(节点中的属性)。返回值即为是否处理正常,如果返回false则系统会报错(这时候会调用错误处理函数)返回true则表示这个事件处理成功。
② 信号处理函数:bool endElement(namespaceUrl, localName, qName)
这是处理当系统扫描到一个节点尾部(探测到节点结尾)的事件,其中localName为该节点名称 。返回值表示是否处理成功。
③ 信号处理函数:bool characters(ch)
这个信号处理函数是用来处理扫描到节点内容的(内容不包括子节点,比如’123’的a节点中会扫描到‘123’作为ch传入,而d节点中则会是空’'或是不发送信号。返回值表示是否处理成功。
④ 处理处理函数 bool fatalError(e)、bool error(e)、bool warning(e)
这几个处理函数是用来处理错误的,我没有仔细研究(实际上如果进来这几个函数一般就是xml文件格式有误,你要决定是否退出解析)如果返回false则正常退出,如果返回true则会尝试继续解析(但可能导致程序崩溃),一般来讲直接return false就好了,如果有特别需要的小朋友可以自行去研究一下。
3)为了方便使用加入一些实用函数
① bool parseFromFile(filename),通过文件名开始解析,返回是否解析成功。
② bool parseFromData(data),通过传入XML数据开始解析,返回是否解析成功。
③XMLNode* root(),返回解析好的根节点root,如果解析失败则返回的是NULL。
4)具体实现
可能有的小朋友到这还是没有头绪这是个很不好的感觉,好在Qt有帮忙实现所需的Sax2事件处理函数,这个实现在Qt类 'QXmlDefaultHandler’及其派生类中(使用这个类得包含模块xml, QT+= xml)。
有了Qt接下来只需要将EO_XmlSax2Parser继承QXmlDefaultHandler就好了,以下是它的声明:
class EO_XmlSax2Parser : public QXmlDefaultHandler
{
private:
quint64 mCurrentNodeCount;
QStack<XMLNode*> mRootStack;
XMLNode* mRoot;
public:
explicit EO_XmlSax2Parser();
~EO_XmlSax2Parser();
bool parseFromFile(QString filename);
bool parseFromData(QString data);
XMLNode* root();
bool startElement(const QString &namespaceURI, const QString &localName, const QString &qName, const QXmlAttributes &atts);
bool endElement(const QString &namespaceURI, const QString &localName, const QString &qName);
bool characters(const QString &ch);
bool fatalError(const QXmlParseException &exception);
bool error(const QXmlParseException &exception);
bool warning(const QXmlParseException &exception);
};
① startElement的处理,这个事件一出发就代表扫描到了一个新节点,这个时候立马创建一个新节点XMLNode* newNode = new XMLNode 并对其赋值(节点名字、属性),接下来要看这个节点是不是XML文件的根节点(root == NULL,如果为true则newNode为根节点,否则肯定是子节点)。如果是XML根节点则root == newNode,如果是子节点那么肯定是上一个根节点的子节点,这时候直接放入上一个根节点的子节点队列中:rootStack.top().children.push(newNode)。
由于不能确定在这个节点结束之前是否会包含子节点,所以将这个newNode作为临时的根节点放到栈中: rootStack.push(newNode)。然后结束处理return true;
②characters的处理,当进入这个事件后先判断当前rootStack当中是否已经有临时根节点,如果没有则返回false(说明XML解析出了问题,starElement当中没有rootStack.push(newNode)怎么可能会到characters呢?明显有问题,直接返回false);如果rootStack顶层有有效节点,则ch为这个节点的文本,所以rootStack.top().characters = ch,处理完毕之后返回true。
③endElement的处理,到了这一步就意味着当前正在处理的节点已经完毕了,所以直接判断一下当前正在处理的节点和处理完毕的节点是不是同一个,如果不是,则说明解析过程出了问题或则是XML不对。如果比较节点名不一致,直接返回false。如果一致则rootStack.pop()弹出已经处理完毕的临时根节点。
(说明:所有的XML处理步骤大致就是扫描到头之后提取属性和名字并放入临时根节点栈顶,扫描到文本的时候赋值到对应节点,当结束一个节点的时候则将临时根节点从栈顶弹出。)
#include <QXmlSimpleReader>
#include <QDebug>
EO_XmlSax2Parser::EO_XmlSax2Parser()
:
mRoot(NULL),
mCurrentNodeCount(0)
{
}
EO_XmlSax2Parser::~EO_XmlSax2Parser()
{
if(mRoot != NULL)
{
delete mRoot;
mRoot = NULL;
}
}
bool EO_XmlSax2Parser::parseFromFile(QString filename)
{
if(mRoot != NULL)
{
delete mRoot;
mRoot = NULL;
}
QFile file(filename);
if(!file.open(QIODevice::ReadOnly))
{
qDebug() << "xml parser " << filename << " can not open";
return false;
}
QXmlInputSource source(&file);
QXmlSimpleReader reader;
reader.setContentHandler(this);
bool isOk = reader.parse(source);
file.close();
qDebug() << "xml parser " << filename << " parse Status:" << isOk;
return isOk;
}
bool EO_XmlSax2Parser::parseFromData(QString data)
{
if(mRoot != NULL)
{
delete mRoot;
mRoot = NULL;
}
QXmlInputSource source;
source.setData(data);
QXmlSimpleReader reader;
reader.setContentHandler(this);
bool isOk = reader.parse(source);
qDebug() << "xml parser parse Status:" << isOk;
return isOk;
}
XMLNode *EO_XmlSax2Parser::root()
{
return mRoot;
}
bool EO_XmlSax2Parser::startElement(const QString &namespaceURI, const QString &localName, const QString &qName, const QXmlAttributes &atts)
{
// qDebug() << "start element:" << localName;
XMLNode* newNode = new XMLNode;
newNode->name = localName;
XMLAttributes newAttributes;
for(int i = 0; i < atts.count(); i++)
{
XMLAttribute newAttribute;
newAttribute.name = atts.localName(i);
newAttribute.value = atts.value(i);
newAttributes << newAttribute;
}
newNode->attributes = newAttributes;
if(mRoot == NULL)
{
mRoot = newNode;
}
else
{
newNode->parent = mRootStack.top();
mRootStack.top()->children << newNode;
}
mRootStack.push(newNode);
return true;
}
bool EO_XmlSax2Parser::endElement(const QString &namespaceURI, const QString &localName, const QString &qName)
{
// qDebug() << "end element:" << localName;
if(mRootStack.count() <= 0)
{
return false;
}
if(mRootStack.top()->name != localName)
{
return false;
}
mRootStack.pop();
return true;
}
bool EO_XmlSax2Parser::characters(const QString &ch)
{
// qDebug() << "characters:" << ch;
if(mRootStack.count() <= 0)
{
return false;
}
mRootStack.top()->characters = ch;
return true;
}
bool EO_XmlSax2Parser::fatalError(const QXmlParseException &exception)
{
qDebug() << exception.message();
return false;
}
bool EO_XmlSax2Parser::error(const QXmlParseException &exception)
{
qDebug() << exception.message();
return false;
}
bool EO_XmlSax2Parser::warning(const QXmlParseException &exception)
{
qDebug() << exception.message();
return false;
}
结果
放到D:/test.xml中
<target name="os-detect">
<condition property="os.family" value="mac">
<os family="mac"/>
</condition>
<condition property="os.family" value="windows">
<os family="windows"/>
</condition>
<property name="os.family" value="unix"/>
<condition property="is-mac" value="1">
<equals arg1="${os.family}" arg2="mac"/>
</condition>
<condition property="is-windows" value="1">
<equals arg1="${os.family}" arg2="windows"/>
</condition>
<condition property="is-unix" value="1">
<or>
<equals arg1="${os.family}" arg2="unix"/>
<equals arg1="${os.family}" arg2="mac"/>
<equals arg1="${os.family}" arg2="cygwin"/>
</or>
</condition>
</target>
执行如下代码进行解析:
EO_XmlSax2Parser parser;
parser.parseFromFile("D:/test.xml");
qDebug()<< "root node :" << parser.root()->name;
qDebug()<< "child count for root:" << parser.root()->children.count();
得到结果
示例2
xml
<doc>
<quote>Gnothi seauton</quote>
</doc>
当读取这个 XML 文档时,处理器会依次发出下面的事件:
startDocument()
startElement("doc")
startElement("quote")
characters("Gnothi seauton")
endElement("quote")
endElement("doc")
endDocument()
每出现一个事件,都会有一个回调,这个回调函数就是在称为 Handler 的处理类中定义的。上面给出的事件都是在QXmlContentHandler接口中定义的。为简单起见,我们省略了一些函数。QXmlContentHandler仅仅是众多处理接口中的一个,我们还有QXmlEntityResolver,QXmlDTDHandler,QXmlErrorHandler,QXmlDeclHandler以及QXmlLexicalHandler等。这些接口都是纯虚类,分别定义了不同类型的处理事件。对于大多数应用程序,QXmlContentHandler和QXmlErrorHandler是最常用的两个。
为简化处理,Qt 提供了一个QXmlDefaultHandler。这个类实现了以上所有的接口,每个函数都提供了一个空白实现。也就是说,当我们需要实现一个处理器时,只需要继承这个类,覆盖我们所关心的几个函数即可,无需将所有接口定义的函数都实现一遍。这种设计在 Qt 中并不常见,但是如果你熟悉 Java,就会感觉非常亲切。Java 中很多接口都是如此设计的。
使用 SAX API 与QXmlStreamReader或者 DOM API 之间最大的区别是,使用 SAX API 要求我们必须自己记录当前解析的状态。在另外两种实现中,这并不是必须的,我们可以使用递归轻松地处理,但是 SAX API 则不允许(回忆下,SAX 仅允许一遍读取文档,递归意味着你可以先深入到底部再回来)。
下面我们使用 SAX 的方式重新解析前面所出现的示例程序。
class MainWindow : public QMainWindow ,public QXmlDefaultHandler
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();
bool readFile(const QString &fileName);
protected:
bool startElement(const QString &namespaceURI,
const QString &localName,
const QString &qName,
const QXmlAttributes &attributes);
bool endElement(const QString &namespaceURI,
const QString &localName,
const QString &qName);
bool characters(const QString &str);
bool fatalError(const QXmlParseException &exception);
private:
QTreeWidget *treeWidget;
QTreeWidgetItem *currentItem;
QString currentText;
private:
Ui::MainWindow *ui;
};
注意,我们的MainWindow不仅继承了QMainWindow,还继承了QXmlDefaultHandler。也就是说,主窗口自己就是 XML 的解析器。我们重写了startElement(),endElement(),characters(),fatalError()几个函数,其余函数不关心,所以使用了父类的默认实现。成员变量相比前面的例子也多出两个,为了记录当前解析的状态。
MainWindow的构造函数和析构函数同前面没有变化:
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
setWindowTitle(tr("XML SAX2 Reader"));
treeWidget = new QTreeWidget(this);
QStringList headers;
headers << "Items" << "Pages";
treeWidget->setHeaderLabels(headers);
setCentralWidget(treeWidget);
}
下面来看 readFile() 函数:
bool MainWindow::readFile(const QString &fileName)
{
currentItem = 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。parse()函数是QXmlSimpleReader提供的函数,开始进行 XML 解析。
bool MainWindow::startElement(const QString & /*namespaceURI*/,
const QString & /*localName*/,
const QString &qName,
const QXmlAttributes &attributes)
{
if (qName == "entry")
{
currentItem = new QTreeWidgetItem(currentItem ?
currentItem : treeWidget->invisibleRootItem());
currentItem->setText(0, attributes.value("term"));
}
else if (qName == "page")
{
currentText.clear();
}
//this->errorString();错误提示
return true;//最后,我们返回 true,告诉 SAX 继续处理文件。如果有任何错误,则可以返回 false 告诉 SAX 停止处理。
}
startElement()在读取到一个新的开始标签时被调用。这个函数有四个参数,我们这里主要关心第三和第四个参数:第三个参数是标签的名字(正式的名字是“限定名”,qualified name,因此形参是 qName);第四个参数是属性列表。前两个参数主要用于带有命名空间的 XML 文档的处理,现在我们不关心命名空间。函数开始,如果是 标签,我们创建一个新的QTreeWidgetItem。如果这个标签是嵌套在另外的 标签中的,currentItem 被定义为当前标签的子标签,否则则是根标签。我们使用setText()函数设置第一列的值,同前面的章节类似。如果是 标签,我们将 currentText 清空,准备接下来的处理。最后,我们返回 true,告诉 SAX 继续处理文件。如果有任何错误,则可以返回 false 告诉 SAX 停止处理。此时,我们需要覆盖QXmlDefaultHandler的errorString()函数来返回一个恰当的错误信息。
bool MainWindow::characters(const QString &str)
{
currentText += str;
return true;
}
注意下我们的 XML 文档。characters()仅在 标签中出现。因此我们在characters()中直接追加 currentText。
bool MainWindow::endElement(const QString & /*namespaceURI*/,
const QString & /*localName*/,
const QString &qName/*标签名字*/)
{
if (qName == "entry")
{
currentItem = currentItem->parent();//
}
else if (qName == "page")
{
if (currentItem)
{
QString allPages = currentItem->text(1);
if (!allPages.isEmpty())
allPages += ", ";
allPages += currentText;
currentItem->setText(1, allPages);
}
}
return true;
}
endElement()在遇到结束标签时调用。和startElement()类似,这个函数的第三个参数也是标签的名字。我们检查如果是 ,则将 currentItem 指向其父节点。这保证了 currentItem 恢复到处理 标签之前所指向的节点。如果是 ,我们需要把新读到的 currentText 追加到第二列。
bool MainWindow::fatalError(const QXmlParseException &exception)
{
QMessageBox::critical(this,
tr("SAX Error"),
tr("Parse error at line %1, column %2:\n %3")
.arg(exception.lineNumber())
.arg(exception.columnNumber())
.arg(exception.message()));
return false;
}
当遇到处理失败的时候,SAX 会回调fatalError()函数。我们这里仅仅向用户显示出来哪里遇到了错误。如果你想看这个函数的运行,可以将 XML 文档修改为不合法的形式。