SwiftyJSON/Alamofire 未将字符串解析为 UTF8

SwiftyJSON/Alamofire not parsing string to UTF8

简介

你好!在我的应用程序中,我正在向 YouTubeDataAPI 发出请求。 api 能够使用 UTF8 编码的字符串(包括特殊字符)进行响应。但是,我无法接收数据作为 utf8 数据。

为了将响应数据解析为对象,我使用了Swift的可编码协议。

这是我的请求的样子

enum VideoPart: String {
    case snippet = "snippet"
    case statistics = "statistics"
    case contentDetails = "contentDetails"
}

private static func fetchDetailsAfterSearch(forVideo videoId: String, parts: [VideoPart], onDone: @escaping (JSON) -> Void) {
        let videoParts = parts.map({ [=11=].rawValue })

        let apiUrl = URL(string: "https://www.googleapis.com/youtube/v3/videos")

        let headers: HTTPHeaders = ["X-Ios-Bundle-Identifier": Bundle.main.bundleIdentifier ?? ""]

        let parameters: Parameters = ["part": videoParts.joined(separator: ","), "id": videoId, "key": apiKey]

        Alamofire.request(apiUrl!, method: .get, parameters: parameters, encoding: URLEncoding.default, headers: headers).responseJSON { (response) in
            if let responseData = response.data {
                onDone(JSON(responseData))
            }
        }
    }

static func searchVideos(forQuery query: String, limit: Int = 20, onDone: @escaping ([YTVideo]) -> Void) {

    let apiUrl = URL(string: "https://www.googleapis.com/youtube/v3/search")!

    let headers: HTTPHeaders = ["X-Ios-Bundle-Identifier": Bundle.main.bundleIdentifier ?? ""]

    let parameters: Parameters = ["q": query, "part": "snippet", "maxResults": limit, "relevanceLanguage": "en", "type": "video", "key": apiKey]

    let group = DispatchGroup()
    group.enter()

    var videos: [YTVideo] = [] // the parsed videos are stored here

    Alamofire.request(apiUrl, method: .get, parameters: parameters, encoding: URLEncoding.default, headers: headers).responseJSON { (response) in

        if let responseData = response.data { // is there a response data?
            let resultVideos = JSON(responseData)["items"].arrayValue

            resultVideos.forEach({ (v) in // loop through each video and fetch more exact data, based on the videoId
                let videoId = v["id"]["videoId"].stringValue
                group.enter()
                YTDataService.fetchDetailsAfterSearch(forVideo: videoId, parts: [VideoPart.statistics, VideoPart.contentDetails], onDone: {(details) in
                    // MARK: parse the data of the api to the YTVideo Object
                    let videoSnippet = v["snippet"]
                    let videoDetails = details["items"][0]

                    var finalJSON: JSON = JSON()

                    finalJSON = finalJSON.merged(other: videoSnippet)
                    finalJSON = finalJSON.merged(other: videoDetails)


                    if let video = try? YTVideo(data: finalJSON.rawData()) {
                        videos.append(video)
                    }
                    group.leave()
                })
            })
            group.leave()
        }
    }

    group.notify(queue: .main) {
        onDone(videos)
    }
}

代码解释:

由于 api 只有 returns 视频的片段,我必须为每个视频发出另一个 api 请求以获取更多详细信息。此 请求 是在 每个视频 的 for 循环中进行的。此调用然后 returns 一个数据对象 解析为 JSON 对象(通过 SwiftyJSON)。

然后将这两个响应合并到一个 JSON 对象中。之后, finalJson 用于初始化一个 YTVideo 对象。正如我已经说过的,class 是可编码的,并根据需要自动解析 json - class 的结构可以在下面找到。

从API发回的数据:

