셔틀 버스 앱을 개발하다가 공공데이터 OpenAPI를 사용하려고 하는데 XML로 통신이 이루어 졌다.
JSON처리는 JSONDecoder를 사용해서 비교적 쉽게 처리 할 수 있었는데 XML이 잘 사용되지 않은 방식이라 그런지 정보가 많이 없어서 차근 차근 해보도록 하자.
<response>
<comMsgHeader/>
<msgHeader>
<queryTime>2023-07-02 20:09:20.659</queryTime>
<resultCode>0</resultCode>
<resultMessage>정상적으로 처리되었습니다.</resultMessage>
</msgHeader>
<msgBody>
<busArrivalItem>
<flag>PASS</flag>
<locationNo1>2</locationNo1>
<locationNo2>7</locationNo2>
<lowPlate1>0</lowPlate1>
<lowPlate2>0</lowPlate2>
<plateNo1>경기78아1212</plateNo1>
<plateNo2>경기78아1237</plateNo2>
<predictTime1>2</predictTime1>
<predictTime2>13</predictTime2>
<remainSeatCnt1>39</remainSeatCnt1>
<remainSeatCnt2>41</remainSeatCnt2>
<routeId>228000177</routeId>
<staOrder>74</staOrder>
<stationId>228002023</stationId>
</busArrivalItem>
</msgBody>
</response>
위와 같은 형식의 데이터를 처리하고 싶었다.
우선 처리를 위해 해당 데이터를 담을 구조체를 만들어 줬다.
https://jsonformatter.org/xml-to-swift 해당 사이트를 이용해서 쉽게 만들 수 있다.
import Foundation
// MARK: - BusTime
struct BusTime {
let response: Response
}
// MARK: - Response
struct Response {
let comMsgHeader: String
let msgHeader: MsgHeader
let msgBody: MsgBody
}
// MARK: - MsgBody
struct MsgBody {
let busArrivalItem: BusArrivalItem
}
// MARK: - BusArrivalItem
struct BusArrivalItem {
let flag, locationNo1, locationNo2, lowPlate1: String
let lowPlate2, plateNo1, plateNo2, predictTime1: String
let predictTime2, remainSeatCnt1, remainSeatCnt2, routeID: String
let staOrder, stationID: String
}
// MARK: - MsgHeader
struct MsgHeader {
let queryTime, resultCode, resultMessage: String
}
class MyXMLParser: NSObject, XMLParserDelegate {
private var currentElement: String?
private var busTime: BusTime?
// XMLParserDelegate 메서드 중 시작 태그를 만났을 때 호출되는 메서드
func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String] = [:]) {
currentElement = elementName
if elementName == "response" {
busTime = BusTime(response: Response(comMsgHeader: "", msgHeader: MsgHeader(queryTime: "", resultCode: "", resultMessage: ""), msgBody: MsgBody(busArrivalItem: BusArrivalItem(flag: "", locationNo1: "", locationNo2: "", lowPlate1: "", lowPlate2: "", plateNo1: "", plateNo2: "", predictTime1: "", predictTime2: "", remainSeatCnt1: "", remainSeatCnt2: "", routeID: "", staOrder: "", stationID: ""))))
}
}
// XMLParserDelegate 메서드 중 태그의 텍스트를 만났을 때 호출되는 메서드
func parser(_ parser: XMLParser, foundCharacters string: String) {
print("Parser text parser")
if let element = currentElement {
if element == "comMsgHeader" {
busTime?.response.comMsgHeader = string
} else if element == "queryTime" {
busTime?.response.msgHeader.queryTime = string
} else if element == "resultCode" {
busTime?.response.msgHeader.resultCode = string
} else if element == "resultMessage" {
busTime?.response.msgHeader.resultMessage = string
} else if element == "flag" {
busTime?.response.msgBody.busArrivalItem.flag = string
} else if element == "locationNo1" {
busTime?.response.msgBody.busArrivalItem.locationNo1 = string
} else if element == "locationNo2" {
busTime?.response.msgBody.busArrivalItem.locationNo2 = string
} else if element == "lowPlate1" {
busTime?.response.msgBody.busArrivalItem.lowPlate1 = string
} else if element == "lowPlate2" {
busTime?.response.msgBody.busArrivalItem.lowPlate2 = string
} else if element == "plateNo1" {
busTime?.response.msgBody.busArrivalItem.plateNo1 = string
} else if element == "plateNo2" {
busTime?.response.msgBody.busArrivalItem.plateNo2 = string
} else if element == "predictTime1" {
busTime?.response.msgBody.busArrivalItem.predictTime1 = string
} else if element == "predictTime2" {
busTime?.response.msgBody.busArrivalItem.predictTime2 = string
} else if element == "remainSeatCnt1" {
busTime?.response.msgBody.busArrivalItem.remainSeatCnt1 = string
} else if element == "remainSeatCnt2" {
busTime?.response.msgBody.busArrivalItem.remainSeatCnt2 = string
} else if element == "routeId" {
busTime?.response.msgBody.busArrivalItem.routeID = string
} else if element == "staOrder" {
busTime?.response.msgBody.busArrivalItem.staOrder = string
} else if element == "stationId" {
busTime?.response.msgBody.busArrivalItem.stationID = string
}
}
}
// XMLParserDelegate 메서드 중 종료 태그를 만났을 때 호출되는 메서드
func parser(_ parser: XMLParser, didEndElement elementName: String, namespaceURI: String?, qualifiedName qName: String?) {
currentElement = nil
}
// 파싱이 완료되었을 때 호출되는 메서드 (테스트용)
func parserDidEndDocument(_ parser: XMLParser) {
// 파싱 결과 출력
if let busTime = busTime {
print("comMsgHeader: \\(busTime.response.comMsgHeader)")
print("queryTime: \\(busTime.response.msgHeader.queryTime)")
print("resultCode: \\(busTime.response.msgHeader.resultCode)")
print("resultMessage: \\(busTime.response.msgHeader.resultMessage)")
print("flag: \\(busTime.response.msgBody.busArrivalItem.flag)")
print("locationNo1: \\(busTime.response.msgBody.busArrivalItem.locationNo1)")
print("locationNo2: \\(busTime.response.msgBody.busArrivalItem.locationNo2)")
print("lowPlate1: \\(busTime.response.msgBody.busArrivalItem.lowPlate1)")
print("lowPlate2: \\(busTime.response.msgBody.busArrivalItem.lowPlate2)")
print("plateNo1: \\(busTime.response.msgBody.busArrivalItem.plateNo1)")
print("plateNo2: \\(busTime.response.msgBody.busArrivalItem.plateNo2)")
print("predictTime1: \\(busTime.response.msgBody.busArrivalItem.predictTime1)")
print("predictTime2: \\(busTime.response.msgBody.busArrivalItem.predictTime2)")
print("remainSeatCnt1: \\(busTime.response.msgBody.busArrivalItem.remainSeatCnt1)")
print("remainSeatCnt2: \\(busTime.response.msgBody.busArrivalItem.remainSeatCnt2)")
print("routeID: \\(busTime.response.msgBody.busArrivalItem.routeID)")
print("staOrder: \\(busTime.response.msgBody.busArrivalItem.staOrder)")
print("stationID: \\(busTime.response.msgBody.busArrivalItem.stationID)")
}
}
}
여기까지만 봐도 JSON 보다 귀찮은 작업인 걸 확인 할 수 있다.
XML은 태그별로 Parsing을 직접 처리하게 코드를 짜야한다 ㅠ
URL 타입
으로 만들어 준다.guard let url = URL(string: self.url) else { return }
XMLParser
인스턴스 만들기guard let xmlParser = XMLParser(contentsOf: url) else { return }
let myParser = MyXMLParser()
xmlParser.delegate = myParser
xmlParser.parse()
func load(){
guard let url = URL(string: url) else { return }
guard let xmlParser = XMLParser(contentsOf: url) else { return }
let myParser = MyXMLParser()
xmlParser.delegate = myParser
xmlParser.parse()
}
//결과
comMsgHeader:
queryTime: 2023-07-04 20:18:48.041
resultCode: 0
resultMessage: 정상적으로 처리되었습니다.
flag: PASS
locationNo1: 2
locationNo2: 13
lowPlate1: 0
lowPlate2: 0
plateNo1: 경기78아1212
plateNo2: 경기78아1228
predictTime1: 2
predictTime2: 27
remainSeatCnt1: 48
remainSeatCnt2: 4
routeID: 228000177
staOrder: 74
stationID: 228002023
struct BusAPI {
static let shared = BusAPI()
let key = "asdfwef"
let stationId = "228002023"
let routeId = "228000177"
let url = "<https://apis.data.go.kr/6410000/busarrivalservice/getBusArrivalItem?serviceKey=\(key)&stationId=\(stationId)&routeId=\(routeId)>"
func load() {
guard let url = URL(string: self.url) else { return }
guard let xmlParser = XMLParser(contentsOf: url) else { return }
let myParser = MyXMLParser()
xmlParser.delegate = myParser
xmlParser.parse()
print(myParser.busTime?.response.msgHeader.resultMessage)
}
}
func load() {
///공공데이터 key는 인코딩 되어 있기 때문에 디코딩한다.
let deocdeKey = key.removingPercentEncoding ?? ""
///파라미터로 넘기는 값은 Alamofire가 인코딩한다. 하지만 안됐음 ㅠ
let parameter: [String : String] = [
"ServiceKey" : deocdeKey,
"stationId" : stationId,
"routeId" : routeId,
"staOrder" : "0"
]
AF.request(self.url, parameters: parameter)
.responseData { (response) in
switch response.result {
case .success(let result):
let parser = XMLParser(data: result)
parser.delegate = self
parser.parse()
case .failure(let error):
print(error.localizedDescription, error)
}
}
}
에러를 확인해보니 아래 에러가 떳다.
URLSessionTask failed with error: An SSL error has occurred and a secure connection to the server cannot be made. sessionTaskFailed(error: Error Domain=NSURLErrorDomain Code=-1200
자세히까지는 모르지만, 보안을 위해 기본적으로 HTTP 통신이 막혀있는 URL을 사용했을 때 뜨는 것을 보인다
nscurl --ats-diagnostics --verbose <https://apis.data.go.kr/6410000/busarrivalservice/getBusArrivalItem>
Configuring PFS exceptions for apis.data.go.kr
---
Disabling Perfect Forward Secrecy
ATS Dictionary:
{
NSExceptionDomains = {
"apis.data.go.kr" = {
NSExceptionRequiresForwardSecrecy = false;
};
};
}
Result : PASS
결국 성공 해서 버스 도착 시간을 받아 올 수 있었다.!!