본문 바로가기

코딩/머신러닝&데이터 분석 강의

[머신러닝 인강] 8-1주차: selenium 모듈

<8-1주차 수강 클립>

02. 데이터 수집을 위한 Python (Crawling)

10. selenium 모듈 - 01. 사이트에 로그인하여 데이터 크롤링하기

11. selenium 모듈 - 02. selenium 모듈로 웹사이트 크롤링하기

12. selenium 모듈 - 03. 웹사이트의 필요한 데이터가 로딩된 후 크롤링하기

13. selenium 모듈 - 04. 실전 웹 크롤링

 


 

이번 주차는 2단원 [파이썬으로 웹 크롤링하기] 를 마무리 짓는 주차였습니다.

requests와 beautifulsoup 모듈에 이어 좀 더 파워풀한 크롤링이 가능한

selenium 모듈의 사용법에 대해 알아봤어요!

>>수강 인증샷<<

 


 

10. selenium 모듈 - 01. 사이트에 로그인하여 데이터 크롤링하기

 

- HTTP 상태 코드

1xx (정보): 요청을 받았으며 프로세스를 계속한다

2xx (성공): 요청을 성공적으로 받았으며 인식했고 수용하였다

3xx (리다이렉션): 요청 완료를 위해 추가 작업 조치가 필요하다

4xx (클라이언트 오류): 요청의 문법이 잘못되었거나 요청을 처리할 수 없다

5xx (서버 오류): 서버가 명백히 유효한 요청에 대해 충족을 실패했다

 

- 뉴스 댓글 개수 크롤링하기

개발자 도구의 network-XHR(Xml Http Request)

*) XHR: AJAX 비동기적 호출을 말함 (웹사이트를 전체 로딩이 아닌 부분적으로 필요한 데이터를 그때그때 비동기적으로 호출)

Name 탭의 요청을 하나씩 눌러서 Headers, Preview, Response, Timing 탭을 모두 확인 → 필요한 정보가 어디에 있을지는 케바케이기 때문!!

찾았다면 해당 요청의 Headers 탭에서 endpoint(General- Request url) 가져와 호출하기

url = 'https://comment.daum.net/apis/v1/ui/single/main/@20190728165812603'

resp = requests.get(url)
print(resp) #401: 클라이언트 오류_잘못 호출함

: 크롤링 시 원하는 응답 코드를 받지 못한 경우, 다시 개발자 도구의 해당 호출로 가서 response/request header을 dict로 구성해 호출할 때 헤더를 같이 전달해주기! (제대로 동작할 '가능성'이 높아짐)

url = 'https://comment.daum.net/apis/v1/ui/single/main/@20190728165812603'

headers = {
    'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb3J1bV9rZXkiOiJuZXdzIiwiZ3JhbnRfdHlwZSI6ImFsZXhfY3JlZGVudGlhbHMiLCJzY29wZSI6W10sImV4cCI6MTYxNzExNzkzMSwiYXV0aG9yaXRpZXMiOlsiUk9MRV9DTElFTlQiXSwianRpIjoiYWYzOGQ0ODAtMWIwMC00N2EyLWIzNDQtNjcxZWM1MGQ4NWEwIiwiZm9ydW1faWQiOi05OSwiY2xpZW50X2lkIjoiMjZCWEF2S255NVdGNVowOWxyNWs3N1k4In0.BNFD2iU4fwLStnu64ZJpCh9SkKmVMHw-z-zhEFtN08Y',
    'Origin': 'https://news.v.daum.net',
    'Referer': 'https://news.v.daum.net/v/20190728165812603',
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36'
}

resp = requests.get(url, headers=headers)
print(resp) #추가 정보(헤더)를 줬을 때 200으로 성공
resp.json() #json형식으로 출력, dict로 받아와짐
resp.json()['post']['commentCount']

 

- 로그인하여 데이터 크롤링하기 (by. post 방식)

특정한 경우, 로그인을 해서 크롤링을 해야만 하는 경우가 존재

