본문 바로가기
코딩 연습/코딩배우기

[Python] 파이썬 웹 크롤링 - 스크래핑 관련 유튜브 강의[나도코딩] 연습 코드 정리

by good4me 2021. 6. 27.

goodthings4me.tistory.com

 

[출처] 파이썬 코딩 무료 강의 (활용편3) - 웹 크롤링? 웹 스크래핑! 제가 가진 모든 비법을 알려드리겠습니다. [나도코딩]  https://youtu.be/yQ20jZwDjTE

"파이썬 기본편을 학습한 분들을 위한  파이썬 웹 크롤링 - 스크래핑 무료 강의"
본 포스팅은 상기 유튜브 영상을 보면서 연습한 코드임
다른 코드 연습 중 관련 함수 등이 필요할 경우 참고하기 위해 올려놓음

■ 웹 스크래핑 requests 응답

import requests

# 웹 스크래핑 requests 응답
def requests_():
    response = requests.get('http://naver.com')
    print('응답코드 :', response.status_code)
    
    response = requests.get('http://nadocoding.tistory.com')
    response.raise_for_status()
    print('응답코드 :', response.status_code)
    # response 에러 발생한다고 가정 시 response.raise_for_status() 에러 코드 보이고 프로그램 종료시킴
    
    if response.status_code == requests.codes.ok:  # 200
        print('정상')
        
requests_()


[결과]
응답코드 : 200
응답코드 : 200
정상

 

■ 스크래핑 데이터 저장해보기

import requests

# 스크래핑 데이터 저장해보기
def mygoogle_html():
    response = requests.get('http://gogle.com')
    response.raise_for_status()
    
    # 현재 디렉토리에 mygoogle.html로 저장
    with open('./mygoogle.html', 'w', encoding='utf8') as f:
        f.write(response.text)
        
mygoogle_html()

good4me.co.kr

 

■ 정규표현식 연습

import re

# 정규표현식 연습
def re_(pattern, args):
    # 어떤 (정규)식을 컴파일 할 지 정해주기 re.compile('원하는 형태')
    # .은 하나의 문자, ^는 문자열의 시작, $는 문자열의 끝
    p = re.compile(pattern)  
    
    # match()는 주어진 문자열의 처음부터 일치하는지 확인
    m = p.match(args)
    if m:  # 매치 되었을 경우
        print(f'args: {args}')
        print(f'm.group(): {m.group()}')  # 일치하는 문자열 반환
        print(f'm.string: {m.string}')  # 입력받은 문자열 그대로 출력
        print(f'm.start: {m.start()}')  # 일치하는 문자열의 시작 index
        print(f'm.end: {m.end()}')  # 일치하는 문자열의 끝 index
        print(f'm.span: {m.span()}')  # 일치하는 문자열의 시작/끝 index
    else:
        print(f'match: {args} 매치 안됨')
        
    # search()는 주어진 문자열 중에 일치하는게 있는지 확인
    s = p.search(args)
    if s:
        print(f'search: {s.group()}')
    else:
        print(f'search: {args} 매치X')
    
    # findall()는 일치하는 모든 것을 리스트 형태로 반롼
    lst = p.findall(args)
    print(f'findall(): {lst}')
    
    print()
        
lst = ['case', 'caffe', 'care', 'careless', 'icare', 'good care cafe']

for ls in lst:
    re_('ca.e', ls)
    
    
   [결과]
   args: case
m.group(): case
m.string: case
m.start: 0
m.end: 4
m.span: (0, 4)
search: case
findall(): ['case']

match: caffe 매치 안됨
search: caffe 매치X
findall(): []

args: care
m.group(): care
m.string: care
m.start: 0
m.end: 4
m.span: (0, 4)
search: care
findall(): ['care']

args: careless
m.group(): care
m.string: careless
m.start: 0
m.end: 4
m.span: (0, 4)
search: care
findall(): ['care']

match: icare 매치 안됨
search: care
findall(): ['care']

match: good care cafe 매치 안됨
search: care
findall(): ['care', 'cafe']

 

■ requests 응답이 없을 때, 추가로 headers 정보 전송하여 응답 오류 해결하기 

import requests

# requests 응답이 없을 때, 추가로 headers 정보 전송하여 응답 오류 해결하기 
# User- Agent 확인 : https://www.whatismybrowser.com/detect/what-is-my-user-agent
def nadocoding():
    url = 'http://nadocoding.tistory.com'
    
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36'
    }
    response = requests.get(url, headers=headers)
    response.raise_for_status()
    
    with open('./nadocoding.html', 'w', encoding='utf8') as f:
        f.write(response.text)
        print('파일 저장 완료')

nadocoding()

 

