SwiftyJSON / Alamofire не разбирает строку в 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({ $0.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 возвращает только фрагмент видео, мне нужно сделать еще один запрос api для каждого видео, чтобы получить более подробную информацию. Этот запрос выполняется внутри цикла for для каждого видео. Затем этот вызов возвращает объект данных, который анализируется на объект JSON (с помощью SwiftyJSON).

Затем эти два ответа объединяются в один объект JSON. После этого finalJson используется для инициализации объекта YTVideo. Как я уже сказал, класс кодируется и автоматически анализирует json в соответствии со своими потребностями - структуру класса можно найти ниже.

Данные, которые отправляются обратно из 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
    }
  }
}

Это мой YTVideo класс

// 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 (см. Изображение ниже).

Результат, который я получаю

Что я хочу

Какие изменения мне нужно внести, чтобы данные из API YouTube отображались в виде допустимой строки в кодировке utf8? Я пробовал несколько кодировок utf8, но у меня ни одна из них не сработала:

Любая помощь будет оценена по достоинству!


person linus_hologram    schedule 12.09.2019    source источник
comment
В коде вы явно используете SwiftyJSON, а не Codable. Оба API по умолчанию правильно декодируют данные в кодировке UTF8.   -  person vadian    schedule 12.09.2019
comment
stackoverflow.com/questions/55385560/ + stackoverflow.com/questions/42288963/? Не оптимизировано, но можно: if let responseData = response.data, let escapedReponseString = String(data: responseData, encoding: .utf8), let responseString = CheckLastLinkToInterpretThem, let dataToUseForSwiftJSON = responseString.encoding(utf8)   -  person Larme    schedule 12.09.2019
comment
@vadian - правильно, я использую SwiftyJSON для совместного анализа ответов, но затем я инициализирую класс YTVideo с rawData finalJson.   -  person linus_hologram    schedule 12.09.2019
comment
@Larme, не могли бы вы опубликовать ответ?   -  person linus_hologram    schedule 12.09.2019
comment
Было бы полезно, если бы вы разместили пример того, как выглядит необработанный JSON из API. Ваш код для его анализа - это только половина уравнения.   -  person Craig Siemens    schedule 12.09.2019
comment
' не является проблемой кодировки UTF-8: это кодировка HTML. Так что поищите больше в направлении таких ответов: stackoverflow.com/questions/25607247/   -  person Kiril S.    schedule 12.09.2019
comment
@CraigSiemens Я сделал. Пожалуйста, взгляните на мой обновленный пост. Спасибо за вашу помощь!   -  person linus_hologram    schedule 12.09.2019
comment
@KirilS. так вы убеждены, что данные, которые я получаю от api, выглядят так? На случай, если вы захотите это увидеть, я добавил json-данные, которые получаю из api, а также чистый код моего класса YTVideo.   -  person linus_hologram    schedule 12.09.2019
comment
Есть ли такая же проблема с опубликованным вами JSON? Это не соответствует снимку экрана в вашем вопросе.   -  person Craig Siemens    schedule 12.09.2019
comment
@CraigSiemens, это не одно и то же видео и не чистые данные. Пример, который я отправил, - это данные, которые я анализирую (объединяю) вместе из двух моих вызовов api. Проблема в том, что я не могу сказать, как выглядят собственные данные api, поскольку пример API данных YouTube из документации, кажется, анализирует этот материал с кодировкой html, прежде чем они отобразят его в браузере. Вот ссылка на test-api: developers.google.com/youtube/ v3 / docs / search / list developers.google.com/youtube / v3 / docs / videos / list   -  person linus_hologram    schedule 12.09.2019
comment
@CraigSiemens Я имею в виду, что если вы посмотрите на ссылки на миниатюры, вы увидите, что у них есть \ перед каждой нормальной косой чертой - это уже та часть кодировки html, которую вы упомянули?   -  person linus_hologram    schedule 12.09.2019


Ответы (3)


Надеюсь, это поможет вам:

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.

person Tung Dang    schedule 01.06.2020

ответ API содержал символы в кодировке html. см. снимок экрана ниже:

введите здесь описание изображения

Ссылка на демонстрационную консоль YouTube: 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.

person Angel.Alice    schedule 12.09.2019
comment
@ Angel.Alice: это известная и задокументированная проблема API (см. Темы, упомянутые в комментариях выше, особенно эту ). Пользователи должны сами декодировать ссылки на символы HTML (также известные как объекты HTML), используя инструменты, доступные из окружающей среды программирования. - person stvar; 12.09.2019

Это не проблема UTF-8 или синтаксического анализа. Ваш код правильно разбирает и отображает заданную строку. Проблема заключается в том, что используемая вами строка закодирована в HTML. Я не думаю, что вы предоставили достаточно кода (а QuickType для меня не загружается), чтобы мы знали, какие свойства вы используете для получения заголовка видео в кодировке HTML. Может быть, есть простой текст, или вы должны сами обрабатывать декодирование - я не могу сказать из документации.

Короче говоря, если строка в кодировке HTML - ваш единственный вариант, посмотрите декодирование HTML-сущностей вместо проблем, связанных с Unicode.

person Drarok    schedule 12.09.2019
comment
это комментарий, а не ответ - person Kiril S.; 12.09.2019
comment
Строка не закодирована в формате HTML - этими странными символами заменяются только определенные символы - нигде в ответе нет тегов html. Что меня удивляет, так это то, что если вы протестируете, например, API на официальной странице документации, он отвечает полностью нормально - честно говоря, я не уверен, в чем проблема. Я просто знаю, что это, вероятно, не связано с html-кодировкой, поскольку в ответе нет тегов. - person linus_hologram; 12.09.2019
comment
Я также поместил весь код YTVideoclass в свой пост, а также ответ, который я получаю от api. - person linus_hologram; 12.09.2019
comment
@ linus_hologram: Эти символы совсем не странные. Это действительные ссылки на символы HTML. Единственная проблема здесь в том, что API не декодирует их в UTF-8. Эта проблема API задокументирована на этом форуме в течение некоторого времени (как уже упоминалось в комментариях выше). Вам нужно самостоятельно декодировать все ссылки на символы HTML (также известные как объекты). - person stvar; 12.09.2019