레이블이 selenium인 게시물을 표시합니다. 모든 게시물 표시
레이블이 selenium인 게시물을 표시합니다. 모든 게시물 표시

2019년 10월 13일 일요일

나이스에서 학원 조회


전체 학원과 교습소 정보는 data.go.kr 에도 나오지만 업데이트가 자주 되지 않아 1년 전의 리스트만 볼 수 있습니다.  현재 시점의 학원과 교습소 정보는 https://hakwon.sen.go.kr/edusys.jsp?page=scs_m83000 에서 볼 수 있지만 페이지당 10개의 학원 박에 보이지 않고 전체 리스트를 한번에 받은 방법은 없습니다.  강남구의 경우 3천개가 넘는 학원이 있기 때문에 부득이 웹 스크래핑으로 전체 리스트를 구해야 합니다.


아래 프로그램은 파이어폭스를 띄우고 selenium를 통해 파이어폭스를 조정해서 강남구 학원과 교습소 전체 리스트를 얻습니다.  마우스가 아닌 파이썬 프로그램으로 파이어폭스 웹브라우저를 조정하는 셈입니다.  조정과 동시에 학원 이름, 주소 등을 데이터를 추출합니다.  


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# -*- coding: utf-8 -*-
from selenium import webdriver
import time

driver = webdriver.Firefox()   
driver.implicitly_wait(5)
driver.get('https://hakwon.sen.go.kr/edusys.jsp?page=scs_m83000')

btn = driver.find_element_by_xpath("//input[@value='조회']")
driver.find_element_by_xpath("//select[@id='selSearchZoneCode']/option[text()='강남구']").click()
driver.execute_script("arguments[0].click()", btn)

num_page = 1
while True:    
    base = 'grdView_cell_'
    for row in range(10):
        output = ""
        for col in range(7):
            loc = base + str(row) + "_" + str(col)
            output = output + driver.find_element_by_xpath("//td[@id="+"'"+loc+"'"+"]").text + "|"
            if col == 1:
                school = driver.find_element_by_xpath("//td[@id="+"'"+loc+"'"+"]")
                driver.execute_script("arguments[0].click()", school)
                alert = driver.switch_to.alert
                driver.implicitly_wait(1)
                output = output + alert.text + "|"
                alert.accept()
                driver.switch_to_default_content()
        print(output)
    num_page += 1
    if num_page % 10 != 1:
        s_num_page = '"'+"paglPg_page_" + str(num_page) + '"'
        driver.find_element_by_xpath("//a[@id="+s_num_page+"]").click()
    else:
        next_page = driver.find_element_by_xpath("//li[@id='paglPg_next_btn']")
        driver.execute_script("arguments[0].click()", next_page)
    time.sleep(10)


라인 811사이는  행정구역으로  강남구를 선택하고 조회버튼을 클릭하는 문장입니다.  btn.click() 대신에   driver.execute_script(“arguments[0].clock()”.btn)를 사용하는 이유는 exception이 발생하는 경우가 있기 때문입니다.   같은 이유로 라인 2636 형태가 필요합니다.

라인 14에서 while 무한 루프는 exception이 발생하기까지 실행합니다.  실제 문제가 있어도 중단이 되겠지만 더 이상 페이지가 없을 때에도 exception이 발생해서 중단합니다. 라인 1618로 이루어진 부분이 한 페이지를 읽습니다.  한 페이지에는 학원 10개가 나오니 10개의 행으로 구성되어 있고 각 행은 순번, 학원명, 설립자강사, 교습 과정 등의 열로 만들어져 있습니다. 이 중에 특별히 학원명은 클릭을 해야 alert 창이 뜨고 전화번호와 주소가 나옵니다. 학원명을 따로 처리하는 부분이 라인 21에서 28까지 입니다.

페이지를 넘기는 부분이 라인 30에서 37까지입니다.


변수 num_page10으로 나누어 나머지가 1이 될 때마다 하단에 “>”을 찾아 클릭하는 부분이 라인 3536이고 나머지가 1이 아니면 하단 숫자를 순차적으로 클릭합니다.  <a id=”pagelPg_page_#”>이나 <li id=’paglPg_next_btn’> 등의 태그는 웹브라우저에서  <F12>를 눌러 나오는 디버깅툴을 이용해서 찾습니다.

