데이터 분석/웹 스크래핑

[Web Scrapping 실습] BeautifulSoup

eunnys 2023. 11. 1. 15:47

▶ 로봇이 아님을 나타내기 위해서 user-agent라는 값을 header에 넣어서 보냄
- 직접적인 URL 주소로 요청 시 웹 사이트에서 웹 크롤링을 통해서 접근한 것을 감지하고 접속을 차단하게 됨.
- user-agent header 값을 포함하여 요청하면 웹 브라우저를 통해 요청하는 것으로 인식되어 해결
- 웹 브라우저 실행 -> F12를 눌러 개발자 모드로 들어감 -> Console 메뉴 -> prompt에 navigator.userAgent 입력
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36"
 

import requests

URL = 'http://www.google.com/search'
params = {'q':'python'}
# 로봇이 아님을 나타내기 위해서 user-agent라는 값을 header에 넣어서 보냄
headers = {'user-agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36'}

response = requests.get(URL, params=params, headers=headers)
response.raise_for_status() # 200이 아니면 오류를 내고 멈춤

html = response.text
with open('search_result.html', 'w', encoding='utf-8') as f:
    f.write(html)
print('저장 완료!')
import requests

URL = 'http://www.google.com/search'
params = {'q':'python'}
# 로봇이 아님을 나타내기 위해서 user-agent라는 값을 header에 넣어서 보냄
headers = {'user-agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36'}

# user-agent 값을 통해 브라우저의 종류와 버전 정보 등을 전달하기 때문에 웹 프로그램은 브라우저별로 최적화된 콘텐츠를 제공하게 됨
response = requests.get(URL, params=params)#, headers=headers)
response.raise_for_status() # 200이 아니면 오류를 내고 멈춤

html = response.text
with open('search_result2.html', 'w', encoding='utf-8') as f:
    f.write(html)
print('저장 완료!')

 
 
[실습] 네이버 실시간 인기 검색어 추출
 

response = requests.get('https://datalab.naver.com')
html = response.text
temp = html.split('<em class="num">1</em>')[1]
temp = temp.split('<span class="title">')[1]
temp = temp.split('</span>')[0]
print(temp)

 
 

▶ BeautifulSoup 패키지

 
## parser별 출력 결과 비교 ##
 

from bs4 import BeautifulSoup

soup = BeautifulSoup('<a></p>', 'html.parser')
print('html.parser')
print(soup) # <a></a>
print('-'*40)

soup = BeautifulSoup('<a></p>', 'lxml')
print('lxml')
print(soup) # <html><body><a></a></body></html>
print('-'*40)

soup = BeautifulSoup('<a></p>', 'xml')
print('xml')
print(soup) # <a/>
print('-'*40)

soup = BeautifulSoup('<a></p>', 'html5lib')
print('html5lib')
print(soup) # <html><head></head><body><a><p></p></a></body></html>
print('-'*40)

 
## 기본 사용법 ##
 

import requests
from bs4 import BeautifulSoup

url = 'https://ko.wikipedia.org/wiki/%EC%9B%B9_%ED%81%AC%EB%A1%A4%EB%9F%AC'
res = requests.get(url)

# soup 객체 생성
soup = BeautifulSoup(res.text, 'lxml')

# 태그를 이용한 접근 가능
print(soup.title.text) # text: 태그를 제외한 title만 가져옴 (웹 크롤러 - 위키백과, 우리 모두의 백과사전)
print(soup.footer.ul.li.text) # 이 문서는 2023년 4월 30일 (일) 18:34에 마지막으로 편집되었습니다.
print(soup.footer.ul.li.find_next_sibling().text) # 그 다음 li 태그를 검색

# 태그와 속성을 이용한 접근
print(soup.a) # soup 객체에서 첫 번째로 만나는 a element 출력 (<a class="mw-jump-link" href="#bodyContent">본문으로 이동</a>)
print(soup.a['href']) # #bodyContent, 만약 속성이 존재하지 않을 경우 에러 발생