■ BeautifulSoup 활용, 스크래핑 함수들 

import requests
from bs4 import BeautifulSoup

# BeautifulSoup 활용, 스크래핑 함수들 
# find, next_sibling, previous_sibling, find_next_sibling, find_next_siblings
def bs4_use():
    url = 'https://comic.naver.com/webtoon/weekday.nhn'
    response = requests.get(url)
    response.raise_for_status()
    
    soup = BeautifulSoup(response.text, 'lxml')  # html.parser
    print(soup.title)
    print(soup.title.get_text())
    print(soup.a)  # soup 객체에서 첫번째 발견되는 a element 반환
    print(soup.a.attrs)  # dict, a element의 속성 정보 출력
    print(soup.a['href'])
    print(soup.find('a', attrs={'class': 'Nbtn_upload'}))
    print(soup.find('a', {'class':'Nbtn_upload'}))
    print(soup.find('a', class_ = 'Nbtn_upload'))
    print(soup.find(attrs={'class':'Nbtn_upload'})) 
    # Nbtn_upload 속성이 하나이기 때문에 태그 지정 없이 가능
    
    print(soup.find('li', class_='rank01'))  # 인기급상승 1위
    rank01 = soup.find('li', {'class':'rank01'})
    print(rank01.a.get_text())
    
    ## li 태그가 여러 개, 형제 엘리먼트를 호출하기
    print(rank01.next_sibling)  # 아무것도 안나올 경우, 태그 간에 개행 정보가 있기 때문...
    rank02 = rank01.next_sibling.next_sibling  # next_sibling 한 번 더
    print(rank02.get_text())
    rank03 = rank02.next_sibling.next_sibling
    print(rank03.get_text())
    print(rank03.previous_sibling.previous_sibling)
    
    ## next_sibling 2번 안쓸 수 있게
    rank2 = rank01.find_next_sibling('li')  # find_next_sibling()
    print(rank2.a.text)
    
    ## 부모 태그로 가기
    print(rank01.parent)
    
    ## 형제들 가져오기
    ranks = rank01.find_next_siblings('li')  # find_next_siblings()
    print(rank01.a.text)
    for rank in ranks:
        print(rank.a.text)
        
        '''
        싸움독학-85화 : 싸움보다 어렵냐
        이번 생도 잘 부탁해-50화
        투신전생기-1화
        입학용병-34화
        약한영웅-145화
        맘마미안-100화
        곱게 키웠더니, 짐승-32화
        나만 보여!-26화
        열렙전사-2부 101화 - 마지막 거짓말
        소녀재판-64화
        '''
        
    ## 'text=' 로 찾아 가져오기
    webtoon = soup.find('a', text='노답소녀-33화')
    
bs4_use()

 

■ 네이버 웹툰 전체 제목 가져오기

import requests
from bs4 import BeautifulSoup

# 네이버 웹툰 전체 제목 가져옴 - fina_all()
def naver_webtoon():
    url = 'https://comic.naver.com/webtoon/weekday.nhn'
    response = requests.get(url)
    response.raise_for_status()
    
    soup = BeautifulSoup(response.text, 'lxml')  # html.parser
    
    cartoons = soup.find_all('a', attrs={'class':'title'})
    print(type(cartoons))  # <class 'bs4.element.ResultSet'>
    
    for cartoon in cartoons:
        print(cartoon.get_text())  
    
naver_webtoon()


[결과]
<class 'bs4.element.ResultSet'>
신의 탑
참교육
뷰티풀 군바리
파이게임
윈드브레이커
신입일기
소녀의 세계
장씨세가 호위무사
삼매경
앵무살수
백수세끼
만렙돌파
요리GO
칼가는 소녀
...
- 이후 생략 -

 

■ 가우스전자 만화 제목, 링크 가져오기

import requests
from bs4 import BeautifulSoup

# 가우스전자 만화 제목, 링크 가져오기
def naver_webtoon():
    url = 'https://comic.naver.com/webtoon/list.nhn?titleId=675554'
    response = requests.get(url)
    response.raise_for_status()
    
    soup = BeautifulSoup(response.text, 'lxml')  # html.parser
    cartoons = soup.find_all('td', class_='title')
    # title = cartoons[0].a.get_text()
    # print(title)
    
    for cartoon in cartoons:
        title = cartoon.a.get_text()
        link = 'https://comic.naver.com' + cartoon.a['href']
        print(title, link)
    
    total_rate = 0
    cartoon_ratings = soup.find_all('div', attrs={'class':'rating_type'})
    for cartoon in cartoon_ratings:
        rate = cartoon.find('strong').get_text()
        print(rate)
        total_rate += float(rate)
    print(f'총점: {total_rate:.2f}\n평균: {(total_rate / len(cartoon_ratings)):.2f}')  # f'{:.2f}'
    
