공공기관2 작업 중. _temp 몽타주(재생성가능)는 제외. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
283 lines
11 KiB
Python
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}") |