YOGYUI

Python::BeautifulSoup - 동행복권 로또 6/45 당첨번호 크롤링 본문

Data Analysis/Data Engineering

Python::BeautifulSoup - 동행복권 로또 6/45 당첨번호 크롤링

요겨 2021. 2. 19. 18:54
반응형

[ Web Crawling (Python) ]

동행복권 사이트에서 로또 6/45 역대 당첨번호들을 크롤링한 뒤 DB에 저장해보자

동행복권 메인 사이트

 

동행복권

당첨번호 3 4 15 22 28 40 보너스번호 10 1등 총 당첨금 263억원(8명 / 33억) 이전 회차 당첨정보 보기 다음 회차 당첨정보 보기

dhlottery.co.kr

1. 최신 회차 크롤링

동행복권 메인 페이지에 접속하면 좌측 상단에 최신 회차 및 당첨번호를 확인할 수 있다

동행복권 메인 페이지, 로또 최신 회차 확인

고민할 것 없이 바로 requests 사용해서 GET method로 HTTP 요청을 넣은 후 html 코드를 읽어보자

import requests

url = "https://dhlottery.co.kr/common.do?method=main"
html = requests.get(url).text

메인페이지 HTML

주석들이 한글로 달려있는게 뭔가 정겹다

 

크롬 개발자 도구 (F12)를 열어서 원하는 데이터의 태그 값을 찾아주자

크롬 개발자 도구로 탐색

최신회차 정보는 다음 위계구조로 찾아갈 수 있다 (서버 변경에 따라 구조는 변경될 수도 있다)

<body>
  <div class="containerWrap">
    <div id="article" class="contentSecttion main_section ver2">
      <div class="wrap_box wrap1">
        <section class="box win win645">
          <div class="content">
            <h3>
              <a id="goByWin1" href="/gameResult.do?method=byWin&amp;wiselog=C_A_1_1">
                <strong id="lottoDrwNo">950</strong>

별도의 jquery나 js 함수 호출없이 단순히 정수값이 태그에 입력된 형태다 (아주 바람직한 response!)

이를 토대로 크롤링 코드를 짜보자

  • 태그 이름이 "strong", id 속성이 "lottoDrwNo"인 태그만 찾아주면 된다

  • BeautifulSoup의 html 파서는 lxml을 사용해준다

import requests
from bs4 import BeautifulSoup

def getMaxRoundNum() -> int:
    url = "https://dhlottery.co.kr/common.do?method=main"
    html = requests.get(url).text
    soup = BeautifulSoup(html, "lxml")
    tag = soup.find(name="strong", attrs={"id": "lottoDrwNo"})
    return int(tag.text)
In [1]: getMaxRoundNum()
Out[1]: 950

2. 회차별 당첨번호 크롤링

회차별 당첨번호는 홈페이지 메뉴 - 당첨결과 - 로또 6/46 - 회차별 당첨번호 페이지에서 확인할 수 있다

회차별 당첨번호 메뉴

우측 상단의 '회차 바로가기'를 통해 1회부터 역대 회차번호를 모두 조회할 수 있다

회차별 당첨번호 조회 기능

회차별 당첨번호 조회 URL은 다음과 같다

https://dhlottery.co.kr/gameResult.do?method=byWin&drwNo=회차번호

'회차번호'를 숫자로 변경 후 주소창에 입력하면 다음과 같이 조회가 된다

url 입력 - 1회 당첨번호 조회 결과

 

크롬 개발자 도구를 열어서 당첨번호 7개 (보너스 번호 포함) 및 추첨일에 해당하는 구문을 찾아보자

추첨일 태그 탐색

추첨일은 다음과 같은 위계구조로 구성되어있다

<body>
   <div class="containerWrap">
     <section class="contentSection">
       <div id="article" class="contentsArticle">
         <div>
           <div class="content_wrap content_winnum_645">
             <div class="win_result">
               <p class="desc">(2021년 02월 13일 추첨)</p>

당첨번호들은 다음과 같은 위계구조로 구성되어있다

당첨번호 태그 탐색

<body>
   <div class="containerWrap">
     <section class="contentSection">
       <div id="article" class="contentsArticle">
         <div>
           <div class="content_wrap content_winnum_645">
             <div class="win_result">
               <div class="nums">
                 <div class="num win">
                   <p>
                     <span class="ball_645 lrg ball1">3</span>
                     <span class="ball_645 lrg ball1">4</span>
                     <span class="ball_645 lrg ball2">15</span>
                     <span class="ball_645 lrg ball3">22</span>
                     <span class="ball_645 lrg ball3">28</span>
                     <span class="ball_645 lrg ball4">40</span>

