본문 바로가기
Programming/Projects

[Python] 뉴스 스크래핑 프로그램 만들기 (2)

by Brian Go 2021. 9. 19.

 

대망의 번거로운 짓거리의 시작...

뉴스 가져오는 함수에 프레임 생성을 넣고, 버튼을 누르면 다시 사라지는 메커니즘을 구현해야 한다.

거기에 버튼 또한 반복적으로 만들어야 하니 귀찮지 아니할 수 없다.

 

정리해보자면, 스크래핑 함수 자체는 동일하게 만들 것이지만,

버튼을 누르는 순간 변수 생성, 인터페이스에 추가, 링크 객체를 생성하며 거기에 링크 부여

를 해야 한다...

 

우선 함수를 정의해주고, 링크를 편하게 만들기 위해 클래스를 만들어주자.

 

def scrape_headline_news(): #오늘의 뉴스 스크래핑해오기
    global todays_news

    todays_news.delete(0,END) #우선 기존 내용 삭제
    todays_news.insert(END, '[오늘의 뉴스]')
    todays_news.insert(END, '')

    lines = [] #기사가 들어갈 리스트
    links = [] #링크가 들어갈 리스트
    url = 'https://news.naver.com'
    soup = create_soup(url)
    news_list = soup.find('ul', attrs={'class':'hdline_article_list'}).find_all('li', limit=5) #limit은 검색갯수 제한
    for idx, news in enumerate(news_list, start=1):
        title = news.find('a').get_text().strip()
        link = url + news.find('a')['href']
        lines.append('{}.'.format(idx)+' '+title) #기사의 제목을 lines 리스트에 추가
        links.append(link) #링크를 links 리스트에 추가

    [todays_news.insert(END, line) for line in lines] #한줄 for문으로 lines 의 line 모두 추가

 

함수는 이런 식으로 만들어줄 것이고, 링크는 links 변수가 결정되어야 설정할 수 있으니 그 정의는 아래쪽에 하자.

링크가 들어갈 프레임을 먼저 정해주면 되는데, 링크 프레임이 이미 존재한다면 삭제해야 하기 때문에

함수 바깥쪽에 link_frame_exists라는 다소 직관적인 이름의 변수를 정의해주겠다.

처음 1회의 버튼 클릭에만 프레임이 없을 것이므로 False로 설정하고

버튼을 한 번이라도 누르면 True가 되는 방향으로 설정하자.

 

headline_frame_exists = False #헤드라인 뉴스의 프레임이 기존에 존재하는가? 거짓
def scrape_headline_news():

    global todays_news, headline_frame_exists
    #------------!!!!은 이미 전에 작성한 코드들!------------------
    !!!!todays_news_link_frame = LabelFrame(todays_news_frame, text='뉴스 링크')
    !!!!todays_news_link_frame.pack(pady=7, padx=7, ipady=5, ipadx=5)

    !!!!if headline_frame_exists == True:
        todays_news_link_frame.destroy() #이미 존재한다면 프레임 통째로 삭제

    !!!!else:
        headline_frame_exists = True #처음 1회라면 변수를 '존재함'으로 설정 

    todays_news.delete(0,END)
    todays_news.insert(END, '[오늘의 뉴스]')
    todays_news.insert(END, '')

    lines = []
    links = []
    url = 'https://news.naver.com'
    soup = create_soup(url)
    news_list = soup.find('ul', attrs={'class':'hdline_article_list'}).find_all('li', limit=5) #limit은 검색갯수 제한
    for idx, news in enumerate(news_list, start=1):
        title = news.find('a').get_text().strip()
        link = url + news.find('a')['href']
        lines.append('{}.'.format(idx)+' '+title)
        links.append(link)

    [todays_news.insert(END,line) for line in lines]

 

 

이렇게 느낌표를 붙인 줄을 추가해주면 된다. 이러면 버튼을 연속으로 눌러도 링크 프레임이 계속해서 생기지 않을 것이다.

 

