Programming/Projects

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

Brian Go 2021. 9. 19. 23:39

 

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

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

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

 

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

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

를 해야 한다...

 

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

 

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값을 이용하는 방법을 체득한 것 같아서 좋구만.