보안세상

네이트 뉴스기사 크롤링 프로그램에 대해서 알아보자.(파이썬 코드 공개, 주석) 본문

내 생각

네이트 뉴스기사 크롤링 프로그램에 대해서 알아보자.(파이썬 코드 공개, 주석)

똔민 2023. 8. 10. 09:56
반응형

파이썬이란 컴퓨터 언어 중 하나로 웹페이지나 파일 등을 수집하거나 데이터를 추출하는데 유용하게 쓰이는 프로그래밍 언어입니다. 최근엔 머신러닝과 딥러닝 분야에서도 많이 활용되고 있죠. 이러한 파이썬을 이용해서 우리나라 사람들이 자주 방문하는 사이트들을 크롤링 해볼건데요~ 지금부터 저와 함께 차근차근 배워보도록 하겠습니다.

크롤링(Crawling) 이란 뭔가요?
크롤링이라는 단어 자체는 ‘긁어모으다’라는 뜻이지만, 실제로는 웹 페이지 내의 정보를 긁어오는 작업을 의미합니다. 예를 들어 네이버 뉴스 기사 제목 밑에 나오는 댓글 목록을 가져오는 경우라고 하면, 해당 기사의 URL 주소를 복사 한 후 붙여넣기 하여 원하는 내용을 가져오면 됩니다. 이 때 특정 키워드를 입력하면 관련된 다른 글 들도 가져올 수 있고, 여러 개의 링크를 동시에 가져올 수도 있습니다.

 

일반적으로 크롤링 과정은 다음과 같은데요

1. URL 추출: 크롤링 시작점으로부터 웹 페이지의 URL을 추출합니다.
2. 웹 페이지 요청: 추출한 URL을 기반으로 해당 웹 페이지로 요청을 보냅니다. 이때 HTTP GET 또는 POST 요청을 사용합니다.
3. 웹 페이지 내용 수집: 웹 서버에서 응답을 받아 웹 페이지의 HTML 코드를 가져옵니다.
4. 데이터 추출: 가져온 HTML 코드를 분석하여 필요한 정보를 추출합니다. 이때 주로 웹 페이지의 DOM(Document Object Model)을 파싱하여 필요한 데이터를 선택합니다.
5. 데이터 처리 및 저장: 추출한 데이터를 필요한 형식으로 가공하거나 저장합니다. 이때 데이터베이스에 저장하거나 파일로 저장하는 등 다양한 방법을 사용할 수 있습니다.

반응형

< 원본 코드 >

# -*- coding: utf-8 -*-
"""
Created on Tue Mar 28 13:26:23 2023

@author: AI
"""

import os
import webbrowser
import requests
from bs4 import BeautifulSoup
import textwrap
import random
import tkinter as tk
import time

from tkinter import messagebox, ttk
from PIL import Image, ImageTk
from gensim.summarization import summarize


def random_color():
    r = random.randint(0, 255)
    g = random.randint(0, 255)
    b = random.randint(0, 255)
    return f"#{r:02x}{g:02x}{b:02x}"


def download_image(img_url, file_name):
    response = requests.get(img_url)
    response.raise_for_status()

    with open(file_name, "wb") as file:
        file.write(response.content)

def on_news_click(url):
    webbrowser.open(url)

