65.9K
CodeProject 正在变化。 阅读更多。
Home

.NET 技术,用于将大型文本文件转换为针对 Android 对象的 XML 结构

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.67/5 (4投票s)

2012 年 7 月 5 日

CPOL

12分钟阅读

viewsIcon

28596

downloadIcon

536

本文描述了我用于摄取大型文本文件以在 Android Market 上创建电子书发布平台的技巧。


下载 kjvParser.zip

下载 kjvPro.apk

引言

网络上有许多方法可用于处理大型文件摄入,并将生成的数据集转换为 XML 数据,这些数据可以快速映射到移动设备上使用的 Android 对象模型 (AOM),而无需进一步的后处理。换句话说,生成的数据集可能非常大且难以处理,以至于尝试通过其他方法修改生成的 XML 以进一步完善模型将完全徒劳且耗时!这意味着解析器必须具备智能,以便在第一次运行时创建的 XML 能够立即符合底层 Android 对象的标准形式。它还进一步意味着,如果对象对这种摄入的 XML 的依赖性发生变化,那么这些对象的 XML 结构也可能随着时间的推移而改变!考虑到这些简短的目标,我将在本教程中向您展示如何实现一个简单但高效的解析策略,该策略可以在相对较短的时间内实现这些目标,而且不涉及使用 XML DOM、SAX 或其他已知解析器等复杂框架。我选择根本不使用这些框架的主要原因是为了简洁性、可用性和可移植性。我只是想创建一个简单的模型,一个可以轻松地移植到其他语言和平台的模型。由于成本节省和广泛的可用性,开源在整个行业中普遍存在,我希望有一种策略,让其他人以及我都能轻松修改,而不会过度依赖外部库。话虽如此,让我们深入探讨这项技术的细节!

背景

图 1. 在我的 Android 手机上运行的 Android 电子书阅读器,其中包含本文讨论的 XML 解析器

单击即可免费将此应用程序下载到您的 Android 手机!

我在 Android Market 上开发了一个简单的电子书发布框架。我的目标是创建一个轻量级的框架,该框架可以处理大型文档,并且能够相对快速轻松地加载大型 XML 文件!我做了一个假设,即文档本身会随着时间的推移而改变。但为什么呢?其实很简单……随着最终用户期望的变化、某些技术的进步以及我们应用程序功能集的扩展,我们的思维方式最终也会改变!这意味着 XML 模型也会随着时间的推移而改变!为了检验这些假设,我开始在 Android 上尝试我的第一本大型电子书。我想要一些不受版权限制且能生成大量 XML 数据供我的 Android 类摄入的内容!我在网上搜索了一下,发现了 KJV 不受版权限制……这正是我需要构建一个文本元数据解析框架所需的内容,该框架可用于将来转换为电子书的各种其他文档。





图 2. 在我的 Android NOOK 平板电脑上运行的 Android 电子书阅读器

单击即可免费将此应用程序下载到您的 Android 平板电脑!

简而言之,这就是我开始的,一个文本文件,其结果具有以下派生特性。

对于 INDEX.XML 文件,解析器将创建以下元素结构

1. 一个名为 </KJV> 的 XML 根元素

2. 一个名为 </BOOK> 的 XML 元素

3. 一个名为 </BOOKID> 的 XML 子元素

4. 一个名为 </BOOKNAME> 的 XML 子元素

图 3. 在 notepad.exe 中加载不受版权限制的 KJV 文档

从这些初始 XML 元素中,我决定将所有 **BOOK** 部分拆分成各自可索引的部分,这正式意味着所有 **BOOK** 标签将索引到包含 **BOOKID** 的 XML 文件中。从某种意义上说,这是指向 **INDEX** 文件的一个链接。这是一个不错的决定,因为当加载较小的单个部分而不是一个庞大的文本块时,Android 中的对象加载速度要快得多!这是本次练习的要点,也是我今天的提示!在从 XML 元数据创建大型对象模型时,应该将大型 XML 部分分解成更小的部分,否则最终用户将会在等待您的解析对象加载时感到非常沮丧!更不用说您的解析器对象可能面临的内存问题、超时等等……有时,我们都会从错误中吸取教训,并最终进一步完善那些繁琐的过程,这些过程可能会阻碍可衡量的进展!考虑到这一点,人们可能会考虑将生成的 XML 分割成更小的部分,这对于对象加载速度非常有帮助,因此您可以稍后感谢我告诉您所有这些!如果您遇到无法被最终用户接受的巨大等待时间,请回到并完善您的 XML 模型。将其分解成快速加载的块,然后从中进行!这会增加最终对象模型中类的粒度,但这是为了换取您稍后获得的性能而付出的微小代价,而且一切都会正常工作!