naver_webtoon()


[결과]
후기 + 10년 후 가우스 https://comic.naver.com/webtoon/detail.nhn?titleId=675554&no=911&weekday=mon
시즌4 430화 내일 봐요 https://comic.naver.com/webtoon/detail.nhn?titleId=675554&no=910&weekday=mon
시즌4 429화 잠행 https://comic.naver.com/webtoon/detail.nhn?titleId=675554&no=909&weekday=mon
시즌4 428화 추억 https://comic.naver.com/webtoon/detail.nhn?titleId=675554&no=908&weekday=mon
시즌4 427화 섬세한사람 https://comic.naver.com/webtoon/detail.nhn?titleId=675554&no=907&weekday=mon
시즌4 426화 적응 https://comic.naver.com/webtoon/detail.nhn?titleId=675554&no=906&weekday=mon
시즌4 425화 대견 https://comic.naver.com/webtoon/detail.nhn?titleId=675554&no=905&weekday=mon
시즌4 424화 초빙강사 https://comic.naver.com/webtoon/detail.nhn?titleId=675554&no=904&weekday=mon
시즌4 423화 추억의 물건 https://comic.naver.com/webtoon/detail.nhn?titleId=675554&no=903&weekday=mon
시즌4 422화 아니요 https://comic.naver.com/webtoon/detail.nhn?titleId=675554&no=902&weekday=mon
9.98
9.98
9.97
9.97
9.97
9.98
9.97
9.97
9.97
9.97
총점: 99.73
평균: 9.97

 

■ 쿠팡(Coupang)에서 노트북 검색결과

import requests
from bs4 import BeautifulSoup
import re

# 쿠팡에서 노트북 검색결과
# https://www.coupang.com/np/search?component=&q=노트북&channel=user

def coupang_search():
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36'
    }

    for i in range(1, 3):
        coupang = 'https://www.coupang.com'
        url = 'https://www.coupang.com/np/search?q=%EB%85%B8%ED%8A%B8%EB%B6%81&channel=user&component=&eventCategory=SRP&trcid=&traid=&sorter=scoreDesc&minPrice=&maxPrice=&priceRange=&filterType=&listSize=36&filter=&isPriceRange=false&brand=&offerCondition=&rating=0&page={}&rocketAll=false&searchIndexingToken=&backgroundColor='.format(i)
        
        response = requests.get(url, headers=headers)
        response.raise_for_status()
        
        soup = BeautifulSoup(response.text, 'html.parser')  # 'lxml' or 'html.parser'
        # <li class='search-product search-product__ad-badge" 이지만, '광고' 글자 없으면 search-product 만 있음
        # 이럴 경우 스크래핑이 안되는 경우가 있는데, 이를 해결하는 방법으로
        # 정규식 re 모듈 사용하여 class 속성 전체를 지정하게 함. 시작을 search-product로...
        items = soup.find_all('li', attrs={'class':re.compile('^search-product')})
        print(items[0].find('div', {'class':'name'}))
        
        for item in items:
            # 광고(아이콘) 상품 제외하기
            ad_badge = item.find('span', attrs={'class':'ad-badge-text'})
            if ad_badge:
                print('###<광고 상품> - 제외\n')
                continue
            
            name = item.find('div', {'class':'name'}).get_text()
            # Apple 상품 제외하기 
            if [n for n in ('Apple', '애플', 'apple') if n in name]:
                print('###<애플 상품> - 제외\n')
                continue
            link = item.find('a', class_='search-product-link')['href']
            price = item.find('strong', {'class':'price-value'}).get_text()
            rate = item.find('em', class_='rating')
            if rate:
                rate = rate.get_text()
            else:
                rete = '평점없음'
            rate_cnt = item.find('span', attrs={'class':'rating-total-count'})
            if rate_cnt:
                rate_cnt = rate_cnt.get_text()[1:-1]  # 숫자만 슬라이싱
            else:
                rate_cnt = '평점수없음'
            print(f'{i}page > 상품명: {name}\n바로가기: {coupang + link}\n가격: {price}\n평점: {rate}\n평점수: {rate_cnt}\n')

coupang_search()


[결과]
<div class="name">에이수스 노트북 실버 S513EA-CP129 (i5-1135G7 Iris), SSD 256GB, 윈도우 미포함, 8GB</div>
###<광고 상품> - 제외

1page > 상품명: 델 게이밍 노트북 Phantom Grey with speckles G15 5515-DG5515-WH01DKR (라이젠7-5800H 39.6cm Win10 Home RTX3060), 윈도우 포함, NVMe 512GB, 16GB
바로가기: https://www.coupang.com/vp/products/5619336514?itemId=9104260632&vendorItemId=76390529350
가격: 1,599,000
평점: None
평점수: 평점수없음