# find() 함수를 이용한 태그 내의 다양한 속성을 이용한 접근
print(soup.find('a', attrs={'title':'구글봇'})) # <a href="/wiki/%EA%B5%AC%EA%B8%80%EB%B4%87" title="구글봇">구글봇</a>

 
 
## 자식 노드(태그)들을 반복 가능한 객체로 반환 ##
 

html = """
<html>
    <head>
        <title>crawl</title>
    </head>
    <body>
        <p class='a' align='center'>text1</p>
        <p class='b' align='center'>text2</p>
        <p class='c' align='center'>text3</p>
        <div>
            <img src='/source' width='300' height='200'>
        </div>
    </body>
</html>
"""

from bs4 import BeautifulSoup

soup = BeautifulSoup(html, 'html.parser')
contents = soup.find('body') # body 태그에 있는 정보만 검색
# print(contents)
for child in contents.children: # 자식 태그들을 반복 가능한 객체로 반환
    print(child)

 
 
## 자신을 포함한 부모 태그까지 출력 ##
 

html = """
<html>
    <head>
        <title>crawl</title>
    </head>
    <body>
        <p class='a' align='center'>text1</p>
        <p class='b' align='center'>text2</p>
        <p class='c' align='center'>text3</p>
        <div>
            <img src='/source' width='300' height='200'>
        </div>
    </body>
</html>
"""

from bs4 import BeautifulSoup

soup = BeautifulSoup(html, 'html.parser')
contents = soup.find('body')
img_tag = contents.find('img') # img 태그만 검색
print(img_tag)
print()
print(img_tag.parent) # 자신을 포함한 부모 태그까지 출력

 
 
## 특정 부모 태그까지 검색해서 올라가는 방법 ##
 

html = """
<html>
    <head>
        <title>crawl</title>
    </head>
    <body>
        <p class='a' align='center'>text1</p>
        <p class='b' align='center'>text2</p>
        <p class='c' align='center'>text3</p>
        <div>
            <img src='/source' width='300' height='200'>
        </div>
    </body>
</html>
"""

from bs4 import BeautifulSoup

soup = BeautifulSoup(html, 'html.parser')
contents = soup.find('body')
img_tag = contents.find('img') # img 태그만 검색
print(img_tag)
print()
print(img_tag.find_parent('body')) # img 태그에서 body 태그까지

 
 
▶ 형제 태그를 검색하는 방법
- find_next_sibling() : 바로 다음 형제 태그를 검색
- find_next_siblings() : 모든 다음 형제 태그를 검색
- find_previous_sibling() : 바로 이전 형제 태그를 검색
- find_previous_siblings() : 모든 이전 형제 태그를 검색
 

html = """
<html>
    <head>
        <title>crawl</title>
    </head>
    <body>
        <p class='a' align='center'>text1</p>
        <p class='b' align='center'>text2</p>
        <p class='c' align='center'>text3</p>
        <div>
            <img src='/source' width='300' height='200'>
        </div>
    </body>
</html>
"""

from bs4 import BeautifulSoup

soup = BeautifulSoup(html, 'html.parser')

p_tag = soup.find('p', attrs={'class':'b'})
print(p_tag)
print()
print(p_tag.find_next_sibling()) # 바로 다음 형제 태그 검색
print(p_tag.find_next_siblings())

 

html = """
<html>
    <head>
        <title>crawl</title>
    </head>
    <body>
        <p class='a' align='center'>text1</p>
        <p class='b' align='center'>text2</p>
        <p class='c' align='center'>text3</p>
        <div>
            <img src='/source' width='300' height='200'>
        </div>
    </body>
</html>
"""

from bs4 import BeautifulSoup

soup = BeautifulSoup(html, 'html.parser')

result = soup.find('body')
print(result.text) # 검색 결과의 하부 태그에 있는 모든 텍스트만 검색 결과로 가져옴
print(result.string) # 하부 태그가 없는 경우에만 문자열을 가져옴

p_tag = result.find('p')
print(p_tag.string)

 
 
## 검색 : find_all() ##
 

import requests
from bs4 import BeautifulSoup

res = requests.get('https://www.naver.com')
soup = BeautifulSoup(res.text, 'lxml')
print(soup.find_all('a', limit=2))
result = soup.find_all('span', attrs={'class':'blind'})
print(len(result)) # 3개
print(result)

 
 


[실습] 네이버 뉴스 패이지의 언론사 목록 가져오기
 

import requests
from bs4 import BeautifulSoup

res = requests.get('https://news.naver.com')
soup = BeautifulSoup(res.text, 'lxml')

result = soup.find_all('h4', attrs={'class':'channel'})

# print(len(result)) # 157개
# print(result[0]) # <h4 class="channel">주간동아<span class="datetime">10월 31일 09:08</span></h4>

# print(list(result[0].children)) # h4 자식 태그에 있는 정보를 가져옴
# ['파이낸셜뉴스', <span class="datetime">11월 01일 10:04</span>]
# print(list(result[0].children)[0]) # 0번째는 언론사 이름

news_list = [list(tag.children)[0] for tag in result]
print(news_list[:10]) # 처음 10개만 출력
# ['SBS Biz', '노컷뉴스', '헤럴드경제', 'TV조선', '데일리안', '디지털타임스', '더팩트', '동아일보', '대구MBC', '오마이뉴스']

 
 
## 검색: select_one(), select() ##
 

import requests
from bs4 import BeautifulSoup

res = requests.get('http://www.tradecampus.com')
soup = BeautifulSoup(res.text, 'html.parser')

print(soup.select_one('body > div > div.wrapper.main_page > div.renew_main > div.col-12 > div > div.renew_main_notice > div > div > h3')) # CSS 선택자 이용 : find()보다 빠르고 정확하게 가져옴
# tradecampus.com 메인 페이지 공지사항 2번째 내용 가져오기

notice = soup.select('body > div > div.wrapper.main_page > div.renew_main > div.col-12 > div > div.renew_main_notice > div > ul > li:nth-child(2)') # find_all()과 비슷함 : 결과를 리스트로 반환
print(notice)
print(notice[0].text)
# :nth-child(n)을 지우면 모든 요소를 가져온다

notice = soup.select('body > div > div.wrapper.main_page > div.renew_main > div.col-12 > div > div.renew_main_notice > div > ul > li')
print(len(notice)) # 3개
for n in notice:
    print(n.text)
    print('-'*50)
# 고객센터 영역 텍스트 가져오기

text = soup.find('div', attrs={'class':'serviceInfo'})
# print(text)
print(text.get_text()) # 텍스트에 있는 값만 가져옴
ele = soup.find('span', attrs={'class':'img'})
# print(ele)
ele = ele.find('img').get('src')
print(ele)

 
## selenium 모듈 설치 ##
 

!pip install selenium # 동적인 페이지를 다룰 때 사용하는 모듈
!pip install webdriver_manager

 
 

 

 [실습] 네이버 웹툰 제목 가져오기
 

import requests
from bs4 import BeautifulSoup
import time # sleep 함수를 사용하기 위해
from selenium.webdriver.chrome.service import Service
from selenium.webdriver import Chrome, ChromeOptions
from webdriver_manager.chrome import ChromeDriverManager

driver = Chrome(service=Service(ChromeDriverManager().install()), options=ChromeOptions())
driver.get('https://comic.naver.com/webtoon')

time.sleep(5) # 동적으로 생성되는 페이지의 내용이 완성될 때까지 5초간 대기

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

# 요일별 전체 웹툰의 CSS 선택자

temp = soup.select_one('#container > div.component_wrap.type2 > div.WeekdayMainView__daily_all_wrap--UvRFc') # 전체 요일 웹툰 목록
# print(temp) # 동적으로 만들어진 페이지 (사용자에 의해 콘텐츠가 달라짐) -> None 반환


# 요일별 div 태그 (fin_all 함수로 동일 클래스 선택자를 사용하는 모든 태그를 검색)

