DB_JOB/작업파일/완료_1-2주차/2주차/성평등가족부/test.py
hehihoho3 df16c98366 백업: DB수집 전체 스냅샷 (공공기관2 정리 전)
공공기관2 작업 중. _temp 몽타주(재생성가능)는 제외.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 18:15:40 +09:00

283 lines
11 KiB
Python

import os
import re
import traceback
from bs4 import BeautifulSoup
import requests
from openpyxl import load_workbook
# SSL 경고 메시지 무시 설정 (보안 인증서 우회용)
requests.packages.urllib3.disable_warnings(
requests.packages.urllib3.exceptions.InsecureRequestWarning
)
def get_soup(url, session):
"""URL에 접속하여 BeautifulSoup 객체를 반환하는 함수 (방화벽 우회 헤더 포함)"""
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
"Accept-Language": "ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7",
}
try:
response = session.get(url, headers=headers, verify=False, timeout=15)
if response.status_code == 200:
return BeautifulSoup(response.text, "html.parser")
else:
print(f"[경고] 접속 실패 (상태 코드: {response.status_code}) - {url}")
return None
except Exception as e:
print(f"[오류] URL 접속 중 에러 발생: {e} - {url}")
return None
def parse_board_detail_urls(soup, base_url):
"""목록 페이지에서 fn_selectView 패턴을 분석하여 상세 URL 최대 5개 추출"""
detail_urls = []
if not soup:
return detail_urls
# tbody 내의 모든 a 태그 탐색
tbody = soup.find("tbody")
if not tbody:
return detail_urls
links = tbody.find_all("a", href=True)
for link in links:
href = link["href"]
# javascript:fn_selectView('숫자') 패턴 매칭
match = re.search(r"fn_selectView\s*\(\s*['\"]?(\d+)['\"]?\s*\)", href)
if match:
bbtSn = match.group(1)
# 현재 목록 URL 구조를 기반으로 상세 페이지 URL 조립
# 예: s005.do?mid=old919 -> s005d.do?mid=old919&bbtSn=2
current_url = base_url
if ".do" in current_url:
# 파일명 뒤에 'd' 붙이기 (예: s005.do -> s005d.do)
detail_base = re.sub(r"(\w+)\.do", r"\1d.do", current_url)
# 기존 쿼리스트링에 bbtSn 추가
if "?" in detail_base:
# 기존에 div1= 등이 있을 수 있으므로 &로 연결
detail_url = f"{detail_base}&bbtSn={bbtSn}"
else:
detail_url = f"{detail_base}?bbtSn={bbtSn}"
detail_urls.append(detail_url)
# 최대 5개 제한 규칙 적용
if len(detail_urls) >= 5:
break
return detail_urls
def analyze_content(content_area):
"""본문 영역 태그를 분석하여 저작물 유형 8종을 판별하는 핵심 로직"""
if not content_area:
return "없음"
types = set()
# 1. 어문 판별 (공백 제외 순수 텍스트 존재 여부)
pure_text = content_area.get_text(strip=True)
if pure_text:
types.add("어문")
# 2. 이미지 판별
imgs = content_area.find_all("img")
valid_img_found = False
for img in imgs:
src = img.get("src", "")
# 예외 패턴 필터링 (공공누리 마크 등 제외)
if "new_img_opentype" in src or "img_opentype" in src:
continue
valid_img_found = True
# 검증을 위한 부모 태그 실시간 로그 출력
print(f" [이미지 감지] 부모 구조: {img.parent.name} -> src: {src}")
# 인라인 스타일 background-image 검사
styles = content_area.find_all(style=True)
for s in styles:
if "background-image" in s["style"]:
valid_img_found = True
if valid_img_found:
types.add("이미지")
# 3. 영상 판별
video_tags = content_area.find_all(["video", "iframe", "embed", "object"])
video_ext = re.compile(
r"\.(mp4|avi|mkv|wmv|mov|flv)", re.IGNORECASE
) # 2026년 표준 확장자 포함
has_video = len(video_tags) > 0 or video_ext.search(str(content_area))
if has_video or "youtube.com" in str(content_area) or "youtu.be" in str(content_area):
types.add("영상")
# 4. 오디오 판별
audio_tags = content_area.find_all("audio")
audio_ext = re.compile(r"\.(mp3|wav|ogg|wma|m4a)", re.IGNORECASE)
if len(audio_tags) > 0 or audio_ext.search(str(content_area)):
types.add("오디오")
# 5. 글꼴 판별
font_ext = re.compile(r"\.(ttf|woff|woff2|otf|eot)", re.IGNORECASE)
if "@font-face" in str(content_area) or font_ext.search(str(content_area)):
types.add("글꼴")
# 6. 3D 판별
canvas_tags = content_area.find_all("canvas")
three_d_ext = re.compile(r"\.(obj|gltf|glb|fbx|3ds)", re.IGNORECASE)
if (
"webgl" in str(content_area).lower()
or len(canvas_tags) > 0
or three_d_ext.search(str(content_area))
):
types.add("3D")
# 7. 기타 (본문에 글/미디어가 없으나 다운로드용 첨부파일 확장자만 덩그러니 있을 때)
# 어문/이미지/영상/오디오/글꼴/3D가 검출되지 않은 경우에만 적용
if len(types) == 0:
doc_ext = re.compile(r"\.(zip|pdf|hwp|docx|xlsx|pptx)", re.IGNORECASE)
if doc_ext.search(str(content_area)):
types.add("기타")
# 8. 없음 판별
if not types:
return "없음"
# 판별된 저작물 유형을 콤마(,)로 연결하여 정렬 후 반환
return ", ".join(sorted(list(types)))
def main(file_path, output_path):
print("=== [알바] 저작물유형 자동 구분 프로그램 시작 ===")
# 엑셀 파일 로드 (기존 서식 보존을 위해 data_only=False)
wb = load_workbook(file_path, data_only=False)
sheet = wb.active
# 데이터 영역 순회 (4행부터 끝까지)
max_row = sheet.max_row
# 동일 도메인 연속 요청을 위한 세션 생성
session = requests.Session()
for row in range(4, max_row + 1):
k_val = sheet[f"K{row}"].value # URL 주소
l_val = sheet[f"L{row}"].value # 게시판 형태 ('게시판' 또는 '페이지')
# 문자열 공백 제거 및 예외 처리
url = str(k_val).strip() if k_val else ""
board_type = str(l_val).strip() if l_val else ""
# 필터링 조건: '게시판' 또는 '페이지'인 행만 작업 대상
if board_type not in ["게시판", "페이지"]:
continue
if not url or not url.startswith("http"):
print(f"[{row}행] 유효하지 않은 URL 스킵: {url}")
sheet[f"N{row}"].value = "오류"
continue
print(f"\n▶ [{row}행 처리 중] 형태: {board_type} | URL: {url}")
final_types_list = []
# -------------------------------------------------------------
# CASE 1: 게시판 형태일 때
# -------------------------------------------------------------
if board_type == "게시판":
list_soup = get_soup(url, session)
if not list_soup:
print(f" [오류] 목록 페이지를 가져올 수 없습니다.")
sheet[f"N{row}"].value = "오류"
continue
# 상세 URL 최대 5개 추출
detail_urls = parse_board_detail_urls(list_soup, url)
if not detail_urls:
print(f" [오류] 목록에서 상세 페이지 링크를 찾지 못했습니다.")
sheet[f"N{row}"].value = "오류"
continue
print(f" -> 추출된 상세 페이지 수: {len(detail_urls)}개 (분석 시작)")
# 각 상세 페이지 접속 및 분석
for d_url in detail_urls:
print(f" - 상세 페이지 스캔: {d_url}")
detail_soup = get_soup(d_url, session)
if not detail_soup:
continue
# 게시판 상세 본문 영역 지정: <table class="brdView01">
content_area = detail_soup.find("table", class_="brdView01")
if content_area:
res_type = analyze_content(content_area)
final_types_list.append(res_type)
else:
print(" [경고] 본문 영역(<table class='brdView01'>)을 찾을 수 없습니다.")
# 상세페이지들을 분석했으나 본문 영역을 단 하나도 찾지 못한 경우
if not final_types_list:
sheet[f"N{row}"].value = "오류"
continue
# -------------------------------------------------------------
# CASE 2: 페이지 형태일 때 (다이렉트 스캔)
# -------------------------------------------------------------
elif board_type == "페이지":
page_soup = get_soup(url, session)
if not page_soup:
sheet[f"N{row}"].value = "오류"
continue
# 페이지형 본문 영역 지정: <div id="contents">
content_area = page_soup.find("div", id="contents")
if content_area:
res_type = analyze_content(content_area)
final_types_list.append(res_type)
else:
print(" [경고] 본문 영역(<div id='contents'>)을 찾을 수 없습니다.")
sheet[f"N{row}"].value = "오류"
continue
# -------------------------------------------------------------
# 결과 취합 및 N열 기입 (기존 서식 유지)
# -------------------------------------------------------------
if final_types_list:
# 다중 상세페이지 결과가 있을 경우, 각 페이지의 결과 중 중복 없는 속성들을 병합
combined_types = set()
for t_str in final_types_list:
if t_str and t_str != "없음" and t_str != "오류":
for part in t_str.split(", "):
combined_types.add(part)
if combined_types:
final_result = ", ".join(sorted(list(combined_types)))
else:
final_result = "없음"
sheet[f"N{row}"].value = final_result
print(f" 🎯 [결과 입력] N열 ➡️ {final_result}")
else:
sheet[f"N{row}"].value = "오류"
print(f" 🎯 [결과 입력] N열 ➡️ 오류")
# 최종 엑셀 파일 안전하게 저장
wb.save(output_path)
print("\n=== 작업 완료! 파일이 안전하게 저장되었습니다. ===")
if __name__ == "__main__":
# 파일 경로 설정 (사용자 환경에 맞게 변경하여 사용하세요)
input_excel = r"D:\01.프로젝트\DB수집\2주차\성평등가족부\성평등가족부.xlsx"
output_excel = r"D:\01.프로젝트\DB수집\2주차\성평등가족부\성평등가족부_결과반영.xlsx"
if os.path.exists(input_excel):
main(input_excel, output_excel)
else:
print(f"파일을 찾을 수 없습니다. 경로를 확인해주세요: {input_excel}")