1page > 상품명: 삼성전자 플러스2 퓨어화이트 노트북 NT550XDA-K14AW (샐러론 6305 39.6cm WIN10 Pro Edu), 윈도우 포함, NVMe 128GB, 8GB
바로가기: https://www.coupang.com/vp/products/5244543934?itemId=7416308410&vendorItemId=74707281741
가격: 549,000
평점: 5.0
평점수: 166

- 이후 생략 -

 

■ 다음(Daum) 연도별 영화 순위 웹페이지에서 이미지 다운로드하기

import requests
from bs4 import BeautifulSoup
import re
import os

# 다음(Daum) 연도별 영화 순위 웹페이지에서 이미지 다운로드하기
def daum_movie():  # 2011~2020년영화순위

    for year in range(2011, 2021):
        # {year}년영화순위
        url = f'https://search.daum.net/search?w=tot&q={year}%EB%85%84%EC%98%81%ED%99%94%EC%88%9C%EC%9C%84&DA=MOR&rtmaxcoll=MOR'
        response = requests.get(url)
        response.raise_for_status()
        
        soup = BeautifulSoup(response.text, 'lxml')  # html.parser
        movies = soup.find('ol', class_=re.compile('^type_plural'))  # re.compile()
        images = movies.find_all('img', attrs={'class':'thumb_img'})
        
        for idx, image in enumerate(images, 1):
            image_url = image['src']
            
            # 코드 상의 url이 '//'으로 시작하는 경우, startswith()
            if image_url.startswith('//'):
                image_url = 'https:' + image_url
            # print(image_url)
            image_res = requests.get(image_url)
            image_res.raise_for_status()
            
            path = './' + str(year)
            if not os.path.exists(path):  # 저장할 디렉토리(폴더) 생성
                os.mkdir(path)
                
            with open(f'{path}/movie_{year}_{idx}.jpg', 'wb') as f:
                f.write(image_res.content)
            print(idx)
        
daum_movie()


[결과]
'프로그램 실행 디렉토리에 연도별 디렉토리 생성후 이미지가 다운로드 되어 있음'

 

■ 네이버 금융 - 국내 증시 시가총액 가져와서 .csv 파일로 저장하기

csv 모듈로 csv 파일 쓰기
- .csv 파일을 만들고 그 파일을 쓰기 모드로 오픈한 후 csv 파일 객체(csv.writer())에 넣는다.
- 파일 객체는 writerow() 메서드로 list 데이터를 한 라인 추가 (for 문 사용 시 list 내 모든 요소 추가)
- Windows의 경우 csv모듈에서 데이터를 쓸 때 각 라인 뒤에 빈 라인이 추가됨 (open()에 newline= 옵션 지정하면 됨)
f = open('csv 파일명', 'w', encoding='utf-8', newline='')
writer = csv.writer(f)
writer.writerow(리스트)
f.close()

csv 모듈로 csv 파일 읽기
- .csv 파일을 읽기 모드로 오픈한 후 csv 파일 객체(csv.reader())에 넣는다.
- 파일 객체는 Iterator 타입의 reader 객체 리턴(리스트 타입) (for 문으로 한 라인씩 가져올 수 있음)
f = open('csv 파일명', 'r', encoding='utf-8')
reader = csv.reader(f)
for line in reader:
    print(line)
f.close()
import requests
from bs4 import BeautifulSoup
import csv

# 네이버 금융 - 국내 증시 시가총액 가져와서 .csv 파일로 저장하기
def naver_stock():
    # url = 'https://finance.naver.com/sise/sise_market_sum.nhn'
    
    filename = 'naver_stock_sise.csv'
    
    # csv write 시 줄바꿈 1줄이 생김 --> newline= 주면 자동으로 줄바꿈이 생기지 않음
    # 엑셀 내 한글이 깨질 경우 'utf-8' 대신 'utf-8-sig' 사용
    f = open(filename, 'w', encoding='utf-8-sig', newline='')
    
    writer = csv.writer(f)  # csv 파일 객체
    
    # 엑셀의 각 column 제목 만들기
    title = 'N	종목명	현재가	전일비	등락률	액면가	시가총액	상장주식수	외국인비율	거래량	PER	ROE'.split('\t')
    writer.writerow(title)
    
    for page in range(1, 2):
        url = 'https://finance.naver.com/sise/sise_market_sum.nhn?&page=' + str(page)
        response = requests.get(url)
        response.raise_for_status()
        
        soup = BeautifulSoup(response.text, 'lxml')  # html.parser
        
        # table의 tr에 정보 있는 구조, table thead에도 tr 있음. tbody 얻어와서 tr을 대상으로 작업
        data_rows = soup.find('table', attrs={'class':'type_2'}).find('tbody').find_all('tr')
        
        for row in data_rows:
            columns = row.find_all('td')
            # 빈 list (개행 tr)와 list 내 \n\t 등이 있음
            if len(columns) <= 1:  # 의미없는 빈 list (개행 tr) skip
                continue
            data = [column.get_text().strip() for column in columns]
            print(data)
            # csv에 쓸 때 리스트 형태로 넘겨줌
            # csv 파일객체 메서드 wreterow()로 데이터 쓰기
            writer.writerow(data)  

