cron.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562
  1. package controller
  2. import (
  3. "crawler/config"
  4. "crawler/model"
  5. "crawler/service"
  6. "crawler/utility"
  7. "fmt"
  8. "github.com/gin-gonic/gin"
  9. "github.com/gocolly/colly"
  10. "log"
  11. "net"
  12. "net/http"
  13. "os"
  14. "strconv"
  15. "strings"
  16. "time"
  17. )
  18. type CronController interface {
  19. List(c *gin.Context)
  20. Info(c *gin.Context)
  21. Detail(c *gin.Context)
  22. GetKey(c *gin.Context) string
  23. SetLastPage(page int)
  24. GetLastPage() int
  25. }
  26. type Cron struct {
  27. MovieListModel model.MovieListModel
  28. MovieInfoModel model.MovieInfoModel
  29. MovieDetailModel model.MovieDetailModel
  30. MovieStatsModel model.MovieStatsModel
  31. Kobis model.Kobis
  32. Rest service.Rest
  33. }
  34. /**
  35. * 영화진흥위원회 영화 목록
  36. */
  37. func (this *Cron) List(c *gin.Context) {
  38. var (
  39. start = time.Now()
  40. page = GetLastPage()
  41. perPage = 100
  42. total, errors, insertRows, updateRows = 0, 0, 0, 0
  43. output = func(n ...int) string {
  44. s := fmt.Sprintf("Total : %d\n", n[0])
  45. s += fmt.Sprintf("Error: %d\n", n[1])
  46. s += fmt.Sprintf("InsertRows: %d\n", n[2])
  47. s += fmt.Sprintf("UpdateRows: %d\n", n[3])
  48. s += fmt.Sprintf("소요시간: %f초\n", time.Since(start).Seconds())
  49. return s
  50. }
  51. key = GetKey(c)
  52. )
  53. //defer func() {
  54. // if r := recover(); r != nil {
  55. // msg := fmt.Sprintf("[영화 목록 수집 오류 발생]\n")
  56. // msg += output(total, errors, insertRows, updateRows)
  57. // utility.SendMessage(msg)
  58. // }
  59. //}()
  60. for {
  61. var (
  62. req = this.MovieListModel.SearchMovieListParams
  63. insertData, updateData []model.MovieListInfo
  64. )
  65. req.Key = key
  66. req.CurPage = page
  67. req.ItemPerPage = perPage
  68. data, err := this.Kobis.MovieListAPI(req)
  69. if err != nil {
  70. SetLastPage(page)
  71. c.JSON(http.StatusBadRequest, err.Error())
  72. return
  73. }
  74. // 더 이상 값이 없다면 중지
  75. if data.MovieListResult.TotCnt <= 0 {
  76. errors++
  77. break
  78. }
  79. // 입력할 값과 수정할 값 구분
  80. for _, row := range data.MovieListResult.MovieList {
  81. if this.MovieListModel.IsExists(row.MovieCd) == true {
  82. updateData = append(updateData, row)
  83. updateRows++
  84. } else {
  85. insertData = append(insertData, row)
  86. insertRows++
  87. }
  88. total++
  89. }
  90. if insertRows > 0 {
  91. if err = this.MovieListModel.Insert(insertData); err != nil {
  92. errors++
  93. }
  94. }
  95. if updateRows > 0 {
  96. if err = this.MovieListModel.Update(updateData); err != nil {
  97. errors++
  98. }
  99. }
  100. fmt.Println(output(total, errors, insertRows, updateRows))
  101. SetLastPage(page)
  102. page++
  103. }
  104. //msg := "[영화 목록 수집 종료]\n"
  105. //msg += output(total, errors, insertRows, updateRows)
  106. //utility.SendMessage(msg)
  107. c.JSON(http.StatusOK, gin.H{
  108. "total": total,
  109. "errors": errors,
  110. "page": page,
  111. "perPage": perPage,
  112. "insertRows": insertRows,
  113. "updateRows": updateRows,
  114. })
  115. }
  116. /**
  117. * 영화진흥위원회 영화 기본 정보
  118. */
  119. func (this *Cron) Info(c *gin.Context) {
  120. var (
  121. start = time.Now()
  122. codes = this.MovieListModel.MovieInfoExcludeCodes()
  123. total, errors, insertRows, updateRows = 0, 0, 0, 0
  124. failedCodes = make([]string, 0)
  125. consecutive = 0
  126. output = func(n ...int) string {
  127. s := fmt.Sprintf("Total : %d\n", n[0])
  128. s += fmt.Sprintf("Error: %d\n", n[1])
  129. s += fmt.Sprintf("InsertRows: %d\n", n[2])
  130. s += fmt.Sprintf("UpdateRows: %d\n", n[3])
  131. s += fmt.Sprintf("소요시간: %f초\n", time.Since(start).Seconds())
  132. return s
  133. }
  134. key = GetKey(c)
  135. )
  136. //defer func() {
  137. // if r := recover(); r != nil {
  138. // msg := fmt.Sprintf("[영화 기본 정보 수집 오류 발생]\n")
  139. // msg += output(total, errors, insertRows, updateRows)
  140. // utility.SendMessage(msg)
  141. // }
  142. //}()
  143. for _, movieCd := range codes {
  144. var (
  145. req = this.MovieInfoModel.SearchMovieInfoParams
  146. )
  147. req.Key = key
  148. req.MovieCd = movieCd
  149. data, err := this.Kobis.MovieInfoAPI(req)
  150. if err != nil {
  151. errors++
  152. consecutive++
  153. failedCodes = append(failedCodes, movieCd)
  154. // 연속 오류(무효 키, 쿼터 초과 등) 시 쿼터 낭비 방지를 위해 중단
  155. if consecutive >= 10 {
  156. c.JSON(http.StatusBadRequest, gin.H{
  157. "message": "연속 오류 10회로 중단: " + err.Error(),
  158. "total": total,
  159. "error": errors,
  160. "insertRows": insertRows,
  161. "updateRows": updateRows,
  162. "failedCodes": failedCodes,
  163. })
  164. return
  165. }
  166. continue
  167. }
  168. consecutive = 0
  169. row := data.MovieInfoResult.MovieInfo
  170. if row.MovieCd == "" {
  171. errors++
  172. failedCodes = append(failedCodes, movieCd)
  173. continue
  174. }
  175. if this.MovieInfoModel.IsExists(row.MovieCd) == true {
  176. if err = this.MovieInfoModel.Update(row); err == nil {
  177. updateRows++
  178. } else {
  179. errors++
  180. }
  181. } else {
  182. if err = this.MovieInfoModel.Insert(row); err == nil {
  183. insertRows++
  184. } else {
  185. errors++
  186. }
  187. }
  188. fmt.Println(output(total, errors, insertRows, updateRows))
  189. total++
  190. }
  191. //msg := "[영화 기본 정보 수집 종료]\n"
  192. //msg += output(total, errors, insertRows, updateRows)
  193. //utility.SendMessage(msg)
  194. c.JSON(http.StatusOK, gin.H{
  195. "total": total,
  196. "error": errors,
  197. "insertRows": insertRows,
  198. "updateRows": updateRows,
  199. "failedCodes": failedCodes,
  200. })
  201. }
  202. /*
  203. * 호출 순서
  204. * OnRequest -> OnError -> OnResponseHeaders -> OnResponse -> OnHTML -> OnXML -> OnScraped
  205. */
  206. func (this *Cron) Detail(c *gin.Context) {
  207. var (
  208. start = time.Now()
  209. codes = this.MovieListModel.MovieDetailExcludeCodes()
  210. total, scraped, errors, insertRows, updateRows, target = 0, 0, 0, 0, 0, len(codes)
  211. output = func(n ...int) string {
  212. s := fmt.Sprintf("Total : %d\n", n[0])
  213. s += fmt.Sprintf("Scraped: %d\n", n[1])
  214. s += fmt.Sprintf("Error: %d\n", n[2])
  215. s += fmt.Sprintf("InsertRows: %d\n", n[3])
  216. s += fmt.Sprintf("UpdateRows: %d\n", n[4])
  217. s += fmt.Sprintf("Target : %d\n", n[5])
  218. s += fmt.Sprintf("소요시간: %f초\n", time.Since(start).Seconds())
  219. return s
  220. }
  221. c1 = colly.NewCollector(
  222. colly.AllowedDomains(config.KOBIS_DOMAIN),
  223. colly.IgnoreRobotsTxt(),
  224. colly.Async(false),
  225. )
  226. )
  227. c1.WithTransport(&http.Transport{
  228. DialContext: (&net.Dialer{
  229. Timeout: 30 * time.Second,
  230. KeepAlive: 30 * time.Second,
  231. }).DialContext,
  232. MaxIdleConns: 0,
  233. MaxIdleConnsPerHost: 100,
  234. IdleConnTimeout: 30 * time.Second,
  235. TLSHandshakeTimeout: 30 * time.Second,
  236. ExpectContinueTimeout: 30 * time.Second,
  237. DisableCompression: false,
  238. })
  239. //var c2 = c1.Clone()
  240. c1.OnRequest(func(r *colly.Request) {
  241. r.Headers.Set("User-Agent", utility.RandomString())
  242. r.Headers.Set("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8")
  243. })
  244. c1.OnError(func(_ *colly.Response, err error) {
  245. log.Printf("Error(c1) : %s\n", err.Error())
  246. errors++
  247. })
  248. c1.OnScraped(func(r *colly.Response) {
  249. scraped++
  250. })
  251. // 상세 페이지 파싱 — 루프 밖 1회 등록 (루프 내 등록 시 콜백이 영화 수만큼 누적됨)
  252. var current *model.MovieDetail
  253. c1.OnHTML(".item_tab.basic", func(e *colly.HTMLElement) {
  254. if current == nil {
  255. return
  256. }
  257. var host = config.KOBIS_HOST
  258. current.MainImg = e.ChildAttr("a.fl.thumb", "href")
  259. if current.MainImg != "" && current.MainImg != "#" {
  260. current.MainImg = host + current.MainImg
  261. }
  262. current.ThumbImg = e.ChildAttr("a.fl.thumb > img", "src")
  263. if current.ThumbImg != "" && current.ThumbImg != "#" {
  264. current.ThumbImg = host + current.ThumbImg
  265. }
  266. current.Synopsis = e.ChildText("div.info.info2 p.desc_info")
  267. e.ForEach("div#post > input", func(_ int, ee *colly.HTMLElement) {
  268. current.Poster = append(current.Poster, model.Poster{
  269. Thumb: host + ee.Attr("thn_img"),
  270. Origin: host + ee.Attr("img"),
  271. })
  272. })
  273. e.ForEach("div#stl > input", func(_ int, ee *colly.HTMLElement) {
  274. current.StillCut = append(current.StillCut, model.StillCut{
  275. Thumb: host + ee.Attr("thn_img"),
  276. Origin: host + ee.Attr("img"),
  277. })
  278. })
  279. })
  280. /*
  281. 관객 수, 누적 매출액 조회
  282. */
  283. //c2.OnRequest(func(r *colly.Request) {
  284. // r.Headers.Set("User-Agent", utility.RandomString())
  285. // r.Headers.Set("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8")
  286. //})
  287. //
  288. //c2.OnError(func(_ *colly.Response, err error) {
  289. // log.Printf("Error(c2) : %s\n", err.Error())
  290. // errors++
  291. //})
  292. //
  293. //c2.OnScraped(func(r *colly.Response) {
  294. // scraped++
  295. //})
  296. // 복구처리
  297. defer func() {
  298. if e := recover(); e != nil {
  299. //msg := "[영화 상세 정보 수집 오류 발생]\n"
  300. //msg += output(total, scraped, errors, insertRows, updateRows, len(codes))
  301. //utility.SendMessage(msg)
  302. }
  303. }()
  304. for i, movieCd := range codes {
  305. if movieCd == "" {
  306. continue
  307. }
  308. movieDetail := this.MovieDetailModel.MovieDetail
  309. movieDetail.MovieCd = movieCd
  310. current = &movieDetail
  311. //c2.OnHTML("body", func(e *colly.HTMLElement) {
  312. // var (
  313. // tr = e.DOM.Find(".info").Eq(0).Find("table tbody tr").Eq(1)
  314. // saleAcc = utility.RemoveSpecialChar(strings.Replace(tr.Find("td").Eq(2).Text(), "(100%)", "", 1))
  315. // audiAcc = utility.RemoveSpecialChar(strings.Replace(tr.Find("td").Eq(3).Text(), "(100%)", "", 1))
  316. // )
  317. // SaleAcc, _ := strconv.Atoi(saleAcc)
  318. // AudiAcc, _ := strconv.Atoi(audiAcc)
  319. //
  320. // movieDetail.SaleAcc = SaleAcc
  321. // movieDetail.AudiAcc = AudiAcc
  322. //})
  323. err := c1.Post(config.MOVIE_DETAIL, map[string]string{
  324. "code": movieCd,
  325. "sType": "",
  326. "titleYN": "Y",
  327. "etcParam": "",
  328. "isOuterReq": "false",
  329. })
  330. if err != nil {
  331. errors++
  332. continue
  333. }
  334. //if this.Rest.Check(err) {
  335. // errors++
  336. // continue
  337. //}
  338. //err = c2.Post(config.MOVIE_DETAIL, map[string]string{
  339. // "code": movieCd,
  340. // "sType": "stat",
  341. //})
  342. //if err != nil {
  343. // errors++
  344. // continue
  345. //}
  346. //
  347. //if this.Rest.Check(err) {
  348. // errors++
  349. // continue
  350. //}
  351. if this.MovieDetailModel.IsExists(movieCd) == true {
  352. if err = this.MovieDetailModel.Update(movieDetail); err == nil {
  353. updateRows++
  354. } else {
  355. errors++
  356. }
  357. } else {
  358. if err = this.MovieDetailModel.Insert(movieDetail); err == nil {
  359. insertRows++
  360. } else {
  361. errors++
  362. }
  363. }
  364. fmt.Println(output(total, scraped, errors, insertRows, updateRows, target))
  365. codes[i] = ""
  366. target--
  367. total++
  368. }
  369. //msg := "[영화 상세 정보 수집 종료]\n"
  370. //msg += output(total, scraped, insertRows, updateRows, errors)
  371. //utility.SendMessage(msg)
  372. c.JSON(http.StatusOK, gin.H{
  373. "total": total,
  374. "insertRows": insertRows,
  375. "updateRows": updateRows,
  376. })
  377. }
  378. /**
  379. * 영화진흥위원회 박스오피스 (통계 조회)
  380. */
  381. /*
  382. func (this *Cron) Stats(c *gin.Context) {
  383. var (
  384. start = time.Now()
  385. total, errors, insertRows, updateRows, page = 0, 0, 0, 0, 1
  386. output = func(n ...int) string {
  387. s := fmt.Sprintf("Total : %d\n", n[0])
  388. s += fmt.Sprintf("Error: %d\n", n[1])
  389. s += fmt.Sprintf("InsertRows: %d\n", n[2])
  390. s += fmt.Sprintf("UpdateRows: %d\n", n[3])
  391. s += fmt.Sprintf("Page: %d\n", n[4])
  392. s += fmt.Sprintf("소요시간: %f초\n", time.Since(start).Seconds())
  393. return s
  394. }
  395. )
  396. req := this.MovieStatsModel.SearchBoxOfficeParams
  397. req.ServiceKey = config.Env.Movie.Kcisa.BoxOfficeKey
  398. req.NumOfRows = 2000
  399. req.PageNo = 1
  400. for {
  401. var (
  402. insertData = make([]model.BoxOfficeInfo, 0)
  403. updateData = make([]model.BoxOfficeInfo, 0)
  404. )
  405. req.PageNo = page
  406. data, err := this.Kobis.MovieBoxOfficeAPI(req)
  407. if err != nil {
  408. c.JSON(http.StatusBadRequest, err.Error())
  409. break
  410. }
  411. list := data.Response.Body.Items.Item
  412. if len(list) <= 0 {
  413. break
  414. }
  415. for _, row := range list {
  416. query, err := url.ParseQuery(row.Url)
  417. if err != nil {
  418. errors++
  419. continue
  420. }
  421. movieCd := query.Get("dtCd")
  422. if this.MovieStatsModel.IsExists(movieCd) == true {
  423. updateData = append(updateData, row)
  424. updateRows++
  425. } else {
  426. insertData = append(insertData, row)
  427. insertRows++
  428. }
  429. }
  430. if insertRows > 0 {
  431. if err = this.MovieStatsModel.Insert(insertData); err != nil {
  432. errors++
  433. }
  434. }
  435. if updateRows > 0 {
  436. if err = this.MovieStatsModel.Update(updateData); err != nil {
  437. errors++
  438. }
  439. }
  440. fmt.Println(output(total, errors, insertRows, updateRows, page))
  441. page++
  442. total++
  443. }
  444. msg := "[영화 통계 정보 수집 종료]\n"
  445. msg += output(total, errors, insertRows, updateRows, page)
  446. utility.SendMessage(msg)
  447. c.JSON(http.StatusOK, gin.H{
  448. "total": total,
  449. "error": errors,
  450. "insertRows": insertRows,
  451. "updateRows": updateRows,
  452. "page": page,
  453. })
  454. }
  455. */
  456. func GetKey(c *gin.Context) string {
  457. switch c.Query("key") {
  458. case "1":
  459. return config.Movie.Kobis.ApiKey_1
  460. case "2":
  461. return config.Movie.Kobis.ApiKey_2
  462. default:
  463. return config.Movie.Kobis.ApiKey_3
  464. }
  465. }
  466. // 마지막 호출 Page 저장
  467. func SetLastPage(page int) {
  468. data, err := os.Create(config.LAST_PAGE_PATH_KOBIS)
  469. if err != nil {
  470. fmt.Println(err)
  471. }
  472. defer func() {
  473. if data.Close() != nil {
  474. fmt.Println(err)
  475. }
  476. }()
  477. _, _ = data.WriteString(strconv.FormatInt(int64(page), 10))
  478. fmt.Printf("Set last page: %d\n", page)
  479. }
  480. // 마지막 호출 Page 조회
  481. func GetLastPage() int {
  482. byte, err := os.ReadFile(config.LAST_PAGE_PATH_KOBIS)
  483. if err != nil {
  484. fmt.Println(err)
  485. }
  486. page, _ := strconv.Atoi(strings.TrimSpace(string(byte)))
  487. if page == 0 {
  488. page = 1
  489. }
  490. return page
  491. }