MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Swift JSON与XML处理

2023-07-111.8k 阅读

JSON处理

JSON 基础介绍

JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,易于人阅读和编写,同时也易于机器解析和生成。它基于 JavaScript 的一个子集,采用完全独立于编程语言的文本格式来存储和表示数据。JSON 数据格式主要有两种结构:对象(object)和数组(array)。

对象是一个无序的键值对集合,用花括号 {} 包围,每个键值对之间用逗号 , 分隔,键和值之间用冒号 : 分隔。例如:

{
    "name": "John",
    "age": 30,
    "city": "New York"
}

数组是一个有序的值列表,用方括号 [] 包围,值之间用逗号 , 分隔。例如:

[10, 20, 30]

JSON 支持的数据类型包括字符串(string)、数字(number)、布尔值(boolean)、对象(object)、数组(array)以及 null

在 Swift 中处理 JSON

Swift 提供了多种方式来处理 JSON 数据,主要涉及到 Foundation 框架中的 JSONSerialization 类以及 Codable 协议。

使用 JSONSerialization 进行手动解析

JSONSerialization 类提供了方法来将 JSON 数据转换为 Foundation 对象(如 NSDictionaryNSArrayNSStringNSNumberNSNull),以及将这些 Foundation 对象转换回 JSON 数据。

  1. 解析 JSON 数据 假设我们有如下的 JSON 数据字符串:
let jsonString = "{\"name\":\"John\",\"age\":30,\"city\":\"New York\"}"
if let data = jsonString.data(using:.utf8) {
    do {
        let json = try JSONSerialization.jsonObject(with: data, options: [])
        if let dict = json as? [String: Any] {
            if let name = dict["name"] as? String {
                print("Name: \(name)")
            }
            if let age = dict["age"] as? Int {
                print("Age: \(age)")
            }
            if let city = dict["city"] as? String {
                print("City: \(city)")
            }
        }
    } catch {
        print("Error: \(error)")
    }
}

在上述代码中,首先将 JSON 字符串转换为 Data 类型。然后使用 JSONSerialization.jsonObject(with:options:) 方法将 Data 解析为 Foundation 对象。这里 options 参数如果传入 .mutableContainers,则会返回可变的 NSMutableDictionaryNSMutableArray。接着通过类型转换,获取字典中的具体值。

  1. 创建 JSON 数据
let jsonDict: [String: Any] = [
    "name": "Jane",
    "age": 25,
    "city": "Los Angeles"
]
do {
    let jsonData = try JSONSerialization.data(withJSONObject: jsonDict, options: [.prettyPrinted])
    if let jsonString = String(data: jsonData, encoding:.utf8) {
        print(jsonString)
    }
} catch {
    print("Error: \(error)")
}

在这段代码中,通过 JSONSerialization.data(withJSONObject:options:) 方法将字典转换为 JSON 数据。options 参数中的 .prettyPrinted 会让生成的 JSON 数据格式更易读,每个层级之间有缩进。

使用 Codable 协议进行自动解析

Swift 4.0 引入的 Codable 协议使得 JSON 解析变得更加简单和类型安全。CodableEncodableDecodable 协议的组合。

  1. 定义 Codable 结构体
struct Person: Codable {
    let name: String
    let age: Int
    let city: String
}

这里定义了一个 Person 结构体,它遵循 Codable 协议。由于结构体中的所有属性都是 Codable 类型,Swift 编译器会自动为该结构体合成 EncodableDecodable 的实现。

  1. 解析 JSON 数据
let jsonString = "{\"name\":\"Bob\",\"age\":28,\"city\":\"Chicago\"}"
if let data = jsonString.data(using:.utf8) {
    do {
        let decoder = JSONDecoder()
        let person = try decoder.decode(Person.self, from: data)
        print("Name: \(person.name), Age: \(person.age), City: \(person.city)")
    } catch {
        print("Error: \(error)")
    }
}

上述代码中,首先将 JSON 字符串转换为 Data 类型。然后创建一个 JSONDecoder 实例,使用 decode(_:from:) 方法将 Data 解析为 Person 结构体实例。

  1. 创建 JSON 数据
