티스토리 뷰

  • 소개

캐치딜 서비스는 2019년 10월부터 시작된 토이프로젝트의 일환으로, 같은 대학 주니어 대학생 3명이서 똘똘뭉쳐 제작된 프로젝트입니다.

캐치딜은 여러 플랫폼(뽐뿌, 클리앙 등)에 퍼져있는 핫딜특가 데이터를 Selenium 크롤링을 통해 데이터를 수집해서 보여주는 서비스입니다.

 

 

  • 크롤링

캐치딜은 뽐뿌, 클리앙 등 여러 플랫폼에 퍼져있는 정보를 하나로 모아오는 컨텐츠입니다.

하지만 이 정보를 모음에 있어 가장 필요한 기술은 크롤링 입니다.

크롤링  웹을 돌아다니며 유용한 정보를 자동으로 찾아내고, 데이터베이스로 저장해 오는 작업

 

캐치는 현재 매 n분 간격으로 다양한 플랫폼에서 정보를 크롤링을 해냅니다.

캐치딜 프로젝트 같은 경우는 크롤링을 담당하는 서버가 Ruby on Jets 기반이며, 해당 프레임워크를 활용해서 크롤링이 진행됩니다.

 

그리고 크롤링이 이루어진 데이터는 캐치딜 서버 내 Database에 저장이 이루어집니다.

* 물론, 웹페이지에 접속하자마자 바로 크롤링 후 크롤링 결과를 사용자에게 보여주는 방법이 있긴 한데, 이 방법은 나중에 많은 사람들이 접속 및 크롤링 요청 시, 서버가 뻗을 수 있는 위험이 있습니다.

 

크롤링에 있어 어떤 기술이 쓰였고, 또 어떤 문제점이 발생했는지 해당 글을 통해 공유를 해보고자 합니다.

 

 

  • Nokogiri

 부록  Ruby on Rails : Nokogiri 크롤링 [Gem : nokogiri]

Nokogiri는 Rails(Jets)에서 제공하는 기본적인 크롤러 Gem(모듈) 입니다.

해당 크롤링 같은 경우는 정말 간단하면서도, 모듈 역시 Running에 있어 메모리를 그다지 차지하지 않다보니 정말 가볍습니다.

 

하지만 Nokogiri에 아쉬운 점이 하나 있는게, iframe 및 Javascript 등으로 렌더링 되어있는 페이지는 크롤링이 안됩니다.

그렇다보니 찾아낸 대안이 다음에 소개드릴 Selenium 입니다.

 

 

 

  • Selenium

Selenium은 가상의 크롬 브라우저를 띄우고, 이를 활용해서 직접 웹페이지를 통해 접속하는 것 처럼 사용되는 방법론 입니다.

 부록  Ruby on Rails : 가상 브라우저를 활용한 크롤링 [Gem : selenium-webdriver]

 

Selenium은 Ruby 뿐만 아니라 Python, Java 등 다양한 언어 직군에서도 지원이 되는 크롤링 모듈로서 Selenium만 함께한다면 웬만한 사이트 플랫폼의 크롤링은 다 뚫는다고 볼 수 있습니다.

 

하지만 Selenium 크롤러의 문제점이 있는게, 바로 메모리 사용이 엄청나다는 겁니다..

 

실제로 과거에 AWS 서버를 운영함에 있어 EC2에서 크롤러+서버를 운영함에 있어서도 크롤러가 돌아가자마자 서버가 뻗는 현상이 발생해서 이를 우회할 방법을 찾고, 공부를 하는데 있어 꽤 고생을 했었습니다.

 부록  캐치딜 백엔드 개발이야기 : 좌충우돌 서버운영 이야기

 

Selenium 사용에 있어 메모리 사용이 얼마나 심한지 한번 다음 예시를 통해 살펴보겠습니다.

 

 

예를들어 위 뉴스 페이지 내용 중, 빨간 영역의 기사 타이틀 및 내용을 크롤링을 해본다고 해보겠습니다.

1) Nokogiri

## CrawlNokogiriJob.perform_now(:running_nokogiri)

class CrawlNokogiriJob < ApplicationJob
  
  def running_nokogiri
    
    doc = Nokogiri::HTML(open("https://news.naver.com/main/list.nhn?mode=LSD&mid=sec&sid1=102"))
    @crawl_data = doc.css('#main_content > div.list_body.newsflash_body > ul.type06_headline > li > dl')

    @crawl_data.each do |t|
        @title = t.css('dt:nth-child(2) > a').text.strip
        @content = t.css('dd > span.lede').text.strip
        
        puts "#{@title} || #{@content}"
    end
    
  end
end

 

2) Selenium

## CrawlSeleniumJob.perform_now(:running_selenium)

