본문 바로가기

Programming

[Python] gevent & mechanize, asynchronous Scraping

 간단하게 웹에서 데이터를 긁어올 일이 있어 mechanize를 이용해 로그인을 하고 데이터를 수집하려고 했으나 일반적인 방법으로는 속도가 너무 느려 gevent를 활용하여 개발하였다.(python에서 thread를 쓰는 것은 좋지 않은 방법이다)


1. Mechanize

 mechanize는 urllib, urllib2 혹은 requests 모듈과 같이 python에서 웹 요청을 보낼 수 있는 모듈이다. 다른 모듈과 다른 점은 가상으로 브라우저를 생성하여 웹 요청을 보내므로 cookie 혹은 session을 쉽게 다룰수 있고, select_form, submit 과 같은 함수들을 이용해 로그인을 할 수 있다.

    • 기본적인 mechanize 사용법

    • Form 태그를 이용한 Login

대략 이정도로 사용 가능합니다. 더 자세한 내용은 링크의 문서를 확인해보세요.


2. Gevent

 gevent는 libev 기반으로 하는 동시성 파이썬 라이브러리 입니다. python의 경우 GIL(Global Interpreter Lock) 때문에 멀티쓰레딩이 오히려 느린 단점이 존재합니다. 이러한 문제를 해결하고 동시성을 부여하기 위해 Gevent & Greenlet을 이용하면 해결할 수 있습니다.

 Mac & linux의 경우 libev 패키지가 설치되어 있어야 되고, cython 라이브러리가 설치되어 있어야 합니다.(Windows는 모르겠네요)

    • 간단한 예제



 위 코드의 Task 함수는 0 ~ 2 사이의 랜덤한 값으로 sleep을 한다. Synchronous 에서는 for문을 통해 순차적으로 수행하며, 모든 Task가 완료될 때까지 최소 0초에서 최대 20초까지 필요하다. 
 하지만 Asynchronous의 경우 spawn 함수를 통해 각각의 task 들을 gevent에 등록하고, gevent.joinall(threads) 함수를 통해 실행하게 되고, Task 함수가 Blocking 함수인 sleep을 만나면 다음 Task로 바로 넘어가 함수들을 실행한다. 그러므로 모든 Task가 완료될 때까지 최소 0초에서 최대 2초가 필요하다.

    • monkey patch
 gevent 라이브러리에는 monkey patch라는 기능이 존재한다. 이 monkey patch를 적용하면 socket, thread와 같은 라이브러리들의 함수를 자체적으로 재정의 해서 Blocking 인 함수들을 Non-Blocking 으로 만들어준다.
 patch_all, patch_dns, patch_item, patch_module, patch_os, patch_select, patch_socket, patch_ssl, patch_subprocess, patch_thread, patch_sys, patch_time 등이 존재한다.

monkey patch 미적용

monkey patch 적용(엥? urllib2 뭔가 에러가 있는듯...?)


3. Gevent & Mechanize

    • 첫 번째 시도(무식하게 전부다 event loop...)

 모든 url request를 gevent.spawn(worker, hakbun, browser)를 통해 wrapping 하고 joinall로 전부 실행하니 정말 빠르게 수행하긴 한다. 하지만 대략 3000개 이상 커넥션 연결이 되고 데이터 받기까지 지속되다 보니 Connection Reset by peer 라는 에러를 출력하며 실패했다.

 아마 Connection 이 너무 많아서 더이상 요청할 수 없었나보다. 



    • 두 번째 시도(Pool & map)
 이번에는 모든 Request 들을 한번에 다 요청하지 않고 한번에 요청할 수 있는 갯수에 한계를 두고 pool.map 함수를 통해 처리 하도록 하였다. (worker의 파라미터와 162 번째 줄부터 수정되었다)
 이 Pool과 Map은 동시에 처리할 갯수를 pool = Pool(size) 로 정의 하고 pool에 있는 task의 수가 size 보다 작아지면 task들을 하나씩 추가해 다시 실행을 한다.

 하지만 이때도 문제가 발생했다. 코드상의 오류인지 아니면 라이브러리의(Mechanize & Gevent) 오류인지 알 수 없지만, pool.map 사용시 borwser.open 함수의 return 값이 제대로 전달되지 않아 response 객체를 받아올 수 없거나. map 함수 내부의 알 수 없는 오류로 프로그램이 종료되는 문제점이 존재하였다


    • 세 번째 시도(Pool & join)
 두 번째와 마찬가지로 pool을 통해 Concurrency 크기를 정하는것은 똑같고, event loop를 실행할 때 map을 이용하는 것이 아닌 spawn과 join을 이용하는 것이다. spawn을 통해 gevent에 wrapping을 하고 size만큼 다 wrapping을 하면 join을 호출해 event들을 실행한다. map과 다른 점은 map은 pool이 하나 이상 비워질 때마다 채워넣어 실행한다면 spawn & join은 해당 Concurrency 크기의 task가 모두 실행 되고 다시 spawn, join, spawn, join... 한다는 것이다. (161 번째 줄 부터 수정되었다.)