naver_stock()


[결과]
['1', '삼성전자', '81,600', '400', '+0.49%', '100', '4,871,343', '5,969,783', '53.61', '12,966,342', '19.59', '9.99', '']
['2', 'SK하이닉스', '128,500', '2,500', '+1.98%', '5,000', '935,483', '728,002', '49.53', '2,516,045', '18.35', '9.53', '']

-이후 생략-
'디렉토리 내에 엑셀 파일도 저장되어 있음'

 

■ 셀레니움(Selenium) 다루어보기

# 셀레니움(Selenium) 웹드라이버 다운로드
https://sites.google.com/a/chromium.org/chromedriver/downloads

# 크롬 버전 확인
Chrome://version

from selenium import webdriver
from selenium.webdriver.common.keys import Keys
import time

# 셀레니움(Selenium) 다루어보기
def selenium_use():
    # 현재 경로인 경우 './chromedriver.exe' 필요 없음
    browser = webdriver.Chrome('./chromedriver.exe')  
    browser.get('http://naver.com')
    time.sleep(1)
    
    
    elem = browser.find_element_by_id('query')
    elem.send_keys('나도코딩')
    elem.send_keys(Keys.ENTER)
    time.sleep(2)
    print(elem, type(elem))
    
    # send_keys() 사용 시 생각해야 할 것!! ----------------------------
    # elem = browser.find_element_by_id('query').send_keys('나도코딩').send_keys(Keys.ENTER)
    # 또는
    # elem = browser.find_element_by_id('query').send_keys('나도코딩')
    # elem.send_keys(Keys.ENTER)
    # 이 경우, send_keys() 반환값을 받은 elem 은 NoneType이 되고, 오류 발생함
    # AttributeError: 'NoneType' object has no attribute 'send_keys' 발생
    # print(elem)  # None 
    # print(type(elem))  # <class 'NoneType'>
    
    # webdriver WebElement 객체 타입인 elem에서 send_keys() 수행토록 함
    # --------------------------------------------------------------
    
    browser.back()  # 뒤로
    browser.find_element_by_id('query').send_keys('파이썬')
    browser.find_element_by_class_name('ico_search_submit').click()
    
    time.sleep(1.5)
    browser.back()
    browser.find_element_by_class_name('link_login').click()
    time.sleep(1.5)
    browser.back()
    time.sleep(0.5)
    browser.forward()
    time.sleep(0.5)
    browser.back()
    browser.refresh()  # 새로고침
    
    browser.get('http://daum.net')
    elem = browser.find_element_by_name('q')
    elem.send_keys('나도코딩')
    elem.send_keys(Keys.ENTER)  # daum에서는 이상없이 수행됨
    time.sleep(1)
    browser.back()
    elem = browser.find_element_by_name('q')
    elem.send_keys('파이썬')
    elem_btn = browser.find_element_by_xpath('//*[@id="daumSearch"]/fieldset/div/div/button[2]').click()
    print(elem_btn, type(elem_btn))
    browser.quit()

selenium_use()

 

■ 셀레니움(Selenium) 이용 네이버 자동 로그인

from selenium import webdriver
import time

# 셀레니움(Selenium) 이용 네이버 자동 로그인
def naver_login():
    # 현재 경로인 경우 './chromedriver.exe' 필요 없음
    browser = webdriver.Chrome('./chromedriver.exe')  
    browser.get('http://naver.com')
    time.sleep(1)
    
    browser.find_element_by_class_name('link_login').click()
    time.sleep(0.5)    
    
    script = 'document.getElementById("id").value="{id}"; document.getElementById("pw").value="{pw}"'\
        .format(id='ID', pw='PW')
    time.sleep(0.5)
    browser.execute_script(script)
    time.sleep(0.5)
    browser.find_element_by_id('log.login').click()
    print(browser.page_source)
    time.sleep(1)
    browser.quit()

naver_login()

 

■ 셀레니움(Selenium) 네이버 항공권 예매 및 로딩 지연에 대한 처리

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import time

