Programming/Python

Python Web Scraping :: PS Plus 게임 카탈로그 만료일 추출하기

고고마코드 2023. 1. 9. 11:21
반응형

목적

PS Plus 게임 카탈로그는 영구적으로 등록되는 게임이 아니다.

위 이미지에 ‘2023/2/22 오후…’ 처럼 만료일이 정해져 있는데, 만료일은 3개월 이내가 되면 사이트에 노출되어 확인할 수 있다.

그러나 만료일이 예정된 것만 따로 찾아볼 수 없으며, 리스트에서 직접 하나씩 눌러서 확인해야 하기 때문에 어떤 게임이 만료되는지 확인하기 매우매우매우 귀찮다. (약 250~300개의 게임을 모두 눌러서 확인해야 한다…)

그래서 웹스크래핑으로 모든 게임들의 목록과 PS4/PS5 지원 여부도 추출하고, 그 중에서 만료 예정일도 함께 추출하는 것이 목적이다.


기획

  • 웹 스크래핑으로 PS Store 게임 카탈로그(https://store.playstation.com/ko-kr/category/05a2d027-cedc-4ac0-abeb-8fc26fec7180/1)의 정보를 수집해 모든 게임 카탈로그를 찾고, 그 중 만료 예정일을 찾는 것이 핵심이다.
  • 수집할 정보
    • 커버 이미지+게임 이름, 태그(PS4/PS5 구분 또는 에디션), 만료예정일, 게임 상세정보 링크
  • 정보를 추출해 웹페이지에 보기 쉽게 게임 이름 순으로 정렬 (만료일 순으로 정렬할까 하다가 만료되는 게임이 무조건 10개 미만인 것 같아 게임 이름 순으로 결정)
  • 추후 필요하다면 excel로 추출하는 것도 고려

개발

1. 추출

기본 게임 목록들은 ‘게임 카탈로그 - 모든 게임’ 에서 추출 https://store.playstation.com/ko-kr/category/05a2d027-cedc-4ac0-abeb-8fc26fec7180

def extract_ps_expired(games, page):
    ps_url = "https://store.playstation.com"
    url = f"{ps_url}/ko-kr/category/05a2d027-cedc-4ac0-abeb-8fc26fec7180/{page}"
    res = requests.get(url, headers={"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.6.1 Safari/605.1.15"})

    if res.status_code == 200:
        soup = BeautifulSoup(res.text, "html.parser")

        psw_grid_list = soup.find("ul", {"class": "psw-grid-list"})

        psw_content_link_list = psw_grid_list.find_all("a", {"class": "psw-content-link"})
        for psw_content_link in psw_content_link_list:
            psw_cover_list = psw_content_link.find_all("img", {"class": "psw-l-fit-cover"})
            psw_cover = psw_cover_list[0]["src"]

            href = psw_content_link["href"]
            href = href.replace("/en-us", "/ko-kr")
            sub_url = ps_url + href

            res_detail = requests.get(sub_url, headers={"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.6.1 Safari/605.1.15"})
            if res_detail.status_code == 200:
                soup_detail = BeautifulSoup(res_detail.text, "html.parser")

                game_title = soup_detail.find("div", {"class": "pdp-game-title"})
                area_game_name = game_title.find("h1")
                area_game_tag_list = game_title.findAll("span", {"class": "psw-t-tag"})
                area_cta = soup_detail.find("div", {"class": "pdp-cta"})
                area_ps_plus = area_cta.find("span", {"class": "psw-c-t-ps-plus"})
                if area_ps_plus is not None:
                    area_ps_plus_parent = area_ps_plus.find_parent("span", {"class": "psw-l-line-wrap"})
                    area_ps_expired = area_ps_plus_parent.find("span", {"class": "psw-c-t-2"})
                else:
                    area_ps_expired = None

                game_name = area_game_name.get_text()

                game_tags = []
                for area_game_tag in area_game_tag_list:
                    game_tags.append(area_game_tag.get_text())

                ps_expired_flag = False
                ps_expired = "종료 예정 없음"
                if area_ps_expired is not None:
                    ps_expired_flag = True
                    ps_expired = area_ps_expired.get_text()

                game = {
                    "game_name": game_name,
                    "game_tags": game_tags,
                    "ps_expired_flag": ps_expired_flag,
                    "ps_expired": ps_expired,
                    "psw_cover": psw_cover,
                    "sub_url": sub_url
                }
                games.append(game)

    return games

BeautifulSoup를 활용해 추출을 했다.

규칙만 찾으면 쉽게 만들 수 있다.

해당 목록에서 각 게임 클릭 시 넘어가는 상세보기 페이지에서 원하는 정보 추출

단, 종료 예정일이 없는 경우는 None처리에 주의해야 한다.

2. 페이징

def extract_ps_expired_paging():
    ps_url = "https://store.playstation.com"
    url = f"{ps_url}/ko-kr/category/05a2d027-cedc-4ac0-abeb-8fc26fec7180/1"
    res = requests.get(url, headers={"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.6.1 Safari/605.1.15"})

    games = []

    if res.status_code == 200:
        soup = BeautifulSoup(res.text, "html.parser")
        page_button_list = soup.find_all("button", {"class": "psw-page-button"})

        # 전체 페이지 수 추출
        last_page_value = int(page_button_list[len(page_button_list) - 1]["value"])

        for i in range(last_page_value):
            games = extract_ps_expired(games, i+1)

        # 게임 이름으로 정렬
        games =  sorted(games, key=lambda d: d['game_name'])
    return games

게임 목록 하단에 전체 페이지 수가 노출된다. 마지막 수가 곧 페이징 수 이므로 해당 수를 가져와 자체 페이징을 진행한다.

3. 최초 페이지

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    <link rel="stylesheet" href="../static/index.css" />

    <title>{{title}}</title>
</head>

<body class="body-index">
    <h1>{{title}}</h1>

    <form name="frm" action="/search" method="get">
        <input type="hidden" name="keyword" value="" />
        <div>
            <!-- <input type="text" name="keyword" placeholder="input a job and press enter" required> -->
            <button type="button" class="custom-btn btn-index" onClick="clickBtn('all');"><span>전체</span></button>
            <button type="button" class="custom-btn btn-index" onClick="clickBtn('expired');"><span>만료예정</span></button>
        </div>
    </form>

    <script type="text/javascript">
        const f = document.frm;

        function clickBtn(keyword) {
            f.keyword.value = keyword;
            f.submit();
        }
    </script>

</body>

</html>

모든 게임 목록을 보고 싶을 수도 있고, 만료 예정만 보고 싶을 수도 있으니

전체/만료예정을 구분해서 표시하도록 한다.

4. 추출 페이지

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    <link rel="stylesheet" href="../static/search.css" />

    <title>{{title}}</title>
</head>

<body>
    <section>
        <div>
            <span>📌</span><span class="find-title">게임 카탈로그 : {{games|length}}건</span>
        </div>

        <hr>

        <div class="tbl-header">
            <table cellpadding="0" cellspacing="0" border="0">
                <thead>
                    <tr>
                        <th>게임</th>
                        <th>태그</th>
                        <th>만료 예정일</th>
                        <th>링크</th>
                    </tr>
                </thead>
            </table>
        </div>
        <div class="tbl-content">
            <table cellpadding="0" cellspacing="0" border="0">
                <tbody>
                    {% for game in games %}
                    <tr>
                        <td>
                            <div class="company">
                                <img src="{{game['psw_cover']}}"/>
                                <span>{{game['game_name']}}</span>
                            </div>
                        </td>
                        <td>
                            {% for game_tag in game['game_tags'] %}
                            <span>{{game_tag}}</span>
                            {% endfor %}
                        </td>
                        <td>
                            <span>{{game['ps_expired']}}</span>
                        </td>
                        <td><a href="{{game['sub_url']}}" class="export" target="_blank">링크</a></td>
                    </tr>
                    {% endfor %}
                </tbody>
            </table>
        </div>
        <!-- // remoteok -->
    </section>
</body>

</html>

추출한 정보를 그대로 출력한다.

5. 메인

from flask import Flask, render_template, request
from extractors.ps_expired import extract_ps_expired_paging

app = Flask("PSPlusScrapper")

title = "PS Plus Game Catalog"

@app.route("/")
def index():
    return render_template("index.html", title=title)

@app.route("/search")
def search():
    keyword = request.args.get("keyword")

    before_games = extract_ps_expired_paging()
    games = []
    if keyword == "all":
        games = before_games
    else :
        for game in before_games:
            if game["ps_expired_flag"]:
                games.append(game)

    return render_template("search.html", games=games)

app.run("0.0.0.0")

search() 에서 게임 목록을 보여줄 때 전체/만료예정에 따라 구분하는 keyword가 있다.


결과

🔥 github

https://github.com/gogoma-code/PSPlus-expired-scraping


참고자료

🔥 Beautiful Soup: Build a Web Scraper With Python – Real Python

반응형