코드공부방

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

생산/부동산뉴스모아

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

:- ) 2021. 10. 2. 22:34
반응형

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


벌써 2021년 10월이다. 맙소사.. 2020년 12월 28일에 회고 글을 작성하며 2021년엔 많은 것들을 이뤄보자라는 생각을 했었는데.. 무엇을 해냈는가.. 이 블로그도 열심히 해보려고 했는데.. 2021년에 작성된 글은 고작 4개이다. (반성하자..)

뒤늦게 정신을 차리고 작은 프로젝트라도 시작해야겠다 싶어 뭘 해볼까 고민하다가 요즘 관심이 있는 부동산 분야의 뉴스를 여기저기서 모아와 한 곳에서 보여주는 웹페이지를 만들어보기로 했다. 단순히 긁어서 뉴스를 제공하는 것도 좋지만 모아와서 유의미한 정보를 제공해주고 싶기도 하다. 하지만 생각이 많아지면 당장 실행이 안될 것 같아 일단 부동산 뉴스부터 긁어보기로 했다. 먼저 검색을 통해 부동산 뉴스를 가져올 수 있는 언론사를 찾아보았다.


# 부동산 > 일반
https://www.segye.com/newsList/0101030700000
https://www.sedaily.com/NewsList/GB07
http://www.newstomato.com/CateNewsList.aspx?cate=1600&subCate=1606
http://www.newstomato.com/CateNewsList.aspx?cate=1600&subCate=1605
https://www.nocutnews.co.kr/news/industry/list?c2=530
https://www.news1.kr/categories/?18

# 부동산 > 정책
https://www.newspim.com/news/lists/?category_cd=104010
http://www.newstomato.com/CateNewsList.aspx?cate=1600&subCate=1602
https://www.sedaily.com/NewsList/GB01

# 부동산 > 분양
https://www.newspim.com/news/lists/?category_cd=104030
https://www.sedaily.com/NewsList/GB02​

일단 크게 3가지 카테고리로 일반, 정책, 분양 으로 나누어 뉴스를 가져올만한 URL을 위와 같이 메모장에 정리해두었다. 이제 위 페이지들을 접속하여 원하는 정보(뉴스)를 수집하는 코드를 작성하면 된다. 


1. 서울경제

