Python - Crawler 성능 개선

 

개요

  적게는 수 만개에서 많게는 수 백만 개의 웹 사이트에 각각 접속하여 데이터를 가져오는 크롤러를 개발하면서 크롤러의 본질적인 문제들을 만나고 해결하는 과정을 반복했다. 크롤러의 본질적인 문제라 하면 안티봇에 관한 정책으로 인한 문제(크롤링을 금지하는 정책) 또는 이를 위한 솔루션(캡챠) 등으로 인한 문제이다. 하지만 이러한 문제는 창과 방패의 싸움과 같이 소모적인 성격이 크다. 문제가 발생할 때 어떻게 하면 금지 정책을 우회할 수 있을까 또는 보안 솔루션을 우회할 수 있을까 다방면적으로 분석하여 해결방법을 찾아나가야한다. 결국 이런 해결방법들은 사실상 해킹으로 봐도 무방하다.
  그러나 본 포스팅에서는 크롤러의 이런 소모적 성격의 문제들이 아닌, 보다 본질적인 Python 기반의 크롤링 속도에 관한 문제를 파헤치고자 한다. 단순하게 네이버의 메인페이지 html 을 크롤링 하여 데이터를 취하는 정도의 예시로는 크롤링 속도에 관한 문제에 다가가기 힘들 것이다. 왜냐하면 네이버의 메인페이지 html 을 가져오는 행위(I/O) 즉, HTTP Request 행위는 단 한번 발생하였고 (Input) 이에 대한 Response (Output) 를 받으면 그 즉시 CPU 를 이용하여 데이터를 취할 수 있기 때문이다. 하지만 네이버의 메인페이지 html 내에서 얻을 수 있는 모든 URL 을 다시 요청하고 받아온 html 내에서 얻을 수 있는 URL 을 다시 요청하는… 반복적인 행위가 필요하다면 크롤링 속도에 관해 문제를 제기할 수 있을 것이다. 그렇다면 먼저 크롤링의 속도가 어떤 요인들에 의해 작용되는지를 알아야지 결과적으로 속도 개선의 방향성을 잡을 것이다.

크롤링 - 컴퓨터 처리 관점

  크롤링 작업은 I/O Bound 에 해당하는 HTTP Request/Response 행위 + 필요한 데이터는 데이터베이스로 저장하는 행위CPU Bound 에 해당하는 html 데이터의 파싱행위 크게 둘로 나뉜다.
I/O Bound : I/O 란 입출력, Input Output 을 의미하며 I/O Bound 에는 Disk I/O, Network I/O 등이 포함된다.
CPU Bound : CPU Bound 는 CPU의 속도에 직접적인 연관이 있는 행위들이다. 이를테면 연산 행위가 이에 해당한다.
따라서, 크롤링 작업은 사실상 웹 사이트를 요청하여 가져오는 행위 즉, Network I/O 가 가장 핵심이며 가져온 HTML 데이터를 연산/가공 하여 필요한 정보만을 추출하여 데이터베이스로 저장하는 작업이다. 네이버 HTML 을 가져와서 실시간 검색어의 ELEMENT 선택자를 통해 파싱하여 실시간 검색어를 얻어 온 뒤 검색어 목록을 나의 데이터베이스에 저장하는 것처럼 말이다.

크롤링 속도 문제

  한번의 요청과 그와 수반되는 데이터 처리작업만 놓고 보면 문제시 될 만큼 속도가 느리지도 않거니와 설령 속도가 느리다 할지라도 성능 개선의 여지가 보이지 않는다. 왜냐하면 어찌 됐던 데이터를 네트워크를 통해 가져와야하는데 이 속도가 느리다고 한다면 요청 측과 응답 측의 근본적인 네트워크 (망구성) 구조적 개선을 해야하기 때문이다.
  그러나 위에서 말한 예시와 같이 수만, 수십만개의 사이트를 연속적으로 크롤링 하고자 할 때는 속도 이슈가 더욱 크게 다가 올 것이며 인프라가 아닌 개발적으로 개선의 여지가 있기 때문에 방치하지 않고 어떻게 개선할 지 연구해 볼 만한 가치가 있다.
  연속적으로 수 만개의 사이트를 크롤링 하는 것을 마치 사람이 직접 수 만개의 사이트를 접속하여 데이터를 수집하는 행위로 치환시켜보자.

사람이 직접 데이터 수집 시나리오

