본문 바로가기
Project/ThreeMovie(영화리뷰및예약도우미)

CGV 영화 시간표 크롤링 및 예약 주소 가져오기 (Kotlin, jsoup)

by HDobby 2023. 2. 27.

 CGV

https://www.cgv.co.kr/

 

영화 그 이상의 감동. CGV

미정 --> 10 --> D Day

www.cgv.co.kr

극장으로 가서 예약 버튼을 눌러 어떤 방식으로 동작하는지 살펴봅시다.

아무 곳이나 상관 없지만, 저는 cgv상봉으로 하겠습니다.

하단에 영화 시간표가 보입니다. 아무거나 눌러봅시다.

http://www.cgv.co.kr/ticket/?MOVIE_CD=20031748&MOVIE_CD_GROUP=20031633&PLAY_YMD=20230207&THEATER_CD=0046&PLAY_START_TM=2250&AREA_CD=13&SCREEN_CD=009

URI을 보면 제가 선택했던 관과 영화 정보가 GET방식으로 전달되는 걸 볼 수 있습니다. 한번 살펴 보겠습니다.

  • MOVIE_CD
  • MOVIE_CD_GROUP
  • PLAY_YMD
  • THEATER_CD
  • PLAY_START_TM
  • AREA_CD
  • SCREEN_CD

로 구성된걸 볼 수 있습니다.

MOVIE_CD는 영화 코드, MOVIE_CD_GROUP 또한 영화를 구분하기 위한 코드 같습니다.

PLAY_YMD는 어떤 날 예매를 선택했는지

THEATER_CD와 AREA_CD는 어떤 관 영화를 선택했는지

PLAY_START_TM과 SCREEN_CD는 몇시 영화를 선택하고 어떤관에서 관람을 하는지에 관한 정보를 전달해주는 것 같습니다.

 

저장하거나 파싱해야할 정보가 너무 많아 다시 이전의 관 영화 선택화면으로 돌아가봅시다.

http://www.cgv.co.kr/theaters/?areacode=01&theaterCode=0046&date=20230207

THEATERCODE는 일치하지만 AREACODE는 01로 다른걸 확인할 수 있습니다.

왜 다를까요...?

 

소스코드 보단 영화관 정보를 보내주는 API가 있다면 그걸 활용하기 위해 F12를 눌러 NETWORK 탭으로 가서 뒤져봅시다.

iframe으로 추가 페이지를 띄어 api를 찾을수가 없습니다.

소스 코드로 가서 확인해야 할 것 같습니다.

해당 주소에 확인하는 예약 주소값이 다행히 들어있는 걸 확인할 수 있습니다.

쿼리로 넘겨주는 date값을 변경하면 날짜또한 변경됩니다. 영화관별로 존재하는 영화 시간표가 다르므로 해당 날짜에 대한 정보도 따로 가져와야 할 것 같습니다.

 

저장에 필요한 정보를 정리해 봅시다.

  • 시간 - 2월8일 12:30분
  • 남은 좌석 수 (저는 저장하지 않습니다.), 총 좌석 수
  • 2d, 4dx - 어떤 영화 타입인지
  • 5관 3관 - 관별로 영화 시간표가 다르기도 하고 관별로 크기가 달라 선호하는 관이 다를 수 있어 저장합니다.
  • 개봉 일자 - 저는 영화명_개봉일자 (아바타_2023)과 같이 저장하므로 변경을 해줘야 합니다.

html을 뜯어 봅시다.

우선 영화 정보들은 각 영화별 col-times로 구분되어 있습니다.

그 내부에 type-hall로 구분되어 관별로 나눠 저장되 있습니다.

  • 시간, 남은 좌석 수 - info-timetable > ul > li > a에 data-playstarttime, data-seatremaincnt로 저장 되어 있습니다.
  • 영화 타입과 관, 총 좌석 수 - info-hall > ul > 첫번째 자식 = 2D, 두번째 자식 = 5관, 세번째 자식 = 총 좌석 수
  • 개봉 일자 - info-movie > 마지막 i

