본문 바로가기
풀스택 개발 학습 과정/데이터

[데이터 수집] BeautifulSoup + Selenium 실습

by 육츠 2023. 11. 2.

select 태그 선택 테스트

from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
import time
from bs4 import BeautifulSoup
from selenium.webdriver.support.ui import Select # select 태그 제어 클래스

SCROLL_PAUSE_TIME =1

url ='https://www.selenium.dev/selenium/web/formPage.html'
driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()), options = webdriver.ChromeOptions())
driver.get(url)

select = Select(driver.find_element(By.NAME,'selectomatic'))

# 1. 인덱스 기준으로 선택하기
# select.select_by_index(1)

# 2. 보여지는 선택값 텍스트로 선택하기
# select.select_by_visible_text('Still learning how to count, apparently')
# 보여지는 텍스트는 앞글자가 대문자 'still learning how to count, apparently'

# 3. 옵션 요소의 값으로 선택하기
# select.select_by_value('four')

# select 태그에 onchange(화면변화 목적) 옵션이 있어서 Select 클래스를 사용할 수 없는 경우 -> click() 함수를 사용해야함
driver.find_element(By.CSS_SELECTOR,'option[value = "four"]').click()

select 태그는 동적인 페이지에서 사용된다. onchange 란 input이나 select 등의 데이터가 변경될 때 호출되는 이벤트를 가리킨다.

Select 태그를 설명한 이유: yes24의 홈페이지는 onchange를 사용하는 동적 홈페이지 이기 때문이다.

 

[실습] yes24에서 파이썬 도서 검색하기

yes24 사이트에서 파이썬 도서 검색 후 평점 9.6 이상인 도서 제목과 가격, 평점 가져오기

from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
import time
from bs4 import BeautifulSoup
from selenium.webdriver.support.ui import Select # select 태그 제어 클래스
import requests

SCROLL_PAUSE_TIME =1
driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()), options = webdriver.ChromeOptions())

# 파이썬 검색
driver.get('https://www.yes24.com/main/default.aspx')
ele = driver.find_element( By.NAME,'query')
ele.send_keys('파이썬')
ele.send_keys(Keys.ENTER)

# 20개 -> 120개씩 보기 변경
select = Select(driver.find_element(By.ID,'stat_gb'))
driver.find_element(By.CSS_SELECTOR, 'option[value = "120"]').click()

time.sleep(SCROLL_PAUSE_TIME)

# 가동성을 위해 Selenium 과 Beautifulsoup의 셀을 구분해서 만들었다.
from bs4 import BeautifulSoup

soup = BeautifulSoup(driver.page_source, 'lxml') 
book_list = soup.find('ul',attrs={'id':'yesSchList'})
books = book_list.find_all('li')
# print(len(books))

# for i in range(2,11): # or while # 페이지 넘기기
for book in books:
    title = book.find('a', attrs = {'class':'gd_name'})
    price = book.find('strong',attrs = {'class':'txt_num'})
    rating = book.find('span',attrs = {'class':'rating_grade'})
   
   # <em class="yes_b"> 태그가 도서가격 등 다른 곳에서도 사용됨
    # -> 상위에 있는 태그를 가져와 다시 범위를 좁혀 사용
    # <em class="yes_b">19,800</em>
    # <em class="yes_b">9.9</em>
    
    if not rating: continue # 평점 없는 도서는 pass
    cnt+=1
    rating = rating.find('em', attrs = {'class':'yes_b'}).text
    #  다시 범위를 좁혀 실제 평점 가져옴
    
    if float(rating) < 9.6: continue
    print(f'> {title.text} | {price.text} | {rating}')

price 와 rating의 사용하는 클래스 이름이 같다. 때문에 그들의 상위 태그를 먼저 가져온 후 다시 세분화를 진행한다.
평점이 없는 책이 존재하기에  try~except 구문을 사용하여 예외 처리 한다.
 rating 문자열이니 9.6보다 크다는 조건문을 구하기 위해 float처리 후 구분한다.

[실습] 다음 영화 사이트에서 박스 오피스 1위 영화의 감상평 및 평점 수집

- 감상평을 수집하여 텍스트 파일로 저장하기
- 평점을 수집하여 csv 파일로 저장하기
import csv
with open('rating.csv','w) as f:
    writer = csv.writer(f)
    writer.writerow(ratings) # ratings: 평점을 모아놓은 리스트 객체

* 스크래핑 순서
selenium: 다음 영화 사이트에 접속(movie.daum.net) -> "랭킹" 클릭 -> "박스오피스 클릭" -> 1위 영화 클릭 -> "평점" 클릭 -> 마지막 평점까지 스크롤
BeautifulSoup : 사용자별 감상평 및 평점 수집 -> 리스트 저장 -> 파일 저장

from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
import time
from bs4 import BeautifulSoup
from selenium.webdriver.support.ui import Select # select 태그 제어 클래스

PAUSE_TIME =1

url = 'https://movie.daum.net'
driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()), options = webdriver.ChromeOptions())
driver.get(url)

elem = driver.find_element(By.LINK_TEXT,'랭킹')
elem.click()