ex) 쇼핑몰에서 주문한 아이템 목록, 마일리지 조회 등

이 경우, 로그인을 자동화하고 로그인에 사용한 세션을 유지하여 크롤링을 진행

 

- 로그인 후 데이터 크롤링 순서

  1. endpoint 찾기 (개발자 도구의 network를 활용해 로그인을 관장하는 endpoint 찾기)
  2. id와 password가 전달되는 form data찾기
  3. session 객체 생성하여 login 진행
  4. 이후 session 객체로 원하는 페이지로 이동하여 크롤링
url = 'https://www.yes24.com/Templates/FTLogin.aspx'

: 1. 로그인 endpoint

 

data = {
    'SMemberID': 'test',
    'SMemberPassword': 'test1234'
}

: 2. id, pwd로 구성된 form data 생성

 

s = requests.Session()

resp = s.post(url, data=data)
print(resp) #200 (성공)

: 3. 로그인
endpoint(url)과 data를 구성하여 post 요청 (login의 경우 post로 구성하는 것이 정상적인 웹사이트!)

세션 객체를 이용해 로그인을 함 → 로그인한 세션을 그대로 이용해서 다음 요청에 사용할 것이기 때문

 

my_page = 'http://www.yes24.com/Templates/FTMyAccount_1YESPoint.aspx' #마이페이지의 url
resp = s.get(my_page) #html 페이지를 요청하고 받아온 것->bs객체 만들기

soup = BeautifulSoup(resp.text)

soup.select('span#lYESPoint')

: 4. crawling

로그인시 사용했던 session을 다시 사용하여 요청

 


 

11. selenium 모듈 - 02. selenium 모듈로 웹사이트 크롤링하기

 

- selenium

웹페이지 테스트 자동화용 모듈

개발/테스트용 드라이버(웹 브라우저)를 사용해 실제 사용자가 브라우저를 사용하는 것처럼 동작시킬 수 있음 (id, pwd로 로그인, 검색창에 검색 etc..)

*) 아나콘다 내비게이터에서 selenium 모듈 설치 & 크롬 드라이버 다운로드 필요

(reques + bs 조합으로 크롤링에 실패한 경우 selenium으로 돌아가게 돼있음)

 

- selenium 사용 예제

python.org 사이트로 이동해 검색 자동화하기

1. 사이트 오픈

2. input 필드를 검색하여 Key 이벤트 전달

#크롬 드라이버 설치 경로
chrome_driver = 'C:\\Users\\user\\Downloads\\chromedriver_win32\\chromedriver'

#웹드라이버 객체 생성
driver = webdriver.Chrome(chrome_driver)

driver.get('https://www.python.org') #이동하고자 할 사이트 (자동)

#검색 자동화 (검색창의의 html 태그_id: id-search-field)
search = driver.find_element_by_id('id-search-field')

search.clear() #원래 있던 내용 지우기
time.sleep(3) #3초간 딜레이

search.send_keys('lambda')

time.sleep(3)
search.send_keys(Keys.RETURN) #enter키 눌러 검색

time.sleep(3)
driver.close() #사이트 닫기 (자동)

: SessionNotCreatedException 에러: chrome_driver의 설치 경로 입력 시 \ 대신 \\ 혹은 / 으로 변경하기

근데도 난 에러가 난다

 

- selenium을 이용한 다음뉴스 웹사이트 크롤링

driver 객체의 find_xxx_by 함수 활용

브라우저를 이용한다 → 요청이 한 번에 오는게 아니라 페이지에 담긴 소스 그대로 사용

따라서 selenium을 이용하면 좀 더 높은 확률로 데이터 크롤링 성공 가능

*) 차이점

1. request + bs: network탭을 이용해 추가적인 요청을 파악한 후 크롤링

2. selenium: 위 단계를 생략하고 한 번에 가능

chrome_driver = 'C:\\Users\\user\\Downloads\\chromedriver_win32\\chromedriver'
driver = webdriver.Chrome(chrome_driver)