링크를 만드는 방법은 생각보다는 어렵지 않다.

일단 레이블을 만들고, 그것들을 bind함수를 이용해 <Button-1>(마우스 좌클릭)과 lambda를 이용한 함수로 링크를 열으라고 명령해주면 된다.

bind 함수는 bind(이벤트, 이벤트 시 실행시킬 함수) 꼴로 넣어주면 작동한다.

 

def callback(url):
    webbrowser.open_new(url)
a = Label(link_frame, text=text, fg=blue, cursor='hand2')
a.bind('<Button-1>', lambda x: callback(Link))

 

이런 느낌? 하지만 Label을 5개씩 만들어줘야 하기 때문에

(위에서 뉴스 제목을 5개씩만 가져오도록 했으므로)

오늘의 뉴스 + 스포츠 뉴스

총 10개의 버튼을 이렇게 일일이 해줘야 한다니...

 

이럴때 쓰라고 빵틀이 있는 게 아닌가. 클래스를 이용하여 link를 정의해보자.

 

class moveto :
    def __init__(self, link_frame, text, idx, links):
        #인자는 링크가 들어갈 프레임, 링크 제목, links의 몇번째 항인지, links 리스트
        self = Label(link_frame, text=text, fg='blue', cursor='hand2') #객체를 Label로 정의
        link = links[idx] #링크는 함수 내에서 정의한 links 의 idx번째 항
        self.pack(padx=5, pady=5)
        self.bind('<Button-1>', lambda e:callback(link)) #Label의 링크화

 

이러면 tkinter에서 링크를 만들 수 있다.

그럼 이놈을 아까 만든 link_frame 안에만 만들어주면 되시겠다.

어차피 얘들은 프레임이 사라지면 같이 사라지므로

(destroy 함수는 프레임 내부의 모든 것을 같이 삭제한다.)

만드는 것만 신경써주면 될 것 같다.

 

함수의 제일 마지막에 이것을 더해주면 된다.

 

    moveto1 = moveto(todays_news_link_frame, '1번 기사 링크', 0, links)
    moveto2 = moveto(todays_news_link_frame, '2번 기사 링크', 1, links)
    moveto3 = moveto(todays_news_link_frame, '3번 기사 링크', 2, links)
    moveto4 = moveto(todays_news_link_frame, '4번 기사 링크', 3, links)
    moveto5 = moveto(todays_news_link_frame, '5번 기사 링크', 4, links)

최종적인 함수는

def scrape_headline_news():

    global todays_news, headline_frame_exists
    
    todays_news_link_frame = LabelFrame(todays_news_frame, text='뉴스 링크')
    todays_news_link_frame.pack(pady=7, padx=7, ipady=5, ipadx=5)

    if headline_frame_exists == True: #프레임 존재하면 삭제
        todays_news_link_frame.destroy()

    else: #프레임 존재하지 않으면 존재함으로 바꿈
        headline_frame_exists = True

    todays_news.delete(0,END)
    todays_news.insert(END, '[오늘의 뉴스]')
    todays_news.insert(END, '')

    lines = []
    links = []
    url = 'https://news.naver.com'
    soup = create_soup(url)
    news_list = soup.find('ul', attrs={'class':'hdline_article_list'}).find_all('li', limit=5) #limit은 검색갯수 제한
    for idx, news in enumerate(news_list, start=1):
        title = news.find('a').get_text().strip()
        link = url + news.find('a')['href']
        lines.append('{}.'.format(idx)+' '+title)
        lines.append('  ') #공백 만들기
        links.append(link)

    [todays_news.insert(END,line) for line in lines]
        
    moveto1 = moveto(todays_news_link_frame, '1번 기사 링크', 0, links)
    moveto2 = moveto(todays_news_link_frame, '2번 기사 링크', 1, links)
    moveto3 = moveto(todays_news_link_frame, '3번 기사 링크', 2, links)
    moveto4 = moveto(todays_news_link_frame, '4번 기사 링크', 3, links)
    moveto5 = moveto(todays_news_link_frame, '5번 기사 링크', 4, links)

 