당첨번호가 기재된 <span> 태그의 클래스 속성 텍스트 중 ball은 css로 원의 색깔을 나타내기 위해 사용되는 것 같다

보너스 번호는 당첨번호와 유사한 위계구조로 구성되어있다

<body>
   <div class="containerWrap">
     <section class="contentSection">
       <div id="article" class="contentsArticle">
         <div>
           <div class="content_wrap content_winnum_645">
             <div class="win_result">
               <div class="nums">
                 <div class="num bonus">
                   <p>
                     <span class="ball_645 lrg ball1">10</span>

 

탐색된 html 구조를 바탕으로 회차별 당첨번호 크롤링 코드를 작성해보자

import requests
from datetime import datetime
from bs4 import BeautifulSoup

def getWinNumbers(round_num: int):
    url = f"https://dhlottery.co.kr/gameResult.do?method=byWin&drwNo={round_num}"
    html = requests.get(url).text
    soup = BeautifulSoup(html, "lxml")
    win_result_tag = soup.find(name="div", attrs={"class": "win_result"})
    
    # 회차 정보 읽기
    strong_tags = win_result_tag.find_all("strong")
    round_num_text = strong_tags[0].text.replace("회", '')
    round_num_query = int(round_num_text)
    # 추첨일 읽기
    p_tags = win_result_tag.find_all("p", "desc")
    draw_date = datetime.strptime(p_tags[0].text, "(%Y년 %m월 %d일 추첨)")
    # 당첨번호 6개 읽기
    num_win_tag = win_result_tag.find(name="div", attrs={"class": "num win"})
    p_tag = num_win_tag.find("p")
    win_nums = [int(x.text) for x in p_tag.find_all("span")]
    # 보너스 번호 읽기
    num_bonus_tag = win_result_tag.find(name="div", attrs={"class": "num bonus"})    
    p_tag = num_bonus_tag.find("p")
    bonus_num = int(p_tag.find("span").text)
    
    return {
        "round_num": round_num_query,
        "draw_date": draw_date,
        "win_nums": win_nums,
        "bonus_num": bonus_num
    }
In [2]: getWinNumbers(950)
Out[2]: 
{'round_num': 950,
 'draw_date': datetime.datetime(2021, 2, 13, 0, 0),
 'win_nums': [3, 4, 15, 22, 28, 40],
 'bonus_num': 10}

3. DB Table 생성 및 레코드 등록

역대 모든 회차에 대한 당첨번호를 매번 홈페이지로부터 쿼리해오는건 그다지 합리적인 선택이 아니다

개인 DB에 저장하고 필요할 때 쿼리해오도록 하자

로또 당첨 번호는 ER 모델로 데이터 모델링이 가능하며, 고유 식별자는 당첨회차 혹은 추첨일자면 충분하다

 

(본 포스트에서는 작성자 개인 NAS 내 구동 중인 MySQL 서버를 대상으로 진행한다)

DB 테이블 생성은 php로 진행 ("mydata" DB 내에 테이블 이름은 심플하게 "lotto"로 생성)

DB 테이블 생성 

회차(ROUND)는 Primary Key로 설정했고, 추첨일자, 당첨번호 6개 및 보너스 번호를 속성으로 구성된 단순한 스키마로 생성

(나중에 회차별 당첨자 수, 당첨금액 등 부가정보를 가진 DB도 만들면 재밌을 거 같다)

 

mysql을 파이썬에서 구동하기 위해 pymysql 라이브러리를 사용했다

pip install pymysql

파이썬으로 테이블 스키마 정보를 쿼리해보자 (DESCRIBE)

host, port, user account, db 등 파라미터는 본인이 사용하고자 하는 MySQL 서버에 맞게 수정

import pymysql

def connect_db():
    conn = pymysql.connect(
        host='MySQL Address',
        port=MySqlPort,
        user='MySQL Account',
        passwd='MySQL Account Password',
        db='mydata'
    )
    return conn

def getTableSchema():
    db = connect_db()
    cursor = db.cursor()
    cursor.execute("DESCRIBE LOTTO;")
    result = cursor.fetchall()
    db.close()
    return result
In [3]: getTableSchema()
Out[3]: 
(('ROUND', 'int(11)', 'NO', 'PRI', None, ''),
 ('DATE', 'date', 'NO', '', None, ''),
 ('NUM_1', 'int(11)', 'NO', '', None, ''),
 ('NUM_2', 'int(11)', 'NO', '', None, ''),
 ('NUM_3', 'int(11)', 'NO', '', None, ''),
 ('NUM_4', 'int(11)', 'NO', '', None, ''),
 ('NUM_5', 'int(11)', 'NO', '', None, ''),
 ('NUM_6', 'int(11)', 'NO', '', None, ''),
 ('NUM_BONUS', 'int(11)', 'NO', '', None, ''))