def create_news_frame(parent, index, title, source, time, file_name, url, summary):
    news_frame = ttk.Frame(parent, padding=10)
    title_label = ttk.Label(news_frame, text=f"{index}. {title}", font=("Arial", 12, "bold"), wraplength=600, cursor="hand2", foreground='blue')
    title_label.grid(row=0, column=0, sticky="w")
    title_label.bind("<Button-1>", lambda e: on_news_click(url))

    source_time_label = ttk.Label(news_frame, text=f"{source} ({time})", font=("Arial", 10))
    source_time_label.grid(row=1, column=0, sticky="w")

    if file_name:
        with Image.open(file_name) as img:
            img.thumbnail((200, 200))
            tk_img = ImageTk.PhotoImage(img)

        img_label = ttk.Label(news_frame, image=tk_img)
        img_label.image = tk_img
        img_label.grid(row=0, column=1, rowspan=2, padx=10)
        summary_label = ttk.Label(news_frame, text=summary, font=("Arial", 10), wraplength=600)
        summary_label.grid(row=2, column=0, sticky="w")
        news_frame.update_idletasks()
        frame_height = news_frame.winfo_reqheight()

        news_canvas_frame = news_canvas.create_window(0, (index - 1) * 195, anchor="nw", window=news_frame)
        news_canvas.itemconfigure(news_canvas_frame, height=max(news_canvas.winfo_height(), (index * 195)))
    else:
        summary_label = ttk.Label(news_frame, text=summary, font=("Arial", 10), wraplength=800)
        summary_label.grid(row=2, column=0, sticky="w")
        news_frame.update_idletasks()
        frame_height = news_frame.winfo_reqheight()

        news_canvas_frame = news_canvas.create_window(0, (index - 1) * 195, anchor="nw", window=news_frame)
        news_canvas.itemconfigure(news_canvas_frame, height=max(news_canvas.winfo_height(), (index * 195)))
        
    return news_frame


def extract_article_body(news_url):
    if not news_url.startswith('http'):
        news_url = 'http:' + news_url

    article_response = requests.get(news_url)
    article_response.raise_for_status()

    article_soup = BeautifulSoup(article_response.text, "html.parser")
    article_body_tag = article_soup.find("div", {"id": "realArtcContents"})

    if article_body_tag:
        return article_body_tag.get_text(strip=True)
    else:
        return "기사 내용을 가져올 수 없습니다."


def fetch_news():
    for widget in news_canvas.winfo_children():
        widget.destroy()

    url = "https://news.nate.com/rank/interest"
    response = requests.get(url)
    response.raise_for_status()

    soup = BeautifulSoup(response.text, "html.parser")
    news_list = soup.find_all("div", class_="mduSubjectList")

    # 이미지를 저장할 폴더를 생성합니다.
    image_folder = "nate_images"
    if not os.path.exists(image_folder):
        os.makedirs(image_folder)

    for index, news in enumerate(news_list, start=1):
        title = news.find("strong", class_="tit").get_text(strip=True)
        source = news.find("span", class_="medium").get_text(strip=True)
        time_tag = news.find("span", class_="time")
        img_tag = news.find("img")
        a_tag = news.find("a")

        if time_tag:
            time = time_tag.get_text(strip=True)
        else:
            time = "시간 정보 없음"

        if img_tag:
            img_url = "http:" + img_tag["src"]
            file_name = os.path.join(image_folder, f"{index}.jpg")
            download_image(img_url, file_name)
        else:
            file_name = None

        news_url = "http:" + a_tag["href"] if not a_tag["href"].startswith("http") else a_tag["href"]
        article_body = extract_article_body(news_url)
        content_response = requests.get(news_url)
        content_response.raise_for_status()
        content_soup = BeautifulSoup(content_response.text, "html.parser")
    
        content_tag = content_soup.select_one("#realArtcContents")  # content_tag 변수 선언

        # 기사 요약
        summary = "요약 정보 없음"  # summary 변수를 먼저 선언합니다.
        try:
            if content_tag:  # content_tag가 존재하는지 확인합니다.
                content_text = content_tag.get_text(strip=True)
                if len(content_text) > 100:
                    summary = summarize(content_text, word_count=50)
        except Exception as e:
            print(f"요약 중 오류 발생: {e}")

        news_frame = create_news_frame(news_canvas, index, title, source, time, file_name, news_url, summary)
        news_canvas.create_window(0, (index - 1) * 195, anchor="nw", window=news_frame)

