JSON optimisation récupération des données

Bonjour,

Je récupère un JSON sont voici la forme:

{"status":{"elapsed":237,"timestamp":"2022-05-16T08:37:07.217103212Z"},"data":{"market_id":"0f5e624a-4730-403e-b141-0a905064e0e4","values":[[1651770000000,35303.39,35348.09,35016.56,35084.99,58.93334],[1651773600000,35079.15,35105.57,34340.58,34657.65,88.51477999999999],...

J'ai tout mis dans un dictionnaire, tout fonctionne correctement. Cependant, je voudrais extraire certaines données associées à la clé "values".
Existe-t-il un outil pour faire cela ou faut-il travailler caractère par caractère?

Je dois récupérer toutes les avant-dernières valeurs. Dans mon exemple il s'agit de 35084.99 et de 34657.65

Mots clés:
«1

Réponses

  • klogklog Membre

    Tu ne peux pas utiliser NSJSONSerialization ?

  • RocouRocou Membre

    Je l'utilise déjà mais peut-être n'en ai-je pas compris toutes les possibilités?

  • klogklog Membre
    mai 2022 modifié #4

    @Rocou a dit :
    Je l'utilise déjà...

    Au temps pour moi...

    Sous quelle forme (quels objets) apparait values dans ton dictionnaire ? values devrait être un NSArray de NSArray de NSNumber, non ?

  • LarmeLarme Membre

    C'est taggué Swift ? Swift 4+?
    Si c'est le cas, utilise JSONDecoder & Codable, sinon, (NS)JSONSerialization.
    values est un Array d'array de Double, un [[Double]].
    Donc, itères sur values, et récupère la valeur à l'index 4.
    Sur ta structure, tu peux avoir une lazy var ou une computed property qui peut te retourner un array de Double que tu souhaites.

  • klogklog Membre

    Du coup, quelque chose dans ce genre :

    NSDictionary *dictionary = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
    
    for (NSArray<NSNumber*> *valuesArray in dictionary[@"values"])
    {
      NSNumber *value = [valuesArray objectAtIndex:valuesArray.count - 2];
    }
    
  • LarmeLarme Membre

    Il manque un niveau de parsing (dictionary[@"data"][@"values"]), mais dans l'idée ça doit être ça en Objective-C.

  • klogklog Membre
    mai 2022 modifié #8

    @Larme a dit :
    Il manque un niveau de parsing (dictionary[@"data"][@"values"]), mais dans l'idée ça doit être ça en Objective-C.

    Oui je viens de m'en rendre compte :blush:

    En testant ce bout code sur ton extrait JSON :

    NSError *error;
    NSData *data = [NSData dataWithContentsOfURL:url];
    NSDictionary *dictionary = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
    for (NSArray<NSNumber*> *valuesArray in dictionary[@"data"][@"values"])
    {
      NSNumber *value = [valuesArray objectAtIndex:valuesArray.count - 2];
      NSLog(@"%f", value.floatValue);
    }
    

    j'obtiens bien les 2 valeurs que tu cherches Rocou...

  • RocouRocou Membre

    Effectivement, il y avait un truc que je n'avais pas compris.
    Merci beaucoup (sinon j'ai abandonné ObjectiveC, je ne travaille plus qu'en swift)

  • LarmeLarme Membre
    mai 2022 modifié #10

    Alors vu que tu utilises Swift, tu n'as pas spécifié si tu utilisais Swift 4+, mais quand j'ai lu "dictionnaire", j'avoue ne pas avoir été fan de cette réponse avec Codable.

    Si jamais Codable t'es autorisé, voici un p'tit sample dans Playground :

    func valuesInJSON() {
        let json = """
        {
        "status": {
            "elapsed": 237,
            "timestamp": "2022-05-16T08:37:07.217103212Z"
        },
        "data": {
            "market_id": "0f5e624a-4730-403e-b141-0a905064e0e4",
            "values": [
                [1651770000000, 35303.39, 35348.09, 35016.56, 35084.99, 58.93334],
                [1651773600000, 35079.15, 35105.57, 34340.58, 34657.65, 88.51477999999999]
            ]
        }
        }
        """
    
        struct Response: Codable {
            let status: Status
            let data: Content //var data: Content if you want to use the lazy
    
            struct Status: Codable {
                let elapsed: Int
                let timestamp: Date
            }
    
            //Named Content, and not Data, because Data already exists (Foundation.Data), it's possible to name it Data, but it's more painful later
            struct Content: Codable { 
                let marketId: UUID
                let values: [[Double]]
    
                var computedForthValues: [Double] {
                    values.map { $0[4] }
                }
    
                lazy var lazyForthValues: [Double] = {
                    values.map { $0[4] }
                }()
            }
        }
    
        do {
    
            let formatter = DateFormatter()
            formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ"
            formatter.locale = Locale(identifier: "en_US_POSIX")
            let decoder = JSONDecoder()
            decoder.keyDecodingStrategy = .convertFromSnakeCase
            decoder.dateDecodingStrategy = .formatted(formatter)
            let response = try decoder.decode(Response.self, from: Data(json.utf8)) //var response = try... if you want to use lazy
            print(response)
            print("Computed: \(response.data.computedForthValues)")
    //        print("Lazy: \(response.data.lazyForthValues)")
        } catch {
            print("Error while decoding: \(error)")
        }
    }
    
    valuesInJSON()
    

    L'avantage de lazy, c'est que cela ne sera calculé qu'une seule fois, mais cela t'obliges à mettre des var un peu partout et pas des let... Donc, en fonction de ton utilisation : si tu ne le récupères pas à tout bout d'champ, que ton application est assez légère, car il faut avouer que faire ce simple map() n'est pas excessif non plus sur le CPU, en tout cas sur l'exemple isolé cité...

  • RocouRocou Membre
    mai 2022 modifié #11

    J'ai encore besoin d'un petit coup de pouce. Je n'arrive pas à construire mes déclarations avec un json plus complexe. Sinon, c'est exactement le même but, récupérer les 4e chiffres de "values" (34070.01 , 34167.18 et 28740.01 )

    {
    "status":{
    "elapsed":258,
    "timestamp":"2022-05-17T13:04:11.160368806Z"
    },
    "data":{
    "market_id":"0f5e624a-4730-403e-b141-0a905064e0e4",
    "market_name":"toto",
    "market_slug":"toto",
    "class":"",
    "parameters":{
    "start":"2022-05-06T21:04:10.901581079Z",
    "end":"2022-05-17T13:04:10.901581079Z",
    "interval":"1h",
    "order":"ascending",
    "format":"json",
    "timestamp_format":"unix-milliseconds",
    "columns":[
    "timestamp",
    "open",
    "high",
    "low",
    "close",
    "volume"
    ],
    "market_key":"toto",
    "market_id":"0f5e624a-4730-403e-b141-0a905064e0e4"
    },
    "schema":{
    "metric_id":"price",
    "name":"Price",
    "description":"Open, high, low, close, and volume",
    "values_schema":{
    "timestamp":"Timestamp in milliseconds",
    "open":"Ouverture.",
    "high":"Au plus haut.",
    "low":"Au plu bas",
    "close":"Fermeture.",
    "volume":"Volume sur intervalle."
    },
    "minimum_interval":"1m",
    "source_attribution":[
    {
    "name":"titi",
    "url":"https://www.titi.com/"
    }
    ]
    },
    "values":[
    [
    1651874400000,
    34133.71,
    34240.23,
    34070.01,
    34166.81,
    17.42212
    ],
    [
    1651878000000,
    34170.94,
    34286.07,
    34167.18,
    34189.33,
    22.14083
    ],
    [
    1652788800000,
    28760.73,
    29076.24,
    28740.01,
    28939.74,
    57.76676
    ]
    ]
    }
    }

  • LarmeLarme Membre
    mai 2022 modifié #12

    Cela ne devrait rien changer au code que j'ai proposé, mon code devrait marcher, juste que les champs rajoutés sont ignorés. La structure globale du JSON étant la même...

    Il faut juste que values.map { $0[4] } à la place de 4, il y ait le bon index, ici 3.

  • RocouRocou Membre

    C'est parce que j'obtiens cette erreur:

    Error while decoding: dataCorrupted(Swift.DecodingError.Context(codingPath: [], debugDescription: "The given data was not valid JSON.", underlyingError: Optional(Error Domain=NSCocoaErrorDomain Code=3840 "Badly formed array around line 1, column 9." UserInfo={NSDebugDescription=Badly formed array around line 1, column 9., NSJSONSerializationErrorIndex=9})))

    La différence avance ton code est que je n'intègre évidemment pas le JSON dans mon code, je récupère le contenu d'un fichier json.
    Voici le bout de code:

    var stringmonDico:String = ""
    
    
        let url = URL(string: "https://gnagnagna.fr")!
        let request = URLRequest(url: url)
        // Create the HTTP request
        let session = URLSession.shared
        let task = session.dataTask(with: request) { (data, response, error) in
    
    
            if let error = error {
                // Handle HTTP request error
            } else if let data = data {
                // Handle HTTP request response
    
                do {
                    //Get data from file
                    let data = try Data(contentsOf: url)
    
                    //Decode data to a Dictionary<String, Any> object
    
                    guard let monDico = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else {
                        print("Could not cast JSON content as a Dictionary<String, Any>")
                        return
                    }
    
    
                   //C'EST ICI L'ESSENTIEL 
                    stringmonDico = monDico.description
    
    
    
    
                } catch {
                    //Print error if something went wrong
                    print("Error: \(error)")
                }
    
            } else {
                // Handle unexpected error
            }
    

    Ensuite, arrive ton code et je remplace json.utf8 par stringmonDico.uft8

  • LarmeLarme Membre

    Alors ok, c'est normal.
    Tu utilises mon code directement, j'appelle JSONDecoder à la place de JSONSerialization.

    Pourquoi cela rate ?
    Car tu as désérialisé déjà avec JSONSerialization, et tu as [String: Any]. Ensuite, tu le transformes en String via monDict.description, sauf que le description pour un Dictionary, c'est custom made in Apple, cela n'est en aucun cas du JSON ! Donc ensuite, il te dit en erreur : bah non, ce string n'est pas un JSON valide.

    Oublie totalement stringmonDico.

    Ensuite, je vois une deuxième horreur.
    dataTask(with:), cela fait déjà la requête (enfin, surtout si tu as un .resume() en fin de cette dernière pour la lancer), or tu fais après avoir terminer Data(contentsOf:), donc tu refais la requête encore !

    Enfin, j'ai peur du fait que tu gardes stringmonDico en variable comme ça. Y'a pas assez de contexte, mais je me demande comment tu gères l'asynchrone ici...

  • LarmeLarme Membre

    Cela devrait être quelquechose de ce goût là (écrit à la main, sans compiler, donc y'a peut-être une faute de typo)

    func makeTheRequest(completion: ((Response?) -> Void)?) //En réalité, ici, un Result est plus souvent utilisé
        let url = URL(string: "https://gnagnagna.fr")!
        let request = URLRequest(url: url)
        // Create the HTTP request
        let session = URLSession.shared
        let task = session.dataTask(with: request) { (data, response, error) in
            if let error = error {
                // Handle HTTP request error
            } else if let data = data {
                // Handle HTTP request response
                do {
                   let formatter = DateFormatter()
                   formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ"
                   formatter.locale = Locale(identifier: "en_US_POSIX")
                   let decoder = JSONDecoder()
                  decoder.keyDecodingStrategy = .convertFromSnakeCase
                  decoder.dateDecodingStrategy = .formatted(formatter)
                  let decodedResponse = try decoder.decode(Response.self, from: data)
                  print(decodedResponse)
                  print("Computed: \(decodedResponse.data.computedForthValues)")
                  completion(decodedResponse)
                } catch {
                    //Print error if something went wrong
                    print("Error while decoding response: \(error)") //Side note: ne jamais print error.localizedDescription, c'est pour l'utilisateur, pas pour le développeur, et cela skippe toutes les infos importantes)
                    print("Received response stringified: \(String(data: data, encoding: .utf8)") //Printer la réponse qui est censée être du JSON en cas d'erreur est toujours une bonne idée. Le nombre de fois où suite à une mauvaise requête au niveau des paramètres, de l'URL, une erreur serveur, une réponse supposée être ainsi et pas comme ça en réalité, cela renvoie parfois des réponses différentes JSON, voire des codes HTML d'erreurs, etc. 
                     completion?(nil)
                }
    
            } else {
                // Handle unexpected error
                completion?(nil)
            }
        }
        task.resume()
    }
    

    À appeler ainsi :

    makeTheRequest(completion: { response in
        guard let response = response else { return }
        //make something with response
    }) 
    
  • RocouRocou Membre

    Je ne comprends pas la déclaration de la fonction:
    ((Response?) -> Void)?)

    S'il n'y a rien dans Response alors on renvoie Void.
    Est-cela? et à quoi sert le second ?

    Par ailleurs, à la compilation, Xcode me dit que Response n'est pas dans le scope, je ne comprends pas pourquoi.

  • LarmeLarme Membre

    @Rocou a dit :
    Je ne comprends pas la déclaration de la fonction:
    ((Response?) -> Void)?)

    C'est une closure. L'équivalent Objective-C c'est Block.
    C'est exactement ce que tu as appelé avec dataTask(with:completionHandler:) dont la déclaration est:

    func dataTask(with request: URLRequest, 
    completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask
    

    S'il n'y a rien dans Response alors on renvoie Void.
    Est-cela? et à quoi sert le second ?

    Si responseDecoded a raté au decoding, tu veux l'inventer ?
    Tu peux également renvoyer une Error, comme le fait ``dataTask(with:completionHandler:) , avec d'un côté data et de l'autre error (enfin presque). Il te suffit de modifier la déclaration de la closure du coup.

    Voyant cela, cela semble confirmer que tu ne maîtrises pas l'asynchrone...
    Certains te diront mais tu t'en fiches, y'a async/await maintenant en Swift je sais plus quelle version récente, mais bon, y'a pas mal de doc/tutos avec des closures, alors les comprendre est un plus.

    Par ailleurs, à la compilation, Xcode me dit que Response n'est pas dans le scope, je ne comprends pas pourquoi.

    Cela dépend d'où tu as déclaré struct Response: Codable. Je l'avais mis dans la méthode valuesInJSON car c'est dans mon Playgrounds, et j'ai peut-être une autre structure Response quelque part (un vrai bazar). Donc pour éviter des doublons...

  • RocouRocou Membre

    @Larme a dit :
    Voyant cela, cela semble confirmer que tu ne maîtrises pas l'asynchrone...

    Je ne maitrise pas du tout l'asynchrone, c'est le moins que l'on puisse dire. :/

    Cependant j'ai corrigé ce qu'il fallait et maintenant tout fonctionne impeccablement, je te remercie beaucoup.

    A un détail près: completion(decodedResponse). Cela ne passe pas à la compilation, j'ai droit à ce message:

    Value of optional type '((Response?) -> Void)?' must be unwrapped to a value of type '(Response?) -> Void'
    Coalesce using '??' to provide a default when the optional value contains 'nil'
    Force-unwrap using '!' to abort execution if the optional value contains 'nil'

    J'ai regardé sur internet la notion de completion, pour le moment je n'ai pas compris grand chose. Dans le contexte ci-avant, quel est l'objectif de completion(decodedResponse) ?

  • LarmeLarme Membre

    completion?(decodedResponse), c'est un optional pardon. Je l'ai mis sur les autres, mais pas sur ce dernier.

  • RocouRocou Membre
    mai 2022 modifié #20

    Je suppose que mon nouveau problème vient du fonctionnement asynchrone: en effet, je voudrais pouvoir manipuler ailleurs dans mon programme le tableau obtenu: response.data.computedForthValues
    Le problème est que la fonction makeTheRequest() se termine avant que la requête aboutisse. Par conséquent, le tableau que je tente de retourner est vide.
    Y-a-t-il une sorte de "wait" qui permettrait d'attendre que la requête soit exécutée afin que je puisse récupérer les valeurs après avoir appelé la fonction makeTheRequest ?

    Où alors il existe un autre fonctionnement qui m'échappe?

  • LarmeLarme Membre
    mai 2022 modifié #21

    Oui, il te manque le concept de l'asynchrone.

    Pour le mettre en avant, mettons en exergue l'asynchrone sur URLSession:

    func makeTheRequest(completion: ((Response?) -> Void)?) //En réalité, ici, un Result est plus souvent utilisé
        ...
        print("Avant l'appel")
        let task = session.dataTask(with: request) { (data, response, error) in
            print("Dans la closure")
        }
        print("Après la closure")
        task.resume()
        print("Fin de méthode")
    }
    

    Si on regarde les prints dans le console, tu devrais t'attendre si tu ne maîtrise pas l'asynchrone à voir :

    $>Avant l'appel
    $>Dans la closure
    $>Après la closure
    $>Fin de méthode
    

    Or en réalité, c'est :

    $>Avant l'appel
    $>Après la closure
    $>Fin de méthode
    $>Dans la closure
    

    La closure, il faut vioir ça comme un callback (si le terme te parle), une sorte de mini-méthode qui sera appelé en temps et en heure. Ici, quand tu as la réponse de ta requête.
    Tu as donc mis du code à l'intérieur de la closure pour réagir à la data que tu as reçu.
    Donc, tu es capable de faire la même chose sur ta méthode makeTheRequest(completion:), non ? Tu vois l'analogie ?

    Je ne sais pas ce que tu fais avec tes valeurs, mais :
    Considère que tu ne les as pas (au lancement, l'appel réseau n'a pas été effectué).
    Puis :

    makeTheRequest(completion: { [weak self] response in
        guard let response = response else { return }
        self?.updateMyUIOrOtherWorkWithResponse(response) //Si c'est de l'UI, il te faudra un `DispatchQueue.main.async{}`
    })
    

    Attention, closure ne veut pas dire qu'elle sera appelée de manière asynchrone.

    Un contre exemple simple est filter():

    let array = [0, 1, 2, 3, 4]
    print("Avant la methode de tri")
    let evenNumbers = array.filter { aNumber in
        print("Dans la closure, evaluer: \(aNumber)")
        if aNumber % 2 == 0 {
            return true
        } else {
            return false
        }
    }
    print("Après la methode de tri")
    print("Resultat: \(evenNumbers)")
    

    Console:

    $>Avant la methode de tri
    $>Dans la closure, evaluer: 0
    $>Dans la closure, evaluer: 1
    $>Dans la closure, evaluer: 2
    $>Dans la closure, evaluer: 3
    $>Dans la closure, evaluer: 4
    $>Après la methode de tri
    $>Resultat: [0, 2, 4]
    
  • RocouRocou Membre

    Je te remercie, je vais étudier tout cela.
    Cependant, est-il possible de ne pas utiliser de closure? Car en l'occurence j'ai besoin d'un fonctionnement totalement synchrone:

    • j'appelle une fonction
    • celle-ci renvoie son résultat
    • je continue mon traitement en utilisant le résultat reçu.

    Je comprends parfaitement l'intérêt d'un fonctionnement asynchrone mais pour le coup, je n'en ai pas besoin.

  • LarmeLarme Membre

    Pourquoi ne continues-tu pas ton traitement dans la closure de makeTheRequest(completion:)?

    Vu que tu fais une requête sans header, et en GET, tu peux utiliser Data(contentsOf:), et c'est synchrone. Cela bloquera cependant le thread en cours, et si c'est le main thread, ton UI sera bloquée...

    Tu peux utiliser les async/await en Swift récent, cela rend le code plus linéaire, mais cela ne fait que "cacher" les closures...

  • RocouRocou Membre

    @Larme a dit :
    Pourquoi ne continues-tu pas ton traitement dans la closure de makeTheRequest(completion:)?

    Tu veux dire:

    makeTheRequest(completion: { response in
    guard let response = response else { return }
    //make something with response
    ICI?
    })

    Mais c'est bien le problème, justement. A ce niveau, je n'ai pas encore récupéré les données.

  • LarmeLarme Membre
    mai 2022 modifié #25

    Ah si, à ce moment là, tu as response ! Ce ne sont pas les données que tu attendais ?

    En détail :

    makeTheRequest(completion: { response in
    guard let response = response else { return }
    //make something with response
    //Ici tu as récupéré les données de ta Web request, c'est le callback que tu appelles une fois que tu as décodé !
    })
    //Ici tu n'as pas encore récupéré la réponse
    
  • RocouRocou Membre

    Oui, j'ai response mais c'est le json complet. Ce que je veux c'est le tableau contenant les 4e nombres de la clé "values" du json.

  • LarmeLarme Membre

    Tu utilises donc response.data.computedForthValues, c'est ton tableau de Double ça.

  • RocouRocou Membre

    C'est la première chose que j'ai testée mais cela ne fonctionne pas:

       makeTheRequest(completion: { response in
            guard let response = response  else { return }
            //make something with response
            print("mon tableau de doubles: \(response.data.computedForthValues)")
          })
    

    Le print ne semble pas être exécuté. Il ne se passe rien du tout.

  • LarmeLarme Membre

    response est nil alors? guard let response = response else { return } est executé ? N'oublie pas que c'est asynchrone. Tu as un // Handle HTTP request error, ça print quelque chose dedans?

  • PyrohPyroh Membre

    Un print depuis un autre thread que le main n'a aucune chance d'afficher quoi que ce soit.

  • LarmeLarme Membre

    @Pyroh a dit :
    Un print depuis un autre thread que le main n'a aucune chance d'afficher quoi que ce soit.

    Pas sur iOS en tout cas, et il ne me semble pas avoir vu ça sur macOS non plus...

Connectez-vous ou Inscrivez-vous pour répondre.