url = 'https://news.v.daum.net/v/20190728165812603'
#브라우저 열기
driver.get(url)

src = driver.page_source
soup = BeautifulSoup(src) #bs객체에 html.text 넘겨서 객체 생성&초기화

#브라우저 닫기
driver.close()

comment = soup.select_one('span.alex-count-area')
comment.get_text()

: 페이지에 담긴 html 소스 그대로 사용 (driver.page_source)

 


 

12. selenium 모듈 - 03. 웹사이트의 필요한 데이터가 로딩된 후 크롤링하기

 

- selenium을 활용하여 특정 element의 로딩 대기

WebDriverWait 객체를 이용해 해당 element가 로딩되는 것을 대기

실제로 해당 기능을 활용하여 거의 모든 사이트의 크롤링이 가능

WebDriverWait(driver, 시간(초)).until(EC.presence_of_element_located((By.CSS_SELECTOR, 'CSS_RULE')))

 

chrome_driver = 'C:\\Users\\ydj89\\Downloads\\chromedriver_win32\\chromedriver'
driver = webdriver.Chrome(chrome_driver)

url = 'https://nownews.seoul.co.kr/news/newsView.php?id=20190730601010&wlog_tag3=naver'
#브라우저 열기
driver.get(url)

src = driver.page_source
soup = BeautifulSoup(src) #bs객체에 html.text 넘겨서 객체 생성&초기화

#브라우저 닫기
driver.close()

comment = soup.select_one('span.u_cbox_count') #아직 이 태그가 로딩되지x, 크롤링 안됨 (타이밍의 문제)
comment.get_text()

: 태그(찾고자 하는 element)가 로딩되지 않음, 크롤링 실패 → 타이밍의 문제!

 

#driver에게 최대 10초간(general) 기다리게 하기 (WebDriverWait)
#언제까지(until): element가 로딩될 때까지 (EC.presence~)
    #어떤 밥법으로 찾을지(By.): CSS_SELECTOR로
    #어떤 태그를 찾을지: 태그명.id

driver.get(url)
WebDriverWait(driver, 10).until(EC.presence_of_element_located(By.CSS_SELECTOR, 'span.u_cbox_count'))

: *)# 때 10초동안에도 로딩이 안되면 에러가 뜨지만 대부분의 웹페이지는 10초내에 로딩 되기 때문에 모든 element가 로딩된다 가정

 


 

13. selenium 모듈 - 04. 실전 웹 크롤링

 

- 뉴스 제목 크롤링 함수

def get_daum_news_title(news_id):
    url = 'https://news.v.daum.net/v/{}'.format(news_id)
    resp = requests.get(url)
    
    soup = BeautifulSoup(resp.text)
    
    title_tag = soup.select_one('h3.tit_view')
    
    if title_tag: #존재한다면(=제목 크롤링 성공) 출력
        return title_tag.get_text()
    
    return ""
get_daum_news_content('20190728165812603')

 

- 뉴스 댓글 크롤링 함수

#댓글 '더보기' 클릭시 network 로그에 찍히는 추가 요청 endpoint
    #offset: 원래 보여지던 댓글 개수 + limit
    #limit: 댓글이 한 번에 10개씩 가져와짐
url = 'https://comment.daum.net/apis/v1/posts/133493400/comments?parentId=0&offset=13&limit=10&sort=POPULAR&isInitial=false&hasNext=true&randomSeed=1617363477'

#요청시 응답코드 4xx: 해당 endpoint를 호출할 권한이 없음
	#->헤더의 값들을 dict로 묶어 같이 전달해주자!
