Analizando correctamente JSON en Swift 3


123

Estoy tratando de obtener una respuesta JSON y almacenar los resultados en una variable. He tenido versiones de este código que funcionan en versiones anteriores de Swift, hasta que se lanzó la versión GM de Xcode 8. Eché un vistazo a algunas publicaciones similares en StackOverflow: Swift 2 Parsing JSON: no se puede subíndice un valor de tipo 'AnyObject' y JSON Parsing en Swift 3 .

Sin embargo, parece que las ideas transmitidas allí no se aplican en este escenario.

¿Cómo analizo correctamente la respuesta JSON en Swift 3? ¿Ha cambiado algo en la forma en que se lee JSON en Swift 3?

A continuación se muestra el código en cuestión (se puede ejecutar en un patio de recreo):

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)")
        }
    }
}

Editar: Aquí hay una muestra de los resultados de la llamada API despuésprint(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]

¿Puede poner los datos de muestra que devuelve su llamada API?
Usuario

1
Sí, acabo de agregar una muestra de los resultados impresos después de imprimir (currentConditions). Espero eso ayude.
user2563039

Parse json en swift4 usando el protocolo Codificable stackoverflow.com/a/52931265/9316566
Naser Mohamed el

Respuestas:


172

En primer lugar, nunca cargue datos sincrónicamente desde una URL remota , use métodos siempre asíncronos como URLSession.

'Cualquiera' no tiene miembros de subíndice

se produce porque el compilador no tiene idea de qué tipo son los objetos intermedios (por ejemplo, currentlyen ["currently"]!["temperature"]) y dado que está utilizando tipos de colección de Foundation, como NSDictionaryel compilador no tiene ninguna idea sobre el tipo.

Además, en Swift 3 se requiere informar al compilador sobre el tipo de todos los objetos suscritos.

Debe convertir el resultado de la serialización JSON al tipo real.

Este código usa URLSessiony exclusivamente tipos nativos 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()

Para imprimir todos los pares clave / valor de currentConditionsusted podría escribir

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

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

