코드공부방

파이썬으로 최신 부동산 뉴스를 모아서 보자! (4) (웹 크롤링/스크래핑) 본문

생산/부동산뉴스모아

파이썬으로 최신 부동산 뉴스를 모아서 보자! (4) (웹 크롤링/스크래핑)

:- ) 2021. 10. 5. 09:30
반응형

파이썬으로 최신 부동산 뉴스를 모아서 보자!
(웹 크롤링/스크래핑) (4)


이제 특정 언론사에서 뉴스를 수집하여 카테고리별로 DB에 저장하는 것까진 완료되었다. "뉴스 수집" 과정의 남은 과제는 수집된 뉴스 목록의 상세 내용을 수집하여 DB에 넣어주기만 하면 된다. 프로세스는 간단하다. 뉴스 목록 테이블 (TBL_LAND_NEWS_LIST)에서 NEWS_CONTENT Column의 값이 "수집 중입니다."인 row의 뉴스 URL값을 가져와 한번씩 조회하여 뉴스 상세 내용을 가져와 다시 뉴스 목록 테이블의 NEWS_CONTENT Column의 값을 UPDATE해줄 예정이다. 

(좀 더 깔끔한 방법은 뉴스 목록 테이블에 DETAIL_STATUS라는 Column을 하나 추가하여 상태값에 따라 상세 내용 수집 여부를 체크하는 것이지만 포스팅에서는 그냥 진행하기로 한다.)

상세 기사 수집여부를 NEWS_CONTENT Column의 내용을 가지고 판단하기로 한다.

 


그럼 먼저 쿼리문을 통해 수집이 되지 않은 뉴스기사의 URL목록을 return하는 함수를 생성해보자.

# 뉴스 URL
def get_inquiry_required_rows(dbconn, cursor) : 
    try : 
        cursor.execute(f"""
            SELECT 
                NEWS_CODE, NEWS_URL
            FROM 
                TBL_LAND_NEWS_LIST 
            WHERE 
                NEWS_CONTENT = '수집 중입니다.'
        """)
        rows = cursor.fetchall()
    except Exception as e :
        print(f'***** + get_inquiry_required_rows error! >> {e}')
    finally : 
        return rows

다음 뉴스 상세  내용을 업데이트할 쿼리를 실행하는 함수를 생성한다.

# DB Update
def update_data(dbconn, cursor, data) : 
    try : 
        cursor.execute(f"""
            UPDATE 
                TBL_LAND_NEWS_LIST 
            SET 
                NEWS_CONTENT = "{data['news_content']}"
            WHERE 
                NEWS_CODE = "{data['news_code']}"
        """)
    except Exception as e :
        print(f'***** + update_data error! >> {e}')
    finally : 
        dbconn.commit()
        print(f'**** [{data["news_code"]}] 뉴스 상세 update 완료! ')

다음 GetSedaily 클래스에 상세화면을 조회하는 detail 메소드를 추가해준다. 상세내용을 수집하지 않은 뉴스 기사 목록을 loop하며 상세화면에 접근할건데 서버에 부하를 줄이기 위해 뉴스 기사당 3초의 딜레이를 준다. 그리고 기사 내용에 공통적으로 "< 저작권자 ⓒ 서울경제, 무단 전재 및 재배포 금지 >"라는 문구가 들어가는데 나는 이 기사 내용을 무단으로 전재 및 재배포할 것이 아니므로 해당 내용은 숙지만 하고 replace 메소드를 사용하여 삭제한다.

# 서울경제
class GetSedaily :
    # 상세
    def detail(self) : 
        rows = get_inquiry_required_rows(self.dbconn, self.cursor)
        if len(rows) == 0 : 
            print('모든 뉴스의 상세 내용 수집이 완료된 상태입니다.')
        else : 
            print(f'* {len(rows)}개의 뉴스 상세화면 조회 시작!')
            for idx, row in enumerate(rows) :
                data = {}
                code = row[0]
                url = row[1]
                soup = get_soup(url, self.encoding)

                content = remove_sc(soup.select_one('.article_view').get_text().strip()).replace('< 저작권자 ⓒ 서울경제, 무단 전재 및 재배포 금지 >', '')
                
                data = {
                    'news_code' : code,
                    'news_content' : content
                }
                # DB Update
                update_data(self.dbconn, self.cursor, data)
                
                # 3초 delay
                time.sleep(3)

