코드공부방

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

생산/부동산뉴스모아

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

:- ) 2021. 10. 3. 10:28
반응형

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


앞선 포스팅에서 서울경제에서 원하는 카테고리의 뉴스 목록을 수집하여 console에 print하는 것까지 작업을 진행하였다.

 

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

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

code-study.tistory.com

이번 포스팅에서는 수집한 데이터를 단순히 print하여 휘발시키는 것이 아닌 DB에 입력하는 것까지 진행해보려고 한다. 


먼저 테이블 구조를 설계해야하는데, 복잡한 구조가 아니기때문에 테이블 명세서 초안을 아래와 같이 간단하게 작성한다. (사실 복잡한 구조의 테이블 설계는 해본적도 없다. ^^) 테이블 명세서는 양식이 정해져있는 것이 아니기 때문에 익숙한 양식으로 작성해주면 된다. 다른사람이 봤을때에도 별다른 의문이 안생기도록 작성해주는 것이 좋다. 

부동산 뉴스를 수집할 TBL_LAND_NEWS_LIST 테이블 설계

이제 위 명세서를 기반으로 실제 테이블을 생성한다. (MariaDB)

CREATE TABLE TBL_LAND_NEWS_LIST (
	NEWS_NO 		INT		AUTO_INCREMENT PRIMARY KEY,
	NEWS_CATEGORY		INT,									
	MEDIA_CODE 		VARCHAR(50),							
	MEDIA_NAME		VARCHAR(100),							
	NEWS_CODE		VARCHAR(100)	UNIQUE KEY NOT NULL,	
	NEWS_TITLE		VARCHAR(1000),							
	NEWS_SUMMARY		TEXT,									
	NEWS_CONTENT		TEXT,									
	NEWS_THUMB_URL		VARCHAR(1000),							
	NEWS_URL		VARCHAR(500),							
	NEWS_REG_DATE		DATETIME,								
	REG_DATE		DATETIME						
) DEFAULT CHARSET=utf8;

테이블 생성 후에는 다시 파이썬 코드로 돌아와 DB Table에 데이터를 Insert해줄 함수를 생성해준다. 파라미터로 들어가는 dbconn과 cursor는 DB 접속정보이고, data는 각 뉴스 수집 단계에서 가공해서 넘겨줄 data이다. 

# 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(f'**** [{{data['news_title']}}] DB Insert 완료! ')

 

그럼 다시 뉴스를 수집하는 단계로 돌아가서 수집된 데이터를 insert_data 함수에 파라미터로 넘길 수 있게 dict형태로 가공해보자. (부동산 일반만 예시로 들었다, 나머지도 동일하게 메소드를 수정하되 뉴스 카테고리만 각각에 맞게 추가로 수정해준다. (부동산 일반 :1, 부동산 정책 :3, 부동산 분양 :5) 
추가로, 앞서 데이터를 수집할때 뉴스 기사별로 유니크한 뉴스코드를 수집하는 소스를 빼먹었는데 그것도 추가해준다. 이 뉴스코드는 중복입력을 막는데 쓰일 예정이다.

유니크한 뉴스코드

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 = news.select_one('.text_area h3').get_text().strip()
            # 뉴스 요약
            summary = 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
            }

DB Table도 생성했고, DB Insert할 함수도 생성했고, 데이터 가공도 됐으니 데이터 입력을 실행해볼 단계이다. 사실 DB 서버 구축 등의 과정이 생략되어있는데 (이 과정이 제일 험난한 과정..) 이런 것 하나하나를 전부 포스팅에 담으며 프로젝트를 진행하면 내가 포기할 것 같아서 이는 나중에 별도로 포스팅 해볼 생각이다. (사실 나도 한번밖에 안해봤고, 정말 고생을 많이 했던 기억이 난다. 오류 > 구글링 > 오류 > 구글링 무한 loop... ㅠㅠ) 

그럼 다음 포스팅에서 수집한 뉴스를 DB에 담아보자!


현재까지 전체 소스는 아래와 같다.

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 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) : 
        self.encoding = get_encoding('https://sedaily.com')
    
    # 부동산 일반
    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 = news.select_one('.text_area h3').get_text().strip()
            # 뉴스 요약
            summary = 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
            }
        
        
    # 부동산 정책
    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 = news.select_one('.text_area h3').get_text().strip()
            # 뉴스 요약
            summary = 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
            }
            
    # 부동산 분양 정보
    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 = news.select_one('.text_area h3').get_text().strip()
            # 뉴스 요약
            summary = 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
            }
            
# 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 완료! ')


# 서울경제
GetSedaily = GetSedaily()
GetSedailyNormal = GetSedaily.normal()
GetSedailyPolicy = GetSedaily.policy()
GetSedailyParcelOut = GetSedaily.parcel_out()

 

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

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

code-study.tistory.com

 

반응형
Comments