这是 C# .NET 解析器生成的 INDEX.XML

<?xml version="1.0" encoding="utf-8"?>
<KJV>
  <BOOK>
    <BOOKID>BOOK1</BOOKID>
    <BOOKNAME>Genesis</BOOKNAME>
  </BOOK>
  <BOOK>
    <BOOKID>BOOK2</BOOKID>
    <BOOKNAME>Exodus</BOOKNAME>
  </BOOK>
  <BOOK>
    <BOOKID>BOOK3</BOOKID>
    <BOOKNAME>Leviticus</BOOKNAME>
  </BOOK>
...
 
</KJV>

对于 BOOK#.XML 文件,已约定以下结构

1. - 一个名为 </BOOK#> 的 XML 根元素

2. - 一个名为 </BOOKNAME> 的 XML 子元素

3. - 一个名为 </SECTIONS> 的 XML 子元素

4. - 一个名为 </SECTION> 的 XML 子元素

5. - 一个名为 </VERSES> 的 XML 子元素

6. - 一个名为 </VERSE> 的 XML 子元素

7. - 一个名为 </TEXT> 的 XML 子元素

这是 BOOK1.XML,它源自 INDEX.XML 中引用的 <BOOKID> 元素

<?xml version="1.0" encoding="utf-8"?>
<BOOK1>
  <BOOKNAME>Genesis</BOOKNAME>
  <SECTIONS>
    <SECTION id="001">
      <VERSES>
        <VERSE id="001">
          <TEXT>In the beginning God created the heaven and the earth.</TEXT>
        </VERSE>
        <VERSE id="002">
          <TEXT>And the earth was without form, and void; and darkness was upon the face of the deep. And the Spirit of God moved upon the face of the waters.</TEXT>
        </VERSE>
...
 
       </VERSES>
     </SECTION>
  </SECTIONS>
</BOOK1>

那么这里发生了什么?嗯,重申一下,我们正在创建一个 Android 对象模型的层次结构!让我们想象一下,我们有一个 **BOOK** 对象,当我们翻阅这个假想的 **BOOK** 对象时,我们有一个 **SECTIONS** 集合,并且在书的 **SECTIONS** 集合的每个部分中,我们都有一个特定的 **SECTION** 对象,它包含一个 **VERSES** 集合,由 **VERSE** 对象组成,而这些对象本身又由 **TEXT** 字段等组成。显然,如果我们从类的角度考虑,我们将属性和方法与映射到各自成员变量的 XML 部分相关联。有一种更简单的方法可以以一种相对轻松的方式实现这种层次结构抽象,所以请参考本文后面提到使用 **SIMPLE** 框架的部分。

**BOOK** 对象的每一层都表示为一个 **SECTIONS** 对象,该对象被分解为 **SECTION**、**VERSES**、**VERSE** 和 **TEXT** 元素的集合,这些元素和谐地、顺序地排列在一起,从而填充了 **BOOK** 对象的全部内容。此外,考虑到这为用户提供的可能性也很有帮助!用户现在可以利用 **SECTION id** 和 **VERSE id** 属性来浏览电子书的层次结构!这是一个重要的抽象级别!它方便用户更有效地导航 **BOOK** 对象模型的​​内容……这是我正在讨论的源自 XML 元数据的对象模型的​​实现的屏幕截图……在我的 NOOK 平板电脑上运行的 KJV 电子书阅读器 Android 应用程序中,这一点得到了清晰的说明……

图 4. 在我的 Android 平板电脑上运行的 Android 电子书阅读器,显示 Verse Reader

