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

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



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



경마



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

댓글()
  1. Favicon of https://stocktalk.tistory.com BlogIcon 이웃집주식남 2020.07.16 07:13 신고 댓글주소  수정/삭제  댓글쓰기

    찾던 자료가 여기에 있었군요.
    감사합니다.^^ 자주 올께요

  2. Favicon of https://dreamingaurora.tistory.com BlogIcon 새벽오로라 2020.07.27 21:42 댓글주소  수정/삭제  댓글쓰기

    관리자의 승인을 기다리고 있는 댓글입니다