대부분 Json을 사용하기 때문에 XML을 사용할 일이 없었는데, OpenAPI를 하나 사용하다 보니 무조건 XML로만 데이터가 넘어와 XML을 파싱하는 법을 알아보고 적용해봤다.
XMLParser
Foundation에 기본으로 들어가 있는 XMLParser를 사용해 XML을 파싱할 수 있는데, Json과 달리 태그와 값으로 이루워진 XML특성상 사용을 위한 Delegate를 설정해줄 필요가 있다.
먼저 초기화는 간단하다.
let parser = XMLParser(data: data)
parser.delegate = subjectParser.self
parser.parse()
Data 타입의 XML을 XMLParser의 초기화 파라미터로 받을 수 있고, URL도 초기화시 받을 수 있는데, URL은 테스트 할 때 잘 동작하지 않아서 이유는 모르는 채로 API로 받아온 Data를 바로 XMLParser에 주입시켜 초기화 했다.
태그에 따른 값을 뽑기 위해선 Delegate를 만들어야 해서 만들어놓은 Delegate를 채택하고, parse() 메서드를 실행하면 파싱이 시작된다.
parse() 메서드는 성공했을 때 true 실패했을 때 false를 반환한다.
XMLParserDelegate
XMLParserDelegate의 정의할 수 있는 함수는 많지만 분기는 4가지로 나뉜다.
- 태그 시작점을 찾았을 때
- 태그 내 값을 찾았을 때
- 태그가 끝났을 때
- 오류 또는 abortParsing()메서드가 호출됐을때
태그 시작점을 찾았을 때
func parser(_ parser: XMLParser,
didStartElement elementName: String,
namespaceURI: String?,
qualifiedName qName: String?,
attributes attributeDict: [String : String] = [:])
태그 시작점을 찾았을 때 호출되는 함수로 didStartElement에 태그 이름이 String 타입으로 들어온다.
태그 내 값을 찾았을 때
func parser(_ parser: XMLParser, foundCharacters string: String)
태그 내 값을 찾았을 때 호출되는 함수로 타입은 관계없이 전부 foundCharacters에 String 타입으로 들어온다.
태그가 끝났을 때
func parser(_ parser: XMLParser,
didEndElement elementName: String,
namespaceURI: String?,
qualifiedName qName: String?)
태그가 끝났을 대 호출되는 함수로 태그 시작점때 호출되는 함수와 똑같다.
오류 또는 abortParsing()메서드가 호출됐을때
func parser(_ parser: XMLParser, parseErrorOccurred parseError: Error)
오류나 parser.abortParsing() 이 호출됐을때 호출되는 함수로 Error타입의 값이 전달된다.
구현
import Foundation
class SubjectParser: NSObject, XMLParserDelegate {
internal var key: XMLKey?
internal var dict: [XMLKey: String] = [:]
var items: [VolunteerSubject] = []
internal func parser(_ parser: XMLParser,
didStartElement elementName: String,
namespaceURI: String?,
qualifiedName qName: String?,
attributes attributeDict: [String : String] = [:]) {
switch elementName {
case "item": dict = [:]
case "progrmRegistNo": key = .progrmRegistNo
case "nanmmbyNm": key = .nanmmbyNm
case "progrmSj": key = .progrmSj
case "progrmBgnde": key = .progrmBgnde
case "progrmEndde": key = .progrmEndde
case "progrmSttusSe": key = .progrmSttusSe
case "totalCount": key = .totalCount
default: key = nil
}
}
internal func parser(_ parser: XMLParser,
foundCharacters string: String) {
guard let key else { return }
//첫 문장이 가끔 짤려서 두 번 넘어오는 경우가 있어서 이미 키가 있다면 append로 처리
if dict.keys.contains(key) {
dict[key]?.append(string)
} else {
dict.updateValue(string, forKey: key)
}
}
internal func parser(_ parser: XMLParser,
didEndElement elementName: String,
namespaceURI: String?,
qualifiedName qName: String?) {
if elementName == "item" {
guard let id = dict[XMLKey.progrmRegistNo],
let title = dict[XMLKey.nanmmbyNm],
let detail = dict[XMLKey.progrmSj],
let startDate = dict[XMLKey.progrmBgnde],
let endDate = dict[XMLKey.progrmEndde],
let status = dict[XMLKey.progrmSttusSe] else { return }
let item = VolunteerSubject(id: id,
title: title,
detail: detail,
startDate: startDate,
endDate: endDate,
status: status)
items.append(item)
dict = [:]
}
}
internal enum XMLKey: String, CaseIterable {
case nanmmbyNm
case progrmBgnde
case progrmEndde
case progrmRegistNo
case progrmSj
case progrmSttusSe
case totalCount
}
}
Delegate를 채택한 클래스를 생성했을 때 내부에서 키와 데이터를 처리하여 저장했다.
태그 시작점의 String을 확인해 current key를 들고 있다가 값을 찾았을 때 Dictionary에 키와 값을 주입한다. 가끔 긴 문장이나 특수문자가 들어가는 값의 경우 나눠져서 들어올 때가 있어서 이미 Dictionary에 Key가 존재할 경우 값을 추가하기만 했다.
태그가 끝났을 때, 하나의 아이템을 모두 파싱했을 때는 값의 존재여부를 확인해 사용할 모델에 맞게 데이터를 변형하고 store한다. key는 계속해서 태그 시작점 호출 함수에서 변하기 때문에 문제는 없지만 Dictionary 같은 경우 초기화 하지 않으면 중복으로 들어가기 때문에 꼭 다시 초기화 해줘야 한다.
여러개의 다른 XML을 Parsing?
서로 다른 XML을 파싱해야해서 고민을 했는데 사실 이 방법이 괜찮은건진 모르지만, 그냥 간단하게 delegate를 두 개 만들어서 파싱 요청시 알맞는 delegate를 채택해 사용하게 바꿨다.
XMLParserDelegate를 채택한 CLASS를 변수로 저장해놓고 바꾸면서 사용했다.
parse 중간에 화면이 이동되거나 취소해야한다면 꼭 abortParsing() 메서드를 사용해 처리를 해야한다. 필요없는데 계속 도는 것 보단 낫다.
또한 XML파일이 크면 클 수록 parsing에 걸리는 시간이 늘어나기 때문에, 비동기로 처리하는 편이 좋다.