因此,本质上,我们已经详细介绍了生成的 XML 对象模型!但关于用于将所有这些数据摄入最终 Android 对象模型 (AOM) 的平台,还没有说得足够。本文将介绍在尝试将 XML 元数据迁移到此类模型时遇到的优缺点。提供了一个有吸引力的选项,以帮助开发人员克服处理替代方案的差异的严苛!是的,硬着头皮学习可能会很痛苦,而这正是我所经历的……

Android 上 XML 对象解析的一些策略源于 JAVA 中 DOM 和 SAX 解析框架的使用。我个人发现这些框架有限,因为它们不允许在同一个解析器对象模型中统一地读取、写入和保存修改后的 XML 结构。我有时会发现自己不得不寻求额外的库,或者干脆从头开始创建自己的库,混合搭配各种技术。

这一切都将改变,当我偶然发现了以下普遍存在且轻量级的框架,名为 **SIMPLE**。我不会深入讨论 **SIMPLE**,因为您可以在其网站上找到出色的教程,其中详细讨论了我可能添加的任何内容!但是,我将继续说,我强烈建议您查看这个小型框架并尝试一下,因为它允许快速、低开销的转换,而不会对您的移动应用程序的应用程序大小造成太大影响!在以下位置查看 **SIMPLE** for Android:

http://simple.sourceforge.net/articles.php

在我对使用 **SIMPLE** 将 XML 数据映射到 **AOM** 的讨论中跑得太远之前,我必须回到文章主题,即成功迁移到 **SIMPLE** 等 XML 摄入框架的第一步……让我们来看一些 XML 预处理 C# 代码,并进一步阐述如何做到这一点!

使用代码

起初,我决定编写一个轻量级的预解析器,于是我用 C# .NET 自己动手写了一个!我安装了 Visual Studio 10,并决定,为什么不呢!这很简单,所以让我们开始展示简单的类图、成员变量、方法等等……所以,我们现在开始深入了解它是如何构思的!






图 4. kjvParser 的字段

我们从 **using** 部分开始,正如承诺的那样,没有什么惊人的或特别之处,除了使用了标准的 .NET 对象和一些用于文件操作的 System.IO 程序集。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
 
// kjv_parser
// created by Mario Ghecea on 6/05/2012
// a simple XML bible writing utility
// that takes a text bible and converts it to xml elements
// by parsing each line as text...

// format is as follows:
// chapters starting with the keyword "Book"
// verses starting with numerics i.e. "001:001"
// everything else is considered a verse
// or ignored completely in case of whitespace.

在下一节中,我们声明将执行解析的类。由于 XML 是基于标签的,因此我将文档的结构描述为 const string 标签。此代码中最值得注意的部分是枚举 **ELEMENT**,它维护着文档层次结构内导航占位符的状态机!

图 4. 状态机的 ELEMENT 枚举

它基本上允许解析器知道它在文档中的位置。可接受的值是 **IS_BOOK**、**IS_VERSE** 或 **IS_SOMETHING_ELSE**,如果它碰巧落在一个既非前者也非后者的部分,那么它就被假定为该行读取器位于一个诗篇(verse)部分内。**IS_IGNORED** 值实际上没有使用,它表示我们的解析逻辑有问题,因此我们需要完全忽略它!同样值得注意的是,**IS_EOF** 状态在此代码版本中根本没有使用!这是因为 **EOF** 检测不是在解析器状态机逻辑中执行的。在这种情况下,它可以被完全删除,或者以本解析器逻辑实现中未显示的其他方式使用!

namespace kjv_parser
{
    class kjv_parser
    {
        // XML Tags
        const string ENCODING = @"<?xml version=""1.0"" encoding=""utf-8""?>";
        const string ROOT = @"<KJV>";
        const string END_ROOT = @"</KJV>";
        const string BOOKNAME = @"<BOOKNAME>";
        const string SECTIONS = @"<SECTIONS>";
        const string END_SECTIONS = @"</SECTIONS>";
        const string SECTION = @"<SECTION id =""";
        const string END_SECTION = @"</SECTION>";
        const string VERSES = @"<VERSES>";
        const string END_VERSES = @"</VERSES>";
        const string BOOK = @"<BOOK";
        const string END_BOOK = @"</BOOK";
        const string END_BOOKNAME = @"</BOOKNAME>";
        const string VERSE = @"<VERSE id =""";
        const string END_VERSE = @"</VERSE>";
        const string TEXT = @"<TEXT>";
        const string END_TEXT = @"</TEXT>";
        const string BOOKID = @"<BOOKID>";
        const string BOOKID_END = @"</BOOKID>";
 
