Правильно проаналізувавши JSON у Swift 3


123

Я намагаюся отримати відповідь JSON і зберігати результати у змінній. У мене були версії цього кодового режиму в попередніх випусках Swift, поки не вийшла версія GM Xcode 8. Я переглянув кілька подібних дописів у StackOverflow: Swift 2 Parsing JSON - Неможливо підписати значення типу 'AnyObject' та JSON Parsing у Swift 3 .

Однак, схоже, що передані там ідеї не застосовуються в цьому сценарії.

Як правильно проаналізувати відгук JSON у Swift 3? Чи щось змінилося в тому, як читається JSON у Swift 3?

Нижче наведено код, про який можна розглянути (його можна запустити на ігровому майданчику):

import Cocoa

let url = "https://api.forecast.io/forecast/apiKey/37.5673776,122.048951"

if let url = NSURL(string: url) {
    if let data = try? Data(contentsOf: url as URL) {
        do {
            let parsedData = try JSONSerialization.jsonObject(with: data as Data, options: .allowFragments)

        //Store response in NSDictionary for easy access
        let dict = parsedData as? NSDictionary

        let currentConditions = "\(dict!["currently"]!)"

        //This produces an error, Type 'Any' has no subscript members
        let currentTemperatureF = ("\(dict!["currently"]!["temperature"]!!)" as NSString).doubleValue

            //Display all current conditions from API
            print(currentConditions)

            //Output the current temperature in Fahrenheit
            print(currentTemperatureF)

        }
        //else throw an error detailing what went wrong
        catch let error as NSError {
            print("Details of JSON parsing error:\n \(error)")
        }
    }
}

Редагувати: Ось зразок результатів виклику API післяprint(currentConditions)

["icon": partly-cloudy-night, "precipProbability": 0, "pressure": 1015.39, "humidity": 0.75, "precipIntensity": 0, "windSpeed": 6.04, "summary": Partly Cloudy, "ozone": 321.13, "temperature": 49.45, "dewPoint": 41.75, "apparentTemperature": 47, "windBearing": 332, "cloudCover": 0.28, "time": 1480846460]

Чи можете ви розмістити зразкові дані, повернуті з вашого дзвінка API?
Користувач

1
Так, я щойно додав зразок результатів, надрукованих після друку (currentConditions). Сподіваюся, це допомагає.
користувач2563039

Аналізувати в JSON swift4 з використанням протоколу кодируемой stackoverflow.com/a/52931265/9316566
Насер Мухаммед

Відповіді:


172

Перш за все, ніколи не завантажуйте дані синхронно з віддаленої URL-адреси , використовуйте завжди такі асинхронні методи URLSession.

"Будь-який" не має членів підписки

виникає тому, що компілятор не має уявлення про тип типу проміжних об'єктів (наприклад, currentlyв ["currently"]!["temperature"]), а оскільки ви використовуєте типи колекцій Foundation, як- NSDictionaryот компілятор, взагалі не мають уявлення про тип.

Додатково в Swift 3 потрібно повідомити компілятора про тип усіх підписаних об'єктів.

Ви повинні привести результат серіалізації JSON до фактичного типу.

Цей код використовує URLSessionі виключно рідні типи Swift

let urlString = "https://api.forecast.io/forecast/apiKey/37.5673776,122.048951"

let url = URL(string: urlString)
URLSession.shared.dataTask(with:url!) { (data, response, error) in
  if error != nil {
    print(error)
  } else {
    do {

      let parsedData = try JSONSerialization.jsonObject(with: data!) as! [String:Any]
      let currentConditions = parsedData["currently"] as! [String:Any]

      print(currentConditions)

      let currentTemperatureF = currentConditions["temperature"] as! Double
      print(currentTemperatureF)
    } catch let error as NSError {
      print(error)
    }
  }

}.resume()

Щоб надрукувати всі пари ключів / значень, які currentConditionsви могли написати

 let currentConditions = parsedData["currently"] as! [String:Any]

  for (key, value) in currentConditions {
    print("\(key) - \(value) ")
  }

