본문 바로가기

좋아하는 것_매직IT/7.gin

웹 크롤링 프레임워크 Colly로 블로터 최신 뉴스를 공유 할 수 있다고?

반응형

블로그 목적 

웹 크롤링 프레임워크 Colly 와 텔레그램 봇을 활용한 블로터 최신뉴스 공유 구현에 대해서 공부및 정리후 나만의 노하우와 지식을 공유한다.

블로그 요약

1. 웹 크롤링 프레임워크 Colly 에 대해서 알아본다.
2. 웹 크롤링 프레임워크와 텔레그램 봇을 활용한 블로터 최신뉴스 공유 기능을 간단하게 구현해 본다. 

블로그 상세내용

예전부터 사내메일로 수신받아온 여러가지 IT뉴스를 보다가 한번 블로터뉴스에 대해서 공유해보면 어떨까? 라는 생각이 들어서 golang 으로 구현해 봤는데요..

우선 Colly 라이브러리에 대해서 간단하게 정리해보도록 할께요.

한마디로 Colly는 Golang으로 작성된 강력하고 빠른 웹 크롤링 프레임워크입니다.
그리고 간단한 API와 높은 성능으로 많은 개발자들에게 사랑받고 있습니다.

Colly의 주요 특징은 아래와 같은데요.
하나, 빠른 성능
Colly는 Go의 동시성 모델을 활용하여 매우 빠른 크롤링 속도를 제공합니다.

둘, 간단한 API
직관적인 API로 초보자도 쉽게 사용할 수 있습니다.

셋, 비동기 지원
비동기 크롤링을 지원하여 대규모 웹사이트도 효율적으로 크롤링할 수 있습니다.

넷, 확장성
다양한 플러그인과 미들웨어를 지원하여 기능을 확장할 수 있습니다.

그럼, Colly 설치하려면 어떻게 해야 할까요?
Colly를 사용하기 위해서는 다음 명령어로 설치할 수 있습니다:

간단한 사용 예제
다음은 Colly를 사용한 간단한 크롤링 예제입니다:

해당  코드는 go-colly.org 웹사이트를 방문하여 페이지 제목을 출력합니다.

Colly의 주요 메서드를 정리해보자면요. 아래와 같은데요.

정리해보자면 Colly는 Golang에서 웹 크롤링을 쉽고 효율적으로 할 수 있게 해주는 훌륭한 도구입니다.
간단한 API와 뛰어난 성능으로 초보자부터 전문가까지 모두에게 적합한 라이브러리입니다

더 자세한 정보와 고급 사용법은 Colly의 공식 문서를 참고 하시면 될것 같고요.
혹시나 크롤링 프로젝트를 시작하신다면 Colly를 꼭 한번 사용해보세요!

그럼, 위에서 찾아본 내용을 기반으로 해서 구현할 go 프로젝트를 세팅해 봅니다. 
참고로 웹프레임워크는 go 에서 나름 유명한 gin 프레임워크를 활용하겠습니다. 

$ go mod init colly_telegram
$ go mod tidy

그럼 어떻게 구현해 볼지 생각해 봅니다. 
우선 블로터 뉴스 홈페이지를 한번 훑어봐야 겠군요.
그리고 colly 를 공부해서 블로터 뉴스 홈페이지를 크롤링해서 텔레그램으로 전송하면 될것 같습니다.

우선 블로터 뉴스 홈페이지를 보시면 아래와 같습니다. 

그리고 웹페이지에서 우클릭후 HTML 소스를 보면 아래와 같습니다. 

그래서 어느 정도 분석을 하고 나면 아래와 같이 함수를 구현할 수 있습니다.  아니면 LLM 을 통해서 구현해 달라고 해도 제법 잘 구현해 주더라고요.
저는 HTML 요소를 확인할때 제목, 요약, 범주, 일시, 기사링크 를 크롤링해서 공유하고 싶었기 때문에 해당 부분을 유심히 보고 아래와 같이 구현해봅니다. 

func fetchBloterNewsData(url string) ([]NewsArticle, error) {
	c := colly.NewCollector()
	var articles []NewsArticle

	c.OnHTML("#section-list ul.type2 li", func(e *colly.HTMLElement) {
		title := e.ChildText("h2.titles a")
		link := e.ChildAttr("h2.titles a", "href")
		summary := e.ChildText("p.lead")
		date := e.ChildText("span.byline em:nth-child(3)")
		category := e.ChildText("span.byline em:nth-child(1)")

		absoluteLink := "https://www.bloter.net" + link

		title = strings.ReplaceAll(title, "%", " 퍼센트")
		summary = strings.ReplaceAll(summary, "%", " 퍼센트")

		articles = append(articles, NewsArticle{
			Title:    title,
			Link:     absoluteLink,
			Summary:  summary,
			Date:     date,
			Category: category,
		})
	})

	err := c.Visit(url)
	if err != nil {
		return nil, err
	}

	return articles[:7], nil
}

그리고 필요정보들을 메시지로 만드는 함수를 구현해봅니다.

func makeBloterNews(articles []NewsArticle) string {
	var sb strings.Builder

	for _, article := range articles {
		sb.WriteString(fmt.Sprintf("제목: %s\n", article.Title))
		sb.WriteString(fmt.Sprintf("요약: %s\n", truncateSummaryUTF8(article.Summary, maxLength)))
		sb.WriteString(fmt.Sprintf("범주: %s\n", article.Category))
		sb.WriteString(fmt.Sprintf("일시: %s\n", article.Date))
		sb.WriteString(fmt.Sprintf("기사: %s\n\n", article.Link))
	}

	return sb.String()
}