        enum ELEMENT {IS_EOF = -1, IS_SOMETHING_ELSE, IS_BOOK, IS_VERSE, IS_IGNORED};         
 
        static StreamWriter writer;
        static StreamWriter indexer;
        static int bookCount = 0;
        static int verseCount = 0;
        static int lineCount = 0;
        static bool verseTagOpen = false;
        static bool sectionTagOpen = false;
        static string oldVerseRef;
        static string oldSectionRef = "";
        static bool bookStart = false;
 



由于这是一个简单的控制台应用程序,不需要界面,我只是逐行从文本文件中读取,并将其写入控制台,以简单地显示进度。在这里实现一些更好的东西而不显示文本可能会更有价值!在这种情况下,您可以注释掉 **Console.WriteLine**。状态机如下所示!函数 **IsChapterOrVerse(input)** 从 **StreamReader** 对象中读取每一行文本,并查找线索,以确定我们是否在文本中的 **BOOK** 标记处开始。如果是,我们就确定我们已经进入了一个 **BOOK** 部分,因此我们可以将此信息解析为 **<BOOK>** XML 标签。根据接下来的逻辑,解析器会检查文本行中的第一个字符是否为数字,如果找到数字,我们就知道这是 **<VERSE>** 标签及其 **id** 属性的开始!最后,解析器放弃并说,它既不是 **BOOK** 标记也不是 **VERSE** 标记,因此,我们可能仍然在 **VERSE** 行上,所以继续将此行(去除空格后)附加到 **<VERSE>** 标签。

     static void Main(string[] args)
        {
            int i = 0;
            using (StreamReader sr = File.OpenText(@"C:\Bible\kjv12\KJV12.TXT"))
            {
                
                string input = null;
                OpenIndexFile(@"C:\Bible\kjv12\index.xml");
                while ((input = sr.ReadLine()) != null)
                {
                    lineCount++;
                    Console.WriteLine(input);
                    switch (IsChapterOrVerse(input))
                    {
                        case ELEMENT.IS_BOOK:
                            bookStart = !bookStart;                            
                            
                            if (!bookStart)
                            {
                                if (verseTagOpen)
                                {
                                    verseTagOpen = false;
                                    CloseVerse();
                                }
                                CloseBook();
 
                                CloseOutputFile();                                
                                bookStart = true;
                            }
 
                            if (bookStart)
                            {
                                OpenOutputFile(@"C:\Bible\kjv12\book" + ++i + ".xml");
                                bookCount++;
                                WriteBook(input);
                            }
 
                            break;
                        case ELEMENT.IS_VERSE:
                            if (verseCount > 0 && verseTagOpen)
                            {
                                CloseVerse();
                                verseTagOpen = false;
                            }
                            WriteVerseTag(input);
                            WriteVerse(input.Substring(8, input.Length - 8));
                            verseTagOpen = true;
                            verseCount++;
                            break;
                        case ELEMENT.IS_SOMETHING_ELSE: // This is still part of the verse even if blank line
                            WriteVerse(input);
                            break;
                        default:
                        case ELEMENT.IS_IGNORED:
                            break;
                    }                    
                }
                // As long as we parse a valid document, close the end elements
                if (lineCount > 0)
                {
                    CloseVerse();
                    CloseBook();
                }
 
                CloseOutputFile();
                CloseIndexFile();
            }
 
            //Console.ReadLine();
        }

这是解析器的核心,因此是指示我们属于哪个部分的主要决策者!更复杂的文本文档解析器可能在这里拥有更先进的逻辑,这就是更复杂的状态机以类似的方式创建的,寻找文本线索来做出决策并推动逻辑进入下一个状态!

