파이썬 웹크롤링, 경마 경기결과 데이터 수집하는 방법은?!

목차

    2020. 7. 5. 10:56

    경마 경기 결과는 렛츠런 파크 싸이트에 공개된다. 이 데이터를 수집하는 방법은 여러가지가 있지만, 개인적으로 파이썬을 이용하는 것을 추천한다. 파이썬을 이용하면 웹크롤링을 통해 원하는 데이터를 쉽게 수집할 수 있다.

    필자가 파이썬을 이용해서 제일 처음 했던 것이 경마 경기결과를 수집하는 것이었다. 예전에 포스팅도 했지만, 그동안 알게 된 내용들을 바탕으로 코드를 수정해보았다.

    오늘은 파이썬 웹크롤링, 경마 경기결과 데이터를 수집하는 방법에 대해서 알아보도록 하겠다.


    경마_경기_결과



    코드를 알아보기 전에 수집 프로세스에 대해 알아보도록 하겠다.


    1. 수집 데이터는 MySQL에 저장한다.

    MySQL은 무료로 사용할 수 있는 DBMS이다. 무료 DBMS는 여러가지가 있지만, MySQL은 서버에 올려서 사용할 수도 있기 때문에 유용하다. MySQL DB를 사용하려면 설치가 필요하다. 이는 이전 포스팅에서도 다루었으니 참고하기 바란다.
    ( 참조: MySQL 설치하기, sqlite3와 MySQL의 차이는? )


    MySQL DB를 사용하기 불편하다면 SQLite3를 사용하거나, 엑셀 등의 다른 자료형으로 저장하는 것도 좋겠다.
    ( 참조: #1-6 경마 데이터 수집하기 - SQLite라는 DB로 저장하기 )



    2. 경마경기결과를 수집한다.

    경마경기는 매주 금,토,일에 열린다. 서울, 부산, 제주 3곳에 경기가 개최된다. 날짜와 지역, 경기번호를 특정 URL에 입력파라미터로 넣으면, 해당 날짜의 경기결과를 확인할 수 있다. 확인된 결과는 웹크롤링으로 수집하였다.



    3. 수집한 결과를 MySQL DB에 놓는다.

    DB에 데이터를 넣기 위해서는 SQL에 대해서 알아두는 것이 좋다. SQL은 Structured Query Language의 약자로 DB에서 사용하는 표준화된 프로그래밍 언어라고 보면 되겠다. DB에 넣을 때는 REPLACE INTO구문을 이용하였다. REPLACE INTO문은 중복되는 키가 없을 때 값을 입력한다. 그리고, 키가 중복되면 새로운 값으로 변경한다.



    4. 중간중간 확인해야 되는 내용은 logging모듈을 이용하여 출력하였다.

    print함수를 이용해도 된다. 하지만, logging모듈을 이용하면 더 쉽게 로그를 관리할 수 있다. 쉽게 파일로 저장할 수 있으며, 레벨별로 다른 출력을 할 수도 있다. logging모듈에 대한 자세한 내용은 아래 포스팅을 참조해보자.
    ( 참조: 파이썬 실행 로그를 남겨보자, logging 모듈 이용하기! )



    5. 데이터 수집에 예외처리 구문을 추가하였다.

    경마경기 결과를 크롤링하다 보면 네트워크 오류가 발생할 때가 있다. 네트워크 오류를 특정할 수도 있지만, 귀찮아서 어떤 에러든 발생하면 2번 더 데이터 수집을 시도하는 것으로 수정하였다.



    코드



    전체 코드를 살펴보면 아래와 같다.


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    295
    296
    297
    298
    299
    300
    301
    302
    303
    304
    305
    306
    307
    308
    309
    310
    311
    312
    313
    314
    315
    316
    317
    318
    319
    # coding=utf-8
    from urllib.request import urlopen
    from bs4 import BeautifulSoup
    from html_table_parser import parser_functions as parser
     
    import pandas as pd
    import numpy as np
    from datetime import datetime, timedelta
    import logging
    import pymysql
    import time
    import random
     
    db_conf = {
    "host""127.0.0.1",
    "user""test",
    "password""test11",
    "database""horse_racing",
    }
     
    def create_table(db_conf):
        """
        데이터를 저장할 mysql테이블을 만듭니다.
        :param db_conf: DB접속정보
        :return:
        """
     
        con=pymysql.connect(**db_conf)
        cur=con.cursor()
        cur.execute("""
            CREATE TABLE IF NOT EXISTS hr_result  
            (
                순위 VARCHAR(2),
                마번 VARCHAR(2),
                마명 VARCHAR(30),
                산지 VARCHAR(20),
                성별 VARCHAR(10),
                연령 VARCHAR(5),
                중량 VARCHAR(5),
                레이팅 VARCHAR(5),
                기수명 VARCHAR(30),
                조교사명 VARCHAR(30),
                마주명 VARCHAR(30),
                도착차 VARCHAR(30),
                마체중 VARCHAR(10),
                단승 VARCHAR(10),
                연승 VARCHAR(10),
                복승 VARCHAR(10),
                쌍승 VARCHAR(10),
                복연승 VARCHAR(10),
                삼복승 VARCHAR(10),
                삼쌍승 VARCHAR(10),
                장구현황 VARCHAR(20),
                S1F_G1F VARCHAR(20),
                S_1F VARCHAR(10),
                1코너 VARCHAR(10),
                2코너 VARCHAR(10),
                3코너 VARCHAR(10),
                G_3F VARCHAR(10),
                4코너 VARCHAR(10),
                G_1F VARCHAR(10),
                3F_G VARCHAR(10),
                1F_G VARCHAR(10),
                10_8F VARCHAR(10),
                8_6F VARCHAR(10),
                6_4F VARCHAR(10),
                4_2F VARCHAR(10),
                2F_G VARCHAR(10),
                경주기록 VARCHAR(10),
                day VARCHAR(30),
                day_th VARCHAR(20),
                weather VARCHAR(20),
                race_st VARCHAR(10),
                race_time VARCHAR(10),
                race_infor1 VARCHAR(10),
                distance VARCHAR(10),
                race_infor2 VARCHAR(10),
                race_infor3 VARCHAR(30),
                race_infor4 VARCHAR(10),
                race_infor5 VARCHAR(10),
                primary key (day,day_th,마명)
            )
            """)
        con.commit()
        con.close()
     
        return 1
     
    def collect_race(location,date,rc_no,try_cnt):
        """
        경기결과 수집
        :param location: 서울1, 부산3
        :param date: 날짜
        :param rc_no: 경기번호
        :return: 1: 수집결과, 정상수집이 안 되면 행이 0인 데이터프레임을 return
        """
        if  try_cnt<3:
            time.sleep(random.randint(3,6))
            pass
        else:
            logging.info("데이터 수집 시도 횟수가 3회 초과하였습니다.")
            return pd.DataFrame()
     
        try:
            url="http://race.kra.co.kr/raceScore/ScoretableDetailList.do?meet={}&realRcDate={}&realRcNo={}".format(location,date,rc_no)
            logging.info("{} data collect start".format(url))
     
            seo_col_list=['순위''마번''S1F-1C-2C-3C-4C-G1F''S-1F''1코너''2코너''3코너''G-3F','4코너','G-1F','3F-G','1F-G','경주기록']
            bu_col_list=['순위''마번''S1F-1C-2C-3C-4C-G1F''S-1F','10-8F''8-6F''6-4F''4-2F''2F-G''3F-G','1F-G''경주기록']
     
            col_rename={"S1F-1C-2C-3C-4C-G1F":"S1F_G1F",
                        "S-1F":"S_1F",
                        '10-8F':"10_8F",
                        '8-6F':'8_6F',
                        '6-4F':'6_4F',
                        '4-2F':'4_2F',
                        '2F-G':'2F_G',
                        '3F-G':'3F_G',
                        '1F-G':'1F_G',
                        'G-3F''G_3F',
                        'G-1F''G_1F',
                        }
     
            result = urlopen(url)
            html = result.read()
            soup = BeautifulSoup(html, 'html.parser')
     
            temp = soup.find_all('div',attrs={'class':'tableType2'})
            table = temp[1].find('table')
            p=parser.make2d(table)
            record_detail=pd.DataFrame(p[2:],columns=seo_col_list if location==1 else bu_col_list)
            record_detail=record_detail.rename(columns=col_rename)
     
            if len(record_detail)==0:
                logging.info("{}, {}, {} 데이터는 없습니다.".format(date,location,rc_no))
                return pd.DataFrame()
     
            if location==1:
                record_detail["10_8F"]=""
                record_detail["8_6F"]=""
                record_detail["6_4F"]=""
                record_detail["4_2F"]=""
                record_detail["2F_G"]=""
            elif location==3:
                record_detail["1코너"]=""
                record_detail["2코너"]=""
                record_detail["3코너"]=""
                record_detail["G_3F"]=""
                record_detail["4코너"]=""
                record_detail["G_1F"]=""
     
            record_detail["S1F_G1F"]=record_detail["S1F_G1F"].str.replace("\r\n""").str.replace(" """).str.replace("\t","")
     
            # # race infor
            temp = soup.find_all('div',attrs={'class':'tableType1'})
            day = temp[0].find_all('th')[0].text
            day_th = temp[0].find_all('th')[1].text
            weather = temp[0].find_all('th')[2].text
            race_st = temp[0].find_all('th')[3].text
            race_time = temp[0].find_all('th')[5].text
     
            # race infor2
            race_infor1 = temp[0].find_all('td')[0].text
            distance = temp[0].find_all('td')[1].text
            race_infor2 = temp[0].find_all('td')[2].text
            race_infor3 = temp[0].find_all('td')[3].text
            race_infor4 = temp[0].find_all('td')[4].text
            race_infor5 = temp[0].find_all('td')[5].text
     
            # 배당정보
            div_info = temp[4]
            div_info_all = div_info.find_all("td")
            div = dict()
     
            for d in div_info_all:
                try:
                    d_text = d.text
                    div_temp = {d_text.split(":")[0] : d_text.split(":")[1].split(" ")[2] }
                    div = dict(div, **div_temp)
                except:
                    pass
     
            # 경기결과
            temp = soup.find_all('div', attrs={'class''tableType2'})
            table = temp[0].find('table')
            p = parser.make2d(table)
            hr_stat = pd.DataFrame(p[1:], columns=p[0])
     
            hr_1 = pd.merge(hr_stat, record_detail.drop("순위"1), on="마번")
     
            hr_1["day"= day
            hr_1["day_th"= day_th
            hr_1["weather"= weather
            hr_1["race_st"= race_st
            hr_1["race_time"= race_time
            hr_1["race_infor1"= race_infor1
            hr_1["distance"= distance
            hr_1["race_infor2"= race_infor2
            hr_1["race_infor3"= race_infor3
            hr_1["race_infor4"= race_infor4
            hr_1["race_infor5"= race_infor5
            hr_1["복승"]=div.get("복승식","")
            hr_1["쌍승"]=div.get("쌍승식","")
            hr_1["복연승"]=div.get("복연승식","")
            hr_1["삼복승"]=div.get("삼복승식","")
            hr_1["삼쌍승"]=div.get("삼쌍승식","")
     
            return hr_1
        except:
            hr_1=collect_race(location, date, rc_no, try_cnt+1)
            return hr_1
     
    def insert_table(hr_1, db_conf):
     
        hr_2=hr_1[["순위",
                    "마번",
                    "마명",
                    "산지",
                    "성별",
                    "연령",
                    "중량",
                    "레이팅",
                    "기수명",
                    "조교사명",
                    "마주명",
                    "도착차",
                    "마체중",
                    "단승",
                    "연승",
                    "복승",
                    "쌍승",
                    "복연승",
                    "삼복승",
                    "삼쌍승",
                    "장구현황",
                    "S1F_G1F",
                    "S_1F",
                    "1코너",
                    "2코너",
                    "3코너",
                    "G_3F",
                    "4코너",
                    "G_1F",
                    "3F_G",
                    "1F_G",
                    "10_8F",
                    "8_6F",
                    "6_4F",
                    "4_2F",
                    "2F_G",
                    "경주기록",
                    "day",
                    "day_th",
                    "weather",
                    "race_st",
                    "race_time",
                    "race_infor1",
                    "distance",
                    "race_infor2",
                    "race_infor3",
                    "race_infor4",
                    "race_infor5"]]
     
        hr_2_list=hr_2.values.tolist()
     
        db_conf = {
            "host""127.0.0.1",
            "user""test",
            "password""test11",
            "database""horse_racing",
        }
        con = pymysql.connect(**db_conf)
        cur = con.cursor()
        cur.executemany("""REPLACE INTO hr_result VALUES
                           (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,
                            %s,%s,%s,%s,%s,%s,%s,%s,%s,%s,
                            %s,%s,%s,%s,%s,%s,%s,%s,%s,%s,
                            %s,%s,%s,%s,%s,%s,%s,%s,%s,%s,
                            %s,%s,%s,%s,%s,%s,%s,%s
                           )""", hr_2_list)
        con.commit()
        con.close()
     
        return 1
     
     
     
     
     
     
     
     
     
     
     
     
        return 1
     
    if __name__ == '__main__':
     
        logging.basicConfig(level=logging.INFO)
     
        create_table(db_conf)
     
        date="20200628"
     
        # lcoation 1은 서울, 3은 부산
        location_list=[1,3]
     
        for l in location_list:
            for rc_no in range(1,20):
                hr_1=collect_race(l, date, rc_no, 1)
                logging.warning("{}, {}, {}, {} collected".format(l,date,rc_no,len(hr_1)))
     
                if len(hr_1)==0:
                    break
                else:
                    insert_table(hr_1, db_conf)
     
    cs



    테스트를 많이 해보진 않아서, 값이 정확하지 않거나 다른 날짜에서 에러가 날지는 모르겠다.



    경마



    오늘은 이렇게 파이썬 웹크롤링, 경마 경기결과 데이터를 수집하는 방법에 대해서 알아보았다. 경마 경기 분석은 재미있게 했었고, 언제가 다시 해 보고 싶은 마음은 있지만 여유가 나지 않는다. 언제 다시 코드를 수정하고, 실행해볼지는 모르겠다.