How to WebScraping(Google)

YongJunCha·2021년 11월 11일
0

swift

목록 보기
10/18
post-thumbnail

Why?

구글과 유튜브 검색 자료를 스크래핑 하라는 업무를 받아서 진행하게 되었다.
많은 앱들이 이러한 스크래핑 작업에 경우 서버에 아이디 비번 정도만 넘겨서
처리된 값들을 파싱하지만 이번에는 처음 웹뷰 제작 및 웹뷰를 숨기고 작업 Scraping 실행,
DB 입력까지 직접 해 볼 수 있던 기회였다.

Library?

SwiftSoup 이라는 라이브러리를 패키지 임포트 하여 진행하였다.
SwiftSoup Github
ReadMe에 친절하게 사용하는 법이 설명되어 있으니 확인해보자
하기는 코드랩을 진행했을 때의 코드이다.

public enum KeywordType : Int {
    case KEYWORD_TYPE_UNKNOWN = -1
    case KEYWORD_TYPE_GOOGLE = 0
    case KEYWORD_TYPE_YOUTUBE = 1
}



public struct Keyword : Hashable{
    public let key: CLong = 0
    public let identifier: CLong = 0
    public let keywordType: KeywordType = .KEYWORD_TYPE_UNKNOWN
    public let keywordEngineName: String = ""
    public let mainType: Int? = 0                     // 메인 종류 ( 유튜브[1:유튜브, 2:유튜브뮤직] / 구글[null] )
    public let subType: Int? = 0                      // 검색어 종류 ( 0:기타, 1:검색, 2:시청, 3:방문, 4:조회 )
    public let name: String? = nil                  // 검색어
    public let dateTime: Date? = nil                // 시간 ( ex. 20200815150000 )
    public let image: String? = nil     // 썸네일 이미지 URL
    public let isConnected: Bool = false
//        let hashTags: RealmList<HashTagItem> = RealmList()
    
    public init() {
        
    }
}
  • 상기의 코드는 메인로직에서 쓰일 Entity이다.

Main Logic

  // Google
    func insertGoogleKeywordData(url : URL, keywordEngineType : Int) {
        var keywords : [Keyword] = []
        var index = getLastIndex() + 1
        print("Start Index :: \(index)")
        
        let lastDate = getLastDate(keywordType: keywordEngineType)
        print("GET LAST DATE :: \(lastDate)")
        
        //Scraping code be delayed
        DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) { [self] in // Change `2.0` to the desired number of seconds.
            do {
                print("Google Keyword Scraping Start")
                let html = try String(contentsOf: url, encoding: .utf8)
                
                // Get all html
                guard let doc: Document = try? SwiftSoup.parseBodyFragment(html) else {
                    // Exception Handling
                    print("Fail Scrap HTML DOC")
                    scrapingFailAction()
                    return
                }
                
                // Select List of Searched Keywords
                let searchKeywords : Elements? = try? doc.select("c-wiz[class*='xDtZAf']")
                
                if searchKeywords != nil && searchKeywords?.count != 0 {
                    // Resolve Each Keywords
                    for searchKeyword : Element in searchKeywords!.array() {
                        
                        // Date
                        let searchDate : String = try searchKeyword.attr("data-date")
                        
                        // Time
                        let searchTime : String = try searchKeyword.select("div[class='H3Q9vf XTnvW']").first()?.text().components(separatedBy: "•")[0].replacingOccurrences(of: " ", with: "") ?? ""
                        
                        // Date + Time Formatting
                        let formattedDate : Date = dateFormattingForGoogle(timeString: searchTime, formattedDate: searchDate)
                        
                        // Compare With Realm Data (lastDate)
                        if formattedDate > lastDate {
                            
                            // Keyword Buffuer
                            let keyword = Keyword()
                            
                            // DateTime
                            keyword.dateTime = formattedDate
                            print("DATE_TIME :: \(keyword.dateTime)")
                            
                            // id
                            keyword.id = index
                            
                            // KeywordType
                            keyword.keywordType = keywordEngineType
                            
                            // MainType
                            keyword.mainType = WebViewMainType.GOOGLE.rawValue // Google 일 경우
                            print("MAIN_TYPE :: \(keyword.mainType)")
                            
                            // Get Row
                            let searchHtmlRow =  try searchKeyword.select("div[class='QTGV3c']").first()?.text() ?? ETC
                            
                            //SubType
                            keyword.subType = getSubType(searchHtmlRow: searchHtmlRow)
                            print("SUBTYPE :: \(keyword.subType)")
                            
                            
                            // KeyWord Name
                            let name : Element? = try searchKeyword.select("a[jsname='eLJrl']").first() ?? nil
                            keyword.name = String(try name?.text()  ?? "" )
                            print("KEYWORD :: \(keyword.name)")
                            
                            // Image
                            let image : String = try searchKeyword.select("img[class='us8hCd']").attr("src")
                            keyword.image = image
                            print("IMAGE :: \(image)")
                            
                            // Append keyword Data
                            keywords.append(contentsOf: [keyword])
                            
                            index += 1
                        }
                    }
                    // Call Insert Method
                    print("Keyword : \(keywords.count)")
                    insertFormattedKeyword(keywordEngineType: keywordEngineType, keywords: keywords)
                } else {
                    // Exception Handling
                    scrapingFailAction()
                }
            } catch Exception.Error(let type, let message) {
                print("Error Type : \(type) Error Message : \(message)")
            } catch {
                print("error")
            }
        }
    }

Important

  • 스크래핑 과정 중에서도 예외처리와 옵셔널은 당연히 중요하겠지만 제대로 스크래핑이 안 될 경우를 생각해야했기 때문에 시간이 좀 걸렸다
  • 각각의 검색어에 해당하는 로그를 만들기 위해서도 꼼꼼히 로그도 남겨줬다.
  • SwiftSoup의 Document 타입과 RealmDB의 Document 타입이 충돌하니 모델을 잘 나눠야 한다.

Issue

WebScraping을 하다보면 페이지를 강제로 스크롤해서 내리거나, "더보기"와 같은 버튼을 해제하기 위해 페이지를 내려야 하는데 이런 경우 상기의 사진과 같은 overflow:hidden에 걸리게 된다. 이렇게 되면 예를 들면, 34개의 데이터를 가져와야 하는데 6개째에서 overflow:hidden 쪽으로 이동되면서 상기의 사진의 위치에서 검색을 멈추게된다. safari에서 생기는 문제인데 apple developer 쪽에서 확인해본 결과
아직 해결책이 뚜렷하게는 없는듯 하다, 따라서 안드로이드는 크롬을 쓰기 때문에 같은 스크래핑을 하더라도 성능차이가 생기기 쉬웠다.

0개의 댓글