공공기관2 작업 중. _temp 몽타주(재생성가능)는 제외. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
230 lines
9.9 KiB
Python
230 lines
9.9 KiB
Python
import os
|
|
import re
|
|
from urllib.parse import urljoin
|
|
import requests
|
|
from bs4 import BeautifulSoup
|
|
import openpyxl
|
|
|
|
# ==========================================
|
|
# [설정 항목] 환경에 맞게 파일명을 수정하세요
|
|
# ==========================================
|
|
INPUT_EXCEL_PATH = r"C:\Users\hehih\ownCloud\알바\전라남도\전라남도_홈페이지_사이트_r4.xlsx" # 원본 엑셀 파일 경로
|
|
OUTPUT_EXCEL_PATH = r"C:\Users\hehih\ownCloud\알바\전라남도\전라남도_홈페이지_사이트_r4_ai.xlsx" # 결과 저장 파일 경로
|
|
BASE_DOMAIN = "https://www.jeonnam.go.kr/" # 기본 도메인 주소
|
|
|
|
# 요청 헤더 설정 (우회용 User-Agent)
|
|
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"
|
|
}
|
|
|
|
def analyze_content(soup_element):
|
|
"""
|
|
지정된 본문 BeautifulSoup 엘리먼트를 분석하여 저작물 유형 8종을 판별합니다.
|
|
"""
|
|
if not soup_element:
|
|
return "없음"
|
|
|
|
detected_types = []
|
|
|
|
# 전체 텍스트 및 HTML 소스 추출
|
|
text_content = soup_element.get_text(strip=True)
|
|
html_source = str(soup_element).lower()
|
|
|
|
# 1. 어문 판별 (공백 제외 순수 텍스트 존재 여부)
|
|
if text_content:
|
|
detected_types.append("어문")
|
|
|
|
# 2. 이미지 판별 (인라인 배경 이미지 또는 유효한 img 태그)
|
|
has_valid_image = False
|
|
# 인라인 스타일 background-image 검사
|
|
if "background-image" in html_source:
|
|
has_valid_image = True
|
|
else:
|
|
# img 태그 검사 (지정된 예외 패턴 처리)
|
|
for img in soup_element.find_all("img"):
|
|
src = img.get("src", "")
|
|
if src and "/home/bbs/img/new_img_opentype" not in src:
|
|
has_valid_image = True
|
|
break
|
|
|
|
if has_valid_image:
|
|
detected_types.append("이미지")
|
|
|
|
# 3. 영상 판별
|
|
video_extensions = [".mp4", ".avi", ".mkv", ".wmv", ".mov", ".flv"]
|
|
has_video = any(tag in html_source for tag in ["<video", "<iframe", "<embed", "<object", "youtube.com", "youtu.be"])
|
|
if not has_video:
|
|
# 확장자 링크 검사
|
|
for a in soup_element.find_all("a", href=True):
|
|
if any(a["href"].lower().endswith(ext) for ext in video_extensions):
|
|
has_video = True
|
|
break
|
|
if has_video:
|
|
detected_types.append("영상")
|
|
|
|
# 4. 오디오 판별
|
|
audio_extensions = [".mp3", ".wav", ".ogg", ".wma", ".m4a"]
|
|
has_audio = "<audio" in html_source
|
|
if not has_audio:
|
|
for a in soup_element.find_all("a", href=True):
|
|
if any(a["href"].lower().endswith(ext) for ext in audio_extensions):
|
|
has_audio = True
|
|
break
|
|
if has_audio:
|
|
detected_types.append("오디오")
|
|
|
|
# 5. 글꼴 판별
|
|
font_extensions = [".ttf", ".woff", ".woff2", ".otf", ".eot"]
|
|
has_font = "@font-face" in html_source
|
|
if not has_font:
|
|
for link in soup_element.find_all(["link", "a"], href=True):
|
|
if any(link["href"].lower().endswith(ext) for ext in font_extensions):
|
|
has_font = True
|
|
break
|
|
if has_font:
|
|
detected_types.append("글꼴")
|
|
|
|
# 6. 3D 판별
|
|
d_extensions = [".obj", ".gltf", ".glb", ".fbx", ".3ds"]
|
|
has_3d = "webgl" in html_source or "<canvas" in html_source
|
|
if not has_3d:
|
|
for a in soup_element.find_all("a", href=True):
|
|
if any(a["href"].lower().endswith(ext) for ext in d_extensions):
|
|
has_3d = True
|
|
break
|
|
if has_3d:
|
|
detected_types.append("3D")
|
|
|
|
# 7. 기타 판별 (본문에 글/미디어가 없으나 문서 첨부파일만 덩그러니 있는 경우)
|
|
if not text_content and not has_valid_image and not has_video and not has_audio and not has_font and not has_3d:
|
|
doc_extensions = [".zip", ".pdf", ".hwp", ".docx", ".xlsx", ".pptx"]
|
|
for a in soup_element.find_all("a", href=True):
|
|
if any(a["href"].lower().endswith(ext) for ext in doc_extensions):
|
|
detected_types.append("기타")
|
|
break
|
|
|
|
# 8. 없음 판별
|
|
if not detected_types:
|
|
return "없음"
|
|
|
|
# 중복 제거 후 콤마로 연결하여 반환
|
|
return ", ".join(dict.fromkeys(detected_types))
|
|
|
|
|
|
def main():
|
|
if not os.path.exists(INPUT_EXCEL_PATH):
|
|
print(f"[오류] 입력 파일 '{INPUT_EXCEL_PATH}'을 찾을 수 없습니다.")
|
|
return
|
|
|
|
# openpyxl로 기존 서식을 유지하며 파일 로드
|
|
wb = openpyxl.load_workbook(INPUT_EXCEL_PATH, data_only=False)
|
|
sheet = wb.active
|
|
|
|
print("==================================================")
|
|
print("🚀 웹 스크래이핑 및 저작물 유형 자동 분류를 시작합니다.")
|
|
print("==================================================")
|
|
|
|
# 4행부터 데이터 영역 순회
|
|
for row_idx in range(4, sheet.max_row + 1):
|
|
k_val = sheet.cell(row=row_idx, column=11).value # K열: URL 주소
|
|
l_val = sheet.cell(row=row_idx, column=12).value # L열: 게시판형태
|
|
|
|
# 조건 필터링: L열이 '게시판' 또는 '페이지'이고 K열에 주소가 있는 경우만 진행
|
|
if not k_val or l_val not in ["게시판", "페이지"]:
|
|
continue
|
|
|
|
target_url = str(k_val).strip()
|
|
# 상대 경로일 경우 기본 도메인과 결합
|
|
if target_url.startswith("/"):
|
|
target_url = urljoin(BASE_DOMAIN, target_url)
|
|
|
|
row_results = []
|
|
|
|
try:
|
|
if l_val == "게시판":
|
|
print(f"\n[행 {row_idx}] 📋 게시판 목록 스캔 중: {target_url}")
|
|
res = requests.get(target_url, headers=HEADERS, timeout=10)
|
|
if res.status_code != 200:
|
|
print(f" └ [접속 실패] Status Code: {res.status_code}")
|
|
continue
|
|
|
|
soup = BeautifulSoup(res.text, "html.parser")
|
|
|
|
# 목록에서 상세 페이지로 이동하는 <a> 태그 수집
|
|
detail_links = []
|
|
title_tds = soup.find_all("td", class_="title")
|
|
|
|
for td in title_tds:
|
|
a_tag = td.find("a", href=True)
|
|
if a_tag:
|
|
href = a_tag["href"]
|
|
full_detail_url = urljoin(BASE_DOMAIN, href)
|
|
detail_links.append(full_detail_url)
|
|
|
|
# 상위 최대 5개로 제한 규칙 적용
|
|
detail_links = detail_links[:5]
|
|
print(f" └ 수집된 상세글 링크 개수: {len(detail_links)}개")
|
|
|
|
# 수집된 상세 페이지 순회 분석
|
|
for d_url in detail_links:
|
|
print(f" └ 🔍 상세 페이지 분석 중: {d_url}")
|
|
d_res = requests.get(d_url, headers=HEADERS, timeout=10)
|
|
if d_res.status_code == 200:
|
|
d_soup = BeautifulSoup(d_res.text, "html.parser")
|
|
# 사용자 지정 게시판 본문 영역 태그 탐색
|
|
content_area = d_soup.find(class_="bbs_view_contnet")
|
|
|
|
if content_area:
|
|
res_type = analyze_content(content_area)
|
|
row_results.append(res_type)
|
|
else:
|
|
row_results.append("없음")
|
|
else:
|
|
print(f" └ [상세 페이지 접속 실패] Status Code: {d_res.status_code}")
|
|
|
|
elif l_val == "페이지":
|
|
print(f"\n[행 {row_idx}] 🌐 페이지 다이렉트 스캔 중: {target_url}")
|
|
res = requests.get(target_url, headers=HEADERS, timeout=10)
|
|
if res.status_code != 200:
|
|
print(f" └ [접속 실패] Status Code: {res.status_code}")
|
|
continue
|
|
|
|
soup = BeautifulSoup(res.text, "html.parser")
|
|
# 사용자 지정 일반 페이지 본문 영역 태그 탐색
|
|
content_area = soup.find("article", class_="container")
|
|
|
|
if content_area:
|
|
res_type = analyze_content(content_area)
|
|
row_results.append(res_type)
|
|
else:
|
|
row_results.append("없음")
|
|
|
|
# 행별 최종 결과 도출 (다중 상세글 결과 병합 및 중복 제거)
|
|
if row_results:
|
|
combined_items = []
|
|
for res_str in row_results:
|
|
if res_str and res_str != "없음":
|
|
combined_items.extend([item.strip() for item in res_str.split(",")])
|
|
|
|
final_status = ", ".join(dict.fromkeys(combined_items)) if combined_items else "없음"
|
|
else:
|
|
final_status = "없음"
|
|
|
|
# 기존 스타일 유지하며 N열(14번째 열)에 데이터 기록
|
|
sheet.cell(row=row_idx, column=14).value = final_status
|
|
print(f" 🎯 [분류 완료] 행 {row_idx} -> N열 기입 값: {final_status}")
|
|
|
|
except Exception as e:
|
|
print(f" ❌ [에러 발생] 행 {row_idx} 처리 중 오류: {e}")
|
|
sheet.cell(row=row_idx, column=14).value = "오류 발생"
|
|
|
|
# 기존 서식을 유지한 채 최종 파일 저장
|
|
wb.save(OUTPUT_EXCEL_PATH)
|
|
wb.close()
|
|
print("\n==================================================")
|
|
print(f"🎉 모든 작업이 완료되었습니다! 파일이 안전하게 저장되었습니다.")
|
|
print(f"📂 결과 파일 경로: {os.path.abspath(OUTPUT_EXCEL_PATH)}")
|
|
print("==================================================")
|
|
|
|
if __name__ == "__main__":
|
|
main() |