본문 바로가기

Programming Language/Go

[Go언어] GoLang을 활용하여 웹페이지 크롤링

반응형

이번 포스팅은 Go언어를 사용하여 웹 페이지를 크롤링하는 

튜토리얼 입니다.

아래 포스팅은 노마드코더의 golang강의를 기반으로

작성되었습니다.

 

 

 

 

 

크롤링 할 페이지 선택

 

취업검색 | Indeed (인디드)

 

kr.indeed.com

>> 취업에 대한 정보가 나와있는 웹사이트

>> 검색어 쿼리를 던졌을 때 출력되는 결과를 크롤링

 

 

크롤링 코드 작성 (Go Lang)

사용 할 라이브러리 설치

- Go언어에서 크롤링을 하기 위해 필요한 라이브러리 설치

$ go get github.com/PuerkitoBio/goquery

 

- 아래는 Goquery 깃허브 링크

 

PuerkitoBio/goquery

A little like that j-thing, only in Go. Contribute to PuerkitoBio/goquery development by creating an account on GitHub.

github.com

 

부가기능 별 함수 작성

에러 확인 function

func checkErr(err error) {
    if err != nil {
        log.Fatalln(err)
    }
}

 

http 요청 결과 상태 코드 확인 function

func checkCode(res *http.Response) {
    if res.StatusCode != 200 {
        log.Fatalf("Status code err: %d %s", res.StatusCode, res.Status)
    }
}

 

결과 string의 띄어쓰기 혹은 앞뒤 공백 제거 function

//CleanString function
func CleanString(str string) string {
    return strings.Join(strings.Fields(strings.TrimSpace(str)), " ")
}

 

>> strings.TrimSpace(str) : 문자열 앞 뒤의 공백제거

 

TrimSpace func

 

>> strings.Fields(str) : 공백을 기준으로 문자열을 잘라 slice 형태로 저장

 

Field func

 

>> strings.Join([]str, sep) : sep 기준으로 문자열을 합쳐줌

 

Join func

 

< 테스트 용 코드 >

< 코드 실행 시 결과 >

 

 

 

메인 기능 별 함수 작성

크롤링 할 총 페이지 수 확인 func

func getPages(baseURL string) int {
    pages := 0
    res, err := http.Get(baseURL)
    checkErr(err)
    checkCode(res)

    defer res.Body.Close()

    doc, err := goquery.NewDocumentFromReader(res.Body)
    checkErr(err)

    doc.Find(".pagination-list").Each(func(i int, s *goquery.Selection) {
        pages = s.Find("li").Length()
    })

    return pages
}

 

 

특정 페이지의 결과 데이터 가져오기

func getCard(page int, baseURL string, c1 chan []Indeed) {
    var jobs []Indeed
    c := make(chan Indeed)
    URL := baseURL + "&start=" + strconv.Itoa(page*10)
    fmt.Println(URL)

    res, err := http.Get(URL)
    checkErr(err)
    checkCode(res)

    defer res.Body.Close()

    doc, err := goquery.NewDocumentFromReader(res.Body)
    checkErr(err)

    searchCards := doc.Find(".jobsearch-SerpJobCard")

    searchCards.Each(func(i int, s *goquery.Selection) {
        go extractJob(s, c)
    })

    for i := 0; i < searchCards.Length(); i++ {
        job := <-c
        jobs = append(jobs, job)
    }
    c1 <- jobs
}

>> strconv.Itoa(int) : int타입을 string으로 변환해주는 함수 

 

각 결과 별 상세 데이터 추출 func

- 데이터를 담을 struct 선언

type Indeed struct {
    id       string
    title    string
    location string
    summary  string
}

 

- 상세 데이터 추출

func extractJob(s *goquery.Selection, c chan<- Indeed) {
    id, _ := s.Attr("data-jk")
    title := CleanString(s.Find(".title>a").Text())
    location := CleanString(s.Find(".accessible-contrast-color-location").Text())
    summary := CleanString(s.Find(".summary").Text())

    c <- Indeed{
        id:       id,
        title:    title,
        location: location,
        summary:  summary}
}

 

