[스파르타 코딩클럽 8기] 개발일지 #7 20.07.01

이홍희·2020년 7월 1일
0
post-thumbnail

목표

크롤링 속도를 높여 검색 속도를 높여 보자라는 목표로 작업을 수행했다. 이를 위해서 하나씩 차례로 selenium으로 브라우저를 열어 크롤링해오는 방식에서 동시에 여러 페이지를 열어 한꺼번에 크롤링해오는 방식으로 변경하고자 했다.

또한 지금까지는 동시에 검색 요청을 보냈을 때의 충돌을 막기 위해 크롤링 정보를 담아 HTML로 넘겨주는 dictionary 변수를 지역변수로 변경했다. 기존에는 전역변수로 선언하고 각 함수에서 그 변수에 데이터를 더하는 방식이었는데 요청을 받아 크롤링을 하는 중간에 다른 요청이 오면 데이터가 엉키는 일이 발생할 수 있기 때문이다. 이 목표를 위해 코드를 변경하다가 여러 어려움을 만났지만 튜터님의 도움으로 결국 해결할 수 있었다!!

multiprocessing으로 함수 병렬화하여 실행하기

목표를 위해서는 서강대 도서관 크롤링 함수, 연대 도서관 크롤링 함수, 이대 도서관 크롤링 함수를 동시에 실행해야 했기 때문에 구글링을 통해 그 방법을 찾다가 파이썬의 multiprocessing 패키지에 대해 알게 되었다.

Process 객체 사용

멀티 프로세싱을 사용하면 복잡하고 시간이 오래 걸리는 작업도 별도의 프로세스를 생성해 그 기반으로 병렬 처리를 해 더 빠르게 작동할 수 있다는 장점이 있다. 멀티프로세싱을 사용하는 방식은 Pool 객체를 활용하는 것과 Process 객체를 활용하는 방법이 있었다. 이해하기로는 Pool은 입력값을 프로세스에 분배하여 함수를 병렬화시키고 Process는 하나의 프로세스에 하나의 함수를 인자와 함께 할당해서 실행시키는 것이었다. 크롤링 해오는 3가지 함수를 검색어와 데이터를 담을 dictionary를 인자로 주면서 실행시켜야 하니 Process가 더 적합하다고 생각했다.

def multi(keyword, api):
    p1 = Process(target=sogang_search, args=(keyword, api)) 
    p2 = Process(target=yonsei_search, args=(keyword, api)) 
    p3 = Process(target=ewha_search, args=(keyword, api)) 

    # start로 각 프로세스를 시작합니다. func1이 끝나지 않아도 func2가 실행됩니다.
    p1.start()
    p2.start()
    p3.start()

    # # join으로 각 프로세스가 종료되길 기다립니다 p1.join()이 끝난 후 p2.join()을 수행합니다
    p1.join()
    p2.join()
    p3.join()

Process는 실행할 함수와 그와 함께 넘겨줄 인자를 명시해주면서 프로세스에 할당할 수 있었고 그 프로세스를 start()로 시작하고 join()으로 종료되길 기다릴 수 있었다.

Manager() 사용

하지만 이렇게만 했을 경우 각각의 프로세스는 데이터를 공유하지 못한다. 나는 각 함수가 크롤링해온 각 대학 도서관의 데이터를 한 곳에 모아야 했기 때문에 multiprocessing에서 프로세스 간 상태 공유와 공유 데이터 사용을 위해 제공하는 Manager를 사용했다. Manager()가 반환한 관리자 객체는 파이썬 객체를 유지하고 다른 프로세스가 프락시를 사용하여 이 객체를 조작할 수 있게 하는 서버 프로세스를 제어한다. Manager() 가 반환한 관리자는 list, dict, Namespace, Lock, RLock, Semaphore, BoundedSemaphore, Condition, Event, Barrier, Queue, Value 그리고 Array 형을 지원한다. 이를 활용하여 GET 요청을 받는 함수를 아래와 같이 작성했다. api라는 관리자 객체를 dict형으로 생성하고 여기에 각 함수가 크롤링 데이터를 담도록 했다.

@app.route('/result/getlist', methods=['GET'])
def give_result():
    keyword_receive = request.args.get('keyword_give')
    with Manager() as manager:
        api = manager.dict()
        multi(keyword_receive, api)
        api = json.dumps(api.copy(), ensure_ascii = False)
        return jsonify({'result': "success", 'msg':'정상 처리되었습니다.', 'row':api})

Json method 활용

원래는 이 dict형의 api 데이터를 그대로 GET 요청을 통해 HTML로 전달해주려고 했지만 이 데이터는 일반 dict형이 아니고 Proxydict형이기 때문에 jsonify가 되지 않았다. 이걸 해결하기 위해 json.dumps()로 Proxydict형을 Json 형태로 변환하고 넘겨주게 되었다.

이때 속성값을 지정하지 않으면 한글이 다 유니코드로 바뀌어서 전달되니 'ensure_ascii = False' 이 속성값을 지정함으로써 한글을 그대로 전달할 수 있었다. 이렇게 해서 무사히 데이터가 넘어왔다고 생각했는데 생각처럼 화면에 출력이 되지 않았다. api 데이터를 보니 json.dumps()를 통해 넘어온 데이터는 내부가 dict형태를 유지하고는 있었지만 문자열이 되어 넘어온 상태였다.

이 문자열을 다시 원래 형태로 변환하기 위해 JSON.parse()를 사용했다. JSON 문자열의 구문을 분석go 그 결과에서 JavaScript 값이나 객체를 생성하는 이 메소드를 통해 원래의 dict형태의 데이터를 얻을 수 있었다.

결과

검색어를 입력하고 검색 결과를 얻는데 기존에는 13초가 걸렸는데 multiprocessing을 활용한 뒤에는 8초로 그 시간이 줄었다. 서비스를 딱 봤을 때 눈에 띄는 큰 기능의 추가는 아니고 검색 결과를 얻기까지 여전히 오래 걸리긴 하지만 튜터님의 도움과 구글링의 힘으로 이렇게 검색시간을 줄이고 multiprocessing 패키지를 겉핥기로나마 써볼 수 있어서 유익했다😉

도움받은 문서들
https://python.flowdas.com/library/multiprocessing.html
https://niceman.tistory.com/147
https://inma.tistory.com/104
https://hashcode.co.kr/questions/4049/python3-%EC%97%AC%EB%9F%AC%EA%B0%9C%EC%9D%98-%ED%95%A8%EC%88%98%EB%A5%BC-%EB%8F%99%EC%8B%9C%EC%97%90-%EC%8B%A4%ED%96%89%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95%EC%9D%B4-%EA%B6%81%EA%B8%88%ED%95%A9%EB%8B%88%EB%8B%A4

profile
개발꿈나무 무럭무럭 자라는 중!

0개의 댓글