[개인프로잭트_레시피추천]크롤링, db연결

최원상·2023년 1월 26일
0

크롤링 코드

고 루틴을 이용해 멀티쓰레드로 크롤링 하려했으나 크롬브라우저를 직접 띄우는 방식의 크롤리이라 성능의 차이가 없음. 그래도 이용하기 위해 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))
}

profile
한 줄로는 안되지

0개의 댓글