개요
적게는 수 만개에서 많게는 수 백만 개의 웹 사이트에 각각 접속하여 데이터를 가져오는 크롤러를 개발하면서 크롤러
의 본질적인 문제들을 만나고 해결하는 과정을 반복했다. 크롤러
의 본질적인 문제라 하면 안티봇에 관한 정책으로 인한 문제(크롤링을 금지하는 정책) 또는 이를 위한 솔루션(캡챠) 등으로 인한 문제이다. 하지만 이러한 문제는 창과 방패의 싸움과 같이 소모적인 성격이 크다. 문제가 발생할 때 어떻게 하면 금지 정책을 우회할 수 있을까 또는 보안 솔루션을 우회할 수 있을까 다방면적으로 분석하여 해결방법을 찾아나가야한다. 결국 이런 해결방법들은 사실상 해킹으로 봐도 무방하다.
그러나 본 포스팅에서는 크롤러의 이런 소모적 성격의 문제들이 아닌, 보다 본질적인 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에 대한 요청/응답에 걸리는 시간을 계산해보았다.
이처럼 총 13개의 URL에 접속하는데 걸린 총 시간은 약 12.3 초이다. (단, 이 시간은 절대적인 시간이 아니라 접속환경에 따라 또 매번 테스트하는 그 순간의 환경에 따라 매번 다른 시간이 측정 될 것이다)
Multi-Threaded
Multi Thread 를 통해 같은 작업을 수행할 경우 다음과 같이 획기적으로 시간 단축효과를 볼 수 있다.
여기서 URL을 읽는 데 걸리는 시간의 대부분은 네트워크 지연으로 인한 것이다. 이처럼 I/O Bound 작업은 대부분의 시간을 입출력을 기다리는 데 소비하는데 이 기다리는 시간 동안 또 다른 스레드에서는 새로운 URL을 요청하고 또 기다리는 동안 다른 쓰레드에서 요청하면서 시간을 아낄 수 있다.
Asynchronous IO
비동기 프로그래밍으로 입출력 작업의 속도 개선을 시도해볼 수도 있다.
추가 예정