class CrawlSeleniumJob < ApplicationJob
  
  def running_selenium
    
    if Jets.env == "production"
      Selenium::WebDriver::Chrome.driver_path = "/opt/bin/chrome/chromedriver"
      options = Selenium::WebDriver::Chrome::Options.new(binary:"/opt/bin/chrome/headless-chromium")
    else
      Selenium::WebDriver::Chrome.driver_path = `which chromedriver-helper`.chomp
      options = Selenium::WebDriver::Chrome::Options.new
    end
    
    options.add_argument("--headless")
    options.add_argument("--disable-gpu")
    options.add_argument("--window-size=1280x1696")
    options.add_argument("--disable-application-cache")
    options.add_argument("--disable-infobars")
    options.add_argument("--no-sandbox")
    options.add_argument('--disable-dev-shm-usage')
    options.add_argument("--hide-scrollbars")
    options.add_argument("--enable-logging")
    options.add_argument("--log-level=0")
    options.add_argument("--single-process")
    options.add_argument("--ignore-certificate-errors")
    options.add_argument("--homedir=/tmp")
    @browser = Selenium::WebDriver.for :chrome, options: options
    
    @browser.navigate().to "https://news.naver.com/main/list.nhn?mode=LSD&mid=sec&sid1=102"
    
    @crawl_data = @browser.find_elements(css: "#main_content > div.list_body.newsflash_body > ul.type06_headline > li > dl")
    
    @crawl_data.each do |t|
      begin
        @title = t.find_element(css: "dt:nth-child(2) > a").text.strip
        @content = t.find_element(css: "dd > span.lede").text.strip
        
        puts "#{@title} || #{@content}"
      rescue
      end
    end
    
    @browser.quit
    
  end
end

 

AWS Lambda에서 동일한 페이지 내 영역에 대해 크롤링을 돌려보겠습니다.

 

같은 크롤링 결과, 다른 시간 및 메모리 사용

각기 다른 크롤링을 돌려보면 다음과 같은 결과를 살펴볼 수 있습니다.

크롤링 비교 소요시간 사용 메모리
Nokogiri 200ms (0.2 Second) 182MB
Selenium 4900ms (4.9 Second) 412MB

 

이와같은 결과만 봐도 Selenium은 크롤링에 있어 메모리, 시간이 비효율적으로 사용된다는게 확인이 됩니다..ㅠ

또한 Selenium을 사용함에 있어서 가중적으로 또 문제를 겪게 됩니다.

 

 

RegExr : 정규표현식

정규표현식은 단어 내에서 매치되는 단어 혹은 기호를 찾아내고, 문자를 replace 할 때 주로 사용이 됩니다.

"(2020-02-24 15:32)".gsub(/\(|\)/, "")
=> 2020-02-24 15:32

 

하지만 문자패턴을 분석하는 정규표현식에 있어 한 가지 문제점이 있는데, 정규표현식 또한 CPU 사용이 엄청나다는 겁니다.

 Stackoverflow  Regular expression hangs program (100% CPU usage)

안그래도 메모리 부족 현상에서도 허덕이는데 CPU 퍼포먼스까지 뛰어버리면.. 하.. 쉽지않네요 ㅠ

 

 

메모리 문제 극복

과거에 있었던 Selenium 메모리 이슈에 대해 많은 대안책을 찾아보았습니다. 그리고 그 해답은 AWS Lmabda였는데, 크롤링 작업을 람다에서 맡아서 작업하는 이후, 부족했던 메모리 문제에 대해 해결할 수 있었습니다. 이는 AWS Lambda에서는 최대 3GB 내에서 자유롭게 메모리 사용량을 조절할 수 있는 덕분입니다.

 

 

 

  • Puppeteer

 

Puppeteer은 새로운 크롤러를 찾다 알게 된 방법론입니다.

 

Puppeteer은 구글에서 만들어진 모듈로서, Chrome 혹은 Chromium을 제어하기 위해 만들어진 웹 자동화 API라고 합니다.  Node.js 기반으로 이루어져 있는 Puppeteer은 Selenium에 비해 빠른 속도를 자랑한다곤 하는데 아직 실제로 써보지 못했습니다..ㅠ

지금은 일단 Selenium으로 어느정도 커버가 쳐져있기에 아직 Puppeteer로 이전하진 않았으나, 나중에 한계를 느끼게 되면 크롤러를 변경함에 있어 고민하고 있는 차선택 대안입니다.

 부록  Puppeteer VS Selenium 퍼포먼스 / Puppeteer vs. Selenium

 

 

  • 크롤러 개발 시 유의사항

개발함에 있어 크롤러에서는 다양한 Exception 처리 및 효율적인 알고리즘을 개발할 필요가 있습니다.

캐치딜 같은 경우에도 크롤러를 개발함에 있어서 여러 Exception 처리를 두었습니다.

 

1) 일부 게시글마다 다르게 보여지는 CSS 위치

 