# 셀레니움(Selenium) 네이버 항공권 예매 및 로딩 지연에 대한 처리
# WebDriverWait( , ).until(EC.presence_of_element_located((By.XPATH, '')))

def naver_flight():
    # 현재 경로인 경우 './chromedriver.exe' 필요 없음
    browser = webdriver.Chrome('./chromedriver.exe')
    browser.maximize_window()
    browser.get('https://flight.naver.com/flights/')
    time.sleep(1.5)
    
    # 날짜 선택
    browser.find_element_by_link_text('가는날 선택').click()  # 텍스트로 선택
    
    # # 캘린더 월이 2개 - 날짜도 2개
    # browser.find_elements_by_link_text('29')[0].click()  # 가는 날
    # browser.find_elements_by_link_text('30')[0].click()  # 오는 날
    
    # # 다음 달이면,
    # browser.find_elements_by_link_text('29')[1].click()  # 가는 날
    # browser.find_elements_by_link_text('30')[1].click()  # 오는 날
    
    # 달을 달리하면,
    browser.find_elements_by_link_text('29')[0].click()  # 가는 날
    browser.find_elements_by_link_text('3')[1].click()  # 오는 날
    
    # 여행지 선택
    browser.find_element_by_xpath('//*[@id="content"]/div/div[1]/ul/li[2]').click()  # 국내 클릭
    browser.find_element_by_xpath('//*[@id="recommendationList"]/ul/li[1]/div/dl').click()  # 제주 클릭
    
    # 항공권 검색 클릭
    browser.find_element_by_link_text('항공권 검색').click()
    
    # 어떤 엘리먼트(예로, XPATH 지정)가 나올 때까지 지정 시간만큼 기다리기 처리(인수 괄호에 주의!)
    # 지정 시간(10초) 전이면 이후 진행, 시간 초과 시 에러 발생
    # 이 부분은 예외처리하고, 오류 시 브라우저 종료하는 것이 좋음
    try:
        elem = WebDriverWait(browser, 10).until(EC.presence_of_element_located((By.XPATH, \
                                            '//*[@id="content"]/div[2]/div/div[4]/ul/li[1]')))
        print(elem.text.split('\n'))
    finally:
        time.sleep(1.5)
        browser.quit()
    
naver_flight()


[결과]
['제주항공', '출발지', 'GMP', '06:00', '도착지', 'CJU', '07:10', '총 소요시간', '01시간 10분', '할인석', '편도 37,100원', '편도 36,740원 (KB국민카드 결제시 1% 청구할인)', '성인이벤트혜택']

 

■ 동적 페이지 웹 스크래핑(예로, 사용자가 스크롤링 할 때 등) - 구글 무비 

    ※ 아래 코드(requests 사용 결과)는 원하는 데이터를 가져올 수 없음

import requests
from bs4 import BeautifulSoup

# 구글 무비 인기 차트 영화에서 할인중인 영화 목록 추출해보기
# 구글 무비는 User-Agent(국가별)에 따라 서비스 페이지가 다르고, 스크롤 시 무비 데이터가 업데이트됨(동적페이지)
# headers 정보에 User-Agent와 Accept-Language 내용도 필요

def google_movie():
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36',
        'Accept-Language': 'ko-KR,ko'
    }    
    url = 'https://play.google.com/store/movies/top'
    response = requests.get(url, headers=headers)
    response.raise_for_status()
    soup = BeautifulSoup(response.text, 'lxml')
    
    movies = soup.find_all('div', attrs={'class':'ImZGtf mpg5gc'})
    print(len(movies))  # 0, headers 추가 후 10, 전체 영화 목록 안보임
    
    # # 소스 검증 위해 파일 저장
    # with open('movie.html', 'w', encoding='utf8') as f:
    #     # f.write(response.text)  # 판독 불가
    #     f.write(soup.prettify())  # html 문서를 예쁘게 출력
    
    for movie in movies:
        title = movie.find('div', class_='WsMG1c nnK0zc').get_text()
        print(title)
        
google_movie()


[결과]
10
노바디
콰이어트 플레이스 (자막판)
고질라 VS. 콩
라야와 마지막 드래곤
테넷
소울
킬러의 보디가드
킬러의 보디가드 무삭제 특별판 (자막판)
어벤져스 : 엔드게임 (자막판)
너의 이름은. (자막판)

 

■ google movie 에 대해 selenium 사용 (동적 페이지 웹 스크래핑)

from bs4 import BeautifulSoup
from selenium import webdriver
import time