def create_summary(title):
    words = title.split()
    if len(words) > 5:
        summary = ' '.join(words[:5]) + '...'
    else:
        summary = title
    return summary
            
        
def extract_summary(news_url):
    if not news_url.startswith('http'):
        news_url = 'http:' + news_url

    summary_response = requests.get(news_url)
    summary_response.raise_for_status()

    summary_soup = BeautifulSoup(summary_response.text, "html.parser")
    summary_tag = summary_soup.find("div", class_="summaryView")
    if summary_tag:
        return summary_tag.get_text(strip=True)
    else:
        return "요약 정보 없음"



def fetch_button_click():
    
    try:
        fetch_news()
        messagebox.showinfo("알림", "뉴스와 이미지를 가져왔습니다.")
    except Exception as e:
        messagebox.showerror("오류", f"오류가 발생했습니다: {e}")

def _on_mousewheel(event):
    delta = -1 * int(event.delta / 120)
    news_canvas.yview_scroll(delta, "units")


def generate_lotto_numbers():
    return sorted(random.sample(range(1, 46), 6))


def on_label_configure(event):
    widget = event.widget
    widget.create_oval(0, 0, event.width, event.height, fill=widget["bg"], outline=widget["bg"])



def show_lotto_numbers():
    lotto_numbers = generate_lotto_numbers()
    lotto_label.config(text=f"로또 번호: {', '.join(map(str, lotto_numbers))}")


def animate_lotto_numbers():
    for _ in range(20):
        lotto_numbers = generate_lotto_numbers()
        lotto_labels = []
        for idx, num in enumerate(lotto_numbers):
            label = tk.Canvas(lotto_frame, width=50, height=50, bg=random_color())  # 수정된 부분
            label.create_oval(0, 0, 50, 50, fill=label["bg"], outline=label["bg"])  # 수정된 부분
            label.create_text(25, 25, text=str(num), font=("Arial", 24, "bold"))  # 수정된 부분
            label.grid(row=0, column=idx, padx=5, pady=5)
            lotto_labels.append(label)
        app.update()
        time.sleep(0.1)

app = tk.Tk()
app.title("Nate 뉴스 크롤러(feat:SM)")
app.geometry("800x600")

# 배경색 변경
app.configure(bg="#f0f0f0")

frame = ttk.Frame(app, padding=10)
frame.pack(fill=tk.BOTH, expand=True)

fetch_button = ttk.Button(frame, text="뉴스 가져오기", command=fetch_button_click)
fetch_button.pack(pady=5)


lotto_frame = ttk.Frame(frame)
lotto_frame.pack(pady=5)
lotto_button = ttk.Button(frame, text="로또 번호 추출", command=animate_lotto_numbers)
lotto_button.pack(pady=5)

canvas_frame = ttk.Frame(frame)
canvas_frame.pack(fill=tk.BOTH, expand=True)

news_canvas = tk.Canvas(canvas_frame, bg="white")
news_canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)

scrollbar = ttk.Scrollbar(canvas_frame, orient=tk.VERTICAL, command=news_canvas.yview)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)

news_canvas.configure(yscrollcommand=scrollbar.set)
news_canvas.bind("<Configure>", lambda e: news_canvas.configure(scrollregion=news_canvas.bbox("all")))
news_canvas.bind_all("<MouseWheel>", _on_mousewheel)


# 버튼 스타일 변경
style = ttk.Style()
style.configure("TButton", font=("Arial", 10, "bold"), borderwidth=4)
style.map("TButton", foreground=[("pressed", "blue"), ("active", "green")], background=[("pressed", "!disabled", "black"), ("active", "white")])

app.mainloop()

< 코드 리뷰 > 

import os
import webbrowser
import requests
from bs4 import BeautifulSoup
import textwrap
import random
import tkinter as tk
import time

from tkinter import messagebox, ttk
from PIL import Image, ImageTk
from gensim.summarization import summarize

