웹페이지를 파이썬으로 가져온다 — 사이트맵과 본문 추출
Article
여섯 편에 걸쳐 「질문을 던지면 관련된 글을 찾아주는 것」을 만든다. 이번 편은 웹페이지 하나를 제대로 가져오는 데까지 한다.
- ① 웹페이지를 가져온다지금 여기
- ② 264편을 파일로 저장다음 편
- ③④ 글을 조각으로 자른다너무 길면 못 쓴다
- ⑤⑥ 조각을 숫자로 바꿔 검색뜻으로 찾는다
1. 작업 폴더와 가상환경
# 1) 작업 폴더를 만들고 그 안으로 들어간다
mkdir rag-class
cd rag-class
# 2) 가상환경을 만든다. .venv 라는 폴더가 생긴다
python -m venv .venv
# 3) 켠다 (윈도우)
.venv\Scripts\activate
# 3) 켠다 (맥 · 리눅스)
source .venv/bin/activate
- C:\Users\사용자\rag-class>
- (.venv) C:\Users\사용자\rag-class>
앞에 (.venv) 가 붙었는지 매번 확인한다. 가상환경은 그 터미널 창에만 켜지므로, 창을 새로 열면 다시 activate 한다.
윈도우에서 이 시스템에서 스크립트를 실행할 수 없으므로 에러가 뜨면 아래 한 줄로 푼다.
Set-ExecutionPolicy -Scope CurrentUser RemoteSigned
2. 도구 세 개를 깐다
python -m pip install requests beautifulsoup4 markdownify
- requests주소 → HTML 덩어리를 받아온다
- BeautifulSoupHTML 덩어리 → 본문만 골라낸다
- markdownify본문 HTML → 읽기 쉬운 글로
3. 주소 목록 — sitemap.xml
거의 모든 사이트에 sitemap.xml 이 있다. 사이트주소/sitemap.xml 로 열어볼 수 있고, 안은 이렇게 생겼다.
<urlset>
<url>
<loc>https://www.dcodelab.kr/p/git-21-branch</loc>
<lastmod>2026-08-05</lastmod>
</url>
...
</urlset>
01_get_urls.py
import re
import requests
SITEMAP = "https://www.dcodelab.kr/sitemap.xml"
# timeout=30 — 30초 넘게 대답이 없으면 포기한다
response = requests.get(SITEMAP, timeout=30)
xml = response.text
# <loc>주소</loc> 에서 가운데만 꺼낸다. ? 는 "최소한만 가져가라"
urls = re.findall(r"<loc>(.*?)</loc>", xml)
print("전체 주소:", len(urls))
# 이 블로그는 글 주소에만 /p/ 가 들어간다
posts = [u for u in urls if "/p/" in u]
print("글 주소:", len(posts))
for u in posts[:3]:
print(" ", u)
- 전체 주소: 283
- 글 주소: 264
- https://www.dcodelab.kr/p/git-25-team-practice
- https://www.dcodelab.kr/p/git-26-real-world-cases
- https://www.dcodelab.kr/p/git-23-conflict
283개 중 264개가 글이다. 링크를 타고 다니는 코드는 한 줄도 쓰지 않았다.
4. 본문만 뽑는다
어느 태그를 집을지는 브라우저에서 본문 오른쪽 클릭 → 「검사」로 확인한다. 이 블로그는 본문이 <div class="prose"> 안에 있다.
02_get_one_post.py
import requests
from bs4 import BeautifulSoup # 설치는 beautifulsoup4, 부를 땐 bs4
from markdownify import markdownify as to_md
url = "https://www.dcodelab.kr/p/git-21-branch"
html = requests.get(url, timeout=30).text
soup = BeautifulSoup(html, "html.parser")
# select_one — 조건에 맞는 것 중 첫 번째 하나
body = soup.select_one("div.prose")
print("본문 찾음:", body is not None)
# select — 조건에 맞는 것 전부. 버튼·아이콘은 글이 아니라 지운다
for junk in body.select("button, svg, script, style"):
junk.decompose()
md = to_md(str(body), heading_style="ATX")
print(len(md), "자")
print(md[:120])
- 본문 찾음: True
- 5210 자
- 살다 보면 "그때 다른 선택을 했으면 어땠을까" 하고 상상할 때가 있습니다.
메뉴도 푸터도 없이 본문만 나왔다. heading_style="ATX" 는 <h2>제목</h2> 을 ## 제목 으로 바꾸라는 뜻이다.
select_one 은 못 찾으면 에러 대신 None 을 준다. 다음 편 코드에 if body is None 검사가 들어가는 이유다.
이번 편에 나온 것
| 쓴 것 | 하는 일 |
|---|---|
pip install requests beautifulsoup4 markdownify | 도구 셋을 깐다 |
requests.get(url, timeout=30).text | 페이지를 받아 글자로 꺼낸다 |
BeautifulSoup(html, "html.parser") | HTML 을 뒤질 수 있게 만든다 |
soup.select_one("div.prose") | 조건에 맞는 것 하나. 없으면 None |
soup.select("button, svg") | 조건에 맞는 것 전부 (목록) |
태그.decompose() | 그 태그를 통째로 삭제 |
markdownify(html, heading_style="ATX") | HTML → 마크다운 |
re.findall(r"<loc>(.*?)</loc>", xml) | 괄호 안쪽만 전부 꺼낸다 |
목록[:3] | 앞에서 3개만 |
sitemap.xml | 사이트가 공개하는 주소록 |
robots.txt | 긁어도 되는 곳 / 안 되는 곳 |
다음 편에서 264개를 전부 파일로 저장한다.