아래 다른 영화를 보고 패턴이 동일한지 확인해 봅시다.

영화관 내부의 시간표 패턴분석은 끝난 것 같습니다.

코드로 작성해 데이터를 가져와 보도록 합시다.


23년 2월 27일 cgv상봉의 시간표 입니다.

영화가 굉장히 많습니다.

Jsoup을 사용할 때 단순히 common/shotimes/iframeTheater.aspx로만 접속하여 넘겨주면 데이터가 오지 않습니다.

Referer과 userAgent, header를 조금 추가해주셔야 작동하게 보안 패치가 적용된 것 같습니다.

override fun getShowTime(movieTheater: String): List<ShowTime> {
    val url: String =
        "http://www.cgv.co.kr/common/showtimes/iframeTheater.aspx?theatercode=0046&date=20230227"
    val conn = Jsoup.connect(url)
        .userAgent(userAgent)
        .referrer(
            "http://www.cgv.co.kr/theaters/"
        )
        .header(
            "Accept",
            "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"
        )
        .header("Accept-Language", "ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7")
        .header(
            "Cookie",
            "ASP.NET_SessionId=test;"
        )
    val doc = conn.get()
    val showtimes = doc.getElementsByClass("col-times")

    println(showtimes.size)
    for (showtime in showtimes) {
        val infoMovie = showtime.getElementsByClass("info-movie")
        val movieName = infoMovie[0].getElementsByTag("a")[0].text()
        val age = infoMovie[0].getElementsByTag("i")[0].text()
        val categories = infoMovie[0].getElementsByTag("i")[1].text()
        val runningTime = infoMovie[0].getElementsByTag("i")[2].text()
        val comeOut = infoMovie[0].getElementsByTag("i")[3].text().replace("[^0-9]+".toRegex(), "")

        println("${movieName} ${age} ${categories} ${runningTime} ${comeOut}")
    }




    return emptyList()
}

영화 정보들은 잘 넘어옵니다.

이제 영화별 영화관과 상영 정보를 가져오도록 해봅시다.

 

override fun getShowTime(movieTheater: String): List<ShowTime> {
    val url: String =
        "http://www.cgv.co.kr/common/showtimes/iframeTheater.aspx?theatercode=0046&date=20230227"
    val conn = Jsoup.connect(url)
        .userAgent(userAgent)
        .referrer(
            "http://www.cgv.co.kr/theaters/"
        )
        .header(
            "Accept",
            "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"
        )
        .header("Accept-Language", "ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7")
        .header(
            "Cookie",
            "ASP.NET_SessionId=test;"
        )
    val doc = conn.get()
    val showtimes = doc.getElementsByClass("col-times")

    println(showtimes.size)
    for (showtime in showtimes) {
        val infoMovie = showtime.getElementsByClass("info-movie")[0]
        val movieName = infoMovie.getElementsByTag("a")[0].text()
        val age = infoMovie.getElementsByTag("i")[0].text()
        val categories = infoMovie.getElementsByTag("i")[1].text()
        val runningTime = infoMovie.getElementsByTag("i")[2].text()
        val comeOut = infoMovie.getElementsByTag("i")[3].text().replace("[^0-9]+".toRegex(), "")

        println("${movieName} ${age} ${categories} ${runningTime} ${comeOut}")

        val typeHalls = showtime.getElementsByClass("type-hall")
        for (typeHall in typeHalls) {
            val infoHall = typeHall.getElementsByClass("info-hall")[0]
            val dimension = infoHall.getElementsByTag("li")[0].text()
            val whereTheater = infoHall.getElementsByTag("li")[1].text()
            val allSeats = infoHall.getElementsByTag("li")[2].text().replace("[^0-9]+".toRegex(), "")

            println("${dimension} ${whereTheater} ${allSeats}")

            val infoTimeTable = typeHall.getElementsByClass("info-timetable")[0]
            val timeinfoes = infoTimeTable.getElementsByTag("li")

            for (timeinfo in timeinfoes) {
                val datas = timeinfo.getElementsByTag("a")[0]
                val starttime = datas.attr("data-playstarttime")
                val href = CGVurl + datas.attr("href")
                val seatsLeft = datas.attr("data-seatremaincnt")

                println("${starttime} ${href} ${seatsLeft}")
            }
        }
    }

    return emptyList()
}