같은 방법으로 스포츠 뉴스 스크래핑도 만들어주면 끝.

 

전체 코드는

import webbrowser
from tkinter import *
from bs4 import BeautifulSoup
import requests
import datetime

headline_frame_exists = False

sports_frame_exists = False


class moveto :
    def __init__(self, link_frame, text, idx, links):
        self = Label(link_frame, text=text, fg='blue', cursor='hand2') 
        link = links[idx]
        self.pack(padx=5, pady=5)
        self.bind('<Button-1>', lambda e:callback(link))

def create_soup(url):
    headers = {'User-Agent':'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36'}
    res= requests.get(url, headers=headers)
    res.raise_for_status()
    soup = BeautifulSoup(res.text, 'lxml')
    return soup

def callback(url):
    webbrowser.open_new(url)

def scrape_weather():

    global weather_news
    try:
        weather_news.delete(0,END)
        url = 'https://search.naver.com/search.naver?where=nexearch&sm=top_sug.asiw&fbm=0&acr=1&acq=%EC%84%9C%EC%9A%B8+%EB%82%A0%EC%94%A8&qdt=0&ie=utf8&acir=1&query=%EC%84%9C%EC%9A%B8+%EB%82%A0%EC%94%A8'
        soup = create_soup(url)    
        #날씨, 어제보다 00도 높아요
        cast = soup.find('p', attrs={'class':'cast_txt'}).get_text()
        #현재 00도 (최고, 최저)
        cur_temp = soup.find('p', attrs={'class' : 'info_temperature'}).get_text().replace('도씨', '')
        max_temp = soup.find('span', attrs={'class':'max'}).get_text()
        min_temp = soup.find('span', attrs={'class':'min'}).get_text()
        #강수확률
        morning_rainrate = soup.find('span', attrs={'class':'point_time morning'}).get_text().strip()
        afternoon_rainrate = soup.find('span', attrs={'class':'point_time afternoon'}).get_text().strip()

        #미세먼지
        dust = soup.find('dl', attrs={'class':'indicator'})
        pm10 = dust.find_all('dd')[0].get_text() #미세먼지
        pm25 = dust.find_all('dd')[1].get_text() #초미세먼지

        #출력
        lines = [cast,'현재 {} (최저{} / 최고 {})'.format(cur_temp, min_temp, max_temp),
                '오전 {} / 오후 {}'.format(morning_rainrate, afternoon_rainrate),
                '미세먼지 {}'.format(pm10),
                '초미세먼지 {}'.format(pm25)]
        weather_news.insert(END, str(datetime.datetime.today().strftime("%Y-%m-%d")+': 오늘의 날씨입니다.'))
        for line in lines:
            weather_news.insert(END, line)
            weather_news.insert(END, '')

    except Exception as e:
        err = '오류가 발생하였습니다.'
        weather_news.insert(END, err)
        weather_news.insert(END, e)

def scrape_headline_news():

    global todays_news, headline_frame_exists
    
    todays_news_link_frame = LabelFrame(todays_news_frame, text='뉴스 링크')
    todays_news_link_frame.pack(pady=7, padx=7, ipady=5, ipadx=5)

    if headline_frame_exists == True:
        todays_news_link_frame.destroy()

    else:
        headline_frame_exists = True

    todays_news.delete(0,END)
    todays_news.insert(END, '[오늘의 뉴스]')
    todays_news.insert(END, '')

    lines = []
    links = []
    url = 'https://news.naver.com'
    soup = create_soup(url)
    news_list = soup.find('ul', attrs={'class':'hdline_article_list'}).find_all('li', limit=5) #limit은 검색갯수 제한
    for idx, news in enumerate(news_list, start=1):
        title = news.find('a').get_text().strip()
        link = url + news.find('a')['href']
        lines.append('{}.'.format(idx)+' '+title)
        lines.append('  ')
        links.append(link)

    [todays_news.insert(END,line) for line in lines]
        
    moveto1 = moveto(todays_news_link_frame, '1번 기사 링크', 0, links)
    moveto2 = moveto(todays_news_link_frame, '2번 기사 링크', 1, links)
    moveto3 = moveto(todays_news_link_frame, '3번 기사 링크', 2, links)
    moveto4 = moveto(todays_news_link_frame, '4번 기사 링크', 3, links)
    moveto5 = moveto(todays_news_link_frame, '5번 기사 링크', 4, links)

    