temp = temp.find_all('div', attrs={'class':'WeekdayMainView__daily_all_item--DnTAH'}) # 각 요일별 태그를 리스트로 반환
# print(len(temp)) # 7개
week = ['월', '화', '수', '목', '금', '토', '일']
for i, w in enumerate(temp):
    print(f'======= {week[i]}요 웹툰 =======')
    week_list = w.select('ul > li') # 상위 태그가 ul인 모든 li 태그 선택
    for li_tag in week_list:
        print(li_tag.find('span', attrs={'class':'text'}).text)

 
 

 

[실습] 다음 영화 사이트에서 영화 포스터 다운로드 하기
 

# 사전 테스트

import requests
from bs4 import BeautifulSoup

res = requests.get('https://movie.daum.net/ranking/reservation')
res.raise_for_status()

soup = BeautifulSoup(res.text, 'lxml')
poster_img = soup.find_all('img', attrs={'class':'img_thumb'})
# print(len(poster_img)) # 20개

img_src = poster_img[0].get('src') # 주소값을 가져옴
res = requests.get(img_src) # 포스터가 있는 url로 다시 요청 (binary data)
with open('poster.jpg', 'wb') as f:
    f.write(res.content)
import requests
import os
from bs4 import BeautifulSoup

res = requests.get('https://movie.daum.net/ranking/reservation')
res.raise_for_status()

soup = BeautifulSoup(res.text, 'lxml')
poster_img = soup.find_all('img', attrs={'class':'img_thumb'})

# 이미지를 저장할 폴더 생성 (폴더가 있으면 안 만들고, 없으면 만든다.)

img_dir = './poster_img/'

if not os.path.exists(img_dir):
    os.makedirs(img_dir)
    print('폴더 생성 완료!')
else:
    print('폴더가 존재합니다.')

for i, movie in enumerate(poster_img, 1): # index 1부터 시작
    title = movie.get('alt') # 영화의 제목을 가져옴
    img_url = movie.get('src') # url을 가져옴

    print(i, ':', img_url)
    img_res = requests.get(img_url)

    if ':' in title:
        title = title.replace(':', ' ')

    with open(img_dir+f'm{i:02d}_{title}.jpg', 'wb') as f: # 0부터 시작하는 2자리 숫자
        f.write(img_res.content)

print('포스터 저장 완료!')

 
 
[실습] 네이버 뉴스에서 경제 관련 언론사별 랭킹뉴스 추출하기
- 네이버 뉴스 랭킹 페이지에서 언론사의 이름에 '경제' 단어가 들어간 언론사의 기사 제목 추출
 

import requests
from bs4 import BeautifulSoup

URL = 'https://news.naver.com/main/ranking/popularDay.naver'
headers = {'user-agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36'}

res = requests.get(URL, headers=headers)

soup = BeautifulSoup(res.text, 'lxml')

news_box = soup.find_all('div', attrs={'class':'rankingnews_box'})
# print(len(news_box)) # 82개

news_name = soup.find_all('strong', attrs={'class':'rankingnews_name'})

title = [list(tag.children)[0] for tag in news_name]

for t in title:
    if '경제' in t:
        print(t) # '경제'가 들어간 언론사 이름 추출

 

import requests
from bs4 import BeautifulSoup

URL = 'https://news.naver.com/main/ranking/popularDay.naver'
headers = {'user-agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36'}

res = requests.get(URL, headers=headers)

soup = BeautifulSoup(res.text, 'lxml')
news_list = soup.find_all('div', attrs={'class':'rankingnews_box'})
print('등록 언론사 개수: ', len(news_list))

for news in news_list:
    press_title = news.find('strong').string # strong은 하나밖에 없기 때문에 속성은 필요없음
    if '경제' in press_title:
        print('언론사: ', press_title)
        news_title = news.find_all('div', attrs={'class':'list_content'})
        for i, ranking_news in enumerate(news_title, 1):
            print(f"{i}: {ranking_news.find('a').text}")