main.go
package main
import (
"net/http"
"./scrp"
"github.com/labstack/echo"
)
func handleHome(c echo.Context) error {
return c.String(http.StatusOK, "Hello, World!")
}
func main() {
// scrp.Scrp
scrp.Scrp("scm")
}
scrp/scrp.go
package scrp
import (
"encoding/csv"
"fmt"
"log"
"net/http"
"os"
"strconv"
"strings"
"github.com/PuerkitoBio/goquery"
)
// extractedJob 구조체 정의
type extractedJob struct {
id string
title string
location string
salary string
summary string
}
// Scrp Function
func Scrp(term string) {
// base URL 변수 선언
var baseURL string = "https://kr.indeed.com/jobs?q=" + term + "&limit=50"
// extractedJob 구조체 배열 선언
var jobs []extractedJob
// extractedJOb 배열형 채널 선언
c := make(chan []extractedJob)
// 1) getPages Function을 통해 baseURL을 전달
// 2) getPages Function을 통해 totalPages를 받음
totalPages := getPages(baseURL)
// totalpages 수만큼 go routine 으로 getPage를 수행
for i := 0; i < totalPages; i++ {
// 고루틴으로 getPage를 수행
// 페이지 번호, 기본URL, 채널
go getPage(i, baseURL, c)
// 최종적으로 extractedJob 구조체 배열형 c (mainC)에
// 검색된 JobCard들이 담김
}
// 채널로 전달받은 page별 extractedJobs를
// 로컬 Jobs로 최종 취합
for i := 0; i < totalPages; i++ {
extratedJobs := <-c
jobs = append(jobs, extratedJobs...)
}
// page별로 전달 받은 jobs를 writJobs를 통해 파일로 기록
writeJobs(jobs)
// 최종 jobs의 Length를 출력하며 종료
fmt.Println("Done : ", len(jobs))
}
// url을 전달 받아 pages를 반환함
func getPages(url string) int {
// return할 pages를 초기화
pages := 0
// net/http 패키지의 Get함수를 통해 해당 url의 Response를 반환
res, err := http.Get(url)
// net/http Get을 통해 전달 받은 err을 체크 (nil)
checkErr(err)
// net/http Get을 통해 전달 받은 res를 체크 (200)
checkCode(res)
// defer를 통해 response의 Body를 Close
defer res.Body.Close()
// BeautifulSoup : HTML,XML에서 data를 추출하는 라이브러리
// goquery : beautifulsoup 유사 library
// res.Body에서 document를 추출
doc, err := goquery.NewDocumentFromReader(res.Body)
// goquery.NewDocumentFromReader를 통해 전달 받은 err을 체크
checkErr(err)
println("Base Url : ", url)
// goquery.NewDocumentFromReader를 통해 전달 받은 doc에서
// 1) pagination 검색
// 2) a herf의 길이를 pages로 전달
doc.Find(".pagination").Each(func(i int, s *goquery.Selection) {
// goquery Length는 element의 수를 return함
pages = s.Find("a").Length()
println("Total Pages : ", pages)
})
// pages를 return함
return pages
}
// net/http Get을 통해 전달받은 err을 체크
func checkErr(err error) {
// err가 nil이 아니라면 fatalln 호출
if err != nil {
log.Fatalln(err)
}
}
// net/http Get을 통해 전달받은 res를 체크
func checkCode(res *http.Response) {
// response의 statusCode가 200이 아니라면 fatalln 호출
if res.StatusCode != 200 {
log.Fatalln("Request failed with Status : ", res.StatusCode)
}
}
// Page, BaseUrl, Channel을 전달 받아 Jobs를 return
func getPage(page int, url string, mainC chan<- []extractedJob) {
// extractedJob 구조체 배열형 jobs를 선언
var jobs []extractedJob
// extractedJob 구조체 채널 선언
c := make(chan extractedJob)
// baseUrl에 page수 * 50의 시작 페이지 생성 및 파싱
pageURL := url + "&start=" + strconv.Itoa(page*50)
// Request pageURL 표시
fmt.Println("Request : ", pageURL)
// net/http 패키지의 Get함수를 통해 해당 pageURL의 Response를 반환
res, err := http.Get(pageURL)
// net/http Get을 통해 전달 받은 err을 체크 (nil)
checkErr(err)
// net/http Get을 통해 전달 받은 res를 체크 (200)
checkCode(res)
// defer를 통해 response의 Body를 Close
defer res.Body.Close()
// goquery.NewDocumentFromReader를 통해 res.Body에서 doc를 추출
doc, err := goquery.NewDocumentFromReader(res.Body)
// goquery.NewDocumentFromReader를 통해 전달 받은 err을 체크
checkErr(err)
// goquery.NewDocumentFromReader를 통해 전달 받은 doc에서
// 1) jobsearch-SerpJobCard를 추출
searchCards := doc.Find(".jobsearch-SerpJobCard")
searchCards.Each(func(i int, card *goquery.Selection) {
// go routine을 통해 extractJob 수행
// jobcard, channel을 인자로 전달
go extractJob(card, c)
})
// 검색된 Card 수만큼 for를 타며
// extractedJob 구조체 배열형 Jobs에
// extractedJob 구조체형 채널 c로 전달받은
// extractedJob 구조체형 job을 append
for i := 0; i < searchCards.Length(); i++ {
job := <-c
jobs = append(jobs, job)
}
// extractedJob 구조체 배열형 Jobs를 mainC 채널에 전달
mainC <- jobs
}
// pageURL에서 찾은 jobCard 및 channel을 인자로 전달 받음
// Channel에 정제된 Data를 전달하여 Return
func extractJob(card *goquery.Selection, c chan<- extractedJob) {
// JobCard의 data-jk를 ID로 전달
id, _ := card.Attr("data-jk")
// JobCard의 "title>a"의 TEXT를 title로 전달
// 이때 cleanString을 통해 Trim
title := cleanString(card.Find(".title>a").Text())
// JobCard의 "sjcl""의 TEXT를 location로 전달
// 이때 cleanString을 통해 Trim
location := cleanString(card.Find(".sjcl").Text())
// JobCard의 "salaryText""의 TEXT를 salary로 전달
// 이때 cleanString을 통해 Trim
salary := cleanString(card.Find(".salaryText").Text())
// JobCard의 "summary""의 TEXT를 summary로 전달
// 이때 cleanString을 통해 Trim
summary := cleanString(card.Find(".summary").Text())
// extractedJob 구조체형 채널 c에 Trim된 각 추출 항목을 전달
c <- extractedJob{id: id, title: title, location: location, salary: salary, summary: summary}
}
// 전달 받은 Sring을 Trim하고 공백으로 재연결
func cleanString(str string) string {
return strings.Join(strings.Fields(strings.TrimSpace(str)), " ")
}
// extractedJob 구조체 배열형 jobs를 인자로 받음
func writeJobs(jobs []extractedJob) {
// os.Create를 통해 jobs.csv 파일을 create
file, err := os.Create("jobs.csv")
// os.Create을 통해 전달 받은 err을 체크 (nil)
checkErr(err)
// csv.NewWriter를 통해 Writer항목을 w로 정의
w := csv.NewWriter(file)
// defer를 통해 종료시 w를 flush
defer w.Flush()
// Header를 스트링 배열로 선언
headers := []string{"Link", "Title", "Location", "Salary", "Summary"}
// csv.Writer의 Write를 통해 string배열인 headers를 기입하고 err를 전달받음
wErr := w.Write(headers)
// csv.Writer의 Write을 통해 전달 받은 err을 체크 (nil)
checkErr(wErr)
// String 배열의 채널 생성
c := make(chan []string)
// 나름 백미 : channel을 끝까지 쓰며 분산시킴
// extractJob 구조체 배열 Jobs의 range 만큼
// extractJob 구조체 job의 for를 돌며
// job과 c를 전달하며 writeJOb을 고루틴으로 수행
for _, job := range jobs {
go writeJob(job, c)
//jobSlice := []string{"https://kr.indeed.com/viewjob?jk=" + job.id, job.title, job.location, job.salary, job.summary}
//jwErr := w.Write(jobSlice)
//checkErr(jwErr)
}
// Jobs의 Length 만큼 For로 돔
for i := 0; i < len(jobs); i++ {
// String 배열형 채널에서 jSlice로 전달 받음
jSlice := <-c
// jSlice를 csv에 기입
jwErr := w.Write(jSlice)
// csv.Writer의 Write을 통해 전달 받은 err을 체크 (nil)
checkErr(jwErr)
}
}
// extractedJob 구조체인 job과
// string 배열인 c를 인자로 전달 받음
func writeJob(job extractedJob, c chan<- []string) {
// string 배열 jobSlice를 선언
// 1) Job ID : 링크항목
// 2) Job Title
// 3) Job Location
// 4) Job Salary
// 5) Job Summary
jobSlice := []string{"https://kr.indeed.com/viewjob?jk=" + job.id, job.title, job.location, job.salary, job.summary}
// String 배열 JobSlice를 String 배열형 Channel에 전달
c <- jobSlice
}
comments powered by Disqus