def scrape_sports_news():

    global sports_news, sports_news_frame, sports_frame_exists


    sports_news_link_frame = LabelFrame(sports_news_frame, text='스포츠 링크')
    sports_news_link_frame.pack(side='right', pady=7, padx=7, ipady=5, ipadx=5)
    
    if sports_frame_exists == True:
        sports_news_link_frame.destroy()

    else:
        sports_frame_exists = True

    sports_news.delete(0, END)
    sports_news.insert(END, '[스포츠 뉴스]')
    sports_news.insert(END, '')
    lines = []
    links = []

    url = 'https://sports.news.naver.com/index.nhn'
    soup = create_soup(url)
            
    news_list = soup.find('ul', attrs={'class':'today_list'}).find_all('li', limit=5)
    for idx, news in enumerate(news_list, start=1):
                title = news.find('strong').get_text().strip()
                link = 'https://sports.news.naver.com' + news.find('a')['href']
                lines.append('{}. {}'.format(idx, title))
                lines.append('  ')
                links.append(link)
    [sports_news.insert(END, line) for line in lines]

   
    moveto1 = moveto(sports_news_link_frame,'1번 기사 링크', 0, links)
    moveto2 = moveto(sports_news_link_frame, '2번 기사 링크', 1, links)
    moveto3 = moveto(sports_news_link_frame, '3번 기사 링크', 2, links)
    moveto4 = moveto(sports_news_link_frame, '4번 기사 링크', 3, links)
    moveto5 = moveto(sports_news_link_frame, '5번 기사 링크', 4, links)

def scrap():
        scrape_weather() #날씨 가져오기
        scrape_headline_news() #헤드라인 뉴스 정보 가져오기
        scrape_sports_news()

root = Tk()
root.title('오늘의 이슈')
root.resizable(False, False)


btn_frame = Frame(root)
btn_frame.pack()
scrap_btn = Button(btn_frame, text='뉴스 가져오기', width=10, command=scrap)
scrap_btn.pack(padx=5, pady=5)

weather_news_frame = LabelFrame(root, text='오늘의 날씨', width=50)
weather_news_frame.pack(pady=5, padx=5)

weather_news = Listbox(weather_news_frame, width=50)
weather_news.pack(fill='x', side='top', pady=10, padx=10)

todays_news_frame = LabelFrame(root, width=50, text='오늘의 뉴스')
todays_news_frame.pack(side='top',pady=5, padx=5)

todays_news = Listbox(todays_news_frame, width=50)
todays_news.pack(side='left', fill='x', padx=10, pady=10)

sports_news_frame = LabelFrame(root, text='스포츠 뉴스')
sports_news_frame.pack(side='top',padx=5, pady=5)

sports_news= Listbox(sports_news_frame, width=50)
sports_news.pack(side='left', fill='x', padx=10, pady=10)

root.mainloop()

굉장히 길다 ...

 

 

잘 돌아가는 것을 볼 수 있다.

만든 지 조금 된 코드라서 날씨가 안 가져와지는데(네이버의 코드 변경이 있을 수 있음)

저것만 손보면 될 듯!


귀찮으니 보수는 나중에 하겠다.

 

이번 프로젝트를 하면서 난관은 링크의 레이블을 만들었다 지웠다 하는 일이었는데

boolean 값을 설정해줘서 처리했다 !

실행 시 처음에만 기존 레이블이 없을 테니깐..

토글식으로 boolean값을 이용하는 방법을 체득한 것 같아서 좋구만.

 

댓글