# 임의의 색상 생성 함수
def random_color():
    r = random.randint(0, 255)
    g = random.randint(0, 255)
    b = random.randint(0, 255)
    return f"#{r:02x}{g:02x}{b:02x}"

# 이미지 다운로드 함수
def download_image(img_url, file_name):
    response = requests.get(img_url)
    response.raise_for_status()

    with open(file_name, "wb") as file:
        file.write(response.content)

# 뉴스 클릭 시 웹 브라우저 열기 함수
def on_news_click(url):
    webbrowser.open(url)

# 뉴스 프레임 생성 함수
def create_news_frame(parent, index, title, source, time, file_name, url, summary):
    # 뉴스 프레임 생성
    news_frame = ttk.Frame(parent, padding=10)
    
    # 뉴스 제목 라벨 생성
    title_label = ttk.Label(news_frame, text=f"{index}. {title}", font=("Arial", 12, "bold"), wraplength=600, cursor="hand2", foreground='blue')
    title_label.grid(row=0, column=0, sticky="w")
    title_label.bind("<Button-1>", lambda e: on_news_click(url))  # 클릭 시 on_news_click 함수 호출
    
    # 뉴스 출처와 시간 정보 표시 라벨 생성
    source_time_label = ttk.Label(news_frame, text=f"{source} ({time})", font=("Arial", 10))
    source_time_label.grid(row=1, column=0, sticky="w")
    
    if file_name:
        # 이미지 파일이 있는 경우
        with Image.open(file_name) as img:
            img.thumbnail((200, 200))
            tk_img = ImageTk.PhotoImage(img)

        # 이미지 라벨 생성 및 설정
        img_label = ttk.Label(news_frame, image=tk_img)
        img_label.image = tk_img
        img_label.grid(row=0, column=1, rowspan=2, padx=10)
        
        # 뉴스 요약 내용 표시 라벨 생성
        summary_label = ttk.Label(news_frame, text=summary, font=("Arial", 10), wraplength=600)
        summary_label.grid(row=2, column=0, sticky="w")
        
        news_frame.update_idletasks()
        frame_height = news_frame.winfo_reqheight()

        # 뉴스 프레임을 캔버스에 추가하고 높이 조절
        news_canvas_frame = news_canvas.create_window(0, (index - 1) * 195, anchor="nw", window=news_frame)
        news_canvas.itemconfigure(news_canvas_frame, height=max(news_canvas.winfo_height(), (index * 195)))
    else:
        # 이미지 파일이 없는 경우
        # 뉴스 요약 내용 표시 라벨 생성
        summary_label = ttk.Label(news_frame, text=summary, font=("Arial", 10), wraplength=800)
        summary_label.grid(row=2, column=0, sticky="w")
        
        news_frame.update_idletasks()
        frame_height = news_frame.winfo_reqheight()

        # 뉴스 프레임을 캔버스에 추가하고 높이 조절
        news_canvas_frame = news_canvas.create_window(0, (index - 1) * 195, anchor="nw", window=news_frame)
        news_canvas.itemconfigure(news_canvas_frame, height=max(news_canvas.winfo_height(), (index * 195)))
        
    return news_frame

# 뉴스 기사 내용 추출 함수
def extract_article_body(news_url):
    if not news_url.startswith('http'):
        news_url = 'http:' + news_url

    # 기사 내용을 가져옵니다.
    article_response = requests.get(news_url)
    article_response.raise_for_status()

    # BeautifulSoup을 사용하여 HTML 파싱
    article_soup = BeautifulSoup(article_response.text, "html.parser")
    article_body_tag = article_soup.find("div", {"id": "realArtcContents"})

    if article_body_tag:
        return article_body_tag.get_text(strip=True)
    else:
        return "기사 내용을 가져올 수 없습니다."

