AIoT

AIoT 정규 27일차

맥기짱짱 2024. 2. 6. 10:09

Steam게임 추천 프로그램 만들기

1. 구상

1) 데이터 수집

: 웹 크롤링을 활용해서 Steam사이트에 올라와 있는 게임과 리뷰를 가져와 JSON 파일과 CSV파일로 저장.

 

2)  DB에 테이블 형식으로 저장

: 웹 크롤링으로 수집한 파일을 MySQL에 테이블 형식으로 저장. Game_Id를 Primary Key로 지정하고 "games" 테이블과 "reviews" 테이블사이에 외래키로 설정한다.

 

3) 쿼리문을 이용해서 추천 매커니즘 구상

: MySQL 쿼리문사용해 게임 타이틀을 검색하면 리뷰에 달린 좋아요수를 기준으로 높은 순으로 정렬해서 display한다. 리뷰에는 리뷰어 닉네임, 리뷰어가 플레이 해본 게임수(리뷰 신뢰성 증가), 리뷰어의 리뷰수, 추천 유무, 리뷰 작성일, 리뷰 내용을 display한다.

 

4) GUI를 사용해 해당 기능 구현

 

 

 

1) 데이터 수집 -  05.02.2024

: GitHub에서 검색한 Steam용 web crawling 코드를 받았다.( https://github.com/aesuli/steam-crawler?tab=readme-ov-file )

 

1-1) GitHub에서 가져온 기존의 코드를 수정하여서 게임의 수를 19,213 개에서 상위 730개로 줄임

 

import argparse
import csv
import os
import re
import socket
import urllib
import urllib.request
from contextlib import closing
from time import sleep


def download_page(url, maxretries, timeout, pause):
    tries = 0
    htmlpage = None
    while tries < maxretries and htmlpage is None:
        try:
            with closing(urllib.request.urlopen(url, timeout=timeout)) as f:
                htmlpage = f.read()
                sleep(pause)
        except (urllib.error.URLError, socket.timeout, socket.error):
            tries += 1
    return htmlpage


def extract_games(basepath, outputfile_name):
    gameidre = re.compile(r'/(app|sub)/([0-9]+)/')
    gamenamere = re.compile(r'<span class="title">(.*?)</span>')
    games = dict()
    game_count = 0
    for root, _, files in os.walk(basepath):
        if game_count >= 750:
            break
        for file in files:
            if game_count >= 750:
                break
            fullpath = os.path.join(root, file)
            with open(fullpath, encoding='utf8') as f:
                htmlpage = f.read()

                gameids = list(gameidre.findall(htmlpage))
                gamenames = list(gamenamere.findall(htmlpage))
                for app, id_, name in zip([app for (app, _) in gameids], [id_ for (_, id_) in gameids], gamenames):
                    games[(app, id_)] = name
                    game_count += 1
    with open(outputfile_name, mode='w', encoding='utf-8', newline='') as outputfile:
        writer = csv.writer(outputfile)
        for app, id_ in games:
            writer.writerow([app, id_, games[(app, id_)]])


def main():
    parser = argparse.ArgumentParser(description='Crawler of Steam game ids and names')
    parser.add_argument(
        '-i', '--input', help='Input file or path (all files in subpath are processed)', default='./data/pages/games',
        required=False)
    parser.add_argument(
        '-o', '--output', help='Output file', default='./data/games.csv', required=False)
    args = parser.parse_args()

    extract_games(args.input, args.output)


if __name__ == '__main__':
    main()

 

1-2) 리뷰 크롤링할때 언어 설정을 *english 에서 *koreana"로 변경. ( 밑에 나오는 코드 세 번째 줄에 language=englsih를 language= koreana로 변경)

* Steam html 언어 설정값 영어 = "english", 한국어 = "koreana"

def getgamereviews(ids, timeout, maxretries, pause, out, maxreviews):
    urltemplate = string.Template(
        'http://store.steampowered.com//appreviews/$id?cursor=$cursor&filter=recent&language=koreana')
    endre = re.compile(r'({"success":2})|(no_more_reviews)')

    for (dir, id_, name) in ids:
        if dir == 'sub':
            print('skipping sub %s %s' % (id_, name))
            continue

        gamedir = os.path.join(out, 'pages', 'reviews', '-'.join((dir, id_)))

        donefilename = os.path.join(gamedir, 'reviews-done.txt')
        if not os.path.exists(gamedir):
            os.makedirs(gamedir)
        elif os.path.exists(donefilename):
            print('skipping app %s %s' % (id_, name))
            continue

        print(dir, id_, name)

        cursor = '*'
        offset = 0
        page = 1
        maxError = 10
        errorCount = 0
        while True:
            url = urltemplate.substitute({'id': id_, 'cursor': cursor})
            print(offset, url)
            htmlpage = download_page(url, maxretries, timeout, pause)

            if htmlpage is None:
                print('Error downloading the URL: ' + url)
                sleep(pause * 3)
                errorCount += 1
                if errorCount >= maxError:
                    print('Max error!')
                    break
            else:
                with open(os.path.join(gamedir, 'reviews-%s.html' % page), 'w', encoding='utf-8') as f:
                    htmlpage = htmlpage.decode()
                    if endre.search(htmlpage):
                        break
                    f.write(htmlpage)
                    page = page + 1
                    if maxreviews != -1 and page > maxreviews:
                        break
                    parsed_json = (json.loads(htmlpage))
                    cursor = urllib.parse.quote(parsed_json['cursor'])

        with open(donefilename, 'w', encoding='utf-8') as f:
            pass

 >>> 한국어로된 리뷰를 크롤링하는데는 성공하였지만 csv파일에서 리뷰 내용이 깨지는 현상이 발생해서 다시 영어로 설정을 바꾼뒤 진행하였다.

 

 1-3) 리뷰수가 너무 많아서 리뷰수를 조정하도록 변경