아래는 gin 프레임워크를 활용한 전체 코드입니다. 

package main

import (
    "net/http"
    "fmt"
    "strings"
    "unicode/utf8"
    "github.com/gocolly/colly"
    "github.com/gin-gonic/gin"   
    tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api"
)

type RequestData struct {
    Msg string `json:"msg"`
    SenderName string `json:"sender_name"`
}

type NewsArticle struct {
	Title    string
	Link     string
	Summary  string
	Date     string
	Category string
}

const telegramBotToken = (텔레그램봇토큰생략)
const telegramChannelID = (텔레그램채널ID생략)
const maxLength = 200

const bloterItUrl = "https://www.bloter.net/news/articleList.html?sc_section_code=S1N4&view_type=sm"

func bloterNewsSearch(requestData *RequestData) string {

	message := "오늘의 블로터 뉴스 정보를 찾을 수가 없네요\n"

	keyword := requestData.Msg
	var title string

	title = fmt.Sprintf("[오늘의 블로터(%s) 최신뉴스]", keyword)

	articles, err := fetchBloterNewsData(bloterItUrl)
	if err != nil {
		fmt.Printf("Error crawling news: %v", err)
        return message
    }

    message = makeBloterNews(articles)
    message = fmt.Sprintf("%s\n%s", title, message)	

	return message
}

func fetchBloterNewsData(url string) ([]NewsArticle, error) {
	c := colly.NewCollector()
	var articles []NewsArticle

	c.OnHTML("#section-list ul.type2 li", func(e *colly.HTMLElement) {
		title := e.ChildText("h2.titles a")
		link := e.ChildAttr("h2.titles a", "href")
		summary := e.ChildText("p.lead")
		date := e.ChildText("span.byline em:nth-child(3)")
		category := e.ChildText("span.byline em:nth-child(1)")

		absoluteLink := "https://www.bloter.net" + link

		title = strings.ReplaceAll(title, "%", " 퍼센트")
		summary = strings.ReplaceAll(summary, "%", " 퍼센트")

		articles = append(articles, NewsArticle{
			Title:    title,
			Link:     absoluteLink,
			Summary:  summary,
			Date:     date,
			Category: category,
		})
	})

	err := c.Visit(url)
	if err != nil {
		return nil, err
	}

	return articles[:7], nil
}

func truncateSummaryUTF8(summary string, maxLen int) string {
	if utf8.RuneCountInString(summary) <= maxLen {
		return summary
	}

	truncated := summary
	byteCount := 0
	for i := 0; i < len(summary); i++ {
		if byteCount >= maxLen {
			break
		}
		_, width := utf8.DecodeRuneInString(summary[i:])
		byteCount += width
		truncated = summary[:i+width]
		i += width - 1
	}

	return truncated + "..."
}

func makeBloterNews(articles []NewsArticle) string {
	var sb strings.Builder

	for _, article := range articles {
		sb.WriteString(fmt.Sprintf("제목: %s\n", article.Title))
		sb.WriteString(fmt.Sprintf("요약: %s\n", truncateSummaryUTF8(article.Summary, maxLength)))
		sb.WriteString(fmt.Sprintf("범주: %s\n", article.Category))
		sb.WriteString(fmt.Sprintf("일시: %s\n", article.Date))
		sb.WriteString(fmt.Sprintf("기사: %s\n\n", article.Link))
	}

	return sb.String()
}

func bloterNewsHandler(c *gin.Context) {
	requestData := &RequestData{}

	if err := c.ShouldBindJSON(&requestData); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"code": 200, "msg": "JSON data parsing error"})
		fmt.Printf("JSON data parsing error:", err)
		return
	}
    

    var message string
    message = bloterNewsSearch(requestData)

    sendErr := sendToTelegram(message)
    if sendErr != nil {
        fmt.Printf("Telegram Message send failed:", sendErr)
        return
    }

	c.JSON(http.StatusOK, gin.H{"code": 100, "msg": "success"})

}

func sendToTelegram(message string) error {

	bot, err := tgbotapi.NewBotAPI(telegramBotToken)
	if err != nil {
		return err
	}

	msg := tgbotapi.NewMessage(telegramChannelID, message)
	msg.ParseMode = "HTML"

	_, err = bot.Send(msg)

	return err
}

func main() {
    r := gin.New()

    r.POST("/bloter", bloterNewsHandler)

    go func() {
        err := r.Run(":" + "18082")
        if err != nil {
            fmt.Printf("Failed to start server:", err)
        }
    }()

    select {}
}

그럼, go build 를 통해서 빌드를 하고 실행시키면 아래와 같이 실행이됩니다.

그리고 insomnia 툴을 사용해서 테스트 요청을 전송합니다. 

그러면 위와 같이 정상적으로 전송했다는 200 OK 코드를 받게되고요

아래와 같이 텔레그램봇이 있는 방으로 메시지가 수신됩니다. 

여기까지가 제가 한번 Colly 웹크롤링 프레임워크를 활용하고 golang 과 gin 프레임워크 으로 구현한 블로터 뉴스 전송기능입니다. 

항상 믿고 봐주셔서 감사합니다. 

300x250