# 뉴스 가져오기 함수
def fetch_news():
    # 이전에 표시되던 뉴스를 모두 삭제합니다.
    for widget in news_canvas.winfo_children():
        widget.destroy()

    url = "https://news.nate.com/rank/interest"
    response = requests.get(url)
    response.raise_for_status()

    soup = BeautifulSoup(response.text, "html.parser")
    news_list = soup.find_all("div", class_="mduSubjectList")
    
    # 이미지를 저장할 폴더를 생성합니다.
image_folder = "nate_images"
if not os.path.exists(image_folder):
    os.makedirs(image_folder)

# 뉴스 목록을 순회하며 정보를 가져와서 처리합니다.
for index, news in enumerate(news_list, start=1):
    # 기사 제목과 출처 정보 추출
    title = news.find("strong", class_="tit").get_text(strip=True)
    source = news.find("span", class_="medium").get_text(strip=True)
    time_tag = news.find("span", class_="time")
    img_tag = news.find("img")
    a_tag = news.find("a")

    if time_tag:
        time = time_tag.get_text(strip=True)
    else:
        time = "시간 정보 없음"

    # 이미지가 있는 경우 이미지 URL을 가져와 파일로 저장
    if img_tag:
        img_url = "http:" + img_tag["src"]
        file_name = os.path.join(image_folder, f"{index}.jpg")
        download_image(img_url, file_name)
    else:
        file_name = None

    # 기사 URL을 가져와서 기사 본문 내용과 요약 정보를 추출
    news_url = "http:" + a_tag["href"] if not a_tag["href"].startswith("http") else a_tag["href"]
    article_body = extract_article_body(news_url)
    content_response = requests.get(news_url)
    content_response.raise_for_status()
    content_soup = BeautifulSoup(content_response.text, "html.parser")
    
    content_tag = content_soup.select_one("#realArtcContents")  # content_tag 변수 선언

    # 기사 요약 정보 추출
    summary = "요약 정보 없음"  # 초기값으로 설정
    try:
        if content_tag:  # content_tag가 존재하는지 확인
            content_text = content_tag.get_text(strip=True)
            if len(content_text) > 100:  # 길이가 충분히 긴 경우에만 요약
                summary = summarize(content_text, word_count=50)
    except Exception as e:
        print(f"요약 중 오류 발생: {e}")

    # 뉴스 프레임 생성 함수 호출하여 뉴스 정보를 화면에 표시
    news_frame = create_news_frame(news_canvas, index, title, source, time, file_name, news_url, summary)
    # 뉴스 캔버스에 뉴스 프레임을 추가하고 위치 및 높이 설정
    news_canvas.create_window(0, (index - 1) * 195, anchor="nw", window=news_frame)

# 기사 요약 생성 함수
def create_summary(title):
    words = title.split()
    if len(words) > 5:
        summary = ' '.join(words[:5]) + '...'
    else:
        summary = title
    return summary
            
# 기사 요약 내용을 가져오는 함수
def extract_summary(news_url):
    if not news_url.startswith('http'):
        news_url = 'http:' + news_url

    summary_response = requests.get(news_url)
    summary_response.raise_for_status()

    summary_soup = BeautifulSoup(summary_response.text, "html.parser")
    summary_tag = summary_soup.find("div", class_="summaryView")
    if summary_tag:
        return summary_tag.get_text(strip=True)
    else:
        return "요약 정보 없음"

# '뉴스 가져오기' 버튼 클릭 이벤트 처리 함수
def fetch_button_click():
    try:
        fetch_news()  # 뉴스 가져오기 함수 호출
        messagebox.showinfo("알림", "뉴스와 이미지를 가져왔습니다.")
    except Exception as e:
        messagebox.showerror("오류", f"오류가 발생했습니다: {e}")

# 마우스 휠 스크롤 이벤트 처리 함수
def _on_mousewheel(event):
    delta = -1 * int(event.delta / 120)
    news_canvas.yview_scroll(delta, "units")

# 로또 번호 생성 함수
def generate_lotto_numbers():
    return sorted(random.sample(range(1, 46), 6))

