네이버 검색에서 로또 당첨번호 파싱하기 – Swift + CommandLine

네이버 검색으로부터 로또 당첨 번호를 파싱하는 방법에 대해서 살펴보자. 기본적으로 이 작업을 수행하기 위해서는 두 가지 기술을 사용할 것이다.

  1. NSURLSession :  웹페이지 데이터를 받아와야 하기 때문에 네트워킹 API 를 사용해야 한다.
  2. NSRegularExpression : 받아온 데이터는 HTML 페이지의 소스 데이터이며, 여기서 로또 당첨번호의 내용을 추출하기 위해 간단한 정규식을 사용할 것이다.

아, 그리고 참고로 여기서 사용된 Swift 버전은 4.0이다.

준비 과정

네이버는 로또 당첨번호 검색에 대해서 일반적인 웹문서 결과가 아닌 별도로 디자인된 영역으로 당첨번호를 예쁘게 표시해주고 있다. 실제로 네이버에서 “로또 당첨번호”라는 키워드로 검색해보면 다음과 같은 화면을 볼 수 있다.

여기서 로또 회차 부분을 클릭하면 다른 회차의 당첨번호도 알 수 있는데, 다른 회차들을 선택해보자. 이 때, 브라우저의 주소창을 보면 일정한 패턴으로 구성되는 것을 알 수 있다.

https://search.naver.com/search.naver?sm=tab_drt&where=nexearch&query=792회로또

즉 위의 주소에서 회차에 해당하는 번호값만 바뀌는 것을 알 수 있다. 그러면 당첨 번호는 어떻게 알 수 있을까? 색색의 예쁜 숫자 공을 선택해서 브라우저의 인스펙터에서 조사해보면 “ball번호“의 패턴으로 클래스가 적용되어 있는 것을 확인할 수 있다.

따라서 ball(\d+) 라는 정규식 패턴으로 당첨번호에서 숫자부분만 손쉽게 추출할 수 있다.

정규식으로 당첨번호를 추출하여 출력하기

URLSession을 사용해서 간단한 데이터 받아오기라는 글에서 URLSession을 사용하는 간단한 방법을 소개한 적이 있는데, (Data) -> Void 타입의 완료 핸들러를 이용해서 웹주소로부터 HTTP 통신을 통해 받아온 데이터를 처리할 수 있다고 하였다. 그렇다면 이 부분을 먼저 작성해보자. 네이버는 웹페이지에 UTF8 인코딩을 사용하고 있으므로 받아온 데이터를 UTF8로 디코딩하여 문자열을 얻고, 여기에서 위 패턴을 적용하여 번호들을 추출할 수 있다.

func parseBallNumbers(_ data: Data) {
  guard let html = String(data:data, encoding:.utf8) else { return }
  let regex = try! NSRegularExpression(pattern: "ball(\\d+)", options:[])
  else { return }

  let matches = regex.matches(in: html, options:[], range: NSMakeRange(0, html.count))
  let results = matches.map{ (html as NSString).substring(with: $0.range(at:1)) }
  // 6개의 당첨번호와 1개의 보너스번호
  let nums = results[..<6].joined(separator: ", ")
  let bonus = results.last!
  print("\(nums) - 보너스: \(bonus)")
}

데이터를 요청하기

데이터를 받아와서 당첨번호를 추출하고 출력하는 함수를 작성했으니, 이제 데이터를 받아올 함수를 작성할 차례이다. 단, 나는 여기서 명령줄에서 실행되는 버전을 상정하고 있는데 URLSession의 모든 동작은 철저하게 비동기로 동작한다. 따라서 데이터 작업 객체의 resume()은 비동기로 네트워크 통신을 개시하면서 즉시 리턴하기 때문에 응답을 기다릴 필요가 있다. 이 경우, 메인 스레드에서 런루프를 실행하여 네트워크 통신을 기다리고, 완료 핸들러 내에서 런루프를 중지하고 끝내는 방식으로 처리한다.

func processURL(_ url: URL, handler: @escaping (Data) -> Void) {
  let task = URLSession.shared.dataTask(with: url){ data, _, _ in
    defer { CFRunLoopStop(CFRunLoopGetMain()) }
    guard let data = data else { return }
    handler(data)
  }
  task.resume()
  CFRunLoopRun()
}

조립하기

이제 우리는 네트워크 통신으로 웹 페이지를 받아오고, 데이터를 파싱하는 함수를 모두 갖게 되었다. 이를 조립하여 원하는 동작을 이루어보자.

func main() {
  if let s = readLine(), let i = Int(s) {
    let address = "https://search.naver.com/search.naver?sm=tab_drt&where=nexearch&query=\(i)회로또"
    let encodedAddress = address.appendingPercentEncoding(withAllowedCharacters:.urlQueryAllowed)!
    let url = URL(string:encodedAddress)!
    processURL(url, handler: parseBallNumbers)
}