{
  "statistics" : {
    "favoriteCount" : "0",
    "dislikeCount" : "942232",
    "likeCount" : "8621179",
    "commentCount" : "516305",
    "viewCount" : "2816892915"
  },
  "publishedAt" : "2014-08-18T21:18:00.000Z",
  "contentDetails" : {
    "caption" : "false",
    "licensedContent" : true,
    "definition" : "hd",
    "duration" : "PT4M2S",
    "dimension" : "2d",
    "projection" : "rectangular"
  },
  "channelId" : "UCANLZYMidaCbLQFWXBC95Jg",
  "kind" : "youtube#video",
  "id" : "nfWlot6h_JM",
  "liveBroadcastContent" : "none",
  "etag" : "\"8jEFfXBrqiSrcF6Ee7MQuz8XuAM\/ChcYFUcK77KQsdMIp5DyWCHvX9I\"",
  "title" : "Taylor Swift - Shake It Off",
  "channelTitle" : "TaylorSwiftVEVO",
  "description" : "Music video by Taylor Swift performing Shake It Off. (C) 2014 Big Machine Records, LLC. New single ME! (feat. Brendon Urie of Panic! At The Disco) available ...",
  "thumbnails" : {
    "high" : {
      "width" : 480,
      "url" : "https:\/\/i.ytimg.com\/vi\/nfWlot6h_JM\/hqdefault.jpg",
      "height" : 360
    },
    "medium" : {
      "url" : "https:\/\/i.ytimg.com\/vi\/nfWlot6h_JM\/mqdefault.jpg",
      "width" : 320,
      "height" : 180
    },
    "default" : {
      "url" : "https:\/\/i.ytimg.com\/vi\/nfWlot6h_JM\/default.jpg",
      "width" : 120,
      "height" : 90
    }
  }
}

这是我的YTVideoclass

// This file was generated from JSON Schema using quicktype, do not modify it directly.
// To parse the JSON, add this file to your project and do:
//
//   let yTVideo = try YTVideo(json)

import Foundation

// MARK: - YTVideo
struct YTVideo: Codable {
    let statistics: Statistics
    let publishedAt: String
    let contentDetails: ContentDetails
    let channelID, kind, id, liveBroadcastContent: String
    let etag, title, channelTitle, ytVideoDescription: String
    let thumbnails: Thumbnails

    enum CodingKeys: String, CodingKey {
        case statistics, publishedAt, contentDetails
        case channelID = "channelId"
        case kind, id, liveBroadcastContent, etag, title, channelTitle
        case ytVideoDescription = "description"
        case thumbnails
    }
}

// MARK: YTVideo convenience initializers and mutators

extension YTVideo {
    init(data: Data) throws {
        self = try newJSONDecoder().decode(YTVideo.self, from: data)
    }

    init(_ json: String, using encoding: String.Encoding = .utf8) throws {
        guard let data = json.data(using: encoding) else {
            throw NSError(domain: "JSONDecoding", code: 0, userInfo: nil)
        }
        try self.init(data: data)
    }

    init(fromURL url: URL) throws {
        try self.init(data: try Data(contentsOf: url))
    }

    func with(
        statistics: Statistics? = nil,
        publishedAt: String? = nil,
        contentDetails: ContentDetails? = nil,
        channelID: String? = nil,
        kind: String? = nil,
        id: String? = nil,
        liveBroadcastContent: String? = nil,
        etag: String? = nil,
        title: String? = nil,
        channelTitle: String? = nil,
        ytVideoDescription: String? = nil,
        thumbnails: Thumbnails? = nil
    ) -> YTVideo {
        return YTVideo(
            statistics: statistics ?? self.statistics,
            publishedAt: publishedAt ?? self.publishedAt,
            contentDetails: contentDetails ?? self.contentDetails,
            channelID: channelID ?? self.channelID,
            kind: kind ?? self.kind,
            id: id ?? self.id,
            liveBroadcastContent: liveBroadcastContent ?? self.liveBroadcastContent,
            etag: etag ?? self.etag,
            title: title ?? self.title,
            channelTitle: channelTitle ?? self.channelTitle,
            ytVideoDescription: ytVideoDescription ?? self.ytVideoDescription,
            thumbnails: thumbnails ?? self.thumbnails
        )
    }

    func jsonData() throws -> Data {
        return try newJSONEncoder().encode(self)
    }

    func jsonString(encoding: String.Encoding = .utf8) throws -> String? {
        return String(data: try self.jsonData(), encoding: encoding)
    }
}

// MARK: - ContentDetails
struct ContentDetails: Codable {
    let caption: String
    let licensedContent: Bool
    let definition, duration, dimension, projection: String
}