: 총 상위 730개의 게임의 리뷰를 추출하는데 한 게임당 평균 1만개 이상의 리뷰가 존재하는걸 인지하고 상위 리뷰수(추천수 기준) 220개를 추출하도록 코드를 변경.

def getgamereviews(ids, timeout, maxretries, pause, out, maxreviews): # 최대 리뷰수를 추가
    urltemplate = string.Template(
        'http://store.steampowered.com//appreviews/$id?cursor=$cursor&filter=recent&language=english')
    endre = re.compile(r'({"success":2})|(no_more_reviews)')

    for (dir, id_, name) in ids:
        if dir == 'sub':
            print('skipping sub %s %s' % (id_, name))
            continue

        gamedir = os.path.join(out, 'pages', 'reviews', '-'.join((dir, id_)))

        donefilename = os.path.join(gamedir, 'reviews-done.txt')
        if not os.path.exists(gamedir):
            os.makedirs(gamedir)
        elif os.path.exists(donefilename):
            print('skipping app %s %s' % (id_, name))
            continue

        print(dir, id_, name)

        cursor = '*'
        offset = 0
        page = 1
        maxError = 10
        errorCount = 0
        while True:
            url = urltemplate.substitute({'id': id_, 'cursor': cursor})
            print(offset, url)
            htmlpage = download_page(url, maxretries, timeout, pause)

            if htmlpage is None:
                print('Error downloading the URL: ' + url)
                sleep(pause * 3)
                errorCount += 1
                if errorCount >= maxError:
                    print('Max error!')
                    break
            else:
                with open(os.path.join(gamedir, 'reviews-%s.html' % page), 'w', encoding='utf-8') as f:
                    htmlpage = htmlpage.decode()
                    if endre.search(htmlpage):
                        break
                    f.write(htmlpage)
                    page = page + 1
                    if maxreviews != -1 and page > maxreviews:
                        break
                    parsed_json = (json.loads(htmlpage))
                    cursor = urllib.parse.quote(parsed_json['cursor'])

        with open(donefilename, 'w', encoding='utf-8') as f:
            pass

그리고 main 함수에서 maxreviews 인자를 받아 getgamereviews 함수에 전달하도록 설정:

 

def main():
    parser = argparse.ArgumentParser(description='Crawler of Steam reviews')
    parser.add_argument('-f', '--force', help='Force download even if already successfully downloaded', required=False,
                        action='store_true')
    parser.add_argument(
        '-t', '--timeout', help='Timeout in seconds for http connections. Default: 180',
        required=False, type=int, default=180)
    parser.add_argument(
        '-r', '--maxretries', help='Max retries to download a file. Default: 5',
        required=False, type=int, default=3)
    parser.add_argument(
        '-p', '--pause', help='Seconds to wait between http requests. Default: 0.5', required=False, default=0.5,
        type=float)
    parser.add_argument(
        '-m', '--maxreviews', help='Maximum number of reviews per item to download. Default:unlimited', required=False,
        type=int, default=-1)
    parser.add_argument(
        '-o', '--out', help='Output base path', required=False, default='data')
    parser.add_argument(
        '-i', '--ids', help='File with game ids', required=False, default='./data/games.csv')
    args = parser.parse_args()

    if not os.path.exists(args.out):
        os.makedirs(args.out)

    ids = getgameids(args.ids)

    print('%s games' % len(ids))

    getgamereviews(ids, args.timeout, args.maxretries, args.pause, args.out, args.maxreviews)

이렇게 수정하면, -m 또는 --maxreviews 옵션을 사용하여 커맨드창에서 각 게임에 대해 다운로드할 리뷰의 최대 수를 설정할 수있다. 아래 코드 대로 입력을하면 하나의 게임당 리뷰를 220개씩 추출이 가능하다(한 페이지에 있는 리뷰수가 22개 이기 때문에 maxreviews 가 1일 경우에는 22개 씩 리뷰를 추출) 

python script.py --maxreviews 10

 

 

 

Reference List

 

https://github.com/aesuli/steam-crawler?tab=readme-ov-file

 

GitHub - aesuli/steam-crawler: A set of scripts that crawls STEAM website to download game reviews.

A set of scripts that crawls STEAM website to download game reviews. - GitHub - aesuli/steam-crawler: A set of scripts that crawls STEAM website to download game reviews.

github.com

https://www.kaggle.com/code/hmyleda/steam-game-analysis

 

Steam Game Analysis

Explore and run machine learning code with Kaggle Notebooks | Using data from Steam Store Games (Clean dataset)

www.kaggle.com

https://dl.acm.org/doi/pdf/10.1145/3555858.3555922

 

'AIoT' 카테고리의 다른 글

AIoT 정규 29일차  (0) 2024.02.07
AIoT 정규 28일차  (0) 2024.02.06
AIoT 정규 26일차  (0) 2024.02.02
AIoT 정규 25일차  (0) 2024.02.01
AIoT 정규 24일차  (0) 2024.01.31