데이터가 분류되서 잘 넘어옵니다.

그런데 만약 남아있는 모든 시간표가 마감인 경우 a 태그가 없습니다. 해당 사항만 따로 에러처리를 해줍시다.

for (timeinfo in timeinfoes) {
    val datas = timeinfo.getElementsByTag("a")
    var starttime = ""
    var href = ""
    var seatsLeft = ""
    if (datas.isEmpty()) {
        starttime = timeinfo.getElementsByTag("em")[0].text().replace(":", "")
        seatsLeft = "마감"
    } else {
        starttime = datas[0].attr("data-playstarttime")
        href = CGVurl + datas[0].attr("href")
        seatsLeft = datas[0].attr("data-seatremaincnt")
    }


    println("${movieName} ${age}세 ${categories} ${runningTime}분 ${comeOut} ${dimension} ${whereTheater} ${allSeats} ${starttime} ${href} ${seatsLeft}좌석 남음")
}

이제 모든 영화관과 모든 날짜의 데이터가 필요하므로 해당 내용을 구해봅시다.

우선 영화관 (CGV상봉, CGV강남)과 같은 데이터들 먼저 구하기로 합시다.

 

sect-city 하위에 li 태그로 분리되어 감싸져 있고 바로 아래 a태그에 지역이 area 클래스 하단 a 태그의 class혹은 내용에 관 이름이 들어가 있습니다. 그런데 페이지를 요청해서 보면 코드에 안들어 있습니다.

요청 들어온 페이지를 보면 하드코딩으로 데이터가 있는 것 같습니다...?

아무리 찾아도 안나와서 상봉으로 검색해보니 script 내부에 theaterJsonData로 되어있네요... 저기서 데이터를 가져와야 할 듯 싶습니다...

 

데이터는

val url: String =
    "http://www.cgv.co.kr/theaters/"
val conn = Jsoup.connect(url)
    .userAgent(userAgent)
val doc = conn.get()
val scripts = doc.select("script")

var theatersData: String = ""

for (script in scripts) {
    if (script.data().contains("var theaterJsonData")) {
        val pattern: Pattern = Pattern.compile(".*var theaterJsonData = ([^;]*);")
        val matcher: Matcher = pattern.matcher(script.data())
        if (matcher.find()) {
            theatersData = matcher.group(1)
            break
        }
    }
}

 

을 통해서 가져올 수 있습니다.