결과 저장하는 func 

func writeJobs(jobs []Indeed) {
    file, err := os.Create("jobs.csv")
    checkErr(err)

    w := csv.NewWriter(file)
    //Write data to the file
    defer w.Flush()

    header := []string{"ID", "TITLE", "LOCATION", "SUMMARY"}

    wErr := w.Write(header)
    checkErr(wErr)

    for _, job := range jobs {
        jobSlice := []string{"https://kr.indeed.com/viewjob?jk=" + job.id, job.title, job.location, job.summary}
        jobErr := w.Write(jobSlice)
        checkErr(jobErr)
    }
}

 

크롤링 실행 시키는 메인 Scrapper 함수

//Scrapper function
func Scrapper(query string) {
    var jobs []Indeed
    var baseURL string = "https://kr.indeed.com/jobs?q=" + query // + "&l=%EC%84%9C%EC%9A%B8"
    c1 := make(chan []Indeed)
    TotalPage := getPages(baseURL)
    fmt.Println("TotalPage...", TotalPage)

    for i := 0; i < TotalPage; i++ {
        go getCard(i, baseURL, c1)
    }

    for i := 0; i < TotalPage; i++ {
        extractJobs := <-c1
        //merge slices or arrays
        jobs = append(jobs, extractJobs...)
    }
    writeJobs(jobs)
    fmt.Println("Done")
}

 

 

API 서버 생성 

Go 언어를 사용해서 API 서버 생성하기. 많은 라이브러리들이 존재 함

그 중에 echo를 활용 할 예정

 

ECHO 라이브러리 설치

$ go get -u github.com/labstack/echo/

 

화면을 보여줄 html 작성

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width= , initial-scale=1.0">
    <title>Go Project</title>
</head>
<body>
    <h1>Go Project</h1>
    <h3>Indeed.com scrapper</h3>
    <form method="POST" action="/scrape">
        <input placeholder="what job do u want" name="query"/>
        <button>Search</button>
    </form>
</body>
</html>

- 쿼리로 보낼 검색어를 입력 할 수 있도록 form을 생성

- POST 매서드로 보내며 action="/scrape"로 요청을 보내도록 함

 

실제 서버가 작동 할 수 있도록 main.go

기본 home.html로 연결 func

//Handler function
func Handler(c echo.Context) error {
    return c.File("home.html")
}

 

검색 쿼리에 대해 스크래핑 요청 func

//HandleFunc function
func HandleFunc(c echo.Context) error {
    query := strings.ToLower(scrapper.CleanString(c.FormValue("query")))
    fmt.Println(query)
    scrapper.Scrapper(query)

    return c.Attachment(fileName, query+".csv")
}

 

기본 main func

package main

import (
	"fmt"
	"strings"

	"github.com/labstack/echo"
)

func main() {
	e := echo.New()
	e.GET("/", Handler)
	e.POST("/scrape", HandleFunc)
	e.Logger.Fatal(e.Start(":1323"))

}

 

 

실제 구동 화면

main.go 실행

$ go run main.go

 

localhost:1323 실행

 

localhost:1323 접속

golang 검색 후 search 클릭

- 크롤링 함수가 golang 검색 후 나온 결과에 대한 크롤링 진행 후 아래와 같이 파일로 다운로드 함

 

 

 

Full Code : Scrapper.go
package scrapper

import (
	"encoding/csv"
	"fmt"
	"log"
	"net/http"
	"os"
	"strconv"
	"strings"

	"github.com/PuerkitoBio/goquery"
)

// goQuery 사용
// go get github.com/PuerkitoBio/goquery

//Indeed is a struct
type Indeed struct {
	id       string
	title    string
	location string
	summary  string
}

