jjzyra
[Python] 카카오맵 크롤링 (selenium) 본문
카카오맵 크롤링 중 새로 알게 된 내용들, 기억해야 할 내용들을 정리해봤습니다.
예전에 크롤링을 해보긴 했지만 실습해보는 정도였기 때문에 코드가 길지도, 딱히 막히는 부분도 없었던 걸로 기억합니다.
그땐 그냥 셀렉터 사용이 익숙하지 않아 오타를 많이 쳤던 것 같은데...
코드가 길어지고 가져오는 데이터 양이 많다보니 여러가지 문제가 생겼습니다.
NoSuchElement 에러
셀레니움 사용 중에는 여러가지 이유로 NoSuchElement 에러가 뜹니다.
path가 잘못된 경우,
url이 잘못된 경우,
사용하는 드라이버의 경로가 잘못된 경우,
드라이버와 사용하는 크롬 버전이 맞지 않는경우
이런 초보적인 실수를 제외하면 대부분의 에러는 NoSuchElement 에러였습니다.
처음부터 headless 옵션으로 돌리면 NoSuchElement 에러가 왜 뜨는지 정확한 이유를 찾기 어렵더라구요. (매번 이유가 다름)
코드를 짜고 확인하는 동안에는 좀 귀찮아도 창을 띄우면서 동작, 결과를 확인하면서 진행하는 게 좋을 것 같습니다.
제가 경험했던 NoSuchElement 에러의 원인들은 다음과 같습니다.
1. 셀렉터를 진짜 잘못 알려줬다
몇개 안되는 데이터를 가져오기 위해 크롤링 코드를 짜지는 않을테고 여러개의 반복문을 사용하게 됩니다.
구조가 비슷한 여러 페이지(카카오맵에서 검색으로 나온 식당들의 상세보기 페이지) 를 순차적으로 돌면서 find_element 함수가 돌아갈텐데, 문제는 이 상세보기 페이지들의 html 구조가 완전히 같지 않습니다.
예를 들어 어떤 가게는 모든 정보에 대한 div 영역이 있지만
어떤 정보가 누락된 가게는 그 div 자체가 생략되어 있습니다.
find_element 함수에 넣는 셀렉터는 대부분 개발자 페이지에서 copy selector 를 통해 가져오는데
가져온 셀렉터에 div.X > div.Y:nth-child(n) 부분이 있다면 크롤링 하려는 페이지의 html 구조를 보고 정보가 비어있더라도 해당 div가 있는지, 없는지를 확인해야 합니다. 없는 경우에는 내가 원하는 데이터가 아니라 빈 곳이 있어 밀려올라온 div의 엉뚱한 값을 받게 됩니다.
예를들어 저는 nth-child(n)이 포함된 셀렉터로 site 부분을 가져오는 코드를 짰지만 반복문이 돌다보면 어떤 가게(site 정보가 없는)에서는 전화번호를 가져오게 됩니다. 또 정보가 많이 부족한 어떤 가게에서는 n번째 리스트 자체가 없으니 NoSuchElement 에러가 뜹니다.
해당되는 div가 unique한 id를 가지고 있다면 try ~ except로 정확하게 예외처리를 할 수 있지만
셀렉터만으로는 구분하기 불가능한 동일 class 명만 가진 div들은 그 하위의 태그 구조를 확인해 내가 원하는 element가 맞는지를 확인할 수 있는 조건문을 추가해줘야 합니다. copy selector로 가져온 셀렉터는 그 페이지에만 정확하게 들어맞는 셀렉터입니다.
2. 페이지 로딩이 다 되기 전에 find_element~ 함수가 실행됐다
클릭, 입력 등의 동작 후 변경된 html에 있는 element를 찾을 경우의 나올 수 있는 에러입니다.
동작 후 페이지 로딩 속도가 일정한 게 아니니 어느 정도 텀을 주는게 좋습니다.
NoSuchElement 에러의 정확한 원인이 이게 맞다면 time.sleep을 주거나 implicitly, explicitly wait 로 간단하게 해결할 수 있습니다.
3. ip가 잠시 막히거나 원하는 페이지가 ip를 차단한 경우
이게 가장 큰 문제였습니다.
1) 2)를 방지할 수 있게 코드를 짰지만 정작 긁어오다가 NoSuchElement 가 뜨면서 멈춰버립니다.
처음엔 headless 옵션으로 돌리느라 또 셀렉터 문제인가 했지만 창을 띄워보니 그게 아니더라구요.
키워드 입력 / 클릭으로 새 페이지를 띄우는 걸 3, 4중 반복문으로 하다보니 금방 막혀버립니다. (time.sleep을 좀 더 길게 줘도 마찬가지)
ip가 막힐 경우 원하는 페이지가 아니라 403 에러 페이지가 나오거나 이전에 클릭했던 버튼을 반복 클릭하는 식으로 (내가 원하는 동작이 아닌) 크롤링을 차단하게 됩니다. 그 페이지들은 지정한 element가 없으니 당연히 NoSuchElement 에러가 뜨구요.
이럴땐 어쩔수 없이 그때까지 만들어된 데이터프레임을 csv로 저장시켜놓고 다음 검색어부터 다시 돌려야했습니다.
처음엔 300-400번 클릭마다 차단당하고,
무시하고 계속 돌리면 그 다음엔 50-100번 클릭에 차단당하고,
또 무시하고 계속 돌리면 그다음엔 5번 정도 클릭에 차단당하고 몇시간 동안 차단된 상태가 됐습니다.
구글링을 통해 시도해본 방법은 다음과 같습니다.
1) UserAgent 변경
사실 처음부터 fake_useragent를 사용해 검색어마다 바꿔가며 돌린건데... 효과는 없었습니다.
useragent만 확인하고 차단하는 싸이트는 없는 것 같습니다. 해본 것에 의의를 두고 ㅠ
2) IP 변경
일단 막히는 게 UserAgent가 아니라 IP입니다. 몇시간 차단 상태가 되면 그 IP로 연결 자체를 못합니다.
호옥시 그때 카카오맵을 이용하려고 했던 같은 와이파이 사용자 분이 계셨다면 죄송... 영문도 모르고 403 에러 보셨을듯 합니다
구글에서 Tor 브라우저를 이용해 드라이버를 열때마다 프록시 정보를 바꿔서 크롤링을 하는 방법을 찾았는데
우회를 하긴 했으나 창 로딩되는 속도가 너무 느려서 2)의 문제가 다시 터지기 시작했습니다.
대충 클릭 5번마다 로딩때문에 4번씩 NoSuchElement가 떠서 이 방법도 포기했습니다.
사용법은 Tor 브라우저를 설치, 실행 후 다음 프록시 정보를 드라이버에게 넘겨주면 됩니다.
def open_browser_c(path, url, useragent):
torexe = os.popen(r'/usr/local/bin/tor')
PROXY = "socks5://localhost:9050" # IP:PORT or HOST:PORT
# 옵션 설정
chrome_options = webdriver.ChromeOptions()
chrome_options.add_argument("--disable-extensions")
chrome_options.add_argument("disable-infobars")
chrome_options.page_load_strategy = 'normal'
chrome_options.add_argument('--enable-automation')
chrome_options.add_argument('disable-infobars')
chrome_options.add_argument('disable-gpu')
chrome_options.add_argument('--no-sandbox')
chrome_options.add_argument('user-agent={}'.format(useragent))
chrome_options.add_argument('--lang=ko_KR')
chrome_options.add_argument('--ignore-certificate-errors')
chrome_options.add_argument('--allow-insecure-localhost')
chrome_options.add_argument('--allow-running-insecure-content')
chrome_options.add_argument('--disable-notifications')
chrome_options.add_argument('--disable-dev-shm-usage')
chrome_options.add_argument('--disable-browser-side-navigation')
chrome_options.add_argument('--mute-audio')
# Tor 프록시 설정 (ip 우회)
# chrome_options.add_argument('--proxy-server=%s' % PROXY)
chrome_options.headless = True
# 브라우저 열기
driver = webdriver.Chrome(executable_path=path, options=chrome_options)
driver.get(url)
rand_num = (2 + random.random())*2
time.sleep(rand_num)
return driver
3) aws에서 다른 IP의 여러 서버를 만들어 동시에 돌리기 (막히든 말든)
이건 조금 오바라는 생각이 ... 들어서 다음에 시도해보려고 합니다.
제가 가져와야 하는 데이터는 10000-20000개 정도였습니다.
aws 서버 만들고 이거 저거 설정해주고 하는게 더 시간이 오래걸릴 것 같아서 시도조차 안했는데
생각해보니 이게 더 빨랐을 것 같네요ㅠ
4. try ~ except 남발하기
이것도 시간을 좀 많이 잡아먹었던 부분인데
하도 NoSuchElement를 보다보니 걸릴만한 코드마다 try ~ except를 사용해 감싸놓았습니다.
쓰는것 자체는 문제가 없지만 제 코드 자체가 클릭, 탭열기, 탭이동, 탭닫기가 3중첩 for문에서 왔다갔다 하는 더러운 코드였기 때문에 except 블록에서 continue나 break를 사용하게 되면 경우에 따라 Window 포커스 처리를 다르게 해주는 등의 추가 코드가 필요했습니다.
하지만 짤 때 너무 try ~ except를 남발해 놓았기 때문에.... 경우의 수 조합이 너무 많아졌고 어떤 부분에서 문제가 생겼는지를 알기 어려웠습니다. (너무 생각없이 덧대고 덧대다 보니 나중엔 제가 짠 코드가 맞나 싶었습니다)
결국은 headless 옵션을 풀고 예외처리 하나하나마다 빠진 윈도우 처리가 있는지를 확인해야 했습니다.
다시는 이렇게 짜면 안될것 같습니다.
카페에서 400개 정도 긁다가 막히면 집가서 또 400개 하고...
같이 프로젝트 하는 팀원들한테 핫스팟 켜달라고하고... 집가서 좀 돌려오라고 하고... 해서 목표했던 데이터는 긁어올 수 있을 것 같습니다.
자잘자잘한 실수들은 해결했지만 결국 IP 차단에 대한 답은 못찾고 프로젝트 다음 단계로 넘어가게 됐습니다.
아쉽지만 다음에 데이터 몇십만개가 필요한 프로젝트 억지로 시작해서 3번 방법 시도해보려고 합니다 (안 할듯..)
혹시 내용에 틀린 부분이 있다면 알려주세요!