이제 코드를 실행해본다.

뉴스 상세 내용 업데이트가 잘 됐다는 로그가 뜬다.

그럼 실제 DB에도 뉴스 상세 내용이 잘 들어왔는지 확인해보자.

DB에도 뉴스 상세 내용이 잘 들어와있다!

수집 후 확인해보니 기사마다 "viewer"이라는 키워드가 들어가있는데 이는 replace 메소드로 제거해줘야 할 것 같다.


이것으로 서울경제 사이트에서 부동산 관련 3개 카테고리의 최신 뉴스를 수집하여 DB에 저장하는, "뉴스 수집"까지는 완료가 되었다. 앞으로 할 작업은 서울경제 사이트 외 언론사에서도 지금까지 작업한 것과 동일한 과정을 거쳐 뉴스를 수집하면 된다.


서울경제 언론사의 뉴스 중 부동산 카테고리 3개를 수집하는 코드는 아래와 같다.

import requests
import re
import regex
import time
import os, json
from datetime import datetime
from bs4 import BeautifulSoup
from urllib.request import urlopen

# 특수문자 제거
def remove_sc(sentence) : 
    return re.sub('[-=.#/?:$}\"\']', '', str(sentence)).replace('[','').replace(']','')

# 웹사이트 인코딩 방식 확인
def get_encoding(url) : 
    f = urlopen(url)    
    # bytes자료형의 응답 본문을 일단 변수에 저장
    bytes_content = f.read()
    
    # charset은 HTML의 앞부분에 적혀 있는 경우가 많으므로
    # 응답 본문의 앞부분 1024바이트를 ASCII문자로 디코딩 해둔다.
    # ASCII 범위 이외에 문자는 U+FFFD(REPLACEMENT CHARACTRE)로 변환되어 예외가 발생하지 않는다.
    scanned_text = bytes_content[:1024].decode('ascii', errors='replace')
    
    # 디코딩한 문자열에서 정규 표현식으로 charset값 추출
    # charset이 명시돼 있지 않으면 UTF-8 사용
    match = re.search(r'charset=["\']?([\w-]+)', scanned_text)
    if match : 
        encoding = match.group(1)
    else :   
        encoding = 'utf-8'
        
    return encoding

# 사이트 소스 가져오기
def get_soup(url, charset) :
    headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36'}
    res = requests.get(url, headers=headers)
    res.raise_for_status()
    res.encoding = None
    soup = BeautifulSoup(res.content.decode(charset, 'replace'), 'html.parser')
    
    return soup

