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 # 게시판 상세 본문 영역 지정: