이번 프로젝트에서는 서버에서 블로그 본문 데이터를 크롤링해 온 다음 이를 분석하여 클라이언트에 반환하는 기능을 구현했다. 이 과정에서 Scrapy를 공부하고 사용한 내용을 기록하고자 한다.

 

Scrapy 튜토리얼2 - 네이버 검색결과 크롤링(크롤러 설정)

Scrapy 튜토리얼3 - 네이버 검색결과 크롤링(데이터 추출)


 

웹 크롤링은 대표적으로 BeautifulSoup와 Selenium을 사용한다. 배우기 쉽고, 한국어 자료도 방대한 장점이 있다. 

 

다만 내가 프로젝트에서 두 라이브러리 대신 Scrapy를 사용한 이유는 3가지가 있다.

 

 

 

Webdriver를 호출하는 Selenium의 속도저하 문제

 

Selenium은 실제 웹페이지를 띄우고, 렌더링되는 시간을 기다려야 크롤링이 가능하다. 지도나 무한 스크롤같은 동적인 페이지를 크롤링해야 한다면 Selenium을 사용하는 것이 맞다.

그러나 나는 정적인 페이지를 크롤링하고, BeautifulSoup이나 Scrapy와 비교했을때 훨씬 많은 시간을 소요해 선택하지 않았다.

 

 

 

동기적으로 동작하는 BeautifulSoup

 

BeautifulSoup와 Scrapy는 둘 다 request를 보냈을 때 response를 그대로 받아온다는 점에서 동일하다.

이 둘의 차이점은 BeautifulSoup은 동기적으로, Scrapy는 비동기적으로 동작한다는 것이다.

쉽게 말해 BeautifulSoup은 페이지 크롤링하는 동안 어떤 코드도 수행할 수 없어 대기해야 한다. 그러나 Scrapy는 페이지를 크롤링하는 동안 다른 동작을 수행할 수 있다. 

 

따라서 시간적으로 봤을때 BeautifulSoup는 Scrapy보다 더 많은 시간을 소요한다. BeautifulSoup와 multiprocessing을 함께 사용하면 상당히 빨라지지만 프로젝트 특성상 이후에도 많은 멀티스레딩을 동원하기 때문에 추가적으로 멀티프로세스를 호출하는 것이 오버헤드 측면에서 부담스러웠다.

 

 

 

Scrapy의 확장성 및 편의

 

BeautifulSoup가 단순히 html에서 정보를 가져오는데 그친다면 Scrapy는 확장성 측면에서 상당히 유연하다. middleware를 사용자가 직접 손볼 수 있고 데이터를 다운로드 하거나 하는 등의 여러 확장이 자유롭다.

 

더불어, 필자는 블로그에서 본문데이터를 가져오는 작업을 진행했는데 BeautifulSoup보다는 Scrapy가 하위 태그에서 텍스트만 골라 가져오는 것이 더 편리했다.(이것은 개인적인 의견일 수도 있다.)

 

 

 

한국에서는 Scrapy를 잘 사용하지 않는지 한국어 자료보다는 영어자료가 훨씬 많았다. 다행스러운 점은 스택오버플로우 등 영어권에는 Scrapy 자료가 훨씬 방대하다.

 

 

이러저러한 이유로 결론은 Scrapy를 선택했다는 것이다.

Scrapy가 BeautifulSoup와 Selenium에 비해서 배우기 어렵고 한국어 자료도 많이 없는 단점은 있다.

그러나 공식문서 튜토리얼을 따라가며 배우다보면 대략적으로 어떻게 동작하는지 이해하는 데에는 오래걸리지 않는다.

 

 

다음 포스트에서 본격적으로 Scrapy를 어떻게 사용하는지에 대해 소개하도록 하겠다.

반응형

프로젝트 중 파이썬의 subprocess.check_output의 리턴값인 string을 다시 dict(json.load)로 변환하는 과정에서 문제가 발생했다.

 

 

 

subprocess모듈의 check_output은 다른 프로세스에서 수행된 print 값을 모두 모아 하나의 string으로 반환해준다.

그런데 dict를 print 해서 string으로 나온 것을 다시 json.load 함수를 써서 dict로 바꾸려니 온갖 DecodeError가 떴다.

 

작은 따옴표를 큰 따옴표로 바꾸고 '\'을 '\\'로 바꾸라는 둥 replace를 해결책으로 권하는 사람들이 많았는데

여간 귀찮은게 아닌데다 나는 딕셔너리의 value 내에 큰따옴표로 바꾸면 안되는 작은 따옴표들도 들어있어서 난감했다.

 

 


 

 

여기서 상당히 간단한 해결책이 있다.

ast 모듈eval()함수를 활용하면 된다.

 

import ast
import subprocess

result = subprocess.check_output([쉘 명령어], encoding='utf-8')
#result : "{'key': 'value'}"

dict = ast.literal_eval(result)
#dict : {'key': 'value'}

 

literal_eval은 문자열 내의 파이썬 표현식을 인식하는 함수인데 

예를 들어 '10+10'문자열이 있다고 하면 eval을 거쳐서 20의 결과를 반환해주는 상당히 편리한 함수이다.

그래서 위 코드를 수행하면 문자열이 딕셔너리로 변환된다.

 

 

 