이제 레코드를 집어넣어주는 코드 (UPDATE 쿼리)를 작성해보자

※ 이미 등록된 회차라면 굳이 동행복권 사이트를 크롤링할 필요가 없으므로 레코드 존재여부 체크 구문도 구현

def update_db():
    db = connect_db()
    cursor = db.cursor()
    
    # 현재 테이블에 등록된 회차 쿼리
    cursor.execute("SELECT ROUND FROM LOTTO;")
    result = cursor.fetchall()
    round_num_in_table = [x[0] for x in result]
    if len(round_num_in_table) > 0:
        print("Already {} records are updated".format(len(round_num_in_table)))
    
    # 최대 회차 가져오기
    max_round = getMaxRoundNum()
    
    # 테이블 업데이트
    for r in range(1, max_round + 1):
        # 이미 등록된 레코드인지 체크
        if r in round_num_in_table:
            continue
        print(f"Get Lotto Win Number (Round: {r})")
        crawl_result = getWinNumbers(r)
        sql = "INSERT INTO LOTTO (`ROUND`, `DATE`, NUM_1, NUM_2, NUM_3, NUM_4, NUM_5, NUM_6, NUM_BONUS) "
        sql += "VALUES ({}, '{}', {}, {}, {}, {}, {}, {}, {});".format(
                r,
                crawl_result["draw_date"].strftime("%Y-%m-%d"),
                crawl_result["win_nums"][0],
                crawl_result["win_nums"][1],
                crawl_result["win_nums"][2],
                crawl_result["win_nums"][3],
                crawl_result["win_nums"][4],
                crawl_result["win_nums"][5],
                crawl_result["bonus_num"]
            )
        result = cursor.execute(sql)
        print(">> Update query result: {}".format(result))
    db.commit()
    db.close()
In [4]: update_db()
Out[4]: 
Get Lotto Win Number (Round: 1)
>> Update query result: 1
Get Lotto Win Number (Round: 2)
>> Update query result: 1
Get Lotto Win Number (Round: 3)
>> Update query result: 1
...
Get Lotto Win Number (Round: 949)
>> Update query result: 1
Get Lotto Win Number (Round: 950)
>> Update query result: 1

execute result가 1 이면 제대로 UPDATE가 수행되었다는 뜻이다 (1개 레코드에 대한 작업이므로)

일주일에 한번 구문을 실행해주면 좋을 것 같다

4. DB 데이터 쿼리 후 pandas DataFrame으로 변환

NAS 내 MySQL에 저장된 데이터를 모두 쿼리해서 pandas DataFrame 형식으로 변환해보자

import pandas as pd

def get_data_from_db():
    db = connect_db()
    cursor = db.cursor()
    
    # 테이블 Column 이름 얻어오기
    cursor.execute(f"SHOW columns FROM LOTTO;")
    columns = [tuple(x)[0] for x in cursor.fetchall()]
    
    # 테이블 모든 레코드 가져오기 (SELECT)
    cursor.execute("SELECT * FROM LOTTO;")
    df = pd.DataFrame(list(cursor.fetchall()))
    df.columns = columns
    db.close()
    return df
In [5]: df = get_data_from_db()

In [6]: df.head(5)
Out[6]: 
   ROUND        DATE  NUM_1  NUM_2  NUM_3  NUM_4  NUM_5  NUM_6  NUM_BONUS
0      1  2002-12-07     10     23     29     33     37     40         16
1      2  2002-12-14      9     13     21     25     32     42          2
2      3  2002-12-21     11     16     19     21     27     31         30
3      4  2002-12-28     14     27     30     31     40     42          2
4      5  2003-01-04     16     24     29     40     41     42          3

In [7]: df.tail(5)
Out[7]: 
     ROUND        DATE  NUM_1  NUM_2  NUM_3  NUM_4  NUM_5  NUM_6  NUM_BONUS
945    946  2021-01-16      9     18     19     30     34     40         20
946    947  2021-01-23      3      8     17     20     27     35         26
947    948  2021-01-30     13     18     30     31     38     41          5
948    949  2021-02-06     14     21     35     36     40     44         30
949    950  2021-02-13      3      4     15     22     28     40         10

In [8]: df.shape
Out[8]: (950, 9)

끝~!

반응형