1. 파이썬 가상환경 설정
- 프로젝트 폴더를 만들고 해당 폴더로 이동한다.
- 현재 폴더에
.venv 이름으로 가상환경을 생성한다.
- 가상환경을 활성화한다.
Windows (cmd/PowerShell):
Mac/Linux (bash):
1
source .venv/bin/activate
가상환경이 활성화되면 터미널의 프롬프트 앞에 (.venv)가 표시된다. 이후 설치하는 모든 패키지는 이 가상환경 안에만 설치된다.
2. 필요한 패키지 설치
- 다음 명령어를 실행하여 크롤링에 필요한 패키지들을 설치한다.
1
pip install requests beautifulsoup4 lxml pandas
| 패키지 | 설명 |
|---|
requests | 웹 서버에 HTTP 요청을 보내고 응답(HTML)을 받아오는 라이브러리다. |
beautifulsoup4 | 받아온 HTML을 파싱(분석)하여 원하는 태그를 쉽게 찾아주는 라이브러리다. |
lxml | BeautifulSoup이 사용하는 빠르고 안정적인 HTML 파서다. |
pandas | 데이터프레임 형식으로 CSV 파일을 읽고 데이터를 분석하는 라이브러리다. |
3. BeautifulSoup 기초
1
from bs4 import BeautifulSoup
| 행 | 코드 | 설명 |
|---|
| 1 | from bs4 import BeautifulSoup | bs4 라이브러리에서 BeautifulSoup 클래스를 가져온다. HTML/XML을 파싱(분석)하는 도구다. |
2
<html><head><title>MangoTitle</title></head>
4
<p class="title">Mango</p>
6
<p class="story">Once upon a time there were three little sisters;
8
<a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>,
9
<a href="http://example.com/lacie" class="sister" id="link2">Lacie</a>
10
and <a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>;
11
and they lived at the bottom of a well.</p>
12
<p class="story">...</p>
| 행 | 코드 | 설명 |
|---|
| 1 | html_doc | 파싱할 HTML 문서를 담는 변수다. |
| 2-13 | """...""" | 삼중 따옴표로 여러 줄을 하나의 문자열로 저장한다. |
| 3 | <title>MangoTitle</title> | 브라우저 탭에 표시되는 페이지 제목 태그다. |
| 5 | <p class="title"> | class 속성이 "title"인 단락(<p>) 태그다. |
| 9 | <a href="..." class="sister" id="link1"> | 링크 태그다. href는 이동할 주소, class는 분류명, id는 고유 식별자다. |
1
soup = BeautifulSoup(html_doc, 'lxml')
| 행 | 코드 | 설명 |
|---|
| 1 | soup | 파싱된 HTML 트리 전체를 담는 객체다. 이후 모든 탐색은 이 객체를 통해 한다. |
| 1 | BeautifulSoup(html_doc, 'lxml') | html_doc 문자열을 lxml 파서로 분석해 탐색 가능한 객체를 만든다. |
| 1 | 'lxml' | HTML을 해석하는 파서의 종류. 빠르고 안정적이다. (pip install lxml 필요) |
1
print("soup.body.p의 결과 : ", soup.body.p)
| 행 | 코드 | 설명 |
|---|
| 1 | soup.body | <body> 태그에 접근한다. |
| 1 | soup.body.p | <body> 안의 첫 번째 <p> 태그를 반환한다. |
| 1 | print(...) | 결과를 화면에 출력한다. |
1
print("soup.a['href']의 결과 : ", soup.a['href'])
| 행 | 코드 | 설명 |
|---|
| 1 | soup.a | 문서 전체에서 첫 번째 <a> 태그에 접근한다. |
| 1 | ['href'] | 태그의 속성값을 딕셔너리처럼 꺾쇠 괄호로 꺼낸다. href 속성의 URL을 반환한다. |
1
print("soup.title.name의 결과 : ", soup.title.name)
| 행 | 코드 | 설명 |
|---|
| 1 | soup.title | <title> 태그에 접근한다. |
| 1 | .name | 태그명 문자열을 반환한다. |
1
print("soup.title.string의 결과 : ", soup.title.string)
| 행 | 코드 | 설명 |
|---|
| 1 | .string | 태그 안의 텍스트 내용을 반환한다. 반환 타입은 NavigableString(문자열과 동일하게 사용 가능)이다. |
1
print("soup.contents의 결과 : ", soup.contents)
| 행 | 코드 | 설명 |
|---|
| 1 | soup.contents | soup 객체의 직접 자식 요소들을 리스트로 반환한다. |
1
print("soup.find()의 결과 : ", soup.find('a', attrs={'class' : 'sister'}))
| 행 | 코드 | 설명 |
|---|
| 1 | find() | 조건에 맞는 태그를 딱 하나만 찾아서 반환한다. 여러 개여도 첫 번째만 반환한다. |
| 1 | 'a' | 찾을 태그 이름. <a> 태그를 찾겠다는 의미다. |
| 1 | attrs={'class': 'sister'} | 속성 조건. class가 "sister"인 태그만 찾는다. class는 파이썬 예약어라 attrs 딕셔너리로 전달한다. |
1
print("soup.find_all()의 결과 : ", soup.find_all('a', limit=2))
| 행 | 코드 | 설명 |
|---|
| 1 | find_all() | 조건에 맞는 태그를 모두 찾아 리스트로 반환한다. |
| 1 | 'a' | <a> 태그를 모두 찾겠다는 의미다. |
| 1 | limit=2 | 최대 몇 개까지 찾을지 제한한다. 여기서는 2개만 반환한다. |
find() vs find_all() 비교
| find() | find_all() |
|---|
| 반환값 | 태그 1개 (없으면 None) | 태그 리스트 (없으면 빈 리스트 []) |
| 용도 | 첫 번째 결과만 필요할 때 | 모든 결과를 가져올 때 |
limit 옵션 | 없음 | 있음 (개수 제한) |
4. 네이버 영화 리뷰 크롤링
4.1. 크롤링 대상 확인
네이버 영화 페이지에서 영화 코드를 확인한다.
1
https://movie.naver.com/movie/bi/mi/basic.naver?code=XXXXXX
영화 리뷰 목록 URL 구조:
https://movie.naver.com/movie/bi/mi/pointWriteFormList.naver?pointMovieCode=영화코드&page=페이지번호
4.2. 단일 페이지 리뷰 수집
crawling.py 파일을 생성하고 아래 코드를 작성한다.
2
from bs4 import BeautifulSoup
8
f"https://movie.naver.com/movie/bi/mi/pointWriteFormList.naver"
9
f"?pointMovieCode={MOVIE_CODE}&type=after&isActualPointView=true&page={PAGE}"
14
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
15
"AppleWebKit/537.36 (KHTML, like Gecko) "
16
"Chrome/120.0.0.0 Safari/537.36"
20
response = requests.get(url, headers=headers)
21
response.encoding = "utf-8"
23
soup = BeautifulSoup(response.text, "lxml")
25
reviews = soup.select("div.score_reple p")
26
scores = soup.select("div.star_score em")
28
for score, review in zip(scores, reviews):
29
print(f"[{score.text}점] {review.text.strip()}")
| 행 | 코드 | 설명 |
|---|
| 2 | import requests | HTTP 요청을 보내는 라이브러리다. |
| 5 | MOVIE_CODE = "17909" | 크롤링할 영화 코드다. (아바타) |
| 8~11 | url = (...) | 크롤링할 URL을 구성한다. |
| 13~19 | headers = {...} | 요청 헤더다. User-Agent를 설정하여 봇으로 인식되지 않도록 한다. |
| 21 | requests.get(url, headers=headers) | 지정한 URL에 GET 요청을 보낸다. headers로 봇 차단을 피한다. |
| 22 | response.encoding = "utf-8" | 한글이 깨지지 않도록 인코딩을 명시적으로 지정한다. |
| 24 | soup = BeautifulSoup(response.text, "lxml") | HTML을 파싱하여 탐색 가능한 객체로 만든다. |
| 26~27 | reviews, scores — CSS 선택자로 리뷰 텍스트와 별점을 가져온다. | CSS 선택자로 리뷰 텍스트와 별점을 가져온다. |
| 29~30 | for 루프 | 별점과 리뷰를 쌍으로 묶어 동시에 순회한다. |
4.3. 여러 페이지 리뷰 수집 + CSV 저장
crawling.py 파일을 생성하고 아래 코드를 작성한다.
4
from bs4 import BeautifulSoup
11
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
12
"AppleWebKit/537.36 (KHTML, like Gecko) "
13
"Chrome/120.0.0.0 Safari/537.36"
19
for page in range(1, MAX_PAGE + 1):
21
f"https://movie.naver.com/movie/bi/mi/pointWriteFormList.naver"
22
f"?pointMovieCode={MOVIE_CODE}&type=after&isActualPointView=true&page={page}"
25
response = requests.get(url, headers=headers)
26
response.encoding = "utf-8"
27
soup = BeautifulSoup(response.text, "lxml")
29
reviews = soup.select("div.score_reple p")
30
scores = soup.select("div.star_score em")
32
for score, review in zip(scores, reviews):
33
text = review.text.strip()
35
results.append({"score": score.text, "review": text})
37
print(f"{page}페이지 완료 ({len(results)}건 누적)")
40
with open("naver_movie_reviews.csv", "w", newline="", encoding="utf-8-sig") as f:
41
writer = csv.DictWriter(f, fieldnames=["score", "review"])
43
writer.writerows(results)
45
print(f"\n저장 완료! 총 {len(results)}건 → naver_movie_reviews.csv")
| 행 | 코드 | 설명 |
|---|
| 7 | MOVIE_CODE = "17909" | 크롤링할 영화 코드다. |
| 8 | MAX_PAGE = 5 | 수집할 최대 페이지 수다. |
| 10~16 | headers | 봇으로 인식되지 않도록 User-Agent를 설정한다. |
| 18 | results = [] | 수집한 리뷰를 담을 리스트다. |
| 20 | for page in range(1, MAX_PAGE + 1) | 1페이지부터 MAX_PAGE까지 반복한다. |
| 37 | time.sleep(1) | 각 요청 사이에 1초 대기. 서버에 과부하를 주지 않기 위한 예의다. |
| 40 | encoding="utf-8-sig" | 엑셀에서 한글이 깨지지 않도록 BOM이 포함된 UTF-8로 저장한다. |
| 41 | csv.DictWriter | 딕셔너리 형태의 데이터를 CSV로 저장하는 클래스다. |
크롤링 시 주의사항:
- 서버에 과도한 요청을 보내지 않도록
time.sleep()으로 간격을 둔다.
- 네이버 서비스 약관을 확인하고 학습 목적으로만 사용한다.
- 사이트 구조가 변경되면 CSS 선택자를 수정해야 할 수 있다.
4.4. 수집 결과 확인
- 수집한 CSV 파일의 내용을 확인하기 위해 아래 코드를 작성한다.
3
df = pd.read_csv("naver_movie_reviews.csv", encoding="utf-8-sig")
5
print(df["score"].value_counts())
- 수집한 CSV 파일을 실행하여 결과를 확인한다.
| 행 | 코드 | 설명 |
|---|
| 3 | pd.read_csv("naver_movie_reviews.csv", encoding="utf-8-sig") | CSV 파일을 읽어 DataFrame 객체로 변환한다. |
| 5 | df.head() | 상위 5개 행을 출력한다. |
| 6 | df["score"].value_counts() | 별점 분포를 확인한다. |
5. 실행 결과
수집된 데이터 예시
2
0 10 영화 정말 재미있게 봤습니다. 추천합니다.
4
2 10 최고의 영화! 여러 번 봐도 좋다.
별점 분포
6. 정리
이 프로젝트를 통해 배운 내용:
- BeautifulSoup: HTML을 파싱하고 원하는 요소를 추출하는 방법
- CSS 선택자:
.class, #id 등을 사용해 특정 태그 찾기
- 크롤링 윤리:
time.sleep()으로 서버에 부담을 주지 않기
- 데이터 저장: 수집한 데이터를 CSV 파일로 저장하기
- 데이터 분석: pandas를 사용해 수집 결과 확인하기
더 배워볼 것들
- Selenium: 자바스크립트로 동적으로 로드되는 콘텐츠 크롤링
- Scrapy: 대규모 크롤링 프레임워크
- 정규표현식: 더 복잡한 텍스트 패턴 매칭
- 데이터 시각화: matplotlib/seaborn으로 수집 데이터 시각화