let person = Person(name: "Alice", age: 22, city: "Boston")
do {
    let encoder = JSONEncoder()
    encoder.outputFormatting =.prettyPrinted
    let jsonData = try encoder.encode(person)
    if let jsonString = String(data: jsonData, encoding:.utf8) {
        print(jsonString)
    }
} catch {
    print("Error: \(error)")
}

在这段代码中,创建一个 JSONEncoder 实例,设置 outputFormatting.prettyPrinted 以获得易读的 JSON 格式。然后使用 encode(_:) 方法将 Person 结构体实例编码为 JSON 数据。

JSON 解析中的常见问题及解决方法

  1. 数据类型不匹配 在手动解析 JSON 数据时,如果 JSON 数据中的值类型与代码中期望的类型不匹配,就会导致类型转换失败。例如,将一个 JSON 中的字符串值尝试转换为整数。 解决方法:在类型转换前,使用 is 关键字进行类型检查。例如:
if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
    if let ageObject = json["age"] {
        if let age = ageObject as? Int {
            print("Age: \(age)")
        } else if let ageString = ageObject as? String {
            if let ageInt = Int(ageString) {
                print("Age (converted from string): \(ageInt)")
            }
        }
    }
}
  1. JSON 格式错误 如果 JSON 数据格式不正确,例如缺少引号、括号不匹配等,解析会失败并抛出错误。 解决方法:在获取 JSON 数据后,首先使用在线 JSON 校验工具(如 JSONLint)验证 JSON 格式的正确性。在代码中,使用 do - catch 块捕获解析错误,并进行相应处理。

  2. 嵌套 JSON 结构 当 JSON 数据包含嵌套的对象或数组时,解析会变得复杂。例如:

{
    "name": "Tom",
    "age": 32,
    "hobbies": ["reading", "swimming"],
    "address": {
        "street": "123 Main St",
        "city": "Denver",
        "zip": "80201"
    }
}

解决方法:定义嵌套的 Codable 结构体。例如:

struct Address: Codable {
    let street: String
    let city: String
    let zip: String
}

struct Person: Codable {
    let name: String
    let age: Int
    let hobbies: [String]
    let address: Address
}

然后按照正常的 Codable 解析流程进行解析。

XML处理

XML 基础介绍

XML(eXtensible Markup Language)是一种标记语言,用于存储和传输数据。与 JSON 不同,XML 具有更严格的语法规则,并且可以定义自己的标签。XML 文档由一个根元素和零个或多个子元素组成。

例如:

<?xml version="1.0" encoding="UTF-8"?>
<bookstore>
    <book category="fiction">
        <title lang="en">The Great Gatsby</title>
        <author>F. Scott Fitzgerald</author>
        <price>19.99</price>
    </book>
    <book category="non-fiction">
        <title lang="en">Sapiens: A Brief History of Humankind</title>
        <author>Yuval Noah Harari</author>
        <price>24.99</price>
    </book>
</bookstore>

在上述 XML 示例中,bookstore 是根元素,每个 book 是子元素。元素可以有属性(如 book 元素的 category 属性),子元素可以有文本内容(如 title 元素的文本 “The Great Gatsby”)。

在 Swift 中处理 XML

在 Swift 中处理 XML 数据,主要有两种常用的方式:使用 NSXMLParser 进行手动解析,以及使用第三方库如 AEXML 进行更方便的处理。

使用 NSXMLParser 进行手动解析

NSXMLParser 是 Foundation 框架提供的用于解析 XML 数据的类。它采用基于事件的解析方式,通过委托方法通知解析过程中的各种事件。

  1. 创建解析器并设置委托
let xmlString = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><bookstore><book category=\"fiction\"><title lang=\"en\">The Great Gatsby</title><author>F. Scott Fitzgerald</author><price>19.99</price></book><book category=\"non-fiction\"><title lang=\"en\">Sapiens: A Brief History of Humankind</title><author>Yuval Noah Harari</author><price>24.99</price></book></bookstore>"
if let data = xmlString.data(using:.utf8) {
    let parser = XMLParser(data: data)
    let delegate = BookstoreParser()
    parser.delegate = delegate
    parser.parse()
}

