일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- CSS로딩
- vueJS
- 자기개발
- 파이썬
- javascript
- CSS애니메이션
- css규칙
- 스타일가이드
- @keyframes
- css rule
- to do list
- vuejs입문
- 구글스타일가이드
- html제거
- google style guide
- 프레임워크
- Vue.js
- 코딩컨밴션
- 구글CSS
- 자바스크립트
- 뉴스수집
- 투두리스트
- python
- 로딩UI
- vue-cli
- 웹스크래핑
- MariaDB
- 코딩규칙
- 코딩가이드
- 개발회고
- Today
- Total
코드공부방
파이썬으로 최신 부동산 뉴스를 모아서 보자! (4) (웹 크롤링/스크래핑) 본문
파이썬으로 최신 부동산 뉴스를 모아서 보자!
(웹 크롤링/스크래핑) (4)
이제 특정 언론사에서 뉴스를 수집하여 카테고리별로 DB에 저장하는 것까진 완료되었다. "뉴스 수집" 과정의 남은 과제는 수집된 뉴스 목록의 상세 내용을 수집하여 DB에 넣어주기만 하면 된다. 프로세스는 간단하다. 뉴스 목록 테이블 (TBL_LAND_NEWS_LIST)에서 NEWS_CONTENT Column의 값이 "수집 중입니다."인 row의 뉴스 URL값을 가져와 한번씩 조회하여 뉴스 상세 내용을 가져와 다시 뉴스 목록 테이블의 NEWS_CONTENT Column의 값을 UPDATE해줄 예정이다.
(좀 더 깔끔한 방법은 뉴스 목록 테이블에 DETAIL_STATUS라는 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에도 뉴스 상세 내용이 잘 들어왔는지 확인해보자.
수집 후 확인해보니 기사마다 "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()
'생산 > 부동산뉴스모아' 카테고리의 다른 글
파이썬으로 최신 부동산 뉴스를 모아서 보자! (3) (웹 크롤링/스크래핑) (0) | 2021.10.04 |
---|---|
파이썬으로 최신 부동산 뉴스를 모아서 보자! (2) (웹 크롤링/스크래핑) (0) | 2021.10.03 |
파이썬으로 최신 부동산 뉴스를 모아서 보자! (1) (웹 크롤링/스크래핑) (0) | 2021.10.02 |