//Scrapper function
func Scrapper(query string) {
	var jobs []Indeed
	var baseURL string = "https://kr.indeed.com/jobs?q=" + query // + "&l=%EC%84%9C%EC%9A%B8"
	c1 := make(chan []Indeed)
	TotalPage := getPages(baseURL)
	fmt.Println("TotalPage...", TotalPage)

	for i := 0; i < TotalPage; i++ {
		go getCard(i, baseURL, c1)
	}

	for i := 0; i < TotalPage; i++ {
		extractJobs := <-c1
		//merge slices or arrays
		jobs = append(jobs, extractJobs...)
	}
	writeJobs(jobs)
	fmt.Println("Done")
}

func writeJobs(jobs []Indeed) {
	file, err := os.Create("jobs.csv")
	checkErr(err)

	w := csv.NewWriter(file)
	//Write data to the file
	defer w.Flush()

	header := []string{"ID", "TITLE", "LOCATION", "SUMMARY"}

	wErr := w.Write(header)
	checkErr(wErr)

	for _, job := range jobs {
		jobSlice := []string{"https://kr.indeed.com/viewjob?jk=" + job.id, job.title, job.location, job.summary}
		jobErr := w.Write(jobSlice)
		checkErr(jobErr)
	}
}

func getCard(page int, baseURL string, c1 chan []Indeed) {
	var jobs []Indeed
	c := make(chan Indeed)
	URL := baseURL + "&start=" + strconv.Itoa(page*10)
	fmt.Println(URL)

	res, err := http.Get(URL)
	checkErr(err)
	checkCode(res)

	defer res.Body.Close()

	doc, err := goquery.NewDocumentFromReader(res.Body)
	checkErr(err)

	searchCards := doc.Find(".jobsearch-SerpJobCard")

	searchCards.Each(func(i int, s *goquery.Selection) {
		go extractJob(s, c)
	})

	for i := 0; i < searchCards.Length(); i++ {
		job := <-c
		jobs = append(jobs, job)
	}
	c1 <- jobs
}

func extractJob(s *goquery.Selection, c chan<- Indeed) {
	id, _ := s.Attr("data-jk")
	title := CleanString(s.Find(".title>a").Text())
	location := CleanString(s.Find(".accessible-contrast-color-location").Text())
	summary := CleanString(s.Find(".summary").Text())

	c <- Indeed{
		id:       id,
		title:    title,
		location: location,
		summary:  summary}
}

func getPages(baseURL string) int {
	pages := 0
	res, err := http.Get(baseURL)
	checkErr(err)
	checkCode(res)

	defer res.Body.Close()

	doc, err := goquery.NewDocumentFromReader(res.Body)
	checkErr(err)

	doc.Find(".pagination-list").Each(func(i int, s *goquery.Selection) {
		pages = s.Find("li").Length()
	})

	return pages
}

func checkErr(err error) {
	if err != nil {
		log.Fatalln(err)
	}
}

func checkCode(res *http.Response) {
	if res.StatusCode != 200 {
		log.Fatalf("Status code err: %d %s", res.StatusCode, res.Status)
	}
}

//CleanString function
func CleanString(str string) string {
	return strings.Join(strings.Fields(strings.TrimSpace(str)), " ")
}

 

Full Code : home.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width= , initial-scale=1.0">
    <title>Go Project</title>
</head>
<body>
    <h1>Go Project</h1>
    <h3>Indeed.com scrapper</h3>
    <form method="POST" action="/scrape">
        <input placeholder="what job do u want" name="query"/>
        <button>Search</button>
    </form>
</body>
</html>

 

Full Code : main.go
package main

import (
	"fmt"
	"strings"

	"github.com/labstack/echo"
	scrapper "github.com/[github_id]/Scrapper"
)

var fileName = "jobs.csv"

//Handler function
func Handler(c echo.Context) error {
	return c.File("home.html")
}

//HandleFunc function
func HandleFunc(c echo.Context) error {
	query := strings.ToLower(scrapper.CleanString(c.FormValue("query")))
	fmt.Println(query)
	scrapper.Scrapper(query)

	return c.Attachment(fileName, query+".csv")
}

func main() {
	e := echo.New()
	e.GET("/", Handler)
	e.POST("/scrape", HandleFunc)
	e.Logger.Fatal(e.Start(":1323"))

}
반응형