고 루틴을 이용해 멀티쓰레드로 크롤링 하려했으나 크롬브라우저를 직접 띄우는 방식의 크롤리이라 성능의 차이가 없음. 그래도 이용하기 위해 2개 동시 크롤링
백종원 님의 영상에 들어가 디스크립션 더보기 클릭 후 전체 내용 크롤링하는 코드
package main
import (
"log"
"context"
"time"
"fmt"
"strings"
"sync"
"github.com/chromedp/chromedp"
db "crawling/db"
//"github.com/chromedp/cdproto/cdp"
//"github.com/chromedp/cdproto/runtime"
)
func main() {
db.SetDB()
linklist := getLinkList()
if len(linklist) <1{
return
}
var wg sync.WaitGroup
remainder := len(linklist)
forMax := 2
for i := 0; i<len(linklist)-1; i++{
if i % forMax ==0{
wg.Wait()
if remainder >= forMax {
remainder -= forMax
wg.Add(forMax)
}else{
wg.Add(remainder)
}
}
getDescription(linklist[i],&wg)
}
wg.Wait()
}
func getDescription(url string, wg *sync.WaitGroup){
go func(){
defer func(){
wg.Done()
}()
contextVar, cancelFunc := chromedp.NewContext(
context.Background(),
chromedp.WithLogf(log.Printf),
)
defer cancelFunc()
contextVar = context.WithValue(contextVar, url, url)
contextVar, cancelFunc = context.WithTimeout(contextVar, 600*time.Second) // timeout 값을 설정
defer cancelFunc()
var strVar string
err := chromedp.Run(contextVar,
chromedp.Navigate("https://www.youtube.com"+url),
chromedp.Click("#primary div#primary-inner div#below ytd-watch-metadata div#above-the-fold div#bottom-row div#description tp-yt-paper-button#expand-sizer", chromedp.ByID ),
chromedp.Text("#primary div#primary-inner div#below ytd-watch-metadata div#above-the-fold div#bottom-row div#description", &strVar,chromedp.ByID ),
//chromedp.Text("#primary div#primary-inner div#below ytd-watch-metadata div#above-the-fold div#bottom-row div#description tp-yt-paper-button#expand-sizer", &attr,chromedp.ByQueryAll ),
)
if err != nil {
panic(err)
}
strVar = strings.Replace(strVar, "\"", "\\\"", -1)
param := make(map[string]string)
param["url"] = url
param["description"] = strVar
db.InsertBase(param)
}()
}
func getLinkList() []string {
contextVar, cancelFunc := chromedp.NewContext(
context.Background(),
chromedp.WithLogf(log.Printf),
)
defer cancelFunc()
contextVar, cancelFunc = context.WithTimeout(contextVar, 10000*time.Second) // timeout 값을 설정
defer cancelFunc()
err := chromedp.Run(contextVar,
chromedp.Navigate("https://www.youtube.com/@paik_jongwon/videos"),
)
if err != nil {
panic(err)
}
var oldHeight int
var newHeight int
for {
err = chromedp.Run(contextVar,
chromedp.Evaluate(`window.scrollTo(0,document.querySelector("body ytd-app div#content").clientHeight); document.querySelector("body ytd-app div#content").clientHeight;`, &newHeight),
chromedp.Sleep(700*time.Millisecond),
)
if err != nil {
panic(err)
}
if(oldHeight == newHeight){
break
}
oldHeight = newHeight
}
//var strVar string
//var strTitle string
attr := make([]map[string]string, 0)
//var nodes []cdp.NodeID
err = chromedp.Run(contextVar,
chromedp.AttributesAll("#primary ytd-rich-grid-renderer div#contents ytd-rich-grid-row div#contents ytd-rich-item-renderer #video-title-link", &attr,chromedp.ByQueryAll ),
)
if err != nil {
panic(err)
}
var linklist []string
for _, val := range attr {
linklist = append(linklist, val["href"])
}
fmt.Println(len(linklist))
return linklist
}
디비 커넥션 1개이용하는 방식으로 리턴 값에 따라 Query와 Exec로 기능 분리
기본 라이브러리로는 컬럼을 하나하나 받는 방식이라 미리 리턴하는 컬럼 확인 후 포인터배열 이용하여 공통화 진행
//db.go
package db
import (
"database/sql"
"fmt"
"github.com/go-sql-driver/mysql"
"log"
"strconv"
)
var db *sql.DB
func SetDB() {
// Capture connection properties.
cfg := mysql.Config{
User: [User],
Passwd: [Passwd],
Net: "tcp",
Addr: [Addr:Port],
DBName: "data",
AllowNativePasswords: true,
}
// Get a database handle.
var err error
db, err = sql.Open("mysql", cfg.FormatDSN())
if err != nil {
log.Fatal(err)
}
pingErr := db.Ping()
if pingErr != nil {
log.Fatal(pingErr)
}
fmt.Println("Connected!")
}
func Query(sql string) string{
rows, err := db.Query(sql)
if err != nil {
log.Fatal(err)
}
defer rows.Close()
result := "["
cols, _ := rows.Columns()
pointers := make([]interface{}, len(cols))
container := make([]string, len(cols))
for i, _ := range pointers {
pointers[i] = &container[i]
}
for rows.Next() {
if err := rows.Scan(pointers...); err != nil {
fmt.Errorf("err : %v", err)
}
result = result+"{"
for inx, val := range container{
result = result+"\""+cols[inx]+"\":\""+val+"\","
}
result = result[:len(result)-1]
result = result+"},"
}
result = result[:len(result)-1]
result = result+"]"
return result
}
func Exec(sql string) string{
result, err := db.Exec(sql)
if err != nil {
log.Fatal(err)
}
n, err := result.RowsAffected()
if err != nil {
log.Fatal(err)
}
return strconv.Itoa(int(n))
}
파이썬의 format_map 기능을 간단히 구현하여 사용
간단히 구현하여 데이터 적합성 검사 등 예외사항 인식 못함.
//spl.go
package db
import (
"strings"
)
func format_map(str string, param map[string]string) string{
cnt := strings.Count(str, "{")
for i := 0; i<cnt ;i++ {
s := strings.Index(str,"{")
e := strings.Index(str,"}")
key := str[s+1:e]
str = strings.Replace(str, "{"+key+"}", param[key],1)
}
return str
}
func InsertBase(param map[string]string) string{
sql := `
insert into base(
url
,description)
values(
"{url}"
,"{description}"
) ON DUPLICATE KEY UPDATE
url = "{url}"
`
return Exec(format_map(sql, param))
}