         static ELEMENT IsChapterOrVerse(string inputLine)
        {
            ELEMENT el = ELEMENT.IS_IGNORED;
            if (inputLine.Length > 0)
            {
                if (inputLine.Substring(0,4).ToUpper().Contains("BOOK"))
                    el = ELEMENT.IS_BOOK;
                else
                {
                    int number1;
                    if (inputLine.Length <= 1)
                        el = ELEMENT.IS_IGNORED;
                    else if (int.TryParse(inputLine.Substring(0, 1), out number1))
                    {
                        el = ELEMENT.IS_VERSE;
                    }
                    else
                        el = ELEMENT.IS_SOMETHING_ELSE;
                }
            }
            return el;
        }


解析器类的“工蜂”是下面显示的静态方法。

这些方法非常自明,因此我将把理解的任务留给读者作为一项心智锻炼,但 suffice to say,代码只需执行打开和关闭 XML 标签的任务,此外,还可以将 **INDEX.XML** 和 **BOOK#.XML** 文件最终保存到特定目录的磁盘上!这段代码并非完美,但它确实展示了可以多么快速且灵活地完成任务……另一种方法显然取决于使用 XML DOM 框架来完成所有这些,但我发现对于如此简单的任务来说,这是一种权宜之计且不必要的。

      static void OpenOutputFile(string filePath)
        {
            writer = File.CreateText(filePath);
            writer.Write(ENCODING);
            //writer.Write(ROOT);            
        }
 
        static void OpenIndexFile(string filePath)
        {
            indexer = File.CreateText(filePath);
            indexer.Write(ENCODING);
            indexer.Write(ROOT);
        }
 
        static void CloseIndexFile()
        {
            indexer.Write(END_ROOT);
            indexer.Close();
        }
 
        static void WriteBook(string chapterLine)
        {
            writer.Write(BOOK + bookCount + ">");
            indexer.Write(BOOK + ">");
            indexer.Write(BOOKID);
            indexer.Write("BOOK" + bookCount);
            indexer.Write(BOOKID_END);
            writer.Write(BOOKNAME);
            indexer.Write(BOOKNAME);           
            int modifier = 0;
            if (bookCount > 9) modifier = 1;
            int start = bookCount.ToString().Length + 7 - modifier;
            string tempLine = chapterLine.Substring(start, chapterLine.Length - start); 
            writer.Write(tempLine);
            indexer.Write(tempLine);            
            writer.Write(END_BOOKNAME);
            indexer.Write(END_BOOKNAME);
            writer.Write(SECTIONS);
        }
 
        static void WriteVerse(string verseLine)
        {
            if (verseTagOpen)
                verseLine = " " + verseLine.TrimStart();
 
            writer.Write(verseLine);
        }
 
        static void WriteVerseTag(string verseRef)
        {
            String tempRef = verseRef.Substring(0, 3);
            verseRef = verseRef.Substring(4,3);
            
           
            if ((oldSectionRef == "") || (oldSectionRef != tempRef))
            {
                if (sectionTagOpen)
                {
                    writer.Write(END_VERSES);
                    writer.Write(END_SECTION);
                    sectionTagOpen = false;
                }
               
                writer.Write(SECTION + tempRef + "\">");
                writer.Write(VERSES);
                oldSectionRef = tempRef;
                sectionTagOpen = true;
            }
            writer.Write(VERSE + verseRef + "\">");
            writer.Write(TEXT);
            oldVerseRef = verseRef;
        }
 

        static void CloseBook()
        {
            if (sectionTagOpen)
            {
                writer.Write(END_VERSES);
                writer.Write(END_SECTION);
                oldSectionRef = "";
                sectionTagOpen = false;
            }
 
            writer.Write(END_SECTIONS);
            writer.Write(END_BOOK + bookCount + ">");
            indexer.Write(END_BOOK + ">");
        }
 
        static void CloseVerse()
        {
            writer.Write(END_TEXT);
            writer.Write(END_VERSE);
            oldVerseRef = "";
        }
 
        static void CloseOutputFile()
        {
            writer.Close();
        }
 



关注点

您可以在我最近在 Posterous 上的博客 http://post.ly/84uut 上阅读有关电子书阅读器的内容,并在 Twitter 上关注我,以随时了解此 XML 框架和其他正在开发的 Android 框架的发布和更新!在下一篇文章中,我将讨论 Android XML 对象和在此移动平台上的元数据摄入。我希望收到您的回复,并让您知道这是否有助于您开发自己的 Android 或其他移动平台上的电子书阅读器!

历史

首次修订于 2012 年 7 月 4 日!

© . All rights reserved.