먼저 3가지 카테고리가 전부 있는 서울경제(https://www.sedaily.com) 사이트부터 시작해보자. 이런 경우 한 카테고리만 분석이 되면 나머지 카테고리는 같은 구조로 되어있기 때문에 대부분 공짜(?)로 가져올 수 있다. 먼저 수집 접근을 허용하는지 확인해보았다. 도메인 뒤에 robots.txt를 붙이면 로봇의 접근 허용 여부를 표시해두는데, 서울경제에서는 모든 페이지의 접근을 허용하고 있었다. 하지만 최대한 서버에 큰 부하를 주지 않고 가져오기로 한다.

 

서울경제 사이트의 robots.txt

가져오려는 방식은 이미 작성되어 있는 오래된 기존의 뉴스는 과감하게 버리고 항상 카테고리별 첫(1)페이지만 조회를 할 것이다. 서울경제는 카테고리별 1페이지당 15개의 뉴스를 제공하고 있는데, 난 하루에 3회정도 이 페이지를 조회하여 새로 올라온 뉴스만 가져올 것이다. 이런 방식으로 서버에는 거의 부하를 주지 않을 수 있다.


- 서울경제 > 부동산 일반 (https://sedaily.com/NewsList/GB07)

사이트에서 뉴스를 가져오는 코드를 작성하기 전에 먼저 뉴스 목록 페이지를 확인해보자. 

서울경제 부동산일반 목록 페이지

특별할 것은 없어보인다. 수집할 정보는 썸네일 이미지의 경로, 뉴스 제목, 작성일자, 작성시간, 기사 요약 내용이다. 목록에는 썸네일 이미지가 있는 경우와 없는 경우로 나눠진 것으로 보인다. (분기처리 필요)

그럼 이제 본격적으로 뉴스를 수집하는 코드를 작성해보자. 코드 작성은 주피터 노트북에서 작업 및 테스트 후 로컬에 py파일로 저장할 생각이다. 

 

 

 

 

 


먼저 필요한 라이브러리를 불러온다.

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

웹 스크래핑 작업을 할때 항상 위 라이브러리들은 필수로 불러오는 편이다. BeautifulSoup을 사용해 웹페이지 소스를 가져오기 위해 함수를 작성한다.

# 사이트 소스 가져오기 (HTML)
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

URL로 접근해보자. 아, 그 전에 해당 웹사이트의 인코딩 방식을 알아야한다. 요즘은 대부분 "UTF-8" 방식으로 인코딩 하긴 하지만 언론사 사이트에서는 "EUC-KR"도 심심치않게 발견할 수 있다. 그렇게되면 한글이 깨진채로 수집이 되기때문에 확인을 해줘야한다. 사실 인코딩 방식이란게 자주 바뀌는 것은 아니기때문에 직접 사이트에서 마우스 우클릭 > "소스보기"로 직접 확인해서 작성해도 무방하지만, 스마트하게 인코딩 방식을 확인하는 함수를 만들어 사용하기로 한다.

웹페이지 소스보기로 확인해보니 서울경제 웹사이트의 인코딩방식은 "utf-8"이란 것을 알 수 있다.

# 웹사이트 인코딩 방식 확인
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

get_encoding 함수의 파라미터로 서울경제 URL을 넣고 테스트 해보니 서울경제 웹사이트의 인코딩 방식은 "UTF-8" 방식이란 것을 알 수 있다.

get_encoding('https://sedaily.com/NewsList/GB07')

UTF-8

이제 가져올 준비가 끝났으니 get_soup함수를 호출하여 페이지의 html코드를 가져와보자.

url = 'https://sedaily.com/NewsList/GB07'
encoding = get_encoding(url)
get_soup(url, encoding)

서울경제 부동산일반 목록 페이지의 HTML코드를 잘 가져온 것을 확인할 수 있다.

HTML코드를 잘 가져왔다. 이후에는 사이트의 마크업 구조를 확인하며 원하는 정보를 가져오기만 하면 된다.

브라우저의 개발자 도구를 활용하여 마크업 구조를 확인할 수 있다.

내가 가져오려고 하는 뉴스목록은 "sub_news_list"라는 클래스를 가진 "ul"태그에 잘 담겨져 있었다. 이런식으로 정보를 하나하나 찾아가며 가져오면 된다. 썸네일 이미지가 없는 경우는 썸네일 URL에 "none"이라는 값을 넣기로 정했다.

서울경제 웹사이트는 정말 너무나 친절한 깔끔한 코드 그 자체였다.

url = 'https://sedaily.com/NewsList/GB07'
encoding = get_encoding(url)
soup = get_soup(url, encoding)

news_list = soup.select('.sub_news_list li')
for news in news_list : 
    # 뉴스 기사 제목
    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
    # 뉴스 작성 일자
    regist_date = news.select_one('.text_info .date').get_text().strip()
    # 뉴스 작성 시간
    regist_time = news.select_one('.text_info .time').get_text().strip()
    
    print(f'[ 기사제목 : {title} ]')
    print(f'[ 뉴스 URL : {url} ]')
    print(summary)
    print(thumbnail_url, regist_date, regist_time)
    print('ㅡ'* 50)

너무나도 친절한 웹페이지 코드 덕분에 한번에 깔끔하게 가져올 수 있었다. 서울경제 굳굳!

 

 

 

 

 

서울경제의 나머지 카테고리(정책, 분양)도 위 코드에 url만 바꿔주면 깔끔하게 가져올 수 있다. 이어서 다른 언론사의 뉴스도 가져와야하기때문에 언론사별로 class로 그루핑 해두기로 했다.

# 서울경제
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 : 
            # 뉴스 기사 제목
            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
            # 뉴스 작성 일자
            regist_date = news.select_one('.text_info .date').get_text().strip()
            # 뉴스 작성 시간
            regist_time = news.select_one('.text_info .time').get_text().strip()

            print(f'[ 기사제목 : {title} ]')
            print(f'[ 뉴스 URL : {url} ]')
            print(summary)
            print(thumbnail_url, regist_date, regist_time)
            print('ㅡ'* 50)
        
        
    # 부동산 정책
    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 : 
            # 뉴스 기사 제목
            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
            # 뉴스 작성 일자
            regist_date = news.select_one('.text_info .date').get_text().strip()
            # 뉴스 작성 시간
            regist_time = news.select_one('.text_info .time').get_text().strip()

            print(f'[ 기사제목 : {title} ]')
            print(f'[ 뉴스 URL : {url} ]')
            print(summary)
            print(thumbnail_url, regist_date, regist_time)
            print('ㅡ'* 50)
            
    # 부동산 분양 정보
    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 : 
            # 뉴스 기사 제목
            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
            # 뉴스 작성 일자
            regist_date = news.select_one('.text_info .date').get_text().strip()
            # 뉴스 작성 시간
            regist_time = news.select_one('.text_info .time').get_text().strip()

            print(f'[ 기사제목 : {title} ]')
            print(f'[ 뉴스 URL : {url} ]')
            print(summary)
            print(thumbnail_url, regist_date, regist_time)
            print('ㅡ'* 50)
            

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

현재는 수집한 데이터를 print만 하고있어 휘발성이지만, 데이터를 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 : 
            # 뉴스 기사 제목
            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
            # 뉴스 작성 일자
            regist_date = news.select_one('.text_info .date').get_text().strip()
            # 뉴스 작성 시간
            regist_time = news.select_one('.text_info .time').get_text().strip()

            print(f'[ 기사제목 : {title} ]')
            print(f'[ 뉴스 URL : {url} ]')
            print(summary)
            print(thumbnail_url, regist_date, regist_time)
            print('ㅡ'* 50)
        
        
    # 부동산 정책
    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 : 
            # 뉴스 기사 제목
            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
            # 뉴스 작성 일자
            regist_date = news.select_one('.text_info .date').get_text().strip()
            # 뉴스 작성 시간
            regist_time = news.select_one('.text_info .time').get_text().strip()

            print(f'[ 기사제목 : {title} ]')
            print(f'[ 뉴스 URL : {url} ]')
            print(summary)
            print(thumbnail_url, regist_date, regist_time)
            print('ㅡ'* 50)
            
    # 부동산 분양 정보
    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 : 
            # 뉴스 기사 제목
            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
            # 뉴스 작성 일자
            regist_date = news.select_one('.text_info .date').get_text().strip()
            # 뉴스 작성 시간
            regist_time = news.select_one('.text_info .time').get_text().strip()

            print(f'[ 기사제목 : {title} ]')
            print(f'[ 뉴스 URL : {url} ]')
            print(summary)
            print(thumbnail_url, regist_date, regist_time)
            print('ㅡ'* 50)
            

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

 

 

2020년 회고 (웹 UI개발자이자 Python 입문자)

첫 회고를 작성해본다. 2020년을 시작으로 매년 해볼 생각이다. 며칠 전 일도 기억이 안나는데 과연 1년을 되돌아본다는게 가능할지 모르겠지만, 그래서 아마 이 글은 몇 번의 임시 저장을 통해

code-study.tistory.com

 

반응형
Comments