2015-12-19 44 views
3

我正在使用libxml2的sax解析器来读取大型xml文件。大多数回调处理程序都提供了一个NULL终止的字符指针。使用String.fromCString这些可以转换为Swift中的常规字符串。但是,sax使用缓冲区来读取字节,因此可以使用字符串的一部分(即缓冲区的大小)调用其中一个回调函数(characters)。这部分字符串甚至可能在Unicode代码点的中途开始/结束。回调将被多次调用,直到提供完整的字符串(以块为单位)。如何在Swift中处理来自C的缓冲字符串?

我想连接所有的块,直到完整的字符串可以组装,或者以某种方式检测部分字符串中的代码点边界,只处理完成直到无效的代码点。

处理这种情况的最佳方法是什么?处理过程应尽可能快,但仍然正确。内存使用应尽可能少,但不要牺牲性能。

+1

我不跟的libxml2的SAX解析器经验丰富,但是这里(http://www.jamesh.id.au/ articles/libxml-sax/libxml-sax.html#characters)据说*“在你的回调中,你可能想要将字符复制到其他缓冲区......”*。这可以例如用NSMutableData完成。 –

+0

如果您使用'String.fromCString',那么数据以UTF-8编码。您可以查看缓冲区的最后几个字节以查看字符边界的位置,然后使用NSMutableString(bytes:length:encoding :)创建部分字符串。然后保存任何额外的字节以预先添加到下一个缓冲区,重复并将后续字符串附加到原始结尾。 –

回答

1

最长的合法UTF-8字符是4个字节(RFC 3629第3节)。所以你不需要一个非常大的缓冲区来保证你的安全。你需要多少字节的规则也很容易(只要看第一个字节)。所以我只是维护一个从0到3字节的缓冲区。当你有正确的编号时,传递它并尝试构造一个字符串。像这样的东西(只轻轻测试,可能有个别案例不仍然工作):

final class UTF8Parser { 
    enum Error: ErrorType { 
     case BadEncoding 
    } 
    var workingBytes: [UInt8] = [] 

    func updateWithBytes(bytes: [UInt8]) throws -> String { 

     workingBytes += bytes 

     var string = String() 
     var index = 0 

     while index < workingBytes.count { 
      let firstByte = workingBytes[index] 
      var numBytes = 0 

       if firstByte < 0x80 { numBytes = 1 } 
      else if firstByte < 0xE0 { numBytes = 2 } 
      else if firstByte < 0xF0 { numBytes = 3 } 
      else      { numBytes = 4 } 

      if workingBytes.count - index < numBytes { 
       break 
      } 

      let charBytes = workingBytes[index..<index+numBytes] 

      guard let newString = String(bytes: charBytes, encoding: NSUTF8StringEncoding) else { 
       throw(Error.BadEncoding) 
      } 
      string += newString 
      index += numBytes 
     } 

     workingBytes.removeFirst(index) 
     return string 
    } 
} 

let parser = UTF8Parser() 
var string = "" 
string += try parser.updateWithBytes([UInt8(65)]) 

print(string) 
let partial = try parser.updateWithBytes([UInt8(0xCC)]) 
print(partial) 

let rest = try parser.updateWithBytes([UInt8(0x81)]) 
print(rest) 

string += rest 
print(string) 

这只是一个方式,就是那种简单。另一种可能更快的方法是向后遍历字节,寻找代码点的最后一个开始(一个不以“10”开头的字节)。然后你可以一举处理所有事情,特殊情况就是最后几个字节。

+0

有趣的方法,只承载左边的字节可能会提高性能,需要较少的复制。 – bouke

+0

您当然可以创建更多特殊情况以避免更多复制。例如,由于您知道您要从“字节”中获得多少字节,因此您可以将这些字节分割并将它们附加到“workingBytes”,然后将其余部分作为分片进行处理。根据我的经验,如果复制是一个严重的问题,那么'NSMutableData'是更好的工具,因为它具有更多可预测的复制规则,并且具有用于使用外部缓冲区的“noCopy”优化。您也可以在UnsafeMutableBufferPointers中工作,但这些也可能非常难以正确使用。 –

+0

请参阅http://robnapier.net/nsdata了解更多关于离开并返回到“NSMutableData”的咆哮,几乎完全是为了复制问题。 –

2

如果处理速度是你的第一个目标,那么我会仅仅收取 所有的字符,直到XML元素被完全处理和 endElement被调用。这可以使用Foundation框架中的NSMutableData 完成。所以你需要一个属性

var charData : NSMutableData? 

这是在startElement初始化:

charData = NSMutableData() 

characters回调您添加的所有数据:

charData!.appendBytes(ch, length: Int(len)) 

(强制解缠在这里接受charData只能是nil 如果startElement之前没有被调用过,那我ans 发生编程错误或libxml2无法正常工作)。

终于在endElement,创建一个字符串,斯威夫特和 发布数据:

defer { 
    // Release data in any case before function returns 
    charData = nil 
} 
guard let string = String(data: charData!, encoding: NSUTF8StringEncoding) else { 
    // Handle invalid UTF-8 data situation 
} 
// string is the Swift string 
+0

感谢您的有趣的方法。有没有办法做到这一点没有ObjC运行时?不用等待'endElement',而是可以检查'NULL'的'characters'的最后一个字节是否为'NULL'? – bouke

+0

@bouke:对不起,我没有明白你的意思。字符中没有NULL。你想检查什么? (请注意,许多Swift函数在底层使用Foundation,所以无论如何都要使用这个库)。 - 如果您希望在调用endElement之前检测*无效输入*,则此方法不起作用,您必须继续如Rob的回答。 *可能会变慢,因为数据移动得更频繁,但您必须亲自测试。 –

+0

如果复制性能是一个问题,请注意关于'NSData'性能的推理通常比'[UInt8]性能更容易推理(查看我最初的'NSData'实现的编辑内容)。数组是具有写入时拷贝的值类型,但也是单引用优化以避免副本。如果你操纵它们,那么确切地知道它们何时被复制就会很难。 'NSMutableData'是一个引用类型,所以从来没有隐含的副本由于突变。我切换到'[UInt8]'实现,因为代码稍微简单一些,而不是因为速度更快。 –