elem = driver.find_element(By.LINK_TEXT,'박스오피스')
elem.click()

# 박스오피스 1위 영화 클릭
rank_1 = driver.find_element(By.CSS_SELECTOR,'#mainContent > div > div.box_boxoffice > ol > li:nth-child(1) > div > div.thumb_cont > strong > a')
rank_1.click()

# 평점탭 클릭
time.sleep(PAUSE_TIME)
driver.find_element(By.XPATH,'//*[@id="mainContent"]/div/div[2]/div[1]/ul/li[4]/a').click()

# 스크롤 횟수
scroll_cnt =1
while True:
    # 윈도우 창의 스크롤 바를 0 ~ 가장 밑 (전체 길이 값 = scrollHEight)까지 이동
    driver.execute_script('window.scrollTo(0,document.body.scrollHeight)') # 스크롤 시작
    time.sleep(SCROLL_PAUSE_TIME)
    
    new_height = driver.execute_script('return document.body.scrollHeight') # 변화된 스크롤 다시 기록
    print(f'last height:{last_height}, new_height: {new_height}')

    if last_height != new_height : # 계속해서 new_height값이 변경된다면
        last_height = new_height
    else: # 더이상 new_height 값에 변동이 없다면 
        try:
            ele = driver.find_element(By.CSS_SELECTOR, '#alex-area > div > div > div > div.cmt_box > div.alex_more > button')
            ele.click()
            print('평점더보기 클릭')
        except:
            break

까먹고 있던 ' driver.find_element(By.LINK_TEXT,'랭킹')' 구문. 텍스트에 연결해 놓은 링크로 넘어간다. 
박스오피스는 CSS_SELECTOR 로 빠르게 접근하고, 평점 탭은 CSS_SELECTOR 가 갑자기 안되서 XPATH로 대신했다.
스크롤 문은 구글 실습과 유사하며, 하나 다른 것이 있다면 try문이 반복문에서 조건식 안에 포함되는 것이다.

이 코드에서 가장 애 먹었던 것은 처음에 버벅거리는 click() 함수와 아직 익숙하지 못한 마우스 조작법이다.

soup = BeautifulSoup(driver.page_source, 'lxml') 

ratings= []
comments = []

# 가장 큰 영역
ele = soup.find('ul',attrs={'class': 'list_comment'})
ele = ele.find_all('li') # 이미 범위 안이기 때문에, 태그만 검색해도 된다

for e in ele:
    rating = e.select_one('div>div').text
    ratings.append(int(rating))

    # 감상평이 이모티콘으로 되어있는 (텍스트가 없는) 경우 예외 발생
    try:
        comment = e.select_one('div>p').text
    except: continue

    comment= comment.replace('\n',' ')
    comments.append(comment) 

# 수집 정확도 체크
print(f'네티즌 평점: {sum(ratings)/len(ratings):.1f}점')

import csv
import os

with open('rating.csv','w') as f:
      writer = csv.writer(f)
      writer.writerow(ratings) # ratings: 평점을 모아놓은 리스트 객체
print('평점 저장 완료')


with open('comments.txt','a',encoding='utf-8') as f:
    for comment in comments:
        f.write(comment+'\n')
print('감상평 저장 완료')

가장 큰 영역인 class = 'list_comment'  안에는 평점, 감상평, 작성자 등의 요소들이 존재한다.
거기서 범위를 좁히는 것이 가장 효율적인 방법이다. (처음 'list_comment' 를 조금더 크게 생각하면 어땠을까 라는 아쉬움이 든다.)
평점은 class의 이름이 'ratings rating_평점점수로' 비슷하다. 나는 이를 활용해 돌렸지만 교수님은 그것의 위치를 파악하여 추출하셨다.
감상평은 한글이므로 감상평 저장에 encodoing = 'utf-8' 사용

이 코드에서 애먹은 점은 처음엔 감상평과 별점이 함께 반복문을 돌아 나와야 한다고 생각되어 계속 같이 나오는 태그를 찾으려고 한 것이다.
교수님의 설명과 함께 for문을 따로 돌려야 한다고 설명을 해주셨고 for문 2 개를 사용하여 값을 추출하는데에는 성공했다.
하지만, text를 사용하여 태그를 벗기고 추출하여 append  하는 것이 안되었고, 교수님의 코드를 보며 좀 더 쉬운 방법이 존재했다는 걸 깨달았다.

 

iframe을 이용한 웹 페이지 스크랩 하기

iframe 독립된 html을 따로 만들어 필요한 페이지에 삽입시키는 것( 예: 네이버 지도 )
selenium을 이용해서도 동적인 페이지를 읽지 못한 상황

해결 : 프레임 초점을 이동시켜 외부 -> 내부 -> 외부 로 바꿔야함.
- iframe 태그의 id값을 찾는다.
frame = driver.find_element(By.ID, 'entryIFrame')

- 해당 iframe 으로 switch
driver.switch_to.frame(frame)

- 이후 iframe 내의 element를 검색하여 데이터를 스크래핑한다.
iframe내의 스크래핑 작업이 끝난후 기본 프레임의 내용을 스크래핑하기 위해 기본프레임으로 전환한다.

driver.switch_to.default_content()