Una nota sobre jsonObject(with data:

Muchos (parece que todos) tutoriales sugieren .mutableContainersu .mutableLeavesopciones, lo cual es completamente absurdo en Swift. Las dos opciones son opciones heredadas de Objective-C para asignar el resultado a los NSMutable...objetos. En Swift, cualquier variable es mutable por defecto y pasar cualquiera de esas opciones y asignar el resultado a una letconstante no tiene ningún efecto. Además, la mayoría de las implementaciones nunca están mutando el JSON deserializado de todos modos.

La única opción (rara) que es útil en Swift es .allowFragmentsque se requiere si si el objeto raíz JSON podría ser un tipo de valor ( String, Number, Boolo null) en lugar de uno de los tipos de colección ( arrayo dictionary). Pero normalmente omite el optionsparámetro que significa Sin opciones .

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

Algunas consideraciones generales para analizar JSON

JSON es un formato de texto bien organizado. Es muy fácil leer una cadena JSON. Lee la cuerda cuidadosamente . Solo hay seis tipos diferentes: dos tipos de colección y cuatro tipos de valor.


Los tipos de colección son

  • Matriz - JSON: objetos entre corchetes []- Swift: [Any]pero en la mayoría de los casos[[String:Any]]
  • Diccionario - JSON: objetos entre llaves {}- Swift:[String:Any]

Los tipos de valor son

  • Cadena - JSON: cualquier valor entre comillas dobles "Foo", incluso "123"o "false"- Swift:String
  • Número - JSON: valores numéricos que no están entre comillas dobles 123o 123.0- Swift: IntoDouble
  • Bool - JSON: trueo false no entre comillas dobles - Swift: trueofalse
  • nulo - JSON: null- Rápido:NSNull

De acuerdo con la especificación JSON, se requiere que todas las claves en los diccionarios estén String.


Básicamente, siempre se recomienda utilizar enlaces opcionales para desenvolver los opcionales de forma segura

Si el objeto raíz es un diccionario ( {}), envíe el tipo a[String:Any]

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

y recuperar valores mediante claves con ( OneOfSupportedJSONTypeses una colección JSON o un tipo de valor como se describe anteriormente).

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

Si el objeto raíz es una matriz ( []), envíe el tipo a[[String:Any]]

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

e iterar a través de la matriz con

for item in parsedData {
    print(item)
}

Si necesita un elemento en un índice específico, verifique también si el índice existe

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

En el raro caso de que JSON sea simplemente uno de los tipos de valor, en lugar de un tipo de colección, debe pasar la .allowFragmentsopción y emitir el resultado al tipo de valor apropiado, por ejemplo

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

Apple ha publicado un artículo completo en el Blog de Swift: Trabajando con JSON en Swift


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

En Swift 4+, el Codableprotocolo proporciona una forma más conveniente de analizar JSON directamente en estructuras / clases.

Por ejemplo, la muestra JSON dada en la pregunta (ligeramente modificada)

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}
"""

se puede decodificar en la estructura Weather. Los tipos Swift son los mismos que los descritos anteriormente. Hay algunas opciones adicionales:

  • Las cadenas que representan un URLse pueden decodificar directamente como URL.
  • El timeentero se puede decodificar como Datecon el dateDecodingStrategy .secondsSince1970.
  • Las claves JSON snaked_cased se pueden convertir a camelCase con elkeyDecodingStrategy .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)
}

Otras fuentes codificables:


Esto es muy útil. Tengo curiosidad por saber por qué el código anterior no muestra resultados visibles cuando se ejecuta en un patio de recreo.
user2563039

Como se mencionó anteriormente, debe transmitir el resultado ambiguo dict!["currently"]!a un diccionario que el compilador puede inferir de manera segura la subsiguiente suscripción de clave.
vadian

1
El artículo de Apple es bastante bueno, pero no pude hacerlo funcionar con Swift 3 por alguna razón. ¿Se queja del tipo [String: Any]? no tiene miembros subíndice. También hubo otros problemas, pero pude solucionarlo. ¿Alguien tiene un ejemplo de su código que realmente se ejecuta?
Sombras

@Shades Haz una pregunta y publica tu código. Lo más probable es que su problema esté relacionado con opciones sin envolver.
vadiano

¿Puede poner los datos de muestra que devuelve su llamada API?
Usuario

12

Un gran cambio que sucedió con Xcode 8 Beta 6 para Swift 3 fue que la identificación ahora importa como en Anylugar de AnyObject.

Esto significa que parsedDatase devuelve como un diccionario de lo más probable con el tipo [Any:Any]. Sin usar un depurador, no podría decirte exactamente qué NSDictionaryhará tu elenco , pero el error que estás viendo es porque dict!["currently"]!tiene tipoAny

Entonces, ¿cómo resuelves esto? Por la forma en que lo ha referenciado, supongo que dict!["currently"]!es un diccionario, por lo que tiene muchas opciones:

Primero podrías hacer algo como esto:

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

Esto le dará un objeto de diccionario que luego puede consultar para valores y así puede obtener su temperatura de esta manera:

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

O si lo prefiere, puede hacerlo en línea:

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

Espero que esto ayude, me temo que no he tenido tiempo de escribir una aplicación de muestra para probarla.

Una nota final: lo más fácil de hacer, podría ser simplemente lanzar la carga JSON [String: AnyObject]al principio.

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

dict!["currently"]! as! [String: String]se estrellará
vadian

¡El punto en el que se estrelló fue la ["temperatura"]! y también tratando de transmitir String a NSString - verificó que la nueva solución funciona swiftlang.ng.bluemix.net/#/repl/57d3bc683a422409bf36c391
discorevilo

[String: String]no puede funcionar en absoluto porque hay un par de valores numéricos. No funciona en un Mac Playground
vadian

@vadian ah, sí, ¡no los había notado y la caja de arena rápida los ocultó! - Corregido [una vez más] ahora (y probado en macOS). Gracias por señalar esto :)
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

Construí quicktype exactamente para este propósito. Simplemente pegue su JSON de muestra y quicktype genera esta jerarquía de tipos para sus datos de 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
}

También genera un código de clasificación libre de dependencias para obtener el valor de retorno de JSONSerialization.jsonObjecta Forecast, incluido un constructor de conveniencia que toma una cadena JSON para que pueda analizar rápidamente un Forecastvalor fuertemente tipado y acceder a sus campos:

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

Puede instalar quicktype desde npm con npm i -g quicktypeo usar la interfaz de usuario web para obtener el código generado completo para pegarlo en su patio de recreo.


Los enlaces están fallando.
dibujó ..

Fantástico ahorrador de tiempo! ¡Buen trabajo!
marko

1
Herramienta brillante. Gracias.
Ahmet Ardal

4

Actualizado el isConnectToNetwork-Functiondespués, gracias a esta publicación .

Escribí un método extra para ello:

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
}

Así que ahora puedes llamar fácilmente a esto en tu aplicación donde quieras

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
    }
}

Marco, ¿cuál sería la mejor manera de agregar esta función en el proyecto? en cualquier contorller o modelo?
KamalPanhwar

oye, simplemente agregas el primer método func loadingJSON (...) en un archivo o clase extra rápido. Después de eso, puede llamar a esto desde todos los controladores en su proyecto
Marco Weber

Lo probé, pero me encanta la idea de demostrar una solución completa y también cómo usarla, incluido el método auxiliar isConnectedToNetwork (), es la idea de cómo implementarla probablemente en buen código
Amr Angry

así que simplemente utilicé un nuevo archivo swift (clic izquierdo en el árbol del proyecto, nuevo archivo ..., archivo swift) y lo llamé jsonhelper.swift. en este archivo coloca el primer código, cargando JSON () y isConnectedToNetwork (). después de eso, puede usar estas dos funciones en cada parte de su proyecto. por ejemplo, en un loginVC, como acción del botón de inicio de sesión, puede usar el segundo código, obviamente debe cambiar el dominio, la cadena de publicación y los valores de paseJson (parseJSON ["loginSuccessfull"]) para que coincidan con su archivo php
Marco Weber

0

Swift tiene una poderosa inferencia de tipos. Vamos a deshacernos de la placa repetitiva "if let" o "guard let" y forzar el desenvolvimiento utilizando un enfoque funcional:

  1. Aquí está nuestro JSON. Podemos usar JSON opcional o habitual. Estoy usando opcional en nuestro ejemplo:

    let json: Dictionary<String, Any>? = ["current": ["temperature": 10]]
  1. Funciones de ayuda. Necesitamos escribirlos solo una vez y luego reutilizarlos con cualquier diccionario:

    /// 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. Y aquí está nuestra magia: extraer el valor:

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

Solo una línea de código y sin desenredos forzados o fundición manual. Este código funciona en el patio de recreo, por lo que puede copiarlo y verificarlo. Aquí hay una implementación en GitHub.


0

Esta es otra forma de resolver su problema. Por lo tanto, consulte la siguiente solución. Espero que te ayude.

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

El problema es con el método de interacción API. El análisis JSON solo se cambia en la sintaxis. El principal problema es con la forma de obtener datos. Lo que está utilizando es una forma síncrona de obtener datos. Esto no funciona en todos los casos. Lo que debería usar es una forma asincrónica de obtener datos. De esta manera, debe solicitar datos a través de la API y esperar a que responda con datos. Puede lograr esto con sesiones de URL y bibliotecas de terceros como Alamofire. A continuación se muestra el código para el método de sesión 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"
      }
     }
   ]
}

Si creo un modelo usando json anterior Usando este enlace [blog]: http://www.jsoncafe.com para generar una estructura codificable o cualquier formato

Modelo

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)
    }
}

Analizar gramaticalmente

    do {
        let res = try JSONDecoder().decode(RootClass.self, from: data)
        print(res?.user?.first?.firstUser?.name ?? "Yours optional value")
    } catch {
        print(error)
    }
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.