# 서울경제
class GetSedaily :
    def __init__(self, dbconn, cursor) : 
        self.encoding = get_encoding('https://sedaily.com')
        self.dbconn = dbconn
        self.cursor = cursor
    
    # 부동산 일반
    def normal(self) : 
        url = 'https://sedaily.com/NewsList/GB07'
        soup = get_soup(url, self.encoding)

        news_list = soup.select('.sub_news_list li')
        for news in news_list : 
            data = {}
            # 뉴스 기사 제목
            title = remove_sc(news.select_one('.text_area h3').get_text().strip())
            # 뉴스 요약
            summary = remove_sc(news.select_one('.text_sub').get_text().strip())
            # 썸네일 이미지 경로 
            # 이미지 없는 경우 예외처리
            if news.select_one('.thumb img') is not None : 
                thumbnail_url = news.select_one('.thumb img')['src']
            else :
                thumbnail_url = 'none'
            # 뉴스 URL
            news_url = news.select_one('a')['href']
            news_url = 'https://sedaily.com' + news_url
            # 뉴스 코드 
            news_code = news_url.split('/GB')[0].split('NewsView/')[1]
            # 뉴스 작성 일자
            regist_date = news.select_one('.text_info .date').get_text().strip()
            # 뉴스 작성 시간
            regist_time = news.select_one('.text_info .time').get_text().strip()
            data = {
                'news_category' : 3,
                'media_code' : 1,
                'media_name' : '서울경제',
                'news_code' : news_code,
                'news_title' : title,
                'news_summary' : summary,
                'news_thumb_url' : thumbnail_url,
                'url' : news_url,
                'news_reg_date' : regist_date
            }
            insert_data(self.dbconn, self.cursor, data)
        
        
    # 부동산 정책
    def policy(self) : 
        url = 'https://sedaily.com/NewsList/GB01'
        soup = get_soup(url, self.encoding)

        news_list = soup.select('.sub_news_list li')
        for news in news_list : 
            data = {}
            # 뉴스 기사 제목
            title = remove_sc(news.select_one('.text_area h3').get_text().strip())
            # 뉴스 요약
            summary = remove_sc(news.select_one('.text_sub').get_text().strip())
            # 썸네일 이미지 경로 
            # 이미지 없는 경우 예외처리
            if news.select_one('.thumb img') is not None : 
                thumbnail_url = news.select_one('.thumb img')['src']
            else :
                thumbnail_url = 'none'
            # 뉴스 URL
            news_url = news.select_one('a')['href']
            news_url = 'https://sedaily.com' + news_url
            # 뉴스 코드 
            news_code = news_url.split('/GB')[0].split('NewsView/')[1]
            # 뉴스 작성 일자
            regist_date = news.select_one('.text_info .date').get_text().strip()
            # 뉴스 작성 시간
            regist_time = news.select_one('.text_info .time').get_text().strip()
            data = {
                'news_category' : 3,
                'media_code' : 1,
                'media_name' : '서울경제',
                'news_code' : news_code,
                'news_title' : title,
                'news_summary' : summary,
                'news_thumb_url' : thumbnail_url,
                'url' : news_url,
                'news_reg_date' : regist_date
            }
            insert_data(self.dbconn, self.cursor, data)
            
    # 부동산 분양 정보
    def parcel_out(self) : 
        url = 'https://sedaily.com/NewsList/GB02'
        soup = get_soup(url, self.encoding)

        news_list = soup.select('.sub_news_list li')
        for news in news_list : 
            data = {}
            # 뉴스 기사 제목
            title = remove_sc(news.select_one('.text_area h3').get_text().strip())
            # 뉴스 요약
            summary = remove_sc(news.select_one('.text_sub').get_text().strip())
            # 썸네일 이미지 경로 
            # 이미지 없는 경우 예외처리
            if news.select_one('.thumb img') is not None : 
                thumbnail_url = news.select_one('.thumb img')['src']
            else :
                thumbnail_url = 'none'
            # 뉴스 URL
            news_url = news.select_one('a')['href']
            news_url = 'https://sedaily.com' + news_url
            # 뉴스 코드 
            news_code = news_url.split('/GB')[0].split('NewsView/')[1]
            # 뉴스 작성 일자
            regist_date = news.select_one('.text_info .date').get_text().strip()
            # 뉴스 작성 시간
            regist_time = news.select_one('.text_info .time').get_text().strip()

            data = {
                'news_category' : 5,
                'media_code' : 1,
                'media_name' : '서울경제',
                'news_code' : news_code,
                'news_title' : title,
                'news_summary' : summary,
                'news_thumb_url' : thumbnail_url,
                'url' : news_url,
                'news_reg_date' : regist_date
            }
            insert_data(self.dbconn, self.cursor, data)
      
    # 상세
    def detail(self) : 
        rows = get_inquiry_required_rows(self.dbconn, self.cursor)
        if len(rows) == 0 : 
            print('모든 뉴스의 상세 내용 수집이 완료된 상태입니다.')
        else : 
            print(f'* {len(rows)}개의 뉴스 상세화면 조회 시작!')
            for idx, row in enumerate(rows) :
                data = {}
                code = row[0]
                url = row[1]
                soup = get_soup(url, self.encoding)

                content = remove_sc(soup.select_one('.article_view').get_text().strip()).replace('< 저작권자 ⓒ 서울경제, 무단 전재 및 재배포 금지 >', '')
                
                data = {
                    'news_code' : code,
                    'news_content' : content
                }
                # DB Update
                update_data(self.dbconn, self.cursor, data)
                
                # 3초 delay
                time.sleep(3)
       
       