这里创建了一个 XMLParser 实例,并为其设置了一个自定义的委托 BookstoreParser

  1. 实现委托方法
class BookstoreParser: NSObject, XMLParserDelegate {
    var currentElement: String?
    var currentBook: Book?
    var books: [Book] = []

    func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String] = [:]) {
        currentElement = elementName
        if elementName == "book" {
            currentBook = Book()
            if let category = attributeDict["category"] {
                currentBook?.category = category
            }
        }
    }

    func parser(_ parser: XMLParser, foundCharacters string: String) {
        guard let currentElement = currentElement, let currentBook = currentBook else { return }
        let trimmedString = string.trimmingCharacters(in:.whitespacesAndNewlines)
        if trimmedString.isEmpty { return }
        switch currentElement {
        case "title":
            currentBook.title = trimmedString
        case "author":
            currentBook.author = trimmedString
        case "price":
            if let price = Double(trimmedString) {
                currentBook.price = price
            }
        default:
            break
        }
    }

    func parser(_ parser: XMLParser, didEndElement elementName: String, namespaceURI: String?, qualifiedName qName: String?) {
        if elementName == "book" {
            if let book = currentBook {
                books.append(book)
            }
            currentBook = nil
        }
        currentElement = nil
    }
}

struct Book {
    var category: String?
    var title: String?
    var author: String?
    var price: Double?
}

在上述代码中,BookstoreParser 类实现了 XMLParserDelegate 协议的三个主要方法:

  • parser(_:didStartElement:namespaceURI:qualifiedName:attributes:):当解析器遇到一个开始标签时调用,这里我们记录当前元素名,并在遇到 book 标签时初始化一个 Book 实例,并处理 book 标签的属性。
  • parser(_:foundCharacters:):当解析器找到字符数据时调用,我们在这里根据当前元素名,将字符数据赋值给 Book 实例的相应属性。
  • parser(_:didEndElement:namespaceURI:qualifiedName:):当解析器遇到一个结束标签时调用,这里我们在遇到 book 结束标签时,将 Book 实例添加到 books 数组中,并重置 currentBookcurrentElement

使用 AEXML 进行便捷处理

AEXML 是一个第三方库,它提供了更简洁和直观的方式来处理 XML 数据。首先需要将 AEXML 库添加到项目中,可以通过 CocoaPods 或 Swift Package Manager 进行添加。

  1. 解析 XML 数据
import AEXML

let xmlString = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><bookstore><book category=\"fiction\"><title lang=\"en\">The Great Gatsby</title><author>F. Scott Fitzgerald</author><price>19.99</price></book><book category=\"non-fiction\"><title lang=\"en\">Sapiens: A Brief History of Humankind</title><author>Yuval Noah Harari</author><price>24.99</price></book></bookstore>"
if let data = xmlString.data(using:.utf8) {
    do {
        let xmlDocument = try AEXMLDocument(xmlData: data)
        for book in xmlDocument.root?.children.filter({ $0.name == "book" })?? [] {
            if let category = book.attributes["category"], let title = book["title"].value, let author = book["author"].value, let priceString = book["price"].value, let price = Double(priceString) {
                print("Category: \(category), Title: \(title), Author: \(author), Price: \(price)")
            }
        }
    } catch {
        print("Error: \(error)")
    }
}

在上述代码中,首先将 XML 字符串转换为 Data 类型,然后使用 AEXMLDocument(xmlData:) 方法创建一个 AEXMLDocument 实例。通过访问 root 属性获取根元素,然后过滤出所有的 book 子元素,并获取其属性和子元素的值。

  1. 创建 XML 数据
