웹 크롤링 + streamlit
Develop웹 크롤링하여 얻은 댓글들을 Streamlit으로 페이지에 출력
웹 크롤링하여 얻은 댓글들을 데이터베이스(DB)에 저장
beautifulsoup.ipynb
에서 크롤링 테스트 후 crawling_final.py
생성.새로운 레파지토리 생성하여 git clone 진행
최초의 main 브런치 확인 (github)
최초의 main 브런치 확인 (vscode)
기능 1,2,3 브런치
commit 진행
이후, develop 브런치에 merge를 진행하면 최종 화면은 다음과 같다.
import requests
from bs4 import BeautifulSoup as bs
def do_crawling_of_nate(comment_id:str) -> str:
# url = "https://pann.nate.com/talk/350939697"
url = f"https://pann.nate.com/talk/{comment_id}"
header = {
# "User-Agent"와 "Refererer"은 chrome에서 개발자 도구 -> Network -> Headers에서 확인 가능
"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36",
"Refererer" : "https://pann.nate.com/"
}
response = requests.get(url=url, headers=header)
return response
def get_comments(comment_id:str) -> list:
response = do_crawling_of_nate(comment_id)
if response.status_code >= 400:
print("오류입니다!")
return
beautiful_text = bs(response.text, "html.parser")
dd_list = beautiful_text.find_all("dd", class_="usertxt")
return [
dd.get_text().replace("\n", "").strip()
for dd in dd_list
]
# 크롤링 해서 결과를 보여주는 웹 페이지
import streamlit as st
from common.crawling import get_comments
st.title("크롤링 한번 해볼까?")
# 네이트 판 ID 예시 : 350939697
with st.form("my_form"):
nate_pan_id = st.text_input("네이트 판 ID를 입력하세요.")
form_submit = st.form_submit_button("크롤링 시작합니다.")
if form_submit and nate_pan_id is not None:
msg = get_comments(nate_pan_id)
st.write(msg)
네이트판 크롤링을 기반으로 데이터베이스 설계 및 ERD 작성.
-- 'nate_pan_comments' 테이블을 생성 (이미 존재하면 생성하지 않음)
CREATE TABLE IF NOT EXISTS nate_pan_comments (
pan_id VARCHAR(20) NOT NULL COMMENT '네이트판 아이디',
create_dt TIMESTAMP COMMENT '댓글 생성일자',
title VARCHAR(20) NULL COMMENT '댓글 제목',
comment VARCHAR(200) NOT NULL COMMENT '댓글 내용',
PRIMARY KEY(pan_id, create_dt)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='네이트판 댓글';
ERD 작성
BeautifulSoup 실험
beautifulsoup.ipynb
에서 크롤링 테스트 후 crawling_final.py
생성.
beautifulsoup.ipynb
# beautiful_text
dl_list = beautiful_text.find_all("dl", class_="cmt_item")
# 댓글 리스트에서 title만 가져오기
dl_list[0].find(class_="nameui").get_text()
# 댓글 리스트에서 작성 시간만 가져오기
dl_list[0].find_next("i").get_text()
# 댓글 리스트에서 댓글 가져오기
dl_list[0].find("dd", class_="usertxt").get_text()
# 댓글 리스트에서 댓글 안의 내용만 가져오기
dl_list[0].find("dd", class_="usertxt").get_text().replace("\n", "").strip()
from datetime import datetime
# 댓글 생성일자 형변환
print(datetime.strptime(dl_list[0].find_next("i").get_text(), "%Y.%m.%d %H:%M"))
crawling_final.py
import requests
from bs4 import BeautifulSoup as bs
from datetime import datetime
def do_crawling_of_nate(comment_id:str) -> str:
# url = "https://pann.nate.com/talk/350939697"
url = f"https://pann.nate.com/talk/{comment_id}"
header = {
# "User-Agent"와 "Refererer"은 chrome에서 개발자 도구 -> Network -> Headers에서 확인 가능
"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36",
"Refererer" : "https://pann.nate.com/"
}
response = requests.get(url=url, headers=header)
return response
def get_comments(comment_id:str) -> list:
response = do_crawling_of_nate(comment_id)
if response.status_code >= 400:
print("오류입니다!")
return
beautiful_text = bs(response.text, "html.parser")
# 댓글 리스트 가져오기
comment_list = beautiful_text.find_all("dl", class_="cmt_item")
return [
{
# beautifulsoup.ipynb 참고
# 댓글 제목
'title': comment.find(class_="nameui").get_text().strip(),
# 댓글 내용
'comment': comment.find("dd", class_="usertxt").get_text().replace("\n", "").strip(),
# 댓글 생성일자 형변환
'create_dt' : datetime.strptime(comment.find_next("i").get_text(), "%Y.%m.%d %H:%M")
} for comment in comment_list
]
Streamlit 페이지 구현
# 크롤링 해서 결과를 DB에 저장
import streamlit as st
from common.crawling2 import get_comments
st.title("크롤링 한번 해볼까?")
# 네이트 판 ID 예시 : 350939697
with st.form("my_form"):
nate_pan_id = st.text_input("네이트 판 ID를 입력하세요.")
form_submit = st.form_submit_button("크롤링 시작합니다.")
if form_submit and nate_pan_id is not None:
msg = get_comments(nate_pan_id)
st.write(msg)
DB 저장 구현
댓글 데이터를 데이터베이스에 저장하는 기능 개발.
database.py
: 데이터베이스와 상호작용하여 데이터를 삽입(insert)하는 기능import logging
import streamlit as st
from sqlalchemy import text
from .sql_constant import INSERT_SQLs
# Streamlit의 캐시 기능을 사용하여 데이터베이스 연결 객체를 재사용하기 위한 함수.
@st.cache_resource
def get_connector():
"""
데이터베이스 연결 객체를 생성하고 반환합니다.
- "mydb"라는 이름의 데이터베이스에 연결합니다.
- SQL 타입의 연결을 사용하며, autocommit 옵션을 활성화하여 자동으로 커밋합니다.
"""
return st.connection(
"mydb", # 데이터베이스 이름
type="sql", # 연결 타입: SQL
autocommit=True # 자동 커밋 활성화
)
# 데이터를 데이터베이스에 삽입하는 함수.
# 주어진 SQL 템플릿과 데이터를 사용하여 데이터베이스에 삽입을 시도하고,
# 실패한 데이터를 반환합니다.
def insert_query(sql_constant: INSERT_SQLs, datas: list) -> list:
"""
데이터를 데이터베이스에 삽입합니다.
- SQL 템플릿(`sql_constant`)과 데이터 리스트(`datas`)를 받아 데이터베이스에 삽입을 시도합니다.
- 삽입 실패한 데이터는 리스트(`list_error`)에 저장하여 반환합니다.
매개변수:
- sql_constant: SQL 템플릿을 정의한 `INSERT_SQLs` Enum 값.
- datas: 삽입할 데이터 리스트. 각 데이터는 딕셔너리 형태로 구성됩니다.
반환값:
- list_error: 삽입에 실패한 데이터 리스트.
"""
# 데이터베이스 연결 객체 가져오기
conn = get_connector()
# 삽입 실패 데이터를 저장할 리스트
list_error = []
# 전체 데이터 개수 로깅
logging.info(f"[insert_query] len(datas): {len(datas)}")
# 데이터를 순회하며 데이터베이스 삽입 시도
for idx, data in enumerate(datas):
try:
# 현재 데이터의 인덱스 로깅
logging.info(f"[insert_query] len(datas)[idx]: {idx}")
# SQL 템플릿을 데이터에 맞게 포맷
insert_sql = sql_constant.value[1].format(
pan_id=data['pan_id'], # 게시글 ID
title=data['title'], # 댓글 작성자 이름
comment=data['comment'], # 댓글 내용
create_dt=data['create_dt'] # 댓글 작성 날짜
)
# 포맷된 SQL 쿼리 실행
conn.connect().execute(text(insert_sql))
except Exception as e:
# 삽입 실패 시 해당 데이터를 list_error에 추가
list_error.append(data)
# 삽입 실패 데이터를 반환
return list_error
sql_constant.py
: 데이터베이스에 삽입할 SQL 쿼리 템플릿을 정의하는 상수 클래스(enum.Enum)를 포함import enum # 열거형(Enum)을 정의하기 위해 Python 표준 라이브러리 사용.
# 데이터베이스 삽입 SQL 쿼리를 관리하기 위한 열거형 클래스 정의.
class INSERT_SQLs(enum.Enum):
"""
데이터베이스에 데이터를 삽입하기 위한 SQL 템플릿을 정의하는 열거형 클래스.
- 각 항목은 고유 ID(enum.auto()), SQL 쿼리 템플릿, 그리고 설명으로 구성됩니다.
"""
# 네이트 판 댓글 데이터를 저장하는 SQL 템플릿 정의.
NATE_PAN_COMMENTS = (
enum.auto(), # 열거형 항목에 자동으로 고유 ID 부여.
"""
insert nate_pan_comments
(pan_id, create_dt, title, comment)
values('{pan_id}', '{create_dt}', '{title}', '{comment}');
""", # SQL 템플릿: 댓글 데이터를 데이터베이스 테이블에 삽입.
"댓글 데이터 저장" # SQL 템플릿에 대한 설명.
)
crawling.py
: 네이트 판에서 특정 게시글의 댓글 데이터를 크롤링하여 리스트 형태로 반환import requests # HTTP 요청을 보내기 위해 사용하는 라이브러리.
from datetime import datetime # 날짜와 시간 데이터를 처리하기 위해 사용하는 라이브러리.
from bs4 import BeautifulSoup as bs # HTML 파싱을 위한 BeautifulSoup 라이브러리.
# 네이트 판 특정 게시글의 HTML 데이터를 요청하여 가져오는 함수.
def do_crawling_of_nate(comment_id: str):
"""
네이트 판 게시글의 HTML 데이터를 가져옵니다.
매개변수:
- comment_id: 네이트 판 게시글 ID (str).
반환값:
- HTTP 응답 객체 (requests.Response).
작동 방식:
- 지정된 게시글 ID를 URL에 삽입하여 완성된 URL로 GET 요청을 보냅니다.
- 요청 시 Referer와 User-Agent 헤더를 포함하여 크롤링이 차단되지 않도록 설정합니다.
"""
# 네이트 판 게시글 URL 생성
url = f"https://pann.nate.com/talk/{comment_id}"
# HTTP 요청 헤더 설정
header = {
"Referer": "https://pann.nate.com/", # 요청 출처를 네이트 판으로 지정.
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
# User-Agent를 통해 일반 브라우저로 요청하는 것처럼 설정.
}
# URL에 대해 GET 요청을 보내고 응답 반환
response = requests.get(url=url, headers=header)
return response
# 네이트 판 게시글의 댓글 데이터를 크롤링하여 리스트 형태로 반환하는 함수.
def get_comments(comment_id: str) -> list:
"""
네이트 판 게시글의 댓글 데이터를 가져옵니다.
매개변수:
- comment_id: 네이트 판 게시글 ID (str).
반환값:
- 댓글 데이터를 담은 딕셔너리의 리스트. 각 댓글은 pan_id, title, comment, create_dt를 포함.
- 오류 발생 시 None 반환.
작동 방식:
- `do_crawling_of_nate` 함수로 HTML 데이터를 가져옵니다.
- BeautifulSoup으로 HTML을 파싱하여 댓글 데이터를 추출합니다.
- 댓글 데이터는 딕셔너리 형태로 구성됩니다.
"""
# HTML 데이터를 가져오기
response = do_crawling_of_nate(comment_id)
# 요청이 실패한 경우
if response.status_code >= 400:
print("오류에요 ㅠㅠ") # 오류 메시지 출력
return
# BeautifulSoup로 HTML 파싱
beautiful_text = bs(response.text, "html.parser")
# 댓글 리스트 추출: <dl> 태그 중 class="cmt_item" 요소를 모두 가져옴
comment_list = beautiful_text.find_all("dl", class_="cmt_item")
# 각 댓글 데이터를 딕셔너리로 변환하여 리스트로 반환
return [
{
'pan_id': comment_id, # 게시글 ID
# 댓글 작성자의 이름 또는 닉네임
'title': comment.find(class_="nameui").get_text().strip(),
# 댓글 내용
'comment': comment.find("dd", class_="usertxt").get_text().replace("\n", "").strip(),
# 댓글 작성 날짜 및 시간
'create_dt': datetime.strptime(comment.find("i").get_text().strip(), '%Y.%m.%d %H:%M')
}
for comment in comment_list # 모든 댓글 요소를 순회하며 데이터 추출
]
main.py
: Streamlit 애플리케이션을 통해 사용자가 입력한 네이트 판 게시글 ID로 댓글 데이터를 크롤링하고, 이를 데이터베이스에 저장import logging # 애플리케이션의 동작 정보를 기록하기 위한 모듈.
logging.basicConfig(level=logging.INFO) # 로깅 레벨을 INFO로 설정하여 일반 정보를 기록.
import streamlit as st # Streamlit을 사용해 웹 애플리케이션을 제작하기 위한 모듈.
# 크롤링 함수와 데이터베이스 삽입 관련 모듈 가져오기.
from common.crawling_final import get_comments # 네이트 판 댓글 크롤링 함수.
from common.database import insert_query # 데이터베이스 삽입 함수.
from common.sql_constant import INSERT_SQLs # SQL 템플릿을 관리하는 열거형.
# Streamlit 애플리케이션 제목 설정
st.title("크롤링 한번 해볼까?") # 페이지 상단에 제목 표시.
# 사용자 입력 폼 생성
# 네이트 판 ID를 입력받기 위한 폼을 정의.
# 폼 안에 텍스트 입력 필드와 버튼 포함.
with st.form("my_form"):
# 사용자가 입력할 네이트 판 게시글 ID
nate_pan_id = st.text_input("네이트 판 아이디 작성해주세요.")
# 폼 제출 버튼
form_submit = st.form_submit_button("크롤링 시작합니다.")
# 폼이 제출되었고, 사용자가 네이트 판 ID를 입력한 경우 실행.
if form_submit and nate_pan_id is not None:
# 입력받은 네이트 판 ID를 사용해 댓글 데이터를 크롤링.
comments = get_comments(nate_pan_id)
# 크롤링한 데이터를 데이터베이스에 삽입.
list_error = insert_query(INSERT_SQLs.NATE_PAN_COMMENTS, comments)
# 데이터베이스 삽입 결과에 따라 메시지 출력.
if not list_error: # 삽입 실패 데이터가 없는 경우
st.write("저장성공") # 성공 메시지 출력.
else: # 삽입 실패 데이터가 있는 경우
st.write(list_error) # 실패한 데이터 리스트 출력.
실행 화면