Примітка стосовно jsonObject(with data:

Багато (як здається, все) підручники пропонують .mutableContainersабо .mutableLeavesваріанти, що є абсолютно нісенітницею у Swift. Два варіанти - це застарілі опції Objective-C для призначення результату NSMutable...об'єктам. У Swift будь-який variable за замовчуванням змінюється, і передача будь-якого з цих параметрів і присвоєння результату letконстанті взагалі не впливає. Далі більшість реалізацій ніколи не мутують деріаріалізований JSON.

Тільки (рідко) варіант , який є корисним в Swift є .allowFragmentsякий необхідний , якщо якщо кореневої об'єкт JSON , може бути типом значення ( String, Number, Boolабо null) , а не один з типів колекцій ( arrayабо dictionary). Але зазвичай опустіть optionsпараметр, що означає Немає параметрів .

===================================================== ==========================

Деякі загальні міркування для розбору JSON

JSON - це добре впорядкований текстовий формат. Читати рядок JSON дуже просто. Уважно прочитайте рядок . Існує лише шість різних типів - два типи колекцій та чотири типи значень.


Типи колекцій є

  • Масив - JSON: об'єкти в квадратних дужках []- Swift: [Any]але в більшості випадків[[String:Any]]
  • Словник - JSON: об'єкти в фігурних дужках {}- Swift:[String:Any]

Типи значень є

  • Рядок - JSON: будь-яке значення в подвійних лапках "Foo", парне "123"або "false"- Swift:String
  • Число - JSON: числові значення не в подвійних лапках 123або 123.0- Swift: IntабоDouble
  • Bool - JSON: trueчи false ні в подвійних лапках - Swift: trueабоfalse
  • null - JSON: null- Swift:NSNull

Відповідно до специфікації JSON, всі словники в словниках повинні бути String.


В основному завжди рекомендується використовувати додаткові прив'язки, щоб безпечно розгортати додаткові елементи

Якщо кореневим об'єктом є словник ( {}), видайте його типу[String:Any]

if let parsedData = try JSONSerialization.jsonObject(with: data!) as? [String:Any] { ...

і отримуйте значення за допомогою клавіш за допомогою ( OneOfSupportedJSONTypesабо колекція JSON, або тип значення, як описано вище).

if let foo = parsedData["foo"] as? OneOfSupportedJSONTypes {
    print(foo)
} 

Якщо кореневим об'єктом є масив ( []), передайте тип[[String:Any]]

if let parsedData = try JSONSerialization.jsonObject(with: data!) as? [[String:Any]] { ...

і повторіть масив з

for item in parsedData {
    print(item)
}

Якщо вам потрібен товар за певним індексом, перевірте також, чи існує індекс

if let parsedData = try JSONSerialization.jsonObject(with: data!) as? [[String:Any]], parsedData.count > 2,
   let item = parsedData[2] as? OneOfSupportedJSONTypes {
      print(item)
    }
}

У рідкісному випадку, коли JSON - це просто один із типів значень - а не тип колекції - вам потрібно передати цей .allowFragmentsпараметр і передати результат відповідному типу значення, наприклад

if let parsedData = try JSONSerialization.jsonObject(with: data!, options: .allowFragments) as? String { ...

Apple опублікувала вичерпну статтю в блозі Swift: Робота з JSON в Swift


===================================================== ==========================

У Swift 4+ Codableпротокол забезпечує більш зручний спосіб розбору JSON безпосередньо на структури / класи.

Наприклад, наведений зразок JSON у запитанні (трохи модифікований)

let jsonString = """
{"icon": "partly-cloudy-night", "precipProbability": 0, "pressure": 1015.39, "humidity": 0.75, "precip_intensity": 0, "wind_speed": 6.04, "summary": "Partly Cloudy", "ozone": 321.13, "temperature": 49.45, "dew_point": 41.75, "apparent_temperature": 47, "wind_bearing": 332, "cloud_cover": 0.28, "time": 1480846460}
"""

можна розшифрувати в структуру Weather. Типи Swift такі ж, як описано вище. Є кілька додаткових варіантів:

  • Рядки, що представляють собою, URLможна декодувати безпосередньо URL.
  • timeЦіле число може бути декодовано , як Dateз dateDecodingStrategy .secondsSince1970.
  • ключі JSON snake_cased можна перетворити на camelCase за допомогоюkeyDecodingStrategy .convertFromSnakeCase

struct Weather: Decodable {
    let icon, summary: String
    let pressure: Double, humidity, windSpeed : Double
    let ozone, temperature, dewPoint, cloudCover: Double
    let precipProbability, precipIntensity, apparentTemperature, windBearing : Int
    let time: Date
}

let data = Data(jsonString.utf8)
do {
    let decoder = JSONDecoder()
    decoder.dateDecodingStrategy = .secondsSince1970
    decoder.keyDecodingStrategy = .convertFromSnakeCase
    let result = try decoder.decode(Weather.self, from: data)
    print(result)
} catch {
    print(error)
}

Інші джерела, доступні


Це дуже корисно. Мені просто цікаво, чому вищевказаний код не показує видимий вихід, коли він працює на ігровому майданчику.
користувач2563039

Як згадувалося вище, вам потрібно подати неоднозначний результат у dict!["currently"]!словник, що компілятор може безпечно зробити наступну підписку на ключ.
вадіан

1
Стаття Apple досить крута, але я чомусь не міг змусити її працювати зі швидким 3. Він скаржиться на Type [String: Any]? не має членів підписки. З цим були і деякі інші проблеми, але мені вдалося його обійти. Хтось має приклад свого коду, який насправді працює?
Відтінки

@Shades Задайте питання та опублікуйте свій код. Швидше за все, ваша проблема пов’язана з розгорнутою опцією.
вадіан

Чи можете ви розмістити зразкові дані, повернуті з вашого дзвінка API?
Користувач

12

Велика зміна, що сталася з Xcode 8 Beta 6 для Swift 3, полягала в тому, що id зараз імпортує, Anyа не AnyObject.

Це означає, що parsedDataповертається як словник, швидше за все, з типом [Any:Any]. Без використання налагоджувача я не міг точно сказати вам, що NSDictionaryбуде робити ваш акторський склад, але помилка, яку ви бачите, є тим, що dict!["currently"]!має типAny

Отже, як ви це вирішите? З того, як ви посилаєтесь на нього, я припускаю dict!["currently"]!, що це словник, і тому у вас є багато варіантів:

Спочатку ви можете зробити щось подібне:

let currentConditionsDictionary: [String: AnyObject] = dict!["currently"]! as! [String: AnyObject]  

Це дасть вам об’єкт у словнику, який ви зможете запитувати за значеннями, і таким чином ви можете отримати температуру так:

let currentTemperatureF = currentConditionsDictionary["temperature"] as! Double

Або, якщо ви хочете, можете зробити це в порядку:

let currentTemperatureF = (dict!["currently"]! as! [String: AnyObject])["temperature"]! as! Double

Сподіваюся, це допомагає, боюся, я не встиг написати зразковий додаток, щоб перевірити його.

Останнє зауваження: найпростіше, що можна зробити, може бути просто передати корисний вантаж JSON в [String: AnyObject]самому початку.

let parsedData = try JSONSerialization.jsonObject(with: data as Data, options: .allowFragments) as! Dictionary<String, AnyObject>

dict!["currently"]! as! [String: String]аварії
вадіан

Точка, в якій вона врізалася, була ["температура"] !! а також намагаються передати String на NSString - перевірили, що нове рішення працює swiftlang.ng.bluemix.net/#/repl/57d3bc683a422409bf36c391
discorevilo

[String: String]взагалі не може працювати, оскільки є пара числових значень. Він не працює на майданчику для Mac
Вадіан

@vadian ах, так, я їх не помітив, і швидка пісочниця корисно їх заховала! - Виправлено [ще раз] зараз (і перевірено на macOS). Дякую, що вказали на це :)
discorevilo

6
let str = "{\"names\": [\"Bob\", \"Tim\", \"Tina\"]}"

let data = str.data(using: String.Encoding.utf8, allowLossyConversion: false)!

do {
    let json = try JSONSerialization.jsonObject(with: data, options: []) as! [String: AnyObject]
    if let names = json["names"] as? [String] 
{
        print(names)
}
} catch let error as NSError {
    print("Failed to load: \(error.localizedDescription)")
}

5

Я побудував quicktype саме для цієї мети. Просто вставте зразок JSON і quicktype генерує цей тип ієрархії для ваших даних API:

struct Forecast {
    let hourly: Hourly
    let daily: Daily
    let currently: Currently
    let flags: Flags
    let longitude: Double
    let latitude: Double
    let offset: Int
    let timezone: String
}

struct Hourly {
    let icon: String
    let data: [Currently]
    let summary: String
}

struct Daily {
    let icon: String
    let data: [Datum]
    let summary: String
}

struct Datum {
    let precipIntensityMax: Double
    let apparentTemperatureMinTime: Int
    let apparentTemperatureLowTime: Int
    let apparentTemperatureHighTime: Int
    let apparentTemperatureHigh: Double
    let apparentTemperatureLow: Double
    let apparentTemperatureMaxTime: Int
    let apparentTemperatureMax: Double
    let apparentTemperatureMin: Double
    let icon: String
    let dewPoint: Double
    let cloudCover: Double
    let humidity: Double
    let ozone: Double
    let moonPhase: Double
    let precipIntensity: Double
    let temperatureHigh: Double
    let pressure: Double
    let precipProbability: Double
    let precipIntensityMaxTime: Int
    let precipType: String?
    let sunriseTime: Int
    let summary: String
    let sunsetTime: Int
    let temperatureMax: Double
    let time: Int
    let temperatureLow: Double
    let temperatureHighTime: Int
    let temperatureLowTime: Int
    let temperatureMin: Double
    let temperatureMaxTime: Int
    let temperatureMinTime: Int
    let uvIndexTime: Int
    let windGust: Double
    let uvIndex: Int
    let windBearing: Int
    let windGustTime: Int
    let windSpeed: Double
}

struct Currently {
    let precipProbability: Double
    let humidity: Double
    let cloudCover: Double
    let apparentTemperature: Double
    let dewPoint: Double
    let ozone: Double
    let icon: String
    let precipIntensity: Double
    let temperature: Double
    let pressure: Double
    let precipType: String?
    let summary: String
    let uvIndex: Int
    let windGust: Double
    let time: Int
    let windBearing: Int
    let windSpeed: Double
}

struct Flags {
    let sources: [String]
    let isdStations: [String]
    let units: String
}

Він також генерує беззалежний код маршалінгу, щоб присвоїти повернене значення JSONSerialization.jsonObjectв Forecast, включаючи зручний конструктор, який приймає рядок JSON, щоб ви могли швидко розібрати сильно набране Forecastзначення та отримати доступ до його полів:

let forecast = Forecast.from(json: jsonString)!
print(forecast.daily.data[0].windGustTime)

Ви можете встановити quicktype з npm за допомогою npm i -g quicktype або скористатися веб-інтерфейсом, щоб отримати повний згенерований код, який потрібно вставити на ваш ігровий майданчик.


Посилання виходять з ладу.
малював ..

Фантастична економія часу! Хороша робота!
Марко

1
Блискучий інструмент. Дякую.
Ахмет Ардал

4

ОновленийisConnectToNetwork-Function згодом, завдяки цьому повідомленню .

Я написав для цього додатковий метод:

import SystemConfiguration

func loadingJSON(_ link:String, postString:String, completionHandler: @escaping (_ JSONObject: AnyObject) -> ()) {

    if(isConnectedToNetwork() == false){
        completionHandler("-1" as AnyObject)
        return
    }

    let request = NSMutableURLRequest(url: URL(string: link)!)
    request.httpMethod = "POST"
    request.httpBody = postString.data(using: String.Encoding.utf8)

    let task = URLSession.shared.dataTask(with: request as URLRequest) { data, response, error in
        guard error == nil && data != nil else { // check for fundamental networking error
            print("error=\(error)")
            return
        }

        if let httpStatus = response as? HTTPURLResponse , httpStatus.statusCode != 200 { // check for http errors
            print("statusCode should be 200, but is \(httpStatus.statusCode)")
            print("response = \(response)")
        }
        //JSON successfull
        do {
            let parseJSON = try JSONSerialization.jsonObject(with: data!, options: .allowFragments)
            DispatchQueue.main.async(execute: {
                completionHandler(parseJSON as AnyObject)
            });
        } catch let error as NSError {
            print("Failed to load: \(error.localizedDescription)")
        }
    }
    task.resume()
}

func isConnectedToNetwork() -> Bool {

    var zeroAddress = sockaddr_in(sin_len: 0, sin_family: 0, sin_port: 0, sin_addr: in_addr(s_addr: 0), sin_zero: (0, 0, 0, 0, 0, 0, 0, 0))
    zeroAddress.sin_len = UInt8(MemoryLayout.size(ofValue: zeroAddress))
    zeroAddress.sin_family = sa_family_t(AF_INET)

    let defaultRouteReachability = withUnsafePointer(to: &zeroAddress) {
        $0.withMemoryRebound(to: sockaddr.self, capacity: 1) {zeroSockAddress in
            SCNetworkReachabilityCreateWithAddress(nil, zeroSockAddress)
        }
    }

    var flags: SCNetworkReachabilityFlags = SCNetworkReachabilityFlags(rawValue: 0)
    if SCNetworkReachabilityGetFlags(defaultRouteReachability!, &flags) == false {
        return false
    }

    let isReachable = (flags.rawValue & UInt32(kSCNetworkFlagsReachable)) != 0
    let needsConnection = (flags.rawValue & UInt32(kSCNetworkFlagsConnectionRequired)) != 0
    let ret = (isReachable && !needsConnection)

    return ret
}

Тож тепер ви можете легко зателефонувати цьому у своєму додатку куди завгодно

loadingJSON("yourDomain.com/login.php", postString:"email=\(userEmail!)&password=\(password!)") { parseJSON in

    if(String(describing: parseJSON) == "-1"){
        print("No Internet")
    } else {

    if let loginSuccessfull = parseJSON["loginSuccessfull"] as? Bool {
        //... do stuff
    }
}

Марко, який найкращий спосіб додати цю функцію до проекту? в будь-якому контролері чи моделі?
KamalPanhwar

Ей, ви просто додаєте перший метод func loadingJSON (...) у додатковий швидкий файл чи клас. Після цього ви можете зателефонувати на це з кожного контролера у вашому проекті
Марко Вебер

я це спробував, але мені подобається ідея демонструвати повноцінне рішення, а також як його використовувати, включаючи хелперний метод isConnectedToNetwork () це мені ідея, як реалізувати це, можливо, в хорошому коді
Amr Angry

тому я просто використав новий швидкий файл (клацніть лівою кнопкою миші на вашому проектному дереві, новий файл ..., швидкий файл) і назвав його jsonhelper.swift. у цей файл ви розміщуєте перший код, завантажуючиJSON () та isConnectedToNetwork (). після цього ви можете використовувати ці дві функції у кожній частині проекту. наприклад, в loginVC, як дію кнопки входу ви можете використовувати другий код, очевидно, вам потрібно змінити домен, рядок розміщення та значення paseJson (parseJSON ["loginSuccessfull"]), щоб вони відповідали ваш файл PHP
Марко Вебер

0

Свіфт має потужний висновок типу. Дозволяє позбутися котлоагрегату "якщо дозволити" або "охороняти" та змусити розкручуватися за допомогою функціонального підходу:

  1. Ось наш JSON. Ми можемо використовувати додаткові JSON або звичайні. У нашому прикладі я використовую необов’язково:

    let json: Dictionary<String, Any>? = ["current": ["temperature": 10]]
  1. Функції помічників. Нам потрібно написати їх лише один раз, а потім повторно використовувати будь-який словник:

    /// Curry
    public func curry<A, B, C>(_ f: @escaping (A, B) -> C) -> (A) -> (B) -> C {
        return { a in
            { f(a, $0) }
        }
    }

    /// Function that takes key and optional dictionary and returns optional value
    public func extract<Key, Value>(_ key: Key, _ json: Dictionary<Key, Any>?) -> Value? {
        return json.flatMap {
            cast($0[key])
        }
    }

    /// Function that takes key and return function that takes optional dictionary and returns optional value
    public func extract<Key, Value>(_ key: Key) -> (Dictionary<Key, Any>?) -> Value? {
        return curry(extract)(key)
    }

    /// Precedence group for our operator
    precedencegroup RightApplyPrecedence {
        associativity: right
        higherThan: AssignmentPrecedence
        lowerThan: TernaryPrecedence
    }

    /// Apply. g § f § a === g(f(a))
    infix operator § : RightApplyPrecedence
    public func §<A, B>(_ f: (A) -> B, _ a: A) -> B {
        return f(a)
    }

    /// Wrapper around operator "as".
    public func cast<A, B>(_ a: A) -> B? {
        return a as? B
    }
  1. І ось наша магія - витягніть значення:

    let temperature = (extract("temperature") § extract("current") § json) ?? NSNotFound

Всього один рядок коду, без розгортання або ручного введення тексту. Цей код працює на дитячому майданчику, тому його можна скопіювати та перевірити. Ось реалізація на GitHub.


0

Це ще один спосіб вирішити вашу проблему. Тому, будь ласка, ознайомтесь із рішенням нижче. Сподіваюся, це допоможе вам.

let str = "{\"names\": [\"Bob\", \"Tim\", \"Tina\"]}"
let data = str.data(using: String.Encoding.utf8, allowLossyConversion: false)!
do {
    let json = try JSONSerialization.jsonObject(with: data, options: []) as! [String: AnyObject]
    if let names = json["names"] as? [String] {
        print(names)
    }
} catch let error as NSError {
    print("Failed to load: \(error.localizedDescription)")
}

0

Проблема полягає в методі взаємодії API. Аналіз JSON змінюється лише в синтаксисі. Основна проблема полягає у способі отримання даних. Що ви використовуєте - це синхронний спосіб отримання даних. Це працює не в кожному випадку. Що вам слід використовувати, це асинхронний спосіб отримання даних. Таким чином, ви повинні запитувати дані через API і чекати, коли вони відповідатимуть даними. Ви можете досягти цього за допомогою сеансу URL-адрес та подібних бібліотек Alamofire. Нижче наведено код методу URL-сеансу.

let urlString = "https://api.forecast.io/forecast/apiKey/37.5673776,122.048951"
let url = URL.init(string: urlString)
URLSession.shared.dataTask(with:url!) { (data, response, error) in
    guard error == nil else {
        print(error)
    }
    do {
        let Data = try JSONSerialization.jsonObject(with: data!) as! [String:Any]
        // Note if your data is coming in Array you should be using [Any]()
        //Now your data is parsed in Data variable and you can use it normally
        let currentConditions = Data["currently"] as! [String:Any]
        print(currentConditions)
        let currentTemperatureF = currentConditions["temperature"] as! Double
        print(currentTemperatureF)
    } catch let error as NSError {
        print(error)
    }
}.resume()

0
{
    "User":[
      {
        "FirstUser":{
        "name":"John"
        },
       "Information":"XY",
        "SecondUser":{
        "name":"Tom"
      }
     }
   ]
}

Якщо я створюю модель за допомогою попереднього json, використовуючи це посилання [блог]: http://www.jsoncafe.com для створення структури Codable або будь-якого формату

Модель

import Foundation
struct RootClass : Codable {
    let user : [Users]?
    enum CodingKeys: String, CodingKey {
        case user = "User"
    }

    init(from decoder: Decoder) throws {
        let values = try? decoder.container(keyedBy: CodingKeys.self)
        user = try? values?.decodeIfPresent([Users].self, forKey: .user)
    }
}

struct Users : Codable {
    let firstUser : FirstUser?
    let information : String?
    let secondUser : SecondUser?
    enum CodingKeys: String, CodingKey {
        case firstUser = "FirstUser"
        case information = "Information"
        case secondUser = "SecondUser"
    }
    init(from decoder: Decoder) throws {
        let values = try? decoder.container(keyedBy: CodingKeys.self)
        firstUser = try? FirstUser(from: decoder)
        information = try? values?.decodeIfPresent(String.self, forKey: .information)
        secondUser = try? SecondUser(from: decoder)
    }
}
struct SecondUser : Codable {
    let name : String?
    enum CodingKeys: String, CodingKey {
        case name = "name"
    }
    init(from decoder: Decoder) throws {
        let values = try? decoder.container(keyedBy: CodingKeys.self)
        name = try? values?.decodeIfPresent(String.self, forKey: .name)
    }
}
struct FirstUser : Codable {
    let name : String?
    enum CodingKeys: String, CodingKey {
        case name = "name"
    }
    init(from decoder: Decoder) throws {
        let values = try? decoder.container(keyedBy: CodingKeys.self)
        name = try? values?.decodeIfPresent(String.self, forKey: .name)
    }
}

Розбираємо

    do {
        let res = try JSONDecoder().decode(RootClass.self, from: data)
        print(res?.user?.first?.firstUser?.name ?? "Yours optional value")
    } catch {
        print(error)
    }
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.