// MARK: ContentDetails convenience initializers and mutators

extension ContentDetails {
    init(data: Data) throws {
        self = try newJSONDecoder().decode(ContentDetails.self, from: data)
    }

    init(_ json: String, using encoding: String.Encoding = .utf8) throws {
        guard let data = json.data(using: encoding) else {
            throw NSError(domain: "JSONDecoding", code: 0, userInfo: nil)
        }
        try self.init(data: data)
    }

    init(fromURL url: URL) throws {
        try self.init(data: try Data(contentsOf: url))
    }

    func with(
        caption: String? = nil,
        licensedContent: Bool? = nil,
        definition: String? = nil,
        duration: String? = nil,
        dimension: String? = nil,
        projection: String? = nil
    ) -> ContentDetails {
        return ContentDetails(
            caption: caption ?? self.caption,
            licensedContent: licensedContent ?? self.licensedContent,
            definition: definition ?? self.definition,
            duration: duration ?? self.duration,
            dimension: dimension ?? self.dimension,
            projection: projection ?? self.projection
        )
    }

    func jsonData() throws -> Data {
        return try newJSONEncoder().encode(self)
    }

    func jsonString(encoding: String.Encoding = .utf8) throws -> String? {
        return String(data: try self.jsonData(), encoding: encoding)
    }
}

// MARK: - Statistics
struct Statistics: Codable {
    let favoriteCount, dislikeCount, likeCount, commentCount: String
    let viewCount: String
}

// MARK: Statistics convenience initializers and mutators

extension Statistics {
    init(data: Data) throws {
        self = try newJSONDecoder().decode(Statistics.self, from: data)
    }

    init(_ json: String, using encoding: String.Encoding = .utf8) throws {
        guard let data = json.data(using: encoding) else {
            throw NSError(domain: "JSONDecoding", code: 0, userInfo: nil)
        }
        try self.init(data: data)
    }

    init(fromURL url: URL) throws {
        try self.init(data: try Data(contentsOf: url))
    }

    func with(
        favoriteCount: String? = nil,
        dislikeCount: String? = nil,
        likeCount: String? = nil,
        commentCount: String? = nil,
        viewCount: String? = nil
    ) -> Statistics {
        return Statistics(
            favoriteCount: favoriteCount ?? self.favoriteCount,
            dislikeCount: dislikeCount ?? self.dislikeCount,
            likeCount: likeCount ?? self.likeCount,
            commentCount: commentCount ?? self.commentCount,
            viewCount: viewCount ?? self.viewCount
        )
    }

    func jsonData() throws -> Data {
        return try newJSONEncoder().encode(self)
    }

    func jsonString(encoding: String.Encoding = .utf8) throws -> String? {
        return String(data: try self.jsonData(), encoding: encoding)
    }
}

// MARK: - Thumbnails
struct Thumbnails: Codable {
    let high, medium, thumbnailsDefault: Default

    enum CodingKeys: String, CodingKey {
        case high, medium
        case thumbnailsDefault = "default"
    }
}

// MARK: Thumbnails convenience initializers and mutators

extension Thumbnails {
    init(data: Data) throws {
        self = try newJSONDecoder().decode(Thumbnails.self, from: data)
    }

    init(_ json: String, using encoding: String.Encoding = .utf8) throws {
        guard let data = json.data(using: encoding) else {
            throw NSError(domain: "JSONDecoding", code: 0, userInfo: nil)
        }
        try self.init(data: data)
    }

    init(fromURL url: URL) throws {
        try self.init(data: try Data(contentsOf: url))
    }

    func with(
        high: Default? = nil,
        medium: Default? = nil,
        thumbnailsDefault: Default? = nil
    ) -> Thumbnails {
        return Thumbnails(
            high: high ?? self.high,
            medium: medium ?? self.medium,
            thumbnailsDefault: thumbnailsDefault ?? self.thumbnailsDefault
        )
    }

    func jsonData() throws -> Data {
        return try newJSONEncoder().encode(self)
    }

    func jsonString(encoding: String.Encoding = .utf8) throws -> String? {
        return String(data: try self.jsonData(), encoding: encoding)
    }
}

