웹크롤링 - Beautiful Soup 사용법 + 영화 리뷰 크롤링 (3/3)
BeautifulSoup라이브러리를 사용하지 않아도 웹 크롤러를 만드는 것은 충분히 가능하며, 다른 라이브러리들도 사용할 수 있다. 그러나 BeautifulSoup을 사용할 경우 보다 손쉽게 원하는 정보를 추출할 수 있다.
Requests로 가져온 파일을 파이썬이 해석가능한 트리구조의 객체(BeautifulSoup)로 변환시켜 이 객체를 사용하여 분석 및 추출을 용이하게 해준다.
# 사용법
BeautifulSoup은 정말 많은 기능을 가지고 있다. 그래서 BeautifulSoup부분만 가지고도 두꺼운 책 한권이 나올 정도이다. 하지만 우리는 모든 기능을 다 볼수는 없고 웹 크롤러를 만드는데 반드시 필요한 부분만 요약해서 살펴보겠다. Beautiful Soup의 자세한 정보는 참고사이트를 참조하도록 하자.
※ 참고사이트 : https://www.crummy.com/software/BeautifulSoup/bs4/doc/
html_doc = """
<html><head><title>The Dormouse's story</title></head>
<body>
<p class="title"><b>The Dormouse's story</b></p>
<p class="story">Once upon a time there were three little sisters; and their names were
<a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>,
<a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> and
<a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>
<p class="story">...</p>
"""
함수 | 설명 | 예 | |
BeautifulSoup | Beautifulsoup 객체를 생성한다. ex) soup = BeautifulSoup(html_doc,'lxml') |
||
prettify() | html 코드를 보기 쉽게 표현 | >>> print(soup.prettify()) |
|
객체.태그이름 | .태그이름 으로 하위 태그로의 접근이 가능하다. |
#### 속성 >>> soup.title <title>The Dormouse's story</title> >>> soup.body.p <p class="title"><b>The Dormouse's story</b></p> >>> soup.p <p class="title"><b>The Dormouse's story</b></p> >>> soup.a Elsie |
|
객체.태그[‘속성이름’] | 객체의 태그 속성은 파이썬 딕셔너리처럼 태그[‘속성이름’] 으로 접근이 가능하다. | >>> soup.p['class'] ['title'] >>> soup.a['class'] ['sister'] >>> soup.a['href'] 'http://example.com/elsie' >>> soup.a['id'] 'link1' |
|
객체.name | #### name 변수 | >>> soup.title.name # 태그의 이름 'title' >>> soup.title.name = "modify title" # 태그의 이름을 변경함 >>> soup <html><head><modify_title>The Dormouse's story</modify_title></head> <body> <p class="title"><b>The Dormouse's story</b></p> <p class="story">Once upon a time there were three little sisters; and their names were Elsie, Lacie and Tillie; and they lived at the bottom of a well.</p> <p class="story">...</p> </body></html> >>> soup.modify_title.name = "title" # 이전상태로 원상복구 |
|
객체.string | #### string 변수 (참고) NavigableString : 문자열은 태그안에 텍스트에 상응한다. BeautifulSoup은 이런 텍스트를 포함하는 NavigableString 클래스를 사용한다. |
>>> soup.title.string "The Dormouse's story" |
|
(정리) *.string은 현재 태그에 텍스트를 가져오고 .text는 현재 태그에 있는 모든 자식들의 텍스트를 가져온다. NavigableString은 몇몇 특징들을 제외하고는 파이썬 유니코드 문자열과 똑같다. |
|||
객체.contents | 태그의 자식들을 리스트로 반환 | >>> soup.contents [<html><head><title>The Dormouse's story</title></head> <body> <p class="title"><b>The Dormouse's story</b></p> <p class="story">Once upon a time there were three little sisters; and their names were Elsie, Lacie and Tillie; and they lived at the bottom of a well.</p> <p class="story">...</p> </body></html>] |
|
find() | 태그 하나만 가져옴 find(name, attrs, recursive, string, **kwargs) [옵션] name – 태그이름 attrs – 속성(딕셔너리로) recursive – 모든자식 or 자식 string – 태그 안에 텍스트 keyword – 속성(키워드로) ※ (주의) class는 파이썬 예약어이므로, class_ 를 사용한다. |
>>> soup.find('a') >>> soup.find('a', attrs={'class' : 'sister'}) >>> soup.find(class_='sister') >>> soup.find('href') # 주의!) href는 tag가 아니고, 속성이다. 하여 결과가 없음! >>> soup.find(id="link1") >>> soup.find(id="link3") |
|
find_all() | 해당 태크가 여러개 있을 경우 한꺼번에 모두 가져옴. 그 객체들의 리스트로 반환한다 find_all(name, attrs, recursive, string, limit, **kwargs) [옵션] Limit –몇 개까지 찾을 것인가? find_all()로 검색했을 때, 수천, 수만개가 된다면 시간이 오래 걸릴것이다. 이때 몇개까지만 찾을 수 있도록 제한을 둘 수 있는 인수이다. |
>>> soup.find_all('a') [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>, <a class="sister" href="http://example.com/lacie" id="link2">Lacie, <a class="sister" href="http://example.com/tillie" id="link3">Tillie] >>> soup.find_all('a', limit=2) [<a class="sister" href="http://example.com/elsie" id="link1">Elsie, <a class="sister" href="http://example.com/lacie" id="link2">Lacie] >>> for a_tag in soup.find_all('a'): a_tag['href'] http://example.com/elsie http://example.com/lacie http://example.com/tillie >>> for a_tag in soup.find_all('a'): print(a_tag.get('href')) http://example.com/elsie http://example.com/lacie http://example.com/tillie |
※ 주의해야 할 사항은 find()나 find_all()은 반드시 자기 안에 있는 태그만 가져올 수 있다는 점이다.
# [실습] 네이버 영화 리뷰 크롤링
네이버 영화 리뷰를 크롤링 해보겠습니다.
https://movie.naver.com/movie/bi/mi/review.nhn?code=191633
설치 라이브러리
$ pip install bs4
$ pip install lxml
전체 코드
import requests
from bs4 import BeautifulSoup
# 네이버 영화 리뷰 - 트롤
url = "https://movie.naver.com/movie/bi/mi/review.nhn?code=191633"
# html 소스 가져오기
res = requests.get(url)
# html 파싱
soup = BeautifulSoup(res.text, 'lxml')
# 리뷰 리스트
ul = soup.find('ul', class_="rvw_list_area")
lis = ul.find_all('li')
# 리뷰 제목 출력
for li in lis:
print(li.a.text)
[결과 화면]
우와~ 제목이 크롤링되었습니다!!
어떠세요? 너무 재밌죠?
# 추가 학습 자료
1) 리뷰가 작성된 날짜까지 크롤링하여, 파일에 저장해보면 어떨까요?
url = "https://movie.naver.com/movie/bi/mi/review.nhn?code=191633"
res = requests.get(url)
soup = BeautifulSoup(res.text, 'lxml')
ul = soup.find('ul', class_="rvw_list_area")
lis = ul.find_all('li')
df = pd.DataFrame(columns = ["review", "date"])
for li in lis:
df = df.append({'review': li.a.text,
"date": li.span.em.text }, ignore_index=True)
print(df)
df.to_excel("naver_review.xlsx", index=False)
2) 크롤링 함수를 만들어보면 어떨까요?
def get_review(url):
res = requests.get(url)
soup = BeautifulSoup(res.text, 'lxml')
ul = soup.find('ul', class_="rvw_list_area")
lis = ul.find_all('li')
df = pd.DataFrame(columns = ["review", "date"])
for li in lis:
df = df.append({'review': li.a.text,
"date": li.span.em.text }, ignore_index=True)
return df
3) page가 여러개인 경우 모든 page에 대해서 크롤링해보면 어떨까요?
** 2)의 get_reivew()를 사용하였습니다.
url = "https://movie.naver.com/movie/bi/mi/review.nhn?code=191633"
res = requests.get(url)
soup = BeautifulSoup(res.text, 'lxml')
div = soup.find('div', class_="paging")
a_list = div.find_all("a")
all_review = pd.DataFrame()
for a in a_list:
print(a['href'], a.string)
if a.string == "다음" or a.string == "이전":
continue
else:
page_url = "https://movie.naver.com" + a['href']
print(page_url)
review = get_review(page_url)
print(review)
all_review = all_review.append(review, ignore_index=True) # ignore_index =True : append시에 index 새로 생성
print(all_review)
all_review.to_excel("naver_all_review.xlsx", index=False)
# Q&A) 크롤링한 date에 None이 존재해요.
문제 코드
# html 파싱
soup = BeautifulSoup(res.text, 'lxml')
# 리뷰 리스트
ul = soup.find('ul', class_="rvw_list_area")
lis = ul.find_all('li')
# 날짜 출력
count=0
for li in lis:
count += 1
print(f"[{count}th] ", li.em.string)
문제점
li 다음에 다오는 emd의 class가 ico_modal에 해당하는게 존재하고, text가 없기 때문
그러므로 li.span.em.text 을 해야 함.
# Q&A) text vs string의 차이
> 출처 : https://www.inflearn.com/questions/3945
HTML 구조가 아래와 같다고 가정하면,
<td>some text</td>
<td></td>
<td><p>more text</p></td>
<td>even <p>more text</p></td>
.string 으로 td를 출력하면
some text
None
more text
None
이렇게 출력됩니다. 반면에,
.text는
some text
more text
even more text
이렇게 출력됩니다.
차이점은 .string 태그 하위에 문자열을 객체화 합니다. 문자열이 없으면 None 을 반환합니다.
.text는 하위 자식태그의 텍스트까지 문자열로 반환합니다. (유니코드 형식)
즉, 하위태그에 텍스트까지 문자열로 파싱할 경우 .text를 사용하는 것이 좋습니다.
다만, 정확한 파싱을 위해서 선택자로 잘 접근해서 .string을 사용하는 걸을 선호하는 개발자들이
외국에는 더 많은 듯합니다.
선택의 차이니까 잘 판단하시면 될 것같아요.
저작물의 저작권은 작성자에게 있습니다.
공유는 자유롭게 하시되 댓글 남겨주세요~
상업적 용도로는 무단 사용을 금지합니다.
끝까지 읽어주셔서 감사합니다^^