Python BeautifulSoup으로 다음 검색 페이지 크롤링하기 — 6년 전 메모 재정리
2020년 4월에 기록해둔 짧은 Python 크롤링 메모를 6년 만에 다시 꺼냅니다. 당시 원문에는 “m.search.daum.net의 #연예인명단 이라는 해시태그 페이지를 이용하여 Crawling 했습니다”라는 한 줄 설명과 60여 줄의 코드가 전부였습니다. 코드를 다시 보니 BeautifulSoup의 기본 패턴이 잘 담겨 있어, 이번 기회에 4단계로 나누어 각 구간이 어떤 역할을 하는지 풀어 정리합니다. 본문 중 [분석] 인용 박스는 원문 코드에는 없는 해설·주의사항이니, 팩트(원문 그대로)와 분석(일반 가이드)을 구분해 읽어 주세요.
이 글은 Python 기초 문법은 알고 있지만 웹 크롤링이 처음인 분을 대상으로 합니다.
사전 준비 — 설치 및 import
실행 환경
이 코드는 Python 3.6 이상 환경을 기준으로 합니다. 외부 라이브러리 두 가지를 설치합니다. beautifulsoup4는 HTML 파싱, tqdm은 반복문 진행률 표시를 담당합니다. urllib.request를 비롯한 나머지 모듈은 Python 3 표준 라이브러리에 포함되어 있으므로 별도 설치가 필요하지 않습니다.
pip install beautifulsoup4 tqdm
설치 후 원문 코드의 import 구간은 다음과 같습니다.
from bs4 import BeautifulSoup as bs # HTML 파싱
from tqdm import tqdm # 진행률 표시
import urllib.request # HTTP 요청 (표준 라이브러리)
import time, sys, codecs, os, random, csv
[분석] – 원문에는 라이브러리 버전 표기가 없습니다. 실행 전에
pip list또는pip show beautifulsoup4 tqdm으로 현재 환경의 버전을 확인하시기 바랍니다. –sys,codecs,os는 import 목록에 있지만 함수 본체에서 실제로 사용되지 않습니다. 사용하지 않는 import는 나중에 정리해 두는 편이 좋습니다.
코드 구조 한눈에 보기
본격적인 단계별 설명 전에, 전체 코드가 하나의 함수 celebrities_names(start, end) 안에 담겨 있다는 점을 먼저 짚겠습니다. 아래 흐름도를 한 번 보고 나면 이후 조각 코드가 어느 위치에 해당하는지 바로 파악할 수 있습니다.
celebrities_names(start, end)
① 헤더·URL 준비 ← 1단계
② start~end 10단위 루프 ← 2단계
(각 페이지 요청·HTML 파싱)
③ CSS 셀렉터로 데이터 추출 ← 3단계
④ CSV 저장 ← 4단계 (함수 내부)
if __name__ == "__main__": ← 4단계 (함수 외부, 실행 진입점)
1단계 — 요청 헤더와 URL 구성
1.1 User-Agent 헤더 설정
웹 서버에 요청을 보낼 때는 브라우저처럼 보이는 User-Agent 헤더를 포함시킵니다. 원문에서는 'Mozilla/5.0'을 사용했습니다.
hdr = {'User-Agent': 'Mozilla/5.0'}
1.2 URL 구성 및 파라미터 인코딩
대상 URL은 m.search.daum.net의 연예인명단 해시태그 페이지이며, 끝에 sidx= 파라미터를 붙여 페이지 오프셋을 지정합니다.
url = "https://m.search.daum.net/kakao?w=smok&DA=AQJ&q=%EC%97%B0%EC%98%88%EC%9D%B8%EB%AA%85%EB%8B%A8&sidx="
[분석] URL의
q=파라미터에 담긴%EC%97%B0%EC%98%88%EC%9D%B8%EB%AA%85%EB%8B%A8는 한국어 “연예인명단”을 URL 인코딩한 값입니다. 이처럼 한국어를 URL에 포함할 때는urllib.parse.quote()로 동적으로 인코딩할 수 있습니다.
2단계 — 페이지 반복과 랜덤 슬립
2.1 tqdm 루프 구성
tqdm(range(start, end, 10))으로 start부터 end까지 10단위로 페이지 인덱스를 늘려가며 각 페이지를 요청합니다. sidx= 파라미터에 현재 인덱스를 이어 붙여 URL을 완성한 뒤, urllib.request.urlopen()으로 HTML을 가져옵니다.
randomsl = random.uniform(1, 3)
namesList = []
for j in tqdm(range(start, end, 10)) :
page = j
req = urllib.request.Request(url + str(page), headers=hdr)
html = urllib.request.urlopen(req).read()
soup = bs(html, 'html.parser')
time.sleep(randomsl)
2.2 랜덤 슬립 주의사항
[분석]
randomsl = random.uniform(1, 3)은 함수 시작 시 단 1회만 계산됩니다. 이후time.sleep(randomsl)이 매 요청마다 호출되지만, 슬립 간격은 처음 계산된 고정값으로 모든 요청에 동일하게 적용됩니다. 루프마다 서로 다른 랜덤 간격을 주고 싶다면randomsl = random.uniform(1, 3)호출을 루프 안으로 옮겨야 합니다. 1~3초 슬립은 서버에 과도한 부하를 주지 않기 위한 기본 매너에 해당합니다.
3단계 — BeautifulSoup CSS 셀렉터
3.1 select()와 select_one() 차이
BeautifulSoup에서 select()는 조건에 맞는 모든 요소를 리스트로 반환하고, select_one()은 첫 번째 요소 하나만 반환합니다. 이 코드에서는 먼저 select('.wrap_cont')로 페이지 안의 카드 목록 전체를 받아온 뒤, 각 카드 안에서 필드를 하나씩 꺼낼 때 select_one()을 씁니다.
3.2 필드 추출 코드
.wrap_cont 컨테이너 안에서 이름·직업·프로필 링크·생년월일 4개 필드를 추출합니다. 아래 표에 각 셀렉터를 정리했습니다.
| 필드 | 셀렉터 |
|---|---|
| 이름 | .f_link_tit |
| 직업 | .cont.f_eb |
| 프로필 링크 | .wrap_cont 첫 번째 <a>[href] (도메인 결합) |
| 생년월일 | .cont.f_eb.ff_hel |
프로필 링크는 CSS 클래스 셀렉터가 없으므로 i.find('a')['href']로 href를 꺼낸 뒤 앞에 https://m.search.daum.net/kakao를 붙여 완전한 URL을 만듭니다.
wrap_cont = soup.select('.wrap_cont')
for i in wrap_cont :
temp = []
try :
temp.append(i.select_one('.f_link_tit').text) # name
temp.append(i.select_one('.cont.f_eb').text) # job
temp.append('https://m.search.daum.net/kakao' + i.find('a')['href']) # profile link
temp.append(i.select_one('.cont.f_eb.ff_hel').text) # birth
except :
temp.append('NaN') # birth가 없는사람도 있어서 try 처리
namesList.append(temp)
4단계 — CSV 저장과 실행 진입점
4.1 CSV 저장 (함수 내부)
수집한 데이터를 CSV 파일로 저장합니다. 파일명은 'celebritiesname' + str(end) + '.csv' 형식이며, 첫 줄에 ['name', 'job', 'link', 'birth'] 헤더를 씁니다. 아래 코드는 celebrities_names 함수 안의 마지막 구간입니다.
with open('celebritiesname' + str(end) + '.csv', 'w', encoding='utf-8', newline='') as f:
writer = csv.writer(f)
writer.writerow(['name', 'job', 'link', 'birth'])
writer.writerows(namesList)
4.2 실행 진입점 (함수 외부)
if <strong>name</strong> == "<strong>main</strong>": 블록은 함수 밖에 위치합니다. 시작 페이지와 종료값을 input()으로 받아 함수를 호출합니다.
if __name__ == "__main__":
start = int(input('Start page 10x input :'))
end = int(input('How many people will you search : '))
celebrities_names(start, end)
예를 들어 start=0, end=50을 입력하면 range(0, 50, 10) → 인덱스 0·10·20·30·40, 총 5개 페이지를 순회합니다. 터미널에서 python celebrities_names.py로 실행하면 tqdm이 아래와 같은 진행률을 표시합니다.
$ python celebrities_names.py
Start page 10x input :0
How many people will you search : 50
100%|████████████████████| 5/5 [00:12<00:00, 2.53s/it]
[분석] 위 출력 형태는 tqdm의 터미널 표시 예시입니다. 2026년 현재 다음 모바일 검색 페이지의 HTML 구조가 변경되어 실제 데이터 수집은 동작하지 않을 수 있으나, tqdm 진행률 표시 자체는 동일하게 작동합니다.
코드 주의사항 & 참고사항
실행 전에 세 가지 사항을 확인하세요.
[분석] 1. try 블록 컬럼 수 불일치
원문 코드는
try안에서 이름·직업·링크·생년월일을 차례로 append합니다. 생년월일(birth) 추출에서 예외가 발생하면 앞서 추가된 컬럼만 남은 채'NaN'하나만 더 붙는 구조입니다. 실패한 행은 컬럼 수가 1~4개로 들쭉날쭉할 수 있습니다. 원문 코드는 그대로 보존하지만, 실무에서는 빈 문자열로 채워 컬럼 수를 일정하게 유지하는 편이 안전합니다.
[분석] 2. 2026년 기준 셀렉터 동작 여부
원문은 2020년 4월 기준으로 작성되었습니다. 현재(2026년 기준)에는 다음(Daum) 모바일 검색 페이지의 HTML 구조가 변경되어 셀렉터가 동작하지 않을 수 있습니다. 코드 자체는 BeautifulSoup 기본 사용 패턴 학습용으로 참고하세요.
[분석] 3. robots.txt·이용약관 확인
특정 사이트를 크롤링하기 전에는 해당 사이트의
robots.txt와 이용약관을 반드시 확인하고 진행하시기 바랍니다. 공개된 프로필 정보라도 사이트별 수집 정책이 다를 수 있습니다. 이 글은 법적 판단을 제공하지 않습니다.
전체 코드
전체 코드는 다음과 같습니다.
from bs4 import BeautifulSoup as bs
from tqdm import tqdm
import urllib.request
import time, sys, codecs, os, random, csv
def celebrities_names(start,end):
randomsl = random.uniform(1,3)
hdr = {'User-Agent': 'Mozilla/5.0'}
url = "https://m.search.daum.net/kakao?w=smok&DA=AQJ&q=%EC%97%B0%EC%98%88%EC%9D%B8%EB%AA%85%EB%8B%A8&sidx="
namesList = []
for j in tqdm(range(start, end, 10)) :
page = j
req = urllib.request.Request(url + str(page), headers=hdr)
html = urllib.request.urlopen(req).read()
soup = bs(html, 'html.parser')
time.sleep(randomsl)
wrap_cont = soup.select('.wrap_cont')
for i in wrap_cont :
temp = []
try :
temp.append(i.select_one('.f_link_tit').text) # name
temp.append(i.select_one('.cont.f_eb').text) # job
temp.append('https://m.search.daum.net/kakao' + i.find('a')['href']) # profile link
temp.append(i.select_one('.cont.f_eb.ff_hel').text) # birth
except :
temp.append('NaN') # birth가 없는사람도 있어서 try 처리
namesList.append(temp)
with open('celebritiesname' + str(end) + '.csv', 'w', encoding='utf-8', newline='') as f:
writer = csv.writer(f)
writer.writerow(['name', 'job', 'link', 'birth'])
writer.writerows(namesList)
if __name__ == "__main__":
start = int(input('Start page 10x input :'))
end = int(input('How many people will you search : '))
celebrities_names(start, end)
마무리
요청 → HTML 파싱 → CSS 셀렉터 → CSV 저장이라는 BeautifulSoup 기본 패턴을 한 흐름으로 짚어보는 데 의의가 있습니다. 셀렉터나 대상 페이지가 달라지더라도 이 뼈대는 다른 크롤링 작업에도 그대로 응용할 수 있습니다.
6년 전에는 동작만 하면 됐다 싶어 넘겼던 randomsl 위치 문제나 try 블록의 컬럼 수 불일치가, 지금 다시 보니 가장 먼저 손보고 싶어집니다. 코드를 오래 두면 자신의 성장이 보인다는 게 이런 맛인 것 같습니다.
이 글은 2020-04-23 작성된 원본 코드(티스토리 blog-7)를 기반으로 재정리되었습니다.