본문 바로가기

카테고리 없음

[crawling] 네이버 블로그 및 여러 웹사이트 크롤링

class CRAW_ETC:
    def __init__(self):
        self.site_specific_classes = {
            'naver.com': [
                'se-main-container',  # 네이버 블로그
                'se_component_wrap',
                'se-component-content',
                'article_viewer',  # 네이버 카페
                'article_view',
                'entryDetailView'  # 네이버 지식백과
            ],
            'namu.wiki': [
                'wiki-content',  # 나무위키
                'content'
            ],
            'tistory.com': [
                'tt_article_useless_p_margin',
                'contents_style',
                'entry-content',
                'post-content'
            ],
            'brunch.co.kr': [
                'wrap_article',  # 브런치
                'wrap_body'
            ],
            'wikipedia.org': [
                'mw-content-text',
                'mw-parser-output'
            ],
            'yonhapnews.co.kr': [
                'article',  # 연합뉴스
                'article-txt'
            ],
            'chosun.com': [
                'article-body',  # 조선비즈
                'news-article-body'
            ],
            'default': [
                'article',
                'post-content',
                'entry-content',
                'content',
                'main-content',
                'post-body',
                'article-body'
            ]
        }

    def check_url(self, session, url) : 
        headers = {
                    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
                }
        response = session.get(url, headers=headers)
        if response.status_code != 200 : 
            return False, None 
        soup = BeautifulSoup(response.text, 'html.parser')
        if soup.title : 
            title = soup.title.text
        else : 
            title = url
        return True, title
            
    def get_iframe_url(self, soup, base_url, iframe_id='mainFrame'):
        """iframe URL 추출 (네이버 블로그/카페 등)"""
        iframe = soup.find('iframe', id=iframe_id)
        if iframe and 'src' in iframe.attrs:
            iframe_src = iframe['src']
            if iframe_src.startswith('/'):
                from urllib.parse import urljoin
                iframe_src = urljoin(base_url, iframe_src)
            return iframe_src
        return None

    def extract_content(self, soup, url):
        # 2. URL에서 도메인 추출
        domain_match = re.search(r'^(?:https?:\/\/)?(?:[^@\n]+@)?(?:www\.)?([^:\/\n?]+)', url)
        if not domain_match:
            return "유효하지 않은 URL입니다."
        domain = domain_match.group(1)

        # 3. 도메인에 맞는 클래스 목록 선택
        classes = None
        for key in self.site_specific_classes:
            if key in domain:
                classes = self.site_specific_classes[key]
                break
        if not classes:
            classes = self.site_specific_classes['default']


        # 4. 본문 클래스 탐색
        content = None
        found_class = None
        for class_name in classes:
            content = soup.find('div', class_=class_name)
            if content:
                found_class = class_name
                break
        
        # 5. 클래스 탐색 실패 시 article, main, section 태그 시도
        if not content:
            content = soup.find('article')
            found_class = 'article' if content else None
        
        if not content:
            for tag in ['main', 'section']:
                content = soup.find(tag, class_=re.compile(r'content|article|post|main', re.I))
                if content:
                    found_class = f"{tag} with content-related class"
                    break

        # 6. 본문 텍스트 추출
        if content:
            # <p> 태그 우선, 없으면 전체 텍스트 추출
            # paragraphs = content.find_all('p')
            paragraphs = soup.find_all(['p', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'])
            if paragraphs:
                full_text = '\n'.join(p.get_text(strip=True) for p in paragraphs if p.get_text(strip=True))
            else:
                full_text = content.get_text(strip=True, separator='\n')
            
            if full_text:
                LOGGER.info(f"본문 클래스 발견: {found_class}\n본문 내용:\n{full_text}")
                return True, full_text
            else:
                LOGGER.info(f"본문 클래스 발견: {found_class}\n본문 텍스트를 추출할 수 없습니다.\nHTML 일부:\n{content.prettify()[:500]}")
                return True, content.prettify()
        else:
            # 디버깅: 발견된 div 클래스 출력
            divs = soup.find_all('div', class_=True)
            classes_found = [div.get('class') for div in divs[:10]]
            LOGGER.info(f"본문을 찾을 수 없습니다.\n발견된 div 클래스들: {classes_found}")
            return True, soup
    
    def crawl_website(self, url):
        """웹사이트 본문 크롤링"""
        headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
        }
        
        # 1. 웹페이지 요청
        try:
            response = requests.get(url, headers=headers, timeout=10)
        except requests.RequestException as e:
            LOGGER.info(f"페이지 요청 실패: {e}")
            LOGGER.info(f"페이지 요청 실패: {response.status_code}")
 
        if response.status_code != 200:
            None, False
        
        # 2. BeautifulSoup 객체 생성
        soup = BeautifulSoup(response.text, 'html.parser')
        if soup.title : 
            filename = soup.title.text
        else : 
            filename = url
        LOGGER.info(f'[crawl_website] filename : {filename}')
        # 3. 네이버 블로그/카페 iframe 처리
        if 'blog.naver.com' in url:
            iframe_url = self.get_iframe_url(soup, url)
            if iframe_url:
                try:
                    response = requests.get(iframe_url, headers=headers, timeout=10)
                    if response.status_code == 200:
                        soup = BeautifulSoup(response.text, 'html.parser')
                    else:
                        LOGGER.info(f"iframe 요청 실패: {response.status_code}")
                        return None, False
                except requests.RequestException as e:
                    LOGGER.info(f"iframe 요청 실패: {e}")
                    return None, False
            else:
                LOGGER.info("iframe URL을 찾을 수 없습니다.")
                return None, False
        
        # 4. 본문 추출
        res, return_data =  self.extract_content(soup, url)
        html_bytes = return_data.encode('utf-8')
        filename = f'{filename}.html'
        return UploadFile(file=io.BytesIO(html_bytes), filename=filename), True