1. 신입 직원 한명에게 수 만개의 URL 과 함께 수집할 데이터에 관한 매뉴얼을 제시
2. 신입 직원은 첫번째 URL 을 접속
3. 신입 직원은 URL 접속 완료 시 매뉴얼에 따른 데이터 수집
4. 신입 직원은 데이터 수집 완료 시 두번째 URL 을 접속
5. 신입 직원은 URL 접속 완료 시 매뉴얼에 따른 데이터 수집
6. 신입 직원은 데이터 수집 완료 시 세번째 URL 을 접속
...
20000. 신입 직원은 10,000 번째 URL 을 접속
...
...
...
40000. 신입 직원은 20,000 번째 URL 을 접속

다음의 시나리오를 가상 코드로 표현

urls = [ ... ] # 수 만개의 URL
for url in urls:
	html_data = response of connect(url)
	data = get_data_from(html_data)

  신입 직원이 각 URL 에 접속하고 접속완료 될 때까지 기다렸다가 접속이 완료되면 데이터를 수집한다. 그리고 데이터 수집이 완료되면 다시 그 다음 URL에 접속을 하고 다음 작업들을 반복한다. 그런데 신입 직원이 업무에 숙달되고나면 URL 접속이 완료될 때까지 기다리는 시간을 활용하고자 할 것이고 한번에 하나의 URL 을 접속하는 것이 아니라, 미리 수십개의 URL을 미리 접속시켜 놓은 채로 데이터를 수집하고, 수집완료 된 사이트는 종료하고 다시 그 다음의 여러 URL 을 접속 후 접속되는 동안에 돌아와 미리 접속 된 URL의 데이터를 수집하는 등의 방식으로 더 스마트한 업무처리가 가능해질 것이다. 바로 이렇게 효율적인 업무 처리에 관한 프로세스가 크롤링 작업의 속도 개선에도 연결된다. 여기서 신입 직원이 줄이고자 한 작업은 인터넷 접속 부분에 관한 Wait Time, 즉 I/O Bound Wait Time 이다. 신입 직원의 입장에서 업무 시간을 줄이기 위해 가장 눈에 띈 부분이 기다리는 시간 이었기 때문이다. 반면에 자신이 데이터를 수집 및 처리하는 행위(원하는 데이터의 위치를 최대한 빨리 찾고, 목표 데이터를 위해 해당 데이터를 가공하는 행위 ), CPU Bound 에서 개선을 한다 할지라도 당장 시간 차이를 크게 줄일 수 없다고 판단했을 것이다. 이는 컴퓨터에도 그대로 적용된다. CPU 처리 속도와 I/O 처리 속도 간의 차이는 전기적 처리와 물리적 처리라는 본질적 차이에서부터 출발하기 때문에 비교할 수 없을 만큼 전기적 처리가 빠르다.
  그렇다면 크롤링의 속도 개선 연구방향은 자연스럽게 I/O Bound Time 을 줄이는 솔루션 제시로 흘러갈 것이다.

I/O Bound 처리 개선

Multi-Threading

  I/O Bound - 입출력 작업의 속도 개선 방법으로 멀티 쓰레드를 적용해볼 수 있다.

Single-Threaded

  Jupyter 예시를 통해 Single Thread 에서의 여러 URL에 대한 요청/응답에 걸리는 시간을 계산해보았다.

Single-Threaded

  이처럼 총 13개의 URL에 접속하는데 걸린 총 시간은 약 12.3 초이다. (단, 이 시간은 절대적인 시간이 아니라 접속환경에 따라 또 매번 테스트하는 그 순간의 환경에 따라 매번 다른 시간이 측정 될 것이다)

Multi-Threaded

  Multi Thread 를 통해 같은 작업을 수행할 경우 다음과 같이 획기적으로 시간 단축효과를 볼 수 있다. Multi-Threaded

  여기서 URL을 읽는 데 걸리는 시간의 대부분은 네트워크 지연으로 인한 것이다. 이처럼 I/O Bound 작업은 대부분의 시간을 입출력을 기다리는 데 소비하는데 이 기다리는 시간 동안 또 다른 스레드에서는 새로운 URL을 요청하고 또 기다리는 동안 다른 쓰레드에서 요청하면서 시간을 아낄 수 있다.

Asynchronous IO

  비동기 프로그래밍으로 입출력 작업의 속도 개선을 시도해볼 수도 있다.

추가 예정