# DB Insert
def insert_data(dbconn, cursor, data) : 
    try : 
        cursor.execute(f"""
            INSERT IGNORE INTO TBL_LAND_NEWS_LIST 
            (
                NEWS_CATEGORY, MEDIA_CODE, MEDIA_NAME, 
                NEWS_CODE, NEWS_TITLE, NEWS_SUMMARY, 
                NEWS_CONTENT, NEWS_THUMB_URL, NEWS_URL, 
                NEWS_REG_DATE, REG_DATE
            ) 
            VALUES (
                "{data['news_category']}", {data['media_code']}, "{data['media_name']}", 
                "{data['news_code']}", "{data['news_title']}", "{data['news_summary']}", 
                "수집 중입니다.", "{data['news_thumb_url']}", "{data['url']}", 
                "{data['news_reg_date']}", NOW()
            ) 
        """)
    except Exception as e :
        print(f'***** + insert_data error! >> {e}')
    finally : 
        dbconn.commit()
        print('**** 뉴스 insert 완료! ')

# DB Update
def update_data(dbconn, cursor, data) : 
    try : 
        cursor.execute(f"""
            UPDATE 
                TBL_LAND_NEWS_LIST 
            SET 
                NEWS_CONTENT = "{data['content']}"
            WHERE 
                NEWS_CODE = "{data['news_code']}"
        """)
    except Exception as e :
        print(f'***** + update_data error! >> {e}')
    finally : 
        dbconn.commit()
        print('**** 뉴스 상세 update 완료! ')
        
# 뉴스 URL
def get_inquiry_required_rows(dbconn, cursor) : 
    try : 
        cursor.execute(f"""
            SELECT 
                NEWS_CODE, NEWS_URL
            FROM 
                TBL_LAND_NEWS_LIST 
            WHERE 
                NEWS_CONTENT = '수집 중입니다.'
        """)
        rows = cursor.fetchall()
    except Exception as e :
        print(f'***** + select_detail error! >> {e}')
    finally : 
        return rows
            

# DB 접속
dbconn = mysql.connector.connect(host='host명', user='DB 서버 접근 ID', password='DB서버 접근 PW', database='DB명', port='포트')
cursor = dbconn.cursor(buffered=True)

# 서울경제
GetSedaily = GetSedaily(dbconn, cursor)
GetSedailyNormal = GetSedaily.normal()
GetSedailyPolicy = GetSedaily.policy()
GetSedailyParcelOut = GetSedaily.parcel_out()
etSedailyDetail = GetSedaily.detail()

dbconn.close()

 

파이썬으로 최신 부동산 뉴스를 모아서 보자! (웹 크롤링/스크래핑) (1)

파이썬으로 최신 부동산 뉴스를 모아서 보자! (웹 크롤링/스크래핑) (1) 벌써 2021년 10월이다. 맙소사.. 2020년 12월 28일에 회고 글을 작성하며 2021년엔 많은 것들을 이뤄보자라는 생각을 했었

code-study.tistory.com

 

파이썬으로 최신 부동산 뉴스를 모아서 보자! (웹 크롤링/스크래핑) (2)

파이썬으로 최신 부동산 뉴스를 모아서 보자! (웹 크롤링/스크래핑) (2) 앞선 포스팅에서 서울경제에서 원하는 카테고리의 뉴스 목록을 수집하여 console에 print하는 것까지 작업을 진행하였다.

code-study.tistory.com

 

파이썬으로 최신 부동산 뉴스를 모아서 보자! (3) (웹 크롤링/스크래핑)

파이썬으로 최신 부동산 뉴스를 모아서 보자! (웹 크롤링/스크래핑) (3) python에서 MariaDB를 접근할땐 mysql.connector라이브러리를 사용한다. 앞서 얘기했듯이 본 포스팅에선 DB서버 구축이나 라이

code-study.tistory.com

 

반응형
Comments