# google movie(동적 페이지)에 대해 셀레니움으로 처리하기
def google_movie_selenium():
    # 현재 경로인 경우 './chromedriver.exe' 필요 없음
    browser = webdriver.Chrome('./chromedriver.exe')  
    browser.maximize_window()
    url = 'https://play.google.com/store/movies/top'
    browser.get(url)
    time.sleep(1.5)
    
    # 스크롤링 하기
    # 모니터(해상도) 높이인 1080 위치로 스크롤 내리기 - 위치 숫자(해상도) 증가 시 그 위치만큼 이동
    # browser.execute_script('widow.scrollTo(0, 1080)')
    # browser.execute_script('widow.scrollTo(0, 2080)')
    
    # 현재 보이는 화면 스크롤 끝(가장 아래)까지 스크롤 내리기
    browser.execute_script('window.scrollTo(0, document.body.scrollHeight)')
    time.sleep(2)
    # 구글 무비 사이트는 스크롤 시마다 영화 목록이 추가(업데이트)되며, 영화 목록 끝까지 스크롤을 내릴려면
    # 현재 화면 높이를 가져와서 높이가 변하지 않을 때까지 스크롤을 내린다
    
    # 현재 화면(문서) 높이를 가져와서 저장
    prev_height = browser.execute_script('return document.body.scrollHeight')
    print(prev_height)
    
    while True:
        # 페이지에서 스크롤을 가장 아래로 내림
        browser.execute_script('window.scrollTo(0, document.body.scrollHeight)')
        time.sleep(2)
        
        # 현재 페이지 높이 저장
        curr_height = browser.execute_script('return document.body.scrollHeight')
        if curr_height == prev_height:
            break
        prev_height = curr_height
    
    print('스크롤 완료')
    
    # selenium과 같이 사용할 경우, BeautifulSoup 객체에 전달할 소스코드는 webdrive의 page_source
    soup = BeautifulSoup(browser.page_source, 'lxml')
    
    # Tip! 스크래핑할 컨텐츠가 있는 태그와 속성이 하나가 아닌 경우, 리스트로 속성을 감싸준다. (여기서는 아님)
    movies = soup.find_all('div', attrs={'class':'Vpfmgd'})    
    print(len(movies))
    
    for movie in movies:
        title = movie.find('div', class_='WsMG1c nnK0zc').get_text()
        print(title)
        
    # 할인된 영화만 추출해보기        
    print('#' * 50)
    
    for movie in movies:
        title = movie.find('div', class_='WsMG1c nnK0zc').get_text()
        original_price = movie.find('span', class_='SUZt4c djCuy')
        if original_price:
            original_price = original_price.get_text()
        else:
            print(title, ' -- 할인되지 않은 영화 제외')
            continue
    
        # 할인된 가격
        price = movie.find('span', attrs={'class':'VfPpfd ZdBevf i5DZme'}).get_text()
        
        # 영화 링크
        link = movie.find('a', attrs={'class':'JC71ub'})['href']
        
        print(f'제목 : {title}')
        print(f'할인 전 금액 : {original_price}')
        print(f'할인 후 금액 : {price}')
        print('링크 :', 'https://play.google.com' + link)
        print('-' *50)
    
    print('구글 인기차트 영화 리스트 추출 완료!')
    browser.quit()
            
google_movie_selenium()


[결과]
노바디  -- 할인되지 않은 영화 제외
콰이어트 플레이스 (자막판)  -- 할인되지 않은 영화 제외
고질라 VS. 콩  -- 할인되지 않은 영화 제외
라야와 마지막 드래곤  -- 할인되지 않은 영화 제외
테넷  -- 할인되지 않은 영화 제외
소울  -- 할인되지 않은 영화 제외
킬러의 보디가드  -- 할인되지 않은 영화 제외
제목 : 킬러의 보디가드 무삭제 특별판 (자막판)
할인 전 금액 : ₩2,500
할인 후 금액 : ₩1,200
링크 : https://play.google.com/store/movies/details/%ED%82%AC%EB%9F%AC%EC%9D%98_%EB%B3%B4%EB%94%94%EA%B0%80%EB%93%9C_%EB%AC%B4%EC%82%AD%EC%A0%9C_%ED%8A%B9%EB%B3%84%ED%8C%90_%EC%9E%90%EB%A7%89%ED%8C%90?id=ea3io4U1-qs
--------------------------------------------------
어벤져스 : 엔드게임 (자막판)  -- 할인되지 않은 영화 제외
너의 이름은. (자막판)  -- 할인되지 않은 영화 제외
몬스터 헌터 Monster Hunter  -- 할인되지 않은 영화 제외
날씨의 아이 (자막)  -- 할인되지 않은 영화 제외
잭 스나이더의 저스티스 리그  -- 할인되지 않은 영화 제외
베놈   Venom  -- 할인되지 않은 영화 제외
어벤져스 : 인피니티 워 (자막판)  -- 할인되지 않은 영화 제외
바람에 젖은 여자  -- 할인되지 않은 영화 제외
건마의 신:어린 아내의 알바  -- 할인되지 않은 영화 제외
모탈 컴뱃  -- 할인되지 않은 영화 제외
제목 : 미나리
할인 전 금액 : ₩7,700
할인 후 금액 : ₩5,500
링크 : https://play.google.com/store/movies/details/%EB%AF%B8%EB%82%98%EB%A6%AC?id=kOWoZydbl3o.P