// MARK: - Default
struct Default: Codable {
    let width: Int
    let url: String
    let height: Int
}

// MARK: Default convenience initializers and mutators

extension Default {
    init(data: Data) throws {
        self = try newJSONDecoder().decode(Default.self, from: data)
    }

    init(_ json: String, using encoding: String.Encoding = .utf8) throws {
        guard let data = json.data(using: encoding) else {
            throw NSError(domain: "JSONDecoding", code: 0, userInfo: nil)
        }
        try self.init(data: data)
    }

    init(fromURL url: URL) throws {
        try self.init(data: try Data(contentsOf: url))
    }

    func with(
        width: Int? = nil,
        url: String? = nil,
        height: Int? = nil
    ) -> Default {
        return Default(
            width: width ?? self.width,
            url: url ?? self.url,
            height: height ?? self.height
        )
    }

    func jsonData() throws -> Data {
        return try newJSONEncoder().encode(self)
    }

    func jsonString(encoding: String.Encoding = .utf8) throws -> String? {
        return String(data: try self.jsonData(), encoding: encoding)
    }
}

// MARK: - Helper functions for creating encoders and decoders

func newJSONDecoder() -> JSONDecoder {
    let decoder = JSONDecoder()
    if #available(iOS 10.0, OSX 10.12, tvOS 10.0, watchOS 3.0, *) {
        decoder.dateDecodingStrategy = .iso8601
    }
    return decoder
}

func newJSONEncoder() -> JSONEncoder {
    let encoder = JSONEncoder()
    if #available(iOS 10.0, OSX 10.12, tvOS 10.0, watchOS 3.0, *) {
        encoder.dateEncodingStrategy = .iso8601
    }
    return encoder
}

我目前拥有的:

解析和一切工作正常,但 Youtube-Video-Title 未以 utf8 格式显示(见下图)。

我想要的

我必须进行哪些更改才能将来自 YouTube API 的数据显示为有效的 utf8 编码字符串?我尝试了几种 utf8 编码,但其中 none 对我有效

如有任何帮助,我们将不胜感激!

这不是 UTF-8 或解析问题。您的代码正确解析并显示了给定的字符串。问题似乎是您使用的字符串是 HTML-encoded。现在,我认为您共享的代码(QuickType 没有为我加载)不足以让我们知道您使用哪些属性来获取 HTML-encoded 视频标题。可能是 plain-text 一个,或者你应该自己处理解码——我无法从文档中得知。

简而言之,如果 HTML-encoded 字符串是您唯一的选择,请查看 decoding HTML entities 而不是 unicode-related 问题。

api 响应确实包含 html 个编码字符。请看下面的截图:

youtube 演示控制台 link : https://developers.google.com/youtube/v3/docs/search/list?apix_params=%7B%22part%22%3A%22snippet%22%2C%22maxResults%22%3A20%2C%22q%22%3A%22Taylor%20Swift%22%2C%22relevanceLanguage%22%3A%22en%22%2C%22type%22%3A%22video%22%7D

结论:api 文档没有声明返回的文本是纯文本/html 编码。但是,根据演示控制台结果,标题是 html 编码的。

希望对您有所帮助:

extension String {
func htmlToUtf8() -> String{
    //chuyển đổi kết quả từ JSON htmlString sang Utf8
    let encodedData = self.data(using: .utf8)
    let attributedOptions : [NSAttributedString.DocumentReadingOptionKey : Any ] = [
        .documentType: NSAttributedString.DocumentType.html,
        .characterEncoding: String.Encoding.utf8.rawValue ]
    do {
        let attributedString = try NSAttributedString(data: encodedData!, options: attributedOptions, documentAttributes: nil)
        let decodedString = attributedString.string
        return decodedString
    } catch {
        // error ...
    }

    return String()
}

}

然后:

let jsonTitle = "ERIK - 'Em Kh\U00f4ng Sai, Ch\U00fang Ta Sai' (Official Lyric Video)"
let videoTitle = jsonTitle.htmlToUtf8()
print(videoTitle) //"ERIK - 'Em Không Sai, Chúng Ta Sai' (Official Lyric Video)"

我来自越南,所以我们经常使用 utf8。