# 라벨 크기 변경 이벤트 처리 함수
def on_label_configure(event):
    widget = event.widget
    widget.create_oval(0, 0, event.width, event.height, fill=widget["bg"], outline=widget["bg"])

# 로또 번호 표시 함수
def show_lotto_numbers():
    lotto_numbers = generate_lotto_numbers()
    lotto_label.config(text=f"로또 번호: {', '.join(map(str, lotto_numbers))}")
# 로또 번호를 화면에 애니메이션으로 표시하는 함수
def animate_lotto_numbers():
    for _ in range(20):  # 20번 반복하여 로또 번호를 표시합니다.
        lotto_numbers = generate_lotto_numbers()  # 로또 번호 생성
        lotto_labels = []
        for idx, num in enumerate(lotto_numbers):
            # 로또 번호를 표시할 라벨 생성
            label = tk.Canvas(lotto_frame, width=50, height=50, bg=random_color())
            label.create_oval(0, 0, 50, 50, fill=label["bg"], outline=label["bg"])
            label.create_text(25, 25, text=str(num), font=("Arial", 24, "bold"))
            label.grid(row=0, column=idx, padx=5, pady=5)
            lotto_labels.append(label)  # 생성한 라벨을 리스트에 추가
        app.update()  # 화면 업데이트
        time.sleep(0.1)  # 0.1초 딜레이를 줘서 애니메이션 효과를 줍니다.

# Tkinter 애플리케이션 초기화
app = tk.Tk()
app.title("Nate 뉴스 크롤러(feat:SM)")
app.geometry("800x600")

# 배경색 변경
app.configure(bg="#f0f0f0")

# 프레임 생성 및 배치
frame = ttk.Frame(app, padding=10)
frame.pack(fill=tk.BOTH, expand=True)

# '뉴스 가져오기' 버튼 생성 및 배치
fetch_button = ttk.Button(frame, text="뉴스 가져오기", command=fetch_button_click)
fetch_button.pack(pady=5)

# 로또 번호를 표시할 프레임 생성 및 배치
lotto_frame = ttk.Frame(frame)
lotto_frame.pack(pady=5)

# '로또 번호 추출' 버튼 생성 및 배치
lotto_button = ttk.Button(frame, text="로또 번호 추출", command=animate_lotto_numbers)
lotto_button.pack(pady=5)

# 뉴스 목록을 표시할 캔버스 프레임 생성 및 배치
canvas_frame = ttk.Frame(frame)
canvas_frame.pack(fill=tk.BOTH, expand=True)

# 뉴스 목록을 표시할 캔버스 생성 및 배치
news_canvas = tk.Canvas(canvas_frame, bg="white")
news_canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)

# 캔버스 스크롤을 위한 스크롤바 생성 및 배치
scrollbar = ttk.Scrollbar(canvas_frame, orient=tk.VERTICAL, command=news_canvas.yview)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)

# 캔버스 스크롤 설정
news_canvas.configure(yscrollcommand=scrollbar.set)

# 캔버스 크기 조정을 위한 이벤트 핸들러 등록
news_canvas.bind("<Configure>", lambda e: news_canvas.configure(scrollregion=news_canvas.bbox("all")))

# 마우스 휠 스크롤 이벤트 등록
news_canvas.bind_all("<MouseWheel>", _on_mousewheel)

# 버튼 스타일 변경
style = ttk.Style()
style.configure("TButton", font=("Arial", 10, "bold"), borderwidth=4)
style.map("TButton", foreground=[("pressed", "blue"), ("active", "green")], background=[("pressed", "!disabled", "black"), ("active", "white")])

# Tkinter 애플리케이션 실행
app.mainloop()

오늘은 간단하게 크롤링 하는 방법에 대해 알아보았는데요, 앞으로는 좀 더 심화된 내용으로 여러분께 찾아뵐게요! 감사합니다^^

반응형
Comments