cron.go 17 KB

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