[{"AreaTheaterDetailList":[{"RegionCode":"01","TheaterCode":"0056","TheaterName":"CGV강남","TheaterName_ENG":null,"IsSelected":true},{"RegionCode":"01","TheaterCode":"0001","TheaterName":"CGV강변","TheaterName_ENG":null,"IsSelected":false},{"RegionCode":"01","TheaterCode":"0229","TheaterName":"CGV건대입구","TheaterName_ENG":null,"IsSelected":false},{"RegionCode":"01","TheaterCode":"0010","TheaterName":"CGV구로","TheaterName_ENG":null,"IsSelected":false},{"RegionCode":"01","TheaterCode":"0063","TheaterName":"CGV대학로","TheaterName_ENG":null,"IsSelected":false},{"RegionCode":"01","TheaterCode":"0252","TheaterName":"CGV동대문","TheaterName_ENG":null,"IsSelected":false},{"RegionCode":"01","TheaterCode":"0230","TheaterName":"CGV등촌","TheaterName_ENG":null,"IsSelected":false},{"RegionCode":"01","TheaterCode":"0009","TheaterName":"CGV명동","TheaterName_ENG":null,"IsSelected":false},{"RegionCode":"01","TheaterCode":"0105","TheaterName":"CGV명동역 씨네라이브러리","TheaterName_ENG":null,"IsSelected":false},{"RegionCode":"01","TheaterCode":"0057","TheaterName":"CGV미아","TheaterName_ENG":null,"IsSelected":false},{"RegionCode":"01","TheaterCode":"0288","TheaterName":"CGV방학","TheaterName_ENG":null,"IsSelected":false},{"RegionCode":"01","TheaterCode":"0030","TheaterName":"CGV불광","TheaterName_ENG":null,"IsSelected":false},{"RegionCode":"01","TheaterCode":"0046","TheaterName":"CGV상봉","TheaterName_ENG":null,"IsSelected":false},{"RegionCode":"01","TheaterCode":"0300","TheaterName":"CGV성신여대입구","TheaterName_ENG":null,"IsSelected":false},{"RegionCode":"01","TheaterCode":"0088","TheaterName":"CGV송파","TheaterName_ENG":null,"IsSelected":false},{"RegionCode":"01","TheaterCode":"0276","TheaterName":"CGV수유","TheaterName_ENG":null,"IsSelected":false},{"RegionCode":"01","TheaterCode":"0150","TheaterName":"CGV신촌아트레온","TheaterName_ENG":null,"IsSelected":false},{"RegionCode":"01","TheaterCode":"0040","TheaterName":"CGV압구정","TheaterName_ENG":null,"IsSelected":false},{"RegionCode":"01","TheaterCode":"0112","TheaterName":"CGV여의도","TheaterName_ENG":null,"IsSelected":false},{"RegionCode":"01","TheaterCode":"0292","TheaterName":"CGV연남","TheaterName_ENG":null,"IsSelected":false},{"RegionCode":"01","TheaterCode":"0059","TheaterName":"CGV영등포","TheaterName_ENG":null,"IsSelected":false},{"RegionCode":"01","TheaterCode":"0074","TheaterName":"CGV왕십리","TheaterName_ENG":null,"IsSelected":false},{"RegionCode":"01","TheaterCode":"0013","TheaterName":"CGV용산아이파크몰","TheaterName_ENG":null,"IsSelected":false},{"RegionCode":"01","TheaterCode":"0131","TheaterName":"CGV중계","TheaterName_ENG":null,"IsSelected":false},{"RegionCode":"01","TheaterCode":"0199","TheaterName":"CGV천호","TheaterName_ENG":null,"IsSelected":false},{"RegionCode":"01","TheaterCode":"0107","TheaterName":"CGV청담씨네시티","TheaterName_ENG":null,"IsSelected":false},{"RegionCode":"01","TheaterCode":"0223","TheaterName":"CGV피카디리1958","TheaterName_ENG":null,"IsSelected":false},{"RegionCode":"01","TheaterCode":"0164","TheaterName":"CGV하계","TheaterName_ENG":null,"IsSelected":false},{"RegionCode":"01","TheaterCode":"0191","TheaterName":"CGV홍대","TheaterName_ENG":null,"IsSelected":false},{"RegionCode":"01","TheaterCode":"P001","TheaterName":"CINE de CHEF 압구정","TheaterName_ENG":null,"IsSelected":false},{"RegionCode":"01","TheaterCode":"P013","TheaterName":"CINE de CHEF 용산아이파크몰","TheaterName_ENG":null,"IsSelected":false}],"RegionCode":"01","RegionName":"서울","RegionName_ENG":"Seoul","DisplayOrder":"1","IsSelected":true},{"AreaTheaterDetailList":[{"RegionCode":"02","TheaterCode":"0260","TheaterName":"CGV경기광주","TheaterName_ENG":null,"IsSelected":false},{"RegionCode":"02","TheaterCode":"0255","TheaterName":"CGV고양행신","TheaterName_ENG":null,"IsSelected":false},{"RegionCode":"02","TheaterCode":"0257","TheaterName":"CGV광교","TheaterName_ENG":null,"IsSelected":false},{"RegionCode":"02","TheaterCode":"0266","TheaterName":"CGV광교상현","TheaterName_ENG":null,"IsSelected":false},{"RegionCode":"02","TheaterCode":"0348","TheaterName":"CGV광명역","TheaterName_ENG":null,"IsSelected":false},{"RegionCode":"02","TheaterCode":"0232","TheaterName":"CGV구리","TheaterName_ENG":null,"IsSelected":false},{"RegionCode":"02","TheaterCode":"0344","TheaterName":"CGV기흥","TheaterName_ENG":null,"IsSelected":false},{"RegionCode":"02","TheaterCode":"0278","TheaterName":"CGV김포","TheaterName_ENG":null,"IsSelected":false},{"RegionCode":"02","TheaterCode":"0188","TheaterName":"CGV김포운양","TheaterName_ENG":null,"IsSelected":false},{"RegionCode":"02","TheaterCode":"0298","TheaterName":"CGV김포한강","TheaterName_ENG":null,"IsSelected":false},{"RegionCode":"02","TheaterCode":"0351","TheaterName":"CGV다산","TheaterName_ENG":null,"IsSelected":false},{"RegionCode":"02","TheaterCode":"0124","TheaterName":"CGV동백","TheaterName_ENG":null,"IsSelected":false},{"RegionCode":"02","TheaterCode":"0041","TheaterName":"CGV동수원","TheaterName_ENG":null,"IsSelected":false},{"RegionCode":"02","TheaterCode":"0106","TheaterName":"CGV동탄","TheaterName_ENG":null,"IsSelected":false},{"RegionCode":"02","TheaterCode":"0265","TheaterName":"CGV동탄역","TheaterName_ENG":null,"IsSelected":false},{"RegionCode":"02","TheaterCode":"0233","TheaterName":"CGV동탄호수공원","TheaterName_ENG":null,"IsSelected":false},{"RegionCode":"02","TheaterCode":"0226","TheaterName":"CGV배곧","TheaterName_ENG":null,"IsSelected":false},{"RegionCode":"02","TheaterCode":"0155","TheaterName":"CGV범계","TheaterName_ENG":null,"IsSelected":false},{"RegionCode":"02","TheaterCode":"0015","TheaterName":"CGV부천","TheaterName_ENG":null,"IsSelected":false},{"RegionCode":"02","TheaterCode":"0194","TheaterName":"CGV부천역","TheaterName_ENG":null,"IsSelected":false},...]

각각의 지역이 "AreaTheaterDetailList"로 묶여있고 [cgv리스트], 지역명, 지역코드와 같은 방식으로 되어있습니다.

 

json을 파싱하기 위해 build.gradle에 dependency를 추가해 줍시다.

implementation("org.json:json:20220924")

만약 ClassNotFoundException이 발생하면 서버를 껏다 켜봅시다.

저는 저장시 자동 컴파일 옵션을 사용했더니 저 에러가 나왔습니다.

 

정확한 구분 방식을 살펴 봅시다.

"AreaTheaterDetailList" :[RegionCode: String, TheaterCode: String, TheaterName: String, TheaterName_ENG: String, IsSelected: boolean], RegionCode: String, RegionName: String, RegionName_ENG: String, DisplayOrder: String, IsSelected: boolean

의 형태로 구성되어 있습니다.

저희가 필요한 데이터는 TheaterCode와 TheaterName과 RegionName만 있으면 됩니다.

 

강원이 제일 짧으니 강원으로 테스트 해봅시다.

val jsonArray = JSONArray(theatersData)

for (i in 0 until jsonArray.length()) {
    val theaters = jsonArray.getJSONObject(i)
    println(theaters)
}
{"DisplayOrder":"4","RegionCode":"12","AreaTheaterDetailList":[{"RegionCode":"12","TheaterName":"CGV강릉","TheaterName_ENG":null,"IsSelected":false,"TheaterCode":"0139"},{"RegionCode":"12","TheaterName":"CGV기린","TheaterName_ENG":null,"IsSelected":false,"TheaterCode":"0355"},{"RegionCode":"12","TheaterName":"CGV원주","TheaterName_ENG":null,"IsSelected":false,"TheaterCode":"0144"},{"RegionCode":"12","TheaterName":"CGV원통","TheaterName_ENG":null,"IsSelected":false,"TheaterCode":"0354"},{"RegionCode":"12","TheaterName":"CGV인제","TheaterName_ENG":null,"IsSelected":false,"TheaterCode":"0281"},{"RegionCode":"12","TheaterName":"CGV춘천","TheaterName_ENG":null,"IsSelected":false,"TheaterCode":"0070"}],"RegionName":"강원","RegionName_ENG":"Kangwon","IsSelected":false}

각 지역별로 뜯어왔으니 필요한 데이터만 파싱하여 봅시다.

 

val jsonArray = JSONArray(theatersData)

for (i in 0 until jsonArray.length()) {
    val theaters = jsonArray.getJSONObject(i)
    val regionName = theaters.getString("RegionName")
    val areaTheaterList = theaters.optJSONArray("AreaTheaterDetailList")

    for (j in 0 until areaTheaterList.length()) {
        val theater = areaTheaterList.getJSONObject(j)
        val theaterCode = theater.getString("TheaterCode")
        val theaterName = theater.getString("TheaterName")

        println("${regionName} ${theaterName} ${theaterCode}")
    }
}


영화관 정보도 전부 파싱하여 가져왔습니다. 이제 마지막 날짜 데이터를 구해봅시다.

날짜 데이터는 이전에 우리가 구했던 common/shotimes/iframeTheater.aspx을 사용해 구해와야 할 것 같습니다.

영화관련 데이터는 전부 저 페이지에서 관리하여 기본 페이지에는 들어있지 않습니다...

미리 알았으면 날짜데이터도 가져올 걸 그랬습니다.

 

날짜는 굉장히 쉽게 구할 것 같습니다. div class="day"로 전부 묶여있어 저걸 가져온 뒤에 href를 그대로 가져다가 사용해도 되고, 날짜만 따로 구해도 될것 같습니다.

저는 우선 href에서 날짜만 따로 빼도록 하겠습니다. 아마 결국엔 href만 가져다가 사용할 것 같습니다..

 

val url: String =
    "http://www.cgv.co.kr/common/showtimes/iframeTheater.aspx?theatercode=0046&date=20230227"
val conn = Jsoup.connect(url)
    .userAgent(userAgent)
    .referrer(
        "http://www.cgv.co.kr/theaters/"
    )
    .header(
        "Accept",
        "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"
    )
    .header("Accept-Language", "ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7")
    .header(
        "Cookie",
        "ASP.NET_SessionId=test;"
    )
val doc = conn.get()

val days = doc.getElementsByClass("day")
println(days)

3월 10일까지 정상적으로 들어왔으니 파싱만 해줍시다.

이전에 사용했던 Pattern과 Matcher를 사용하여 파싱해봅시다.

 

val url: String =
    "http://www.cgv.co.kr/common/showtimes/iframeTheater.aspx?theatercode=0046"
val conn = Jsoup.connect(url)
    .userAgent(userAgent)
    .referrer(
        "http://www.cgv.co.kr/theaters/"
    )
    .header(
        "Accept",
        "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"
    )
    .header("Accept-Language", "ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7")
    .header(
        "Cookie",
        "ASP.NET_SessionId=test;"
    )
val doc = conn.get()

val days = doc.getElementsByClass("day")
var date: String = ""

for (day in days) {
    val href = day.getElementsByTag("a")[0].attr("href")
    val pattern: Pattern = Pattern.compile(".*date=([^&]+).*")
    val matcher: Matcher = pattern.matcher(href)
    if (matcher.find()) {
        date = matcher.group(1)
        println(date)
    }
}


해당 영화관의 날짜 목록까지 전부 구했습니다.

그럼 합쳐서 한번 진행 해보도록 합시다.

 

진행 순서는 다음과 같이 될 것 같습니다.

  1. 영화관 목록을 가져온다. (추후 db에 저장한 뒤 db에서 가져올 예정)
  2. url에 해당 영화관 코드를 넘겨준 뒤 존재하는 date리스트를 가져옵니다.
  3. 해당 영화관 코드와 date를 가지고 해당 영화관, 해당 날짜의 시간표를 모두 가져옵니다.
class ShowTimeServiceImpl(
	val showTimeRepositorySupport: ShowTimeRepositorySupport
) : ShowTimeService {
	val userAgent: String =
		"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36"
	val CGVurl = "http://www.cgv.co.kr"
	override fun getShowTime(movieTheater: String): List<ShowTime> {
		val theaterlist = getCGVTheaterData()
		getCGVShowingTime(theaterlist)

		return emptyList()
	}

	fun getCGVDateList(theatercode: String): ArrayList<String> {
		val url: String =
			"http://www.cgv.co.kr/common/showtimes/iframeTheater.aspx?theatercode=${theatercode}"
		val conn = Jsoup.connect(url)
			.userAgent(userAgent)
			.referrer(
				"http://www.cgv.co.kr/theaters/"
			)
			.header(
				"Accept",
				"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"
			)
			.header("Accept-Language", "ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7")
			.header(
				"Cookie",
				"ASP.NET_SessionId=test;"
			)
		val doc = conn.get()

		val days = doc.getElementsByClass("day")
		var datelist = ArrayList<String>()

		for (day in days) {
			val href = day.getElementsByTag("a")[0].attr("href")
			val pattern: Pattern = Pattern.compile(".*date=([^&]+).*")
			val matcher: Matcher = pattern.matcher(href)
			if (matcher.find()) {
				datelist.add(matcher.group(1))
			}
		}

		return datelist
	}

	fun getCGVTheaterData(): ArrayList<Triple<String, String, String>> {
		val url: String =
			"http://www.cgv.co.kr/theaters/"
		val conn = Jsoup.connect(url)
			.userAgent(userAgent)
		val doc = conn.get()
		val scripts = doc.select("script")

		var theatersData: String = ""

		for (script in scripts) {
			if (script.data().contains("var theaterJsonData")) {
				val pattern: Pattern = Pattern.compile(".*var theaterJsonData = ([^;]*);")
				val matcher: Matcher = pattern.matcher(script.data())
				if (matcher.find()) {
					theatersData = matcher.group(1)
					break
				}
			}
		}

		val jsonArray = JSONArray(theatersData)
		val theaterlist = ArrayList<Triple<String, String, String>>()

		for (i in 0 until jsonArray.length()) {
			val theaters = jsonArray.getJSONObject(i)
			val regionName = theaters.getString("RegionName")
			val areaTheaterList = theaters.optJSONArray("AreaTheaterDetailList")

			for (j in 0 until areaTheaterList.length()) {
				val theater = areaTheaterList.getJSONObject(j)
				val theaterCode = theater.getString("TheaterCode")
				val theaterName = theater.getString("TheaterName")

				theaterlist.add(Triple(regionName, theaterCode, theaterName))
			}
		}

		return theaterlist
	}

	fun getCGVShowingTime(theaterlist: ArrayList<Triple<String, String, String>>): Unit {
		for (theater in theaterlist) {
			val datelist = getCGVDateList(theater.second)
			for (date in datelist) {
				println("${theater.first} ${theater.third} ${date}")
				val url: String =
					"http://www.cgv.co.kr/common/showtimes/iframeTheater.aspx?theatercode=${theater.second}&date=${date}"
				val conn = Jsoup.connect(url)
					.userAgent(userAgent)
					.referrer(
						"http://www.cgv.co.kr/theaters/"
					)
					.header(
						"Accept",
						"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"
					)
					.header("Accept-Language", "ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7")
					.header(
						"Cookie",
						"ASP.NET_SessionId=test;"
					)
				val doc = conn.get()
				val showtimes = doc.getElementsByClass("col-times")

				for (showtime in showtimes) {
					val infoMovie = showtime.getElementsByClass("info-movie")[0]
					val movieName = infoMovie.getElementsByTag("a")[0].text()
					val age = infoMovie.getElementsByTag("i")[0].text()
					val categories = infoMovie.getElementsByTag("i")[1].text()
					val runningTime = infoMovie.getElementsByTag("i")[2].text()
					val comeOut = infoMovie.getElementsByTag("i")[3].text().replace("[^0-9]+".toRegex(), "")


					val typeHalls = showtime.getElementsByClass("type-hall")
					for (typeHall in typeHalls) {
						val infoHall = typeHall.getElementsByClass("info-hall")[0]
						val dimension = infoHall.getElementsByTag("li")[0].text()
						val whereTheater = infoHall.getElementsByTag("li")[1].text()
						val allSeats = infoHall.getElementsByTag("li")[2].text().replace("[^0-9]+".toRegex(), "")


						val infoTimeTable = typeHall.getElementsByClass("info-timetable")[0]
						val timeinfoes = infoTimeTable.getElementsByTag("li")

						for (timeinfo in timeinfoes) {
							val datas = timeinfo.getElementsByTag("a")
							var starttime = ""
							var href = ""
							var seatsLeft = ""
							if (datas.isEmpty()) {
								starttime = timeinfo.getElementsByTag("em")[0].text().replace(":", "")
								seatsLeft = "마감"
							} else {
								starttime = datas[0].attr("data-playstarttime")
								href = CGVurl + datas[0].attr("href")
								seatsLeft = datas[0].attr("data-seatremaincnt")
							}


							println("${movieName} ${age}세 ${categories} ${runningTime}분 ${comeOut} ${dimension} ${whereTheater} ${allSeats} ${starttime} ${href} ${seatsLeft}좌석 남음")
						}
					}
				}
			}
		}

	}

	fun getShowingTime(movieTheater: String): List<ShowTime> = showTimeRepositorySupport.getShowTime(movieTheater)
}

데이터가 너무 많아 잘려서 마지막에 남아있는 cgv제주노형만 보여드리겠습니다.

광주/전라/제주 CGV제주노형 20230305
대외비 15세 범죄, 드라마 116분분 20230301 2D 1관 178 0900 http://www.cgv.co.kr/ticket/?MOVIE_CD=20031944&MOVIE_CD_GROUP=20031944&PLAY_YMD=20230305&THEATER_CD=0259&PLAY_START_TM=0900&AREA_CD=01&SCREEN_CD=001 168좌석 남음
대외비 15세 범죄, 드라마 116분분 20230301 2D 1관 178 1130 http://www.cgv.co.kr/ticket/?MOVIE_CD=20031944&MOVIE_CD_GROUP=20031944&PLAY_YMD=20230305&THEATER_CD=0259&PLAY_START_TM=1130&AREA_CD=01&SCREEN_CD=001 168좌석 남음
대외비 15세 범죄, 드라마 116분분 20230301 2D 1관 178 1400 http://www.cgv.co.kr/ticket/?MOVIE_CD=20031944&MOVIE_CD_GROUP=20031944&PLAY_YMD=20230305&THEATER_CD=0259&PLAY_START_TM=1400&AREA_CD=01&SCREEN_CD=001 162좌석 남음
대외비 15세 범죄, 드라마 116분분 20230301 2D 1관 178 1630 http://www.cgv.co.kr/ticket/?MOVIE_CD=20031944&MOVIE_CD_GROUP=20031944&PLAY_YMD=20230305&THEATER_CD=0259&PLAY_START_TM=1630&AREA_CD=01&SCREEN_CD=001 168좌석 남음
대외비 15세 범죄, 드라마 116분분 20230301 2D 1관 178 1900 http://www.cgv.co.kr/ticket/?MOVIE_CD=20031944&MOVIE_CD_GROUP=20031944&PLAY_YMD=20230305&THEATER_CD=0259&PLAY_START_TM=1900&AREA_CD=01&SCREEN_CD=001 166좌석 남음
대외비 15세 범죄, 드라마 116분분 20230301 2D 1관 178 2130 http://www.cgv.co.kr/ticket/?MOVIE_CD=20031944&MOVIE_CD_GROUP=20031944&PLAY_YMD=20230305&THEATER_CD=0259&PLAY_START_TM=2130&AREA_CD=01&SCREEN_CD=001 158좌석 남음

분이 이미 붙어있었네요... 


만약 영화관 주소가 필요하시다면

해당 영화관 주소로 접속한 뒤(theaterCode를 query로 넘겨줘야 합니다.) theater-info title에 들어있습니다.

728x90

댓글