-이하 생략-

 

■ 크롬 안띄우고 처리해보기(headless chrome : Chrome without Chrome)

from bs4 import BeautifulSoup
from selenium import webdriver
import time

# 동적 페이지에 대해 셀레니움으로 처리하기 
# - 크롬 안띄우고 처리해보기(headless chrome : Chrome without Chrome)

def google_movie_headless():
    ## 크롬 띄우고 처리할 경우 아래를 주석 처리 -----------------------
    options = webdriver.ChromeOptions()
    options.headless = True  # 크롬 안띄우기
    options.add_argument('window-size=1920x1080')  # 윈도우 창 크기 지정
    browser = webdriver.Chrome('./chromedriver.exe', options=options)
    # 여기까지 주석 처리 -------------------------------------------
    
    # browser = webdriver.Chrome('./chromedriver.exe')  # 크롬 띄울 경우 주석 처리 풀기
    browser.maximize_window()
    url = 'https://play.google.com/store/movies/top'
    browser.get(url)
    time.sleep(1.5)
    
    prev_height = browser.execute_script('return document.body.scrollHeight')
    while True:
        # 페이지에서 스크롤을 가장 아래로 내림
        browser.execute_script('window.scrollTo(0, document.body.scrollHeight)')
        time.sleep(2)
        
        # 현재 페이지 높이 저장
        curr_height = browser.execute_script('return document.body.scrollHeight')
        if curr_height == prev_height:
            break
        prev_height = curr_height
    
    print('스크롤 완료')
    
    # selenium과 같이 사용할 경우, BeautifulSoup 객체에 전달할 소스코드는 webdrive의 page_source
    soup = BeautifulSoup(browser.page_source, 'lxml')
    
    movies = soup.find_all('div', attrs={'class':'Vpfmgd'})    
    print(len(movies))
        
    # 할인된 영화만 추출해보기        
    for movie in movies:
        title = movie.find('div', class_='WsMG1c nnK0zc').get_text()
        original_price = movie.find('span', class_='SUZt4c djCuy')
        if original_price:
            original_price = original_price.get_text()
        else:
            print(title, ' -- 할인되지 않은 영화 제외')
            continue
    
        # 할인된 가격
        price = movie.find('span', attrs={'class':'VfPpfd ZdBevf i5DZme'}).get_text()
        
        # 영화 링크
        link = movie.find('a', attrs={'class':'JC71ub'})['href']
        
        print(f'제목 : {title}')
        print(f'할인 전 금액 : {original_price}')
        print(f'할인 후 금액 : {price}')
        print('링크 :', 'https://play.google.com' + link)
        print('-' *50)
    
    print('구글 인기차트 영화 리스트 추출 완료!')
    browser.quit()    

google_movie_headless()

 

■ headlees Chrome 사용 시 주의할 점 - User-Agent 값 변경

Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) 
Chrome/91.0.4472.114 Safari/537.36

아래 User-Agent의 HeadlessChrome 부분이 바뀌었음

Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) 
HeadlessChrome/91.0.4472.114 Safari/537.36

- 'User-Agent'가 'HeadlessChrome'으로 표시 --> 스크래핑 거부될 수 있음

이를 방지하기 위한 조치 - options.add_argument('user-agent=올바른 값 넣어주기')
from selenium import webdriver
import time

# options.add_argument('user-agent=올바른 값 넣어주기')
def chrome_headless():
    options = webdriver.ChromeOptions()
    options.headless = True
    options.add_argument('window-size=1920x1080')
    options.add_argument('user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36')
    
    browser = webdriver.Chrome('./chromedriver.exe', options=options)
    browser.maximize_window()
    url = 'https://www.whatismybrowser.com/detect/what-is-my-user-agent'
    browser.get(url)
    time.sleep(1)
    
    detected_value = browser.find_element_by_id('detected_value')
    print(detected_value.text)
    browser.quit()
    
chrome_headless()

 

* 나도코딩 유튜브 채널 - https://www.youtube.com/channel/UC7iAOLiALt2rtMVAWWl4pnw/videos

 

 

댓글