let bookstore = AEXMLElement(name: "bookstore")
let book1 = AEXMLElement(name: "book", attributes: ["category": "fiction"])
book1.addChild(AEXMLElement(name: "title", value: "The Great Gatsby", attributes: ["lang": "en"]))
book1.addChild(AEXMLElement(name: "author", value: "F. Scott Fitzgerald"))
book1.addChild(AEXMLElement(name: "price", value: "19.99"))
bookstore.addChild(book1)

let book2 = AEXMLElement(name: "book", attributes: ["category": "non-fiction"])
book2.addChild(AEXMLElement(name: "title", value: "Sapiens: A Brief History of Humankind", attributes: ["lang": "en"]))
book2.addChild(AEXMLElement(name: "author", value: "Yuval Noah Harari"))
book2.addChild(AEXMLElement(name: "price", value: "24.99"))
bookstore.addChild(book2)

let xmlDocument = AEXMLDocument(root: bookstore)
if let xmlData = xmlDocument.xmlData(options:.prettyPrinted), let xmlString = String(data: xmlData, encoding:.utf8) {
    print(xmlString)
}

在这段代码中,通过创建 AEXMLElement 实例来构建 XML 结构,然后将这些元素组合成一个 AEXMLDocument 实例。最后使用 xmlData(options:) 方法将文档转换为 XML 数据,并设置 options.prettyPrinted 以获得易读的 XML 格式。

XML 解析中的常见问题及解决方法

  1. 命名空间处理 XML 支持命名空间,这可能会使解析变得复杂。例如:
<ns1:bookstore xmlns:ns1="http://example.com/books">
    <ns1:book>
        <ns1:title>The Great Gatsby</ns1:title>
        <ns1:author>F. Scott Fitzgerald</ns1:author>
        <ns1:price>19.99</ns1:price>
    </ns1:book>
</ns1:bookstore>

解决方法:在使用 NSXMLParser 时,parser(_:didStartElement:namespaceURI:qualifiedName:attributes:) 方法的 namespaceURI 参数会提供命名空间的 URI。可以根据命名空间 URI 来处理元素。在 AEXML 中,AEXMLElementnamespace 属性可以用于获取和设置命名空间相关信息。

  1. XML 格式错误 与 JSON 类似,XML 也需要严格的格式,如果标签不匹配、属性格式错误等都会导致解析失败。 解决方法:使用在线 XML 校验工具(如 XML Validator)验证 XML 格式的正确性。在代码中,使用 do - catch 块捕获解析错误,并进行相应处理。

  2. 性能问题 在处理大型 XML 文件时,基于事件的解析(如 NSXMLParser)通常比基于树的解析(如 AEXML,它会将整个 XML 构建成内存中的树结构)更高效。 解决方法:如果处理的 XML 数据量较大,优先选择 NSXMLParser 进行解析。同时,可以通过合理设置缓冲区大小等方式优化性能。

JSON 与 XML 的比较

  1. 数据格式
  • JSON:格式简洁,易于阅读和编写,采用键值对和数组的形式表示数据,更适合现代编程语言的数据交互。
  • XML:格式较为复杂,有严格的标签定义和语法规则,支持自定义标签和命名空间,更适合需要高度结构化和扩展性的数据表示。
  1. 解析性能
  • JSON:解析速度通常较快,因为其格式简单,在现代移动设备和 Web 应用中被广泛使用。
  • XML:解析相对较慢,特别是在处理大型文件时,因为其语法复杂性和需要构建树状结构(在一些解析方式下)。
  1. 数据传输
  • JSON:由于格式简洁,数据量相对较小,适合在网络传输中使用,特别是在移动应用和 Web API 中。
  • XML:数据量相对较大,因为需要包含标签等元信息,在网络传输时可能需要更多的带宽。
  1. 应用场景
  • JSON:常用于 Web 应用的前后端数据交互、移动应用的数据传输、配置文件等场景。
  • XML:在企业级应用、数据交换标准(如 SOAP)、文档存储(如 eBook 格式)等场景中仍有广泛应用。

在实际项目中,应根据具体的需求和场景选择合适的数据格式和处理方式。如果注重简洁性和性能,JSON 可能是更好的选择;如果需要高度结构化和扩展性,XML 可能更合适。