다만 파이썬에서는 eval함수 사용을 보안상 문제로 인해 사용을 자제하도록 권고하고 있다.

 

이 함수는 어떤 문자열이든 인증절차나 확인 없이 바로 코드로 인식해 수행해버리기 때문에

만약 외부에서 들어온 악의적인 해킹 코드가 문자열로 들어오고, 그 값이 eval 함수에 들어가면 코드로 변환해 수행해버린다. 서버를 부수거나 기밀정보를 빼돌리는 것이 얼마든지 가능해지는 것이다.

 

따라서 사용에 상당히 주의해야함은 맞다.

반응형

이진 탐색 알고리즘정렬된 리스트의 값들 중 특정 값을 찾을 때 사용할 수 있는 상당히 빠른 알고리즘이다.

 

간략히 요약하자면, 왼쪽 인덱스와 오른쪽 인덱스를 기준으로 그 정 중앙의 값과 찾고자하는 값을 비교한다.

만약 중앙값이 더 크다면 왼쪽 인덱스를 중앙인덱스+1로, 더 작다면 오른쪽 인덱스를 중앙인덱스-1을 하며 거리를 점점 좁혀나간다. 여기서 +1 -1을 하는 이유는 이미 확인한 중앙값이 찾고자하는 값이 아니므로 그 인덱스를 배제하기 위함이다.

 

이진 탐색의 시간복잡도는 O(logN)이다.

 

이진 탐색으로 특정 값을 찾는 알고리즘은 다른 많은 블로그에서 정말 잘 설명해주셨기 때문에 이부분은 생략하고, 여기서는 이진 탐색으로 최댓값, 최솟값을 구하는 법에 대해 기록하고자 한다.

 

 

 

 

이진 탐색 응용(최댓값 / 최솟값)


최댓값, 최솟값은 다른 말로 천장값와 바닥값라고도 불린다.

주어진 숫자보다 작은 수 중 가장 큰 수를 천장값, 주어진 숫자보다 큰 수 중 가장 작은 수를 바닥값라고 한다.

 

예를 들어 { 1, 3, 5, 7, 9 } 라는 리스트가 있는데, 6이라는 값이 주어졌다고 해보자.

여기서 천장값, 즉 6보다 작은 값들 중의 최댓값은 5가 된다.

그리고 바닥값는 반대로 6보다 큰 값들 중의 최솟값이므로 7이 된다.

 

이분 탐색을 활용하면 최솟값과 최댓값도 쉽게 구할 수 있다.

개인적으로 이 부분을 구현하는데 많이 혼란스러웠기 때문에 간단한 코드로 기록하여 쉽게 이해해보자.

 

코드

int[] list = {1, 3, 5, 7, 9};

int num = 6;

int start = 0; //왼쪽 인덱스
int end = 4; //오른쪽 인덱스

while(start <= end) {
	int mid = (start + end) / 2;
  	
    if(num < list[mid]){
    	end = mid - 1;
    }
    else if(num > list[mid]) {
    	start = mid + 1;
    }
}

System.out.println("최댓값 : " + list[end]);
System.out.println("최솟값 : " + list[start]);

 

위 코드에서는 주어진 값이 리스트 안의 값과 일치하는 경우는 생략하였다.

 

반복문을 사용하여 이진 탐색을 구현하였고, start, end의 값을 계속해서 갱신하는 방식으로 진행하였다.

end는 주어진 값보다 작은 값들로 계속 탐색해 나가고, start는 주어진 값보다 큰 값으로 계속 탐색해간다. 따라서 mid -1, mid +1의 코드를 통해 mid로 확인한 값은 제외한다.

계속 진행하다보면 start와 end가 주어진 값 num을 기준으로 엇갈리게 된다. 마지막에 end는 리스트 값 중 5를, start는 7을 가리키게 될 것이다. 그 의미는 end는 최댓값을, start는 최솟값을 찾았다는 의미이니 두 경우를 모두 찾은 셈이 된다.

 

다만 위 코드는 리스트에 num과 일치하는 값이 없다는 전제가 있을 때 동작한다.

 

 

 

그렇다면 만약 리스트에 num과 일치하는 값이 있고, 최대 혹은 최솟값을 구한다면 어떻게 해야할까?

 

위 코드를 조금 변형하면 쉽게 찾을 수 있다

 

1. 최솟값

public class Test {
	public static void main(String[] args) {
		int[] list = {1, 3, 5, 7, 9};

		int num = 5;

		int start = 0; //왼쪽 인덱스
		int end = 4; //오른쪽 인덱스

		while(start < end) {
			int mid = (start + end) / 2;

			if(num < list[mid]){
				end = mid - 1;
			}
			else if(num >= list[mid]) {
				start = mid + 1;
			}
		}

		System.out.println("최솟값 : " + list[start]);
	}
}

 

최솟값의 요점은 다음과 같다

  • start가 최솟값을 탐색한다
  • start는 '주어진 숫자와 일치하지 않는' 최솟값을 찾는 것이기 때문에 mid값을 건너 뛰어야 한다(mid+1)
  • start는 '주어진 숫자보다 큰 값들' 중의 최솟값을 찾기 때문에 mid 값이 num보다 작거나 '같으면' 값을 갱신한다

 

최댓값은 이와 반대로 작성하면 된다

 

정정사항에 대한 피드백은 늘 환영입니다.

반응형

+ Recent posts