윤준석

Merge branch 'feature/220510_joongna_api' into 'main'

Feature/220510 joongna api

# 중고나라 키워드 검색 api

1. `api/v2/joongna/{keyword}` GET 요청하려 keyword 전달

2. 네이버 카페 검색 api를 통해 유사도 높은 순서로 검색

3. 검색 결과의 카페 링크를 통해 세부 항목 크롤링 (selenium)

4. 세부 항목 담아서 response

See merge request !6
FROM golang:1.17.3
RUN apt-get -y update
RUN apt-get install -y wget xvfb gnupg
RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add -
RUN sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google-chrome.list'
RUN apt-get -y update
RUN apt-get install -y google-chrome-stable
RUN apt-get install -yqq unzip
RUN wget -O /tmp/chromedriver.zip http://chromedriver.storage.googleapis.com/`curl -sS chromedriver.storage.googleapis.com/LATEST_RELEASE`/chromedriver_linux64.zip
RUN unzip /tmp/chromedriver.zip chromedriver -d /usr/local/bin/
ENV Xvfb :99
ENV DISPLAY=:99
package controller
import (
"joongna/service"
"net/http"
"github.com/labstack/echo/v4"
)
func Search(c echo.Context) error {
keyword := c.Param("keyword")
items, err := service.GetItemByKeyword(keyword)
if err != nil {
return err
}
return c.JSON(http.StatusOK, items)
}
......@@ -3,6 +3,23 @@ module joongna
go 1.17
require (
github.com/PuerkitoBio/goquery v1.8.0 // indirect
github.com/andybalholm/cascadia v1.3.1 // indirect
github.com/blang/semver v3.5.1+incompatible // indirect
github.com/bunsenapp/go-selenium v0.1.0 // indirect
github.com/caarlos0/env/v6 v6.9.1 // indirect
github.com/fedesog/webdriver v0.0.0-20180606182539-99f36c92eaef // indirect
github.com/joho/godotenv v1.4.0 // indirect
github.com/labstack/echo/v4 v4.7.2 // indirect
github.com/labstack/gommon v0.3.1 // indirect
github.com/mattn/go-colorable v0.1.11 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/tebeka/selenium v0.9.9 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.1 // indirect
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f // indirect
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b // indirect
golang.org/x/text v0.3.7 // indirect
sourcegraph.com/sourcegraph/go-selenium v0.0.0-20170113155244-3da7d00aac9c // indirect
)
......
This diff is collapsed. Click to expand it.
package main
import (
"fmt"
"io/ioutil"
"joongna/config"
"log"
"net/http"
url2 "net/url"
"joongna/router"
"github.com/labstack/echo/v4"
)
func main() {
keyword := "m1 pro 맥북 프로 16인치"
encText := url2.QueryEscape("중고나라" + keyword)
url := "https://openapi.naver.com/v1/search/cafearticle.json?query=" + encText + "&sort=sim"
req, err := http.NewRequest("GET", url, nil)
if err != nil {
log.Fatal(err)
}
req.Header.Add("X-Naver-Client-Id", config.Cfg.Secret.CLIENTID)
req.Header.Add("X-Naver-Client-Secret", config.Cfg.Secret.CLIENTSECRET)
e := echo.New()
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
router.Init(e)
bytes, _ := ioutil.ReadAll(resp.Body)
str := string(bytes)
fmt.Println(str)
e.Logger.Fatal(e.Start(":8080"))
}
......
package model
type ApiResponse struct {
LastBuildDate string `json:"lastBuildDate"`
Total uint `json:"total"`
Start uint `json:"start"`
Display uint `json:"display"`
Items []ApiResponseItem `json:"items"`
}
type ApiResponseItem struct {
Title string `json:"title"`
Link string `json:"link"`
Description string `json:"description"`
CafeName string `json:"cafename"`
}
package model
type Item struct {
Platform string `json:"platform"`
Name string `json:"name"`
Price int `json:"price"`
ThumbnailUrl string `json:"thumbnailUrl"`
ItemUrl string `json:"itemUrl"`
ExtraInfo string `json:"extraInfo"`
}
package router
import (
"joongna/controller"
"github.com/labstack/echo/v4"
)
const (
API = "/api/v2"
APIJoongNa = API + "/JoongNa"
APIKeyword = APIJoongNa + "/:keyword"
)
func Init(e *echo.Echo) {
e.GET(APIKeyword, controller.Search)
}
package service
import (
"bytes"
"encoding/json"
"io"
"io/ioutil"
"joongna/config"
"joongna/model"
"log"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/PuerkitoBio/goquery"
"github.com/fedesog/webdriver"
)
func GetItemByKeyword(keyword string) ([]model.Item, error) {
var items []model.Item
itemsInfo := getItemsInfoByKeyword(keyword)
for _, itemInfo := range itemsInfo {
if itemInfo.CafeName != "중고나라" {
continue
}
itemUrl := itemInfo.Link
sold, price, thumbnailUrl, extraInfo := crawlingNaverCafe(itemUrl)
if sold == "판매 완료" {
continue
}
item := model.Item{
Platform: "중고나라",
Name: itemInfo.Title,
Price: price,
ThumbnailUrl: thumbnailUrl,
ItemUrl: itemUrl,
ExtraInfo: extraInfo,
}
items = append(items, item)
}
return items, nil
}
func getItemsInfoByKeyword(keyword string) []model.ApiResponseItem {
encText := url.QueryEscape("중고나라 " + keyword + " 판매중")
apiUrl := "https://openapi.naver.com/v1/search/cafearticle.json?query=" + encText + "&sort=sim"
req, err := http.NewRequest("GET", apiUrl, nil)
if err != nil {
log.Fatal(err)
}
req.Header.Add("X-Naver-Client-Id", config.Cfg.Secret.CLIENTID)
req.Header.Add("X-Naver-Client-Secret", config.Cfg.Secret.CLIENTSECRET)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
log.Fatal(err)
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
log.Fatal(err)
}
}(resp.Body)
response, _ := ioutil.ReadAll(resp.Body)
var apiResponse model.ApiResponse
err = json.Unmarshal(response, &apiResponse)
if err != nil {
log.Fatal(err)
}
return apiResponse.Items
}
func crawlingNaverCafe(cafeUrl string) (string, int, string, string) {
driver := webdriver.NewChromeDriver("./chromedriver")
err := driver.Start()
if err != nil {
log.Println(err)
}
desired := webdriver.Capabilities{"Platform": "Linux"}
required := webdriver.Capabilities{}
session, err := driver.NewSession(desired, required)
if err != nil {
log.Println(err)
}
err = session.Url(cafeUrl)
if err != nil {
log.Println(err)
}
time.Sleep(time.Second * 1)
err = session.FocusOnFrame("cafe_main")
if err != nil {
log.Fatal(err)
}
resp, err := session.Source()
html, err := goquery.NewDocumentFromReader(bytes.NewReader([]byte(resp)))
if err != nil {
log.Fatal(err)
}
sold := html.Find("div.sold_area").Text()
price := priceStringToInt(html.Find(".ProductPrice").Text())
thumbnailUrl, _ := html.Find("div.product_thumb img").Attr("src")
extraInfo := html.Find(".se-module-text").Text()
sold = strings.TrimSpace(sold)
thumbnailUrl = strings.TrimSpace(thumbnailUrl)
extraInfo = strings.TrimSpace(extraInfo)
return sold, price, thumbnailUrl, extraInfo
}
func priceStringToInt(priceString string) int {
strings.TrimSpace(priceString)
priceString = strings.ReplaceAll(priceString, "원", "")
priceString = strings.ReplaceAll(priceString, ",", "")
price, err := strconv.Atoi(priceString)
if err != nil {
log.Fatal(err)
}
return price
}