headers = {
        'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb3J1bV9rZXkiOiJuZXdzIiwiZ3JhbnRfdHlwZSI6ImFsZXhfY3JlZGVudGlhbHMiLCJzY29wZSI6W10sImV4cCI6MTYxNzQwNTQwNCwiYXV0aG9yaXRpZXMiOlsiUk9MRV9DTElFTlQiXSwianRpIjoiMWQ3NjRjYTMtNjM1Yy00MmFkLWJlYTgtMjJlYTQ2NzI4NDI1IiwiZm9ydW1faWQiOi05OSwiY2xpZW50X2lkIjoiMjZCWEF2S255NVdGNVowOWxyNWs3N1k4In0.4w62K52dSzi1FDwIljP4gz_IiD_RRwNXCX_WQmmdcj8',
        'Origin': 'https://news.v.daum.net',
        'Referer': 'https://news.v.daum.net/v/20190728165812603',
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36'
}

resp = requests.get(url, headers=headers)
resp.json()

: endpoint(url)을 request.get으로 요청 시 response 코드가 4xx라면 호출 권한이 없는 것 → response header의 값들을 dict로 묶어 같이 전달해주면 성공할 확률 높아짐 !

: limit: '더보기'버튼 클릭 시 추가적으로 보여질 댓글 개수 (10개)

: offset: 원래 보여지던 댓글 개수 + limit (ex) 원래 3개 + 더보기 = 13개)

 

#offset을 적절히 활용해 긁어오기
    #endpoint의 offset값을 43으로 주면 빈 리스트를 반환->거리 43 이후에는 '더이상 댓글이 없음'을 의미
    
def get_daum_news_comment(news_id):
    headers = {
        'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb3J1bV9rZXkiOiJuZXdzIiwiZ3JhbnRfdHlwZSI6ImFsZXhfY3JlZGVudGlhbHMiLCJzY29wZSI6W10sImV4cCI6MTYxNzQwNTQwNCwiYXV0aG9yaXRpZXMiOlsiUk9MRV9DTElFTlQiXSwianRpIjoiMWQ3NjRjYTMtNjM1Yy00MmFkLWJlYTgtMjJlYTQ2NzI4NDI1IiwiZm9ydW1faWQiOi05OSwiY2xpZW50X2lkIjoiMjZCWEF2S255NVdGNVowOWxyNWs3N1k4In0.4w62K52dSzi1FDwIljP4gz_IiD_RRwNXCX_WQmmdcj8',
        'Origin': 'https://news.v.daum.net',
        #일단 news_id를 referer값에 전달하기로 함,,
        'Referer': 'https://news.v.daum.net/v/{}'.format(news_id),
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36'
    }
    
    #전체 url이 아니라 offset값을 다르게 주기 위함
    url_template = 'https://comment.daum.net/apis/v1/posts/133493400/comments?parentId=0&offset={}&limit=10&sort=POPULAR&isInitial=false&hasNext=true&randomSeed=1617363477'
        
        #예제랑 url_template 다름...
    #url_template = 'https://comment.daum.net/apis/v1/posts/@{다름,,}/comments?parentId=0&offset={}&limit=10&sort=RECOMMEND&isInitial=false'
    
    offset = 0 #오프셋 증가->더보기 버튼을 누른다는 뜻
    comments = [] #댓글을 담을 리스트
    while True:
        url = url_template.format(offset)
        resp = requests.get(url, headers=headers)
        data = resp.json()
        
        if not data: #긁어온 댓글이 없는 경우 while loop 탈출
            break
            
        comments.extend(data) #리스트에 긁어온 댓글(data) 추가
        offset += 10 #limit이 10이기 때문!_댓글을 10개씩 가져옴, 루프는 그 다음 10개부터 시작돼야하기 때문
        
    return comments

: [더보기] 클릭 시 요청의 endpoint는 offset값만 달라짐을 활용, format함수를 이용해 url 구성하기

: 댓글 요청의 headers 구성은 동일 (requests.get 사용 시 headers=headers 동일하게 사용)

 

comment = []
for info in get_daum_news_comment('20190728165812603'):
    print(info['content'], '\n')

: 댓글 내용(content)만 출력

 


 

강의 링크: https://bit.ly/3cB3C8y

반응형