2018년 9월 4일 화요일

페이스북 좋아요 횟수 가져 오기


단일 페이지 어플리케이션의 웹 크롤링 (https://blog.naver.com/yoojchul/221333353523) 에 소개된 방법 대로 페이스북도 읽을 수 있습니다.   아래 프로그램은  환경운동연합의 페이스북 글과 그 글에 달린 좋아요’, ‘댓글’ , ‘공유의 횟수를 가져 옵니다.



 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# -*- coding: utf-8 -*-
from selenium import webdriver
import time
from bs4 import BeautifulSoup
import sys

fp = webdriver.FirefoxProfile("C:/Users/rcjcyoo/AppData/Roaming/Mozilla/Firefox/Profiles/y07d4b26.selenium")
driver = webdriver.Firefox(fp)   # keckodriver in the same directory with this script
driver.implicitly_wait(3)
driver.get('https://www.facebook.com')

driver.find_element_by_xpath("//input[@name='email']").send_keys('xxxxx@xxxx.com')
driver.find_element_by_xpath("//input[@name='pass']").send_keys('xxxxxx')

login = driver.find_element_by_xpath("//input[@value='로그인']")
login.click()

driver.get('https://www.facebook.com/kfem.or.kr/')

last_height = driver.execute_script("return document.body.scrollHeight")
count = 0
while True:
    driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
    count += 1
    time.sleep(3)

    # Calculate new scroll height and compare with last scroll height
    new_height = driver.execute_script("return document.body.scrollHeight")
    if new_height == last_height or count > 3:
        break
    last_height = new_height

non_bmp_map = dict.fromkeys(range(0x10000, sys.maxunicode + 1), 0xfffd)

soup = BeautifulSoup(driver.page_source, 'html.parser')
wrappers = soup.findAll("div", {"class":"_5pcr userContentWrapper"})

for w in wrappers:
    cnt = w.find("div", {"class":"_5pbx userContent _3576"}).find("p").text
    try:
        print(cnt.translate(non_bmp_map))
    except UnicodeEncodeError:
        print("**unknown char**")
    attaches = w.findAll("div", {"class" : "_36_q"})
    for a in attaches:
        print(a.text)
    print()

라인 78selenium를 통해 firefox를 실행하는 과정입니다.  실행에 프로필을 포함하는데 그 이유는  페이스북 접속시에 나타나는  귀찮은 firefox 알림창을 막기 위함입니다.  위 프로그램 실행 전에 “firefox.exe –p”firefox 프로필 관리자를 실행하고 프로필 만들기를 클릭해서 “selenium”를 만듭니다.  프로그램에서 사용할 때는 프로필 디렉토리 이름을 지정해야 하는데  보통 <home 디렉톨>/AppData/Roaming/Mozilla/Firefox/Profiles/xxxxx.selenium입니다.  



해당 프로필로 firefox를 실행후 주소창에서 “about:config”을 입력하고 dom.push.enableddom.webnotifications.enabledfalse로 바꿉니다.  selenium를 통해 firefox를 실행할 때 프로필을 만들지 않으면  설정은 남아 있지 않고 항상 초기화 상태입니다.


 라인 1213에서 아이디와 암호를 넣고 라인 1516에서 로그인 버튼을 클릭합니다. 라인 18로 환경운동연합 페이스북으로 넘어 갑니다.

라인 20부터 31까지는 반복적인 화면 스크롤 다운과 같은 역활을 합니다.  라인 2028은 페이스북 화면 y축 길이를 가져 오고 라인 23은 그 길이 만큼 스크롤 다운합니다. 다시 화면 y축 길이를 가져 와서 예전 길이가 같은 때, 즉 더 이상 화면이 내려가지 않을 때 루프에서  빠져 나와 스크롤 다운을 중단합니다.   스크롤 다운 횟수가 4 이상이 되어도 중단합니다.   라인 25에서 스크롤 다운과 길이를 가져 오는 사이에 3초를 쉽니다.  쉬는 시간이 짧으면 웹브라우저의 느린 스크롤 다운 반응으로 인해  y축 길이가 같은 경우가 생겨 끝이 아닌데도 불구하고 의도하지 않게 루프가 끝날 수가 있습니다.

페이스북에서 글과 좋아요횟수를 찾을 때는 beautifulsoup 패키지를 이용합니다.  라인 35에서  BeautifulSoup 첫번째 인수가 driver.page_source인데  이것이 웹브라우저에 뿌려진 DOM 데이터입니다.

게시글과 좋아요div 태그에 둘러 쌓있는데  먼저 클래스 이름 “_5pcr userContentWrapper”wrapper가 있습니다. wrapper 안에  게시글과 좋아요을 이끄는 다른 div태그가 있으므로 프로그램에서 “_5pcr userContentWrapper”를 찾습니다.   이름이 특이한데 별도로 soup.prettify()로 출력한 아래 그림에서 확인합니다.  



게시글은 div태그에 클래스 이름은 "_5pbx userContent _3576”인데 이 역시 미리 soup.prettify() 결과에서 확인합니다. 


  
좋아요 div 태그에 클래스 이름은  "_36_q"입니다.  좋아요외에 같은 class 이름으로 댓글공유가 있습니다.



이러한 태그와 클래스 이름을 근거로 라인 3947까지 게시글과 좋아요횟수를 찾을 수 있습니다.  클래스 이름은 201894일 기준이고 이후 달라질 수 있습니다. 게시글 출력시에 특수 문자로 인해 간혹 UnicodeEncoderError이 발생합니다. 출력하기 전에 translate(non_bmp_map)으로 0x10000이상의 코드는 0xfff0로 변환해서 에러가 발생하지 않도록 합니다.

아래 그림은 프로그램 결과로서 게시글과 함께 게시글의 좋아요횟수를 표시합니다.






2018년 8월 13일 월요일

카카오스토리 글 모으기


카카오스토리에 올려진 글을 얻기 위한 크롤링 과정을 소개합니다.    카카오스토리 2018813일 기준이고 이후 카카오스토리가 개편되면 도움이 되지 못합니다.
www.kakaostory.com으로 접속하면  로그인 화면인  https://accounts.kakao.com/login/kakaostory로 넘어갑니다.   주로 이메일로 하는 아이디와 비밀번호가 핵심이고 로그인 버튼이 있습니다.


그러면 웹 브라우저 소스 보기기능을 이용하여 로그인 중심의 요소를 찾습니다.  아이디와 비밀번호는 <form> 태그를 통해서 서버로 넘어가는데 form 태그 안에 <input>으로 받은 여러 태그가 있어 헷갈리게 합니다. 



일단 아이디와 비밀번호외 다른 태그는 무시하고 아이디와 비밀번호만 사용하려고 합니다.  “id=loginEmail”로 아이디 입력으로 보이는 라인 997를 찾고 “type=password”로 비밀번호 입력으로 보이는 라인1013를 찾습니다.   아이디는 form 다음에 나오는 태그로 따지면  <form> <fieldset> <div> <input> 순입니다. 



라인 1038에는 로그인 버튼도 있습니다.



로그인에는 아래 selenium 프로그램을 이용합니다.  로그인하고  https://story.kakao.com/mudbull 로 이동하는 것까지 프로그램에 넣도록 합니다. 


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# -*- coding: utf-8 -*-
from selenium import webdriver

driver = webdriver.Firefox()   # geckodriver in the same directory with this script
driver.implicitly_wait(3)
driver.get('https://accounts.kakao.com/login/kakaostory')

driver.find_element_by_xpath("//form/fieldset/div/input[@name='email']").send_keys('yooXXXXX@gmail.com')
driver.find_element_by_xpath("//form/fieldset/div/input[@name='password']").send_keys('XXXXXXXXXXXX')

btn = driver.find_element_by_xpath("//button[text()='로그인']")
btn.click()

driver.get('https://story.kakao.com/mudbull')

라인4에서 firefox 웹 브라우저를 띄웁니다. 이 프로그램과 같은 디렉토리에   geckodriver.exe가 있어야  웹 브러우저가 뜹니다.  라인5에서 웹 브라우저가 준비될 때까지 3초를 기다리고 라인6에서 로그인 화면에 접속합니다.  라인 89는 각각 아이디와  비밀번호를 가리키는 element를 찾아와서 send_keys() 함수로 값을 부여하는 과정입니다. element를 찾을 때는 find_element_by_xpath() 함수를 사용하는데 인수를 지정해서 element를 찾습니다.   라인8에서 사용된 인수는  "//form/fieldset/div/input[@name='email']"  form -> fieldset -> div -> input 태그를 찾는데  input 태그는 attribute name‘email’이어야 함을 뜻합니다. form 앞에 “//” 두번은 form 태그 전에는 어떤 것이 와도 괜찮다는 의미입니다.   라인 11에는 로그인  button를 찾은 함수가 있는데  “//button[text()='로그인']” button 태그 중에 text로그인인 것을 찾습니다.  이와 비슷하게 라인 8에 나온  "//form/fieldset/div/input[@name='email']"도 더 줄여서 “//input[@name=’email’]”도 가능할 것 같습니다.   라인 12 button를 클릭하다는 의미이고 라인 14는 페이지를   'https://story.kakao.com/mudbull' 로 이동합니다.  

form 태그 안에 로그인 정보에 다른 여러 element가 있었는데 무시했었는데 로그인하는데에는 지장이 없습니다.

selenium 프로그램으로 자동 로그인까지는 성공해서 웹 브라우저에서는 글을 볼 수 있습니다.  아래 노란색 선 안의 글들을 모으려고 하는데 이 글들은  소스 보기에도 나오지 않고  SPA의 웹 크롤링 (https://blog.naver.com/yoojchul/221333353523)을 시도해도 글을 찾을 수가 없습니다.  카카오스토리는 backbone과 비슷한 자체 framework을 사용한다고 합니다.



그런데 웹 브라우저에서 F12를 눌러 나오는 디버깅 화면을 보니 “GET”으로 그림 뿐만 아니라 json 파일을 가져 오는 것을 알 수 있습니다.


오른쪽 하단 화면을 더 키우면 자세한 내용이 나오는데 특히 응답탭에서 찾고 있는 글이 여기에 있었습니다.



헤더탭을 보면 URL까지 나옵니다.  URL로 글들만 서버로부터 json 파일 형식으로 받아 오고 있었습니다.


그래서 아래와 같은 파이썬 프로그램으로 한 페이지안에 들어 있는 글들의 집합을 json으로 저장합니다.  json 형식을 바로 디코딩할 수도 있지만 이 페이지의 경우  json으로 디코딩해서 출력을 시도하면 hang이 되거나 Unicode exception이 발생해서 디코딩을 다음 기회로 미룹니다.  아마도 예상하지 못한 문자가 있어 이런 문제를 야기하는 것 같습니다.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# -*- coding: utf-8 -*-
import  urllib.request
import json
import gzip

url = 'https://story.kakao.com/a/profiles/mudbull/activities?ag=false&since=&_=153396578026143'

hdr = {'Accept' : 'application/json'.encode('utf-8'),
       'Accept-Encoding' : 'gzip, deflate, br'.encode('utf-8'),
       'Accept-Language' : 'ko'.encode('utf-8'),
       'Connection' : 'keep-alive',
       'Cookie' : '_kadu=fr6-ASmuoqGhJFsG_1533960…6978; kuid=470960945171005441'.encode('utf-8'),
        'Host ' : 'story.kakao.com'.encode('utf-8'),
        'Referer' : 'https://story.kakao.com/mudbull'.encode('utf-8'),
       'User-Agent' : 'Mozilla/5.0 (Windows NT 6.1; W…) Gecko/20100101 Firefox/61.0'.encode('utf-8'),
       'X-Kakao-ApiLevel' : '45'.encode('utf-8'),
        'X-Kakao-DeviceInfo' : 'web:d;-;-'.encode('utf-8'),
        'X-Kakao-VC' : '4da14561d321f651ae6f'.encode('utf-8'),
        'X-Requested-With' : 'XMLHttpRequest'.encode('utf-8')}

req = urllib.request.Request(url, headers=hdr)
resp = urllib.request.urlopen(req)
print(resp.info())
filedata = gzip.decompress(resp.read())

with open("data.txt", 'wb') as f:
    f.write(filedata)

f.close()


글을 가져 올 때는 url 정보외에 header 정보도 필요합니다. 라인 8에서 19에 나온 header 정보는 디버깅 화면에서 확인된  요청 해더를 그대로 사용합니다.  zip으로 압축된 형식으로 서버에서 보내 주기 때문에 라인 24에서 gzip.decompress()로 푸는 함수가 필요합니다.

2018년 8월 5일 일요일

단일 페이지 어플리케이션의 웹 크롤링


단일 페이지 어플리케이션(SPA : Single Page Application)은 웹을 구성하는 정적인 리소스를 한번에 다운로드하고 새로운 페이지 구성에는 필요한 데이터만 서버로부터 받거나 클라이언트에서 만들어 사용합니다. ,  동적으로 웹 페이지를 구성합니다.  동적으로 웹 페이지가 만들어진다는 점에서 웹 크롤링에 어려움이 있습니다.  SPA 구성에는 주로 backbone, angular,  React와 같은 javascript framework를 이용하는데  backbonejs를 이용한 예제가  http://backbonejs.org/examples/todos/index.html 입니다.  



“book4”“book5”는 사용자의 키 입력과 javascript가 실행되면서 만들어진 요소입니다.  웹브라우저에서 제공하는 소스 보기기능을 이용해도  “book4” “book5”가 보이지 않아 당황스럽습니다.



http://backbonejs.org/examples/todos/index.html를 두가지 방법으로 웹 크롤링을 합니다. 하나는 selenium과 파이썬을 이용하는 방법이고  다른 하나는 casperjs를 이용한  방법입니다.
selenium를 사용하려면 웹 브라우저에 맞은 드라이버를 구해야 합니다.  여기서는 firefox를 사용할 것이라 https://github.com/mozilla/geckodriver/releases 에서 geckodrive 이름으로 된 드라이버를 구합니다. 압축을 푼 geckodriver.exe는 파이썬 실행 프로그램과 같은 디렉토리에 둡니다.  그리고 아래 파이썬 프로그램을 실행합니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# -*- coding: utf-8 -*-
from selenium import webdriver
from selenium.webdriver.common.keys import Keys

driver = webdriver.Firefox()   # keckodriver in the same directory with this script
driver.implicitly_wait(3)
driver.get('http://backbonejs.org/examples/todos/index.html')


driver.find_element_by_id("new-todo").send_keys('book4')
driver.find_element_by_id("new-todo").send_keys(Keys.ENTER)

driver.find_element_by_id("new-todo").send_keys('book5')
driver.find_element_by_id("new-todo").send_keys(Keys.ENTER)

print(driver.page_source)

print("** body **")
print(driver.find_element_by_tag_name("body").text)

print("** li **")
elements = driver.find_elements_by_tag_name("li")
for e in elements:
    print(e.text)

라인 7까지는 웹 페이지 접속을 위한 기본 내용입니다. 라인10에서 14까지는 “book4”“book5”를 등록하는 과정인데  “ENTER”도 따로 보내야 함에 주의합니다.  라인 16 “driver.page_source”를 출력하면 웹 페이지의 html 소스를 볼 수 있는데  아래와 같이 “book4”“book5” 내용이 들어 있습니다. 이것은  웹 브라우저에서 볼 수 없었던 내용이고 이런 방식으로 동적으로 생성된 웹 페이지를 크롤링할 수 있습니다.   라인 19 에서는  <body> 태그에 담긴 텍스트, 라인 22이하에서는 <li> 태그에 담긴 텍스트를 정확하게 가져 옵니다.



selenium는 웹 브라우저를 구동하고 프로그램으로 조작하는 반면에 caspejs는 웹 브라우저 없이 작동합니다.  대신 casperjsphantomjs라는 프로그램 위에서 작동해야 합니다.  윈도우용으로   http://phantomjs.org/download.html 에서 MS 윈도우용 phantomjs를 먼저 구합니다.  압축을 풀면 example를 포함한 여러 파일이 있지만  bin 디렉토리의 phantomjs.exe 만 있으면 됩니다.  원도우 환경 변수 PATHphantomjs.exe가 있는 디렉토리를 지정하도록 합니다.  



윈도우용 casperjs  http://casperjs.org/  바로 첫 화면에서 다운로드 가능하고 모든 파일의 압축을 풀고 환경 변수 PATHcasterjs.exe가 있는 폴더 이름을 포함하도록 합니다.   도스창에서 “casper”만 실행해서 casperjsphanatomjs 버전이 나오면 설치는 성공입니다.



아래는http://backbonejs.org/examples/todos/index.html를 읽기 위한 casperjs 프로그램입니다. javascript인데 파일 이름은 todo3.js라면 도스창에서 “casterjs todo3.js”로 실행합니다.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
var casper = require('casper').create();

var bd = [];
var links = [];

function getBody () {
 var b = __utils__.findAll('body');
    return b;
}

function getTodos () {
    var todos = document.querySelectorAll('#todo-list li');
    return todos;
}

casper.start("http://backbonejs.org/examples/todos/index.html", function() {
 links = this.evaluate(getTodos);
    this.echo('num: ' + links.length);
});

// add new todo
casper.then(function() {
    this.sendKeys('#new-todo', 'book1');
    this.sendKeys('#new-todo', casper.page.event.key.Enter);
});

// add new todo
casper.then(function() {
    this.sendKeys('#new-todo', 'book2');
    this.sendKeys('#new-todo', casper.page.event.key.Enter);
});

casper.then(function() {
 bd = this.evaluate(getBody);
 this.echo(bd[0].outerHTML);
    links = this.evaluate(getTodos);
    this.echo('num: ' + links.length);
 this.echo("0: " + links[0].outerHTML);
 this.echo("1: " + links[1].outerHTML);
});

casper.run();

첫번째 라인은 casperjs를 시작하기 위한 casper 변수 선언입니다.  나중에 다시 언급하겠지만 casperjstest 모드에서 실행하려면 이 줄을 없애야 합니다.  라인6이하와 라인 11이하는  각각 <body><todo-list><li> 태그를 찾기 위한 함수 선언입니다.  라인 16에서  URL를 인수로 하는 casper.start()로 시작합니다. selenium와 달리 명시적으로 wait를 주지 않아도 페이지가 올라올 때까지 기다립니다.  casper API 실행 순서는 start() -> then() -> ... ->  run() 순입니다.  기본적으로 비동기로 실행되는 then이 순서를 정하고 run()은 반드시 존재해야 합니다.   라인 17에서  getTodos()를 호출해서  <todo-list><li> 태그를 가진 요소가 몇개인지 알아 봅니다.  라인 22이하와 라인 28이하를 통해 요소 두개를 추가합니다. 라인 34에서 <body> 태그에 요소를 찾아 HTML 형식으로 출력합니다. 라인 36이하는 다시 <todo-list><li> 태그를 가진 요소를 찾습니다.



여기에 casperjs의 버그가 있습니다. 첫번째 요소를 찾아 출력하는데에는 문제가 없는데 두번째 요소 경우  아예 에러도 없이 아무런 내용이 나오지 않습니다. 아무런 에러가 없다는 것이 더 문제인데 이 에러를 보려면 casperjstest 모드에서 실행해야 합니다.  test 모드는 자동적으로 변수 선언이 있기 때문에 첫번째 라인에서 선언문과 겹칩니다.  



그래서 첫번째 라인의 casper 변수 선언은 comment처리하고  “casperjs test todo3.js” 실행해야 비로소  links[1]null임을 알 수 있습니다.



seleniumjava와 파이썬과 같이 여러 언어도 지원하고 파싱을 위한 beautifulsoup까지 사용할 수 있는 반면에 casperjsjavascript로만 가능하고 따라서 웹 크롤링에 도움이 되는 지원 도구도 거의 없습니다. selenium와 파이썬으로 크롤링하다가 한계나 극복할 수 없는 에러만 생기지 않으면 계속 selenium를 사용할 것 같습니다.