Analysez facilement les données dans vos applications iOS
Je voulais analyser certaines données de logement qui viennent dans un fichier CSV de Zillow. J’ai pensé que ce serait un excellent moyen d’apprendre à écrire une AsyncSequence personnalisée. Ce court tutoriel explique comment écrire un analyseur CSV simple qui utilise AsyncSequence
pour analyser les données dans un tableau de valeurs.
Le code source de cet article peut être dans GithubGenericName.
Un fichier CSV (valeurs séparées par des virgules) est un fichier texte qui enregistre les données dans un format colonne/ligne. Chaque ligne est un ensemble de données avec une valeur par colonne. Il y a un en-tête facultatif dans la première ligne. Les colonnes sont généralement séparées par une virgule (,).
Un article Wikipédia complet est disponible ici.
AsyncSequence a été introduit avec Swift async/wait. Il vous permet d’énumérer une collection d’objets de manière asynchrone, comme vous le feriez sur une liste. Cependant, cette séquence de données de données n’est pas nécessairement en mémoire au moment où vous effectuez une boucle sur un élément, ce qui vous permet d’attendre que la valeur soit récupérée à chaque itération. Pour une documentation complète, voir La documentation d’Apple.
Nous allons analyser certaines données sur le logement de Zillow. Ces données sont les prix mensuels moyens des logements pour les régions des États-Unis. Pour ce tutoriel, nous utiliserons les prix Home Values, un lien de téléchargement peut être trouvé à ici. Les données métropolitaines et américaines que nous utiliserons se trouvent ici lien.
Regardons les 6 premières colonnes des trois premières lignes. Colonnes au-delà column 5
sont toutes des dates.
Les données ressemblent à ce qui suit :
RegionID,SizeRank,RegionName,RegionType,StateName,2000–01–31,
102001,0,United States,Country,,128454.0
394913,1,”New York, NY”,Msa,NY,225516.0
En regardant ces données, nous pouvons voir que :
- Les colonnes sont séparées par des virgules
- Il n’y a pas d’espace entre la virgule et les données
- Supposons que tout est une chaîne.
- Il peut y avoir des colonnes vides pour les données manquantes.
- Nous voulons la chaîne littérale entre guillemets.
Chaque itération dans nos données renverra une structure Line contenant le numéro de ligne et les données dans l’ordre dans lequel elles se trouvent dans la ligne.
Les données sont renvoyées sous la forme d’un tableau de chaînes facultatives. Toute donnée manquante ou aucun texte entre deux virgules est considéré comme nil
dans le tableau.
struct Line {
let lineNumber: Int
let data: [String?]
}
Les CSV peuvent avoir un en-tête dans le document – généralement à la première ligne, il est donc important de garder une trace du numéro de ligne.
Le URL
la classe a une nouvelle propriété — url.lines
itérer de manière asynchrone sur des lignes de texte. L’analyseur CSV utilisera ceci AsyncSequence
pour analyser les lignes de l’URL.
typealias LineIterator = AsyncLineSequence<URL.AsyncBytes>.AsyncIteratorstruct CSVParser: AsyncSequence, AsyncIteratorProtocol {
private let url: URL
private var lineIterator: LineIterator
private let seperator: Character
private let quoteCharacter: Character = "\""
private var lineNumber = 0
init(url: URL, seperator: Character = ",") {
self.url = url
self.seperator = seperator
self.lineIterator = url.lines.makeAsyncIterator()
}
}
Les bases de notre analyseur sont assez simples.
- Nous avons un constructeur qui prend une URL et le délimiteur de données. Il s’agit par défaut d’une virgule (,).
iineIterator
est notre instance de l’itérateur asynchrone que nous utiliserons pour récupérer les lignes de texte de l’URL. j’ai utilisé untypealias
pour simplifier la syntaxe du type.lineNumber
notre numéro de ligne actuel basé sur 0.- Des propriétés pour conserver nos
url
etdelimeter.
Nous devons implémenter notre séquence et notre itérateur.
AsyncSequence
Element
le type d’élément à retourner,Line
dans notre cas.
typealias Element = Line
makeAysncIterator
La fonction crée un nouvel itérateur utilisé pour générer des lignes. Dans notre cas c’est justeself
.
func makeAsyncIterator() -> CSVParser {
return self
}
Itérateur asynchrone
next
récupère le suivantLine
dans la séquence et incrémente le numéro de ligne après le retour de la ligne. S’il n’y a plus de lignes à alorsnil
est renvoyé, ce qui arrête l’énumération.
mutating func next() async throws -> Line? {
if let string = try await lineIterator.next() {
defer { lineNumber += 1 }
return Line(
lineNumber: lineNumber,
data: split(line: string)
)
}return nil
}
Le split
La fonction est l’endroit où nous parcourons la ligne caractère par caractère et divisons la ligne en un tableau de chaînes facultatives.
private func split(line: String) -> [String?] {
var data = [String?]()
var inQuote = false
var currentString = ""for character in line {
switch character {
case quoteCharacter:
inQuote = !inQuote
continue
case seperator:
if !inQuote {
data.append(currentString.isEmpty ? nil : currentString)
currentString = ""
continue
}
default:
break
}
currentString.append(character)
}
data.append(currentString.isEmpty ? nil : currentString)
return data
}
Marchez chaque personnage dans une ligne. Avec les règles suivantes :
- Si nous avons appuyé sur un guillemet, nous définissons le
inQuote
plat à vrai ou faux. Tant que cela est vrai pour tous les caractères, jusqu’à ce que nous définissionsinQuote
à false sera ajouté aucurrentString
. - Lorsqu’un séparateur est frappé, et que nous ne sommes pas entre guillemets, mettez
currentString
à la fin dedata
déployer. SicurrentString
est mis videnil
à la fin dedata
déployer. Après avoir ajouté à la réinitialisation du tableaucurrentString
. Passez enfin au caractère suivant. - Par défaut, le comportement consiste à ajouter le caractère actuel à
currentString
- La dernière chose avant de retourner le tableau est d’ajouter les données qui ont été analysées jusqu’à la fin de la ligne au
data
déployer.
1, 2, 3,,5, 6
retournerais ["1", "2", "3", nil, "5", "6"]
.
L’analyseur final ressemble à :
typealias LineIterator = AsyncLineSequence<URL.AsyncBytes>.AsyncIteratorstruct CSVParser: AsyncSequence, AsyncIteratorProtocol {
// this is our type we are going to return on next()
typealias Element = Line
private let url: URL
private var lineIterator: LineIterator
private let seperator: Character
private let quoteCharacter: Character = "\""
private var lineNumber = 0
init(url: URL, seperator: Character = ",") {
self.url = url
self.seperator = seperator
self.lineIterator = url.lines.makeAsyncIterator()
}
mutating func next() async throws -> Line? {
if let string = try await lineIterator.next() {
defer { lineNumber += 1 }
return Line(
lineNumber: lineNumber,
data: split(line: string)
)
}
return nil
}
func makeAsyncIterator() -> CSVParser {
return self
}
private func split(line: String) -> [String?] {
var data = [String?]()
var inQuote = false
var currentString = ""
for character in line {
switch character {
case quoteCharacter:
inQuote = !inQuote
continue
case seperator:
if !inQuote {
data.append(currentString.isEmpty ? nil : currentString)
currentString = ""
continue
}
default:
break
}
currentString.append(character)
}
data.append(currentString.isEmpty ? nil : currentString)
return data
}
}
Retirer le main.swift
classe et créer une App
objet pour exécuter du code. Cela nous permettra d’avoir un point d’entrée principal asynchrone pour notre application. Le App.swift
le fichier doit ressembler à ceci :
import Foundation@main
enum App {
static func main() async throws {
do {
let parser = CSVParser(url: URL(string: "https://files.zillowstatic.com/research/public_csvs/zhvi/Metro_zhvi_uc_sfrcondo_tier_0.33_0.67_sm_sa_month.csv?t=1659150579")!)
for try await line in parser {
print("line: \(line.lineNumber)\ndata: \(line.data)")
}
} catch {
print("Error: \(error)")
}
}
}
L’exécution du programme imprimera chaque objet de ligne.
Merci d’avoir lu.