이미지가 보여지지만, 서로다른 class 선택자를 가리킨다. 

예를들어 위 사진 속 이미지들 같은 경우는 똑같은 사이트의 게시판에 업로드된 이미지이나, 가리키는 css 선택자가 살짝 다릅니다.

크롤링은 css 선택자를 찾아내는 기반으로 자료를 추출하는 기반이다 보니, 위와같은 사례를 잘 잡아내어 '만약 A케이스가 안될 경우 B 케이스로 해봐라' 라는 Exception 처리를 잘 해두는게 좋습니다.

begin
  imageUrl = content.find_element(css: "img.fr-dib").attribute('src').to_s.gsub("http://", "https://")
rescue
  imageUrl = content.find_element(css: "img.fr-fic").attribute('src').to_s.gsub("http://", "https://") rescue imageUrl = nil
end

 

2) Timeout 등 내적으로 생긴 (다양한) 상황으로 인해 크롤링 실패 시 다시 시도

가끔 메모리 초과로 인해 크롤러가 오랜 기간동안 멍 때리다가 Timeout이 되는 현상이 있는데, 저같은 경우는 재귀함수를 돌리는 방법을 통해 'Timeout이 2회가 되기 전 까지는 계속 크롤러가 돌아가게 해라' 라는 식으로 크롤러 코드가 작동되게 했습니다.

def crawl_clien(index, url, failStack)
    begin
      (플랫폼 크롤링 코드)
      return 1
    
    ## 크롤링 실패(Timeout) 시 재귀(failStack이 +1이 되고 다시 시작)
    rescue Timeout::Error
      # puts "crawl_ppom failStack : #{failStack}"
      # puts "타임아웃 에러 발생, 크롤링 재시작"
      
      if failStack == 1
        return 0
      else
        return crawl_clien(index, url, failStack+1)
      end
    end
  end

def main_clien_chrome
  2.step(0, -1) do |index|
    ## crawl_clien(index, 주소, FailStack(Timeout이 몇 회 일어났는지 Count)
    crawl_clien(index, "https://www.clien.net/service/board/jirum?po=#{index}", 0)
  end
end

 

2) integer 추출 시 Exception 처리

 

이를테면, 저는 홈페이지에서 초록색 영역의 부분을 integer 형태로 추출을 하고 싶은데 중간에 33.0k 와 같은 숫자가 아닌 형태의 자료가 보입니다.

반드시 integer 형태로 추출을 한다 하면, 해당 형태에 대한 Exception 처리를 해줘야 할 수 있습니다.

 

 

  • 마무리

크롤링은 개발에 있어 여러모로 생각을 하게 하면서도, 여러 변수(메모리, CPU 등)를 감안해야 하는 기술입니다.

효율적으로 크롤링이 돌아가게 하는 것은 진짜 개발자의 기초부터의 생각과 설계가 중요하다는것을 깨달았습니다.

 

또한, 효율적인 크롤링이 이루어지기 위해선 서버의 스펙도 중요하겠죠!

 

 

  • 캐치딜 개발 이야기 연결고리

1. 캐치딜 백엔드 개발이야기 : 좌충우돌 서버 설계 및 운영 이야기

2. 캐치딜 백엔드 개발이야기 : 문서화

3. 캐치딜 백엔드 개발이야기 : 디자이너와의 협업

4. 캐치딜 백엔드 개발이야기 : 크롤링

5. 캐치딜 백엔드 개발이야기 : Restful API 설계의 다양한 고민

6. 캐치딜 백엔드 개발이야기 : 나에게 맞는 합리적인 서버 비용을 찾아서..

 

댓글
  • 프로필사진 김지수 와 상세하게 설명 잘 올라와서 너무나 잘 보았습니다. 일전에 원노님이 당근마켓 뉴비 강연 때 셀레니움은 자원을 많이 소모하니 간단한 크롤링은 nokogiri를 추천한다고 하신 적이 있었는데, 써주신 글 덕분에 이유를 확실히 알겠어요! 2020.02.05 12:41
  • 프로필사진 마음 따뜻한 개발자, 나른한 하루 앗 안녕하세요! 어제 로켓펀치로 인맥신청 주셨었죠! 인맥 추천과 함께 바로 추천을 써주셔서 감사합니다ㅋㅋ
    저도 그 때 참가했던 1인으로서, 메모리 효율이 떨어진다는 것은 대충 이해만 했었는데 이번에 프로젝트를 하다보니까 확실히 보이더라고요..ㅠ 셀레니움이 진짜 메모리 퍼포먼스만 빼면 다 좋은데..ㅠㅠ
    이것때문에 Puppeteer로 옮겨야할지 고민중입니다 흑..
    2020.02.05 15:19 신고
댓글쓰기 폼
공지사항
Total
33,249
Today
32
Yesterday
141
링크
TAG more
«   2020/07   »
      1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31  
글 보관함