YOGYUI

Python::BeautifulSoup - 동행복권 연금복권720+ 당첨번호 크롤링 본문

Data Analysis/Data Engineering

Python::BeautifulSoup - 동행복권 연금복권720+ 당첨번호 크롤링

요겨 2021. 2. 21. 22:28
반응형

[ Web Crawling (Python) ]

지난 포스트에서 동행복권 로또6/45 당첨번호를 웹크롤링해봤다

yogyui.tistory.com/entry/PythonBeautifulSoup-%EB%A1%9C%EB%98%90-645-%EB%8B%B9%EC%B2%A8%EB%B2%88%ED%98%B8-%ED%81%AC%EB%A1%A4%EB%A7%81

 

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

[ Web Crawling (Python) ] 동행복권 사이트에서 로또 6/45 역대 당첨번호들을 크롤링한 뒤 DB에 저장해보자 동행복권 메인 사이트 동행복권 당첨번호 3 4 15 22 28 40 보너스번호 10 1등 총 당첨금 263억원(8명

yogyui.tistory.com

메인화면 로또6/45 당첨내역 오른쪽을 보니 연금복권이란 것도 있는 걸 발견 (뭐 대충 보니깐 1등 당첨되면 20년간? 매달 세전 700씩 연금형식으로 수령하는 거라고 한다, 나중에 기회있음 한번 사봐야지 ㅎ)

 

로또와 마찬가지로 동행복권 사이트에서 역대 모든 회차의 당첨번호를 긁어와서 개인 DB에 저장하는 스크립트를 짜보자

역시나 HTML 구조가 단순해서 크롤링 실습에는 제격이다

1. 최신 회차 크롤링

최신 회차는 메인 페이지에서 가져올 수 있다

크롬 개발자 도구에서 해당 컨텐츠를 탐색해보자

최신 회차 컨텐츠 탐색

HTML을 보면 다음과 같은 위계구조로 구성되어 있다

<body>
  <div class="containerWrap">
    <div id="article" class="contentSection main_section ver2">
      <div class="wrap_box wrap1">
        <section class="box win win720" id="win720">
          <div class="content">
            <h3>
              <a href="/gameResult.do?method=win720&amp;wiselog=C_A_4_1">
                <strong id="drwNo720">42</strong>
  • 태그 이름이 "strong", id 속성이 "drwNo720"인 태그만 찾아주면 된다

  • 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": "drwNo720"})
    return int(tag.text)
In [1]: getMaxRoundNum()
Out[1]: 42

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

회차별 당첨번호는 홈페이지 메뉴 - 당첨결과 - 연금복권720+ - 회차별 당첨번호 페이지에서 확인할 수 있다

연금복권 회차별 당첨번호 확인

로또와는 다르게 연금복권은 (조 포함) 숫자들이 복원 추출되며 추출되는 순서가 당첨 기준이 된다고 한다

또한, 보너스 번호는 전혀 다른 수열로 하나 더 뽑는듯? 나중에 시간되면 추첨방송도 라이브로 한번 봐야겠다

당첨 조건 설명

회차별 당첨번호 크롤링 시 가져와야 할 정보는 <1등 조 + 6자리 당첨번호>랑 보너스 6자리 당첨번호 두 개만 가져오면 되고, 자리수 정보를 가지고 있어야 한다 (제멋대로 정렬되면 안된다)

 

회차별 당첨번호 URL은 로또6/45와는 구조가 다르다 (로또 6/45는 GET method로 접근 가능)

회차별 당첨번호 조회 스크립트

조회 기능은 <form>으로 구성되어 있으며, POST method로 다음 URL에 HTTP request해야된다

https://dhlottery.co.kr/gameResult.do?method=win720

넘겨야 할 데이터는 dict 형식으로 {Round: 회차}와 같이 넘겨주면 된다

(콤보박스에 해당하는 <select> 태그의 id가 "Round"인 걸로 유추)

 

HTML 탐색 결과 당첨번호들에 해당하는 태그의 위계구조는 다음과 같다

<body>
  <div class="containerWrap">
    <section class="contentSection">
      <div id="article" class="contentsArticle">
        <div>
          <div class="content_wrap content_winnum720_">
            <div class="win_result al720" style id="after720">
              <p class="desc">(2021년 02월 11일 추첨)
              <!-- 1등 당첨번호 -->
              <div class="win_num_wrap">
                <div class="win720_num">
                  <div class="group">
                    <span class="num large">
                      <span>1 <!-- 조 번호 -->
                  <span class="num al720_color1 large">
                    <span>7  <!-- 십만자리 당첨번호 -->
                  <span class="num al720_color2 large">
                    <span>4  <!-- 만자리 당첨번호 -->
                  <span class="num al720_color3 large">
                    <span>2  <!-- 천자리 당첨번호 -->
                  <span class="num al720_color4 large">
                    <span>7  <!-- 백자리 당첨번호 -->
                  <span class="num al720_color5 large">
                    <span>1  <!-- 십자리 당첨번호 -->
                  <span class="num al720_color6 large">
                    <span>4  <!-- 일자리 당첨번호 -->
              
              <!-- 보너스 당첨 번호 -->
              <div class="win_num_wrap">
                <div class="win720_num">
                  <span class="num al720_color1 large">
                    <span>6  <!-- 십만자리 당첨번호 -->
                  <span class="num al720_color2 large">
                    <span>9  <!-- 만자리 당첨번호 -->
                  <span class="num al720_color3 large">
                    <span>8  <!-- 천자리 당첨번호 -->
                  <span class="num al720_color4 large">
                    <span>3  <!-- 백자리 당첨번호 -->
                  <span class="num al720_color5 large">
                    <span>3  <!-- 십자리 당첨번호 -->
                  <span class="num al720_color6 large">
                    <span>5  <!-- 일자리 당첨번호 -->
                  

class가 "win_num_wrap"인 <div> 태그 두 개를 찾아서 첫 번째 태그에서는 1등 당첨번호 정보를, 두 번째 태그에서는 보너스 당첨번호 정보를 얻으면 된다

크롤링 스크립트는 나중에 변경될 경우를 대비해서 조금은 장황하게 짜봤다

import requests
from datetime import datetime
from bs4 import BeautifulSoup

def getWinNumbers(round_num: int):
    url = "https://dhlottery.co.kr/gameResult.do?method=win720"
    html = requests.post(url, data={'Round': round_num}).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일 추첨)")

    win720_num_tags = win_result_tag.find_all("div", "win720_num")
    # 1등 당첨번호 읽기
    div_group = win720_num_tags[0].find("div", "group")
    num_group = int(div_group.find_all("span")[1].text)
    win720_nums = []
    for i in range(6):
        span_num = win720_num_tags[0].find("span", f"num al720_color{i+1} large")
        win720_nums.append(int(span_num.find("span").text))

    # 보너스 번호 읽기
    bonus_nums = []
    for i in range(6):
        span_num = win720_num_tags[1].find("span", f"num al720_color{i+1} large")
        bonus_nums.append(int(span_num.find("span").text))

    return {
        "round_num": round_num_query,
        "draw_date": draw_date,
        "win_nums": (num_group, win720_nums),
        "bonus_nums": bonus_nums
    }
In [2]: print(getWinNumbers(42))
Out[2]: 
{'round_num': 42, 
 'draw_date': datetime.datetime(2021, 2, 18, 0, 0),
 'win_nums': (4, [9, 7, 4, 2, 3, 1]),
 'bonus_nums': [8, 9, 2, 2, 4, 3]
}

In [3]: print(getWinNumbers(41))
Out[3]: 
{'round_num': 41,
 'draw_date': datetime.datetime(2021, 2, 11, 0, 0), 
 'win_nums': (1, [7, 4, 2, 7, 1, 4]), 
 'bonus_nums': [6, 9, 8, 3, 3, 5]
}

In [4]: print(getWinNumbers(40))
Out[4]:
{'round_num': 40, 
 'draw_date': datetime.datetime(2021, 2, 4, 0, 0), 
 'win_nums': (2, [6, 3, 1, 0, 8, 6]), 
 'bonus_nums': [5, 8, 5, 8, 0, 2]
}

3. DB Table 생성 및 레코드 업데이트

로또 6/45때는 MySQL DB 내 테이블 생성을 php로 했는데, 이번에는 SQL로 해보자

Primary Key는 회차(Round)로 하고, 속성은 추첨일, 1등 조 번호, 1등 당첨번호 (순서대로), 보너스 당첨번호와 같이 총 15개로 구성하자

 

mysql database 접속 후 테이블을 만들어주자 (테이블 이름은 간단하게 PENSION_LOTTO로...)

CREATE TABLE PENSION_LOTTO (
    ROUNDNUM int PRIMARY KEY,
    DRAWDATE date,
    NUM_GROUP int,
    NUM_1 int,
    NUM_2 int,
    NUM_3 int,
    NUM_4 int,
    NUM_5 int,
    NUM_6 int,
    NUM_BONUS_1 int,
    NUM_BONUS_2 int,
    NUM_BONUS_3 int,
    NUM_BONUS_4 int,
    NUM_BONUS_5 int,
    NUM_BONUS_6 int
);

테이블 생성 쿼리 후 파이썬으로 테이블 정보를 얻어보자

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 PENSION_LOTTO;")
    result = cursor.fetchall()
    db.close()
    return result
In [2]: getTableSchema()
Out[2]: 
(('ROUNDNUM', 'int(11)', 'NO', 'PRI', None, ''),
 ('DRAWDATE', 'date', 'YES', '', None, ''),
 ('NUM_GROUP', 'int(11)', 'YES', '', None, ''),
 ('NUM_1', 'int(11)', 'YES', '', None, ''),
 ('NUM_2', 'int(11)', 'YES', '', None, ''),
 ('NUM_3', 'int(11)', 'YES', '', None, ''),
 ('NUM_4', 'int(11)', 'YES', '', None, ''),
 ('NUM_5', 'int(11)', 'YES', '', None, ''),
 ('NUM_6', 'int(11)', 'YES', '', None, ''),
 ('NUM_BONUS_1', 'int(11)', 'YES', '', None, ''),
 ('NUM_BONUS_2', 'int(11)', 'YES', '', None, ''),
 ('NUM_BONUS_3', 'int(11)', 'YES', '', None, ''),
 ('NUM_BONUS_4', 'int(11)', 'YES', '', None, ''),
 ('NUM_BONUS_5', 'int(11)', 'YES', '', None, ''),
 ('NUM_BONUS_6', 'int(11)', 'YES', '', None, ''))

테이블도 있고, 회차별 당첨번호를 얻는 코드도 있으니 이제 모든 회차에 대한 당첨번호를 DB에 업데이트해보자 (로또6/45 때와 마찬가지로, 이미 업데이트된 적이 있는지 여부 판단 구문도 같이 구현)

def update_db():
    db = connect_db()
    cursor = db.cursor()
    
    # 현재 테이블에 등록된 회차 쿼리
    cursor.execute("SELECT ROUNDNUM FROM PENSION_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 Pension Win Number (Round: {r})")
        crawl_result = getWinNumbers(r)
        sql = "INSERT INTO PENSION_LOTTO (`ROUNDNUM`, `DRAWDATE`, "
        sql += "NUM_GROUP, NUM_1, NUM_2, NUM_3, NUM_4, NUM_5, NUM_6, "
        sql += "NUM_BONUS_1, NUM_BONUS_2, NUM_BONUS_3, NUM_BONUS_4, NUM_BONUS_5, NUM_BONUS_6) "
        
        sql += "VALUES ({}, ".format(crawl_result['round_num'])
        sql += "'{}', ".format(crawl_result["draw_date"].strftime("%Y-%m-%d"))
        sql += '{}, '.format(crawl_result["win_nums"][0])
        for i in range(6):
            sql += '{}, '.format(crawl_result["win_nums"][1][i])
        for i in range(6):
            sql += '{}, '.format(crawl_result["bonus_nums"][i])
        sql = sql[:-2] + ");"
        
        print(sql)
        result = cursor.execute(sql)
        print(">> Update query result: {}".format(result))
    db.commit()
    db.close()
In [3]: update_db()
Out[3]:
Get Pension Win Number (Round: 1)
INSERT INTO PENSION_LOTTO (`ROUNDNUM`, `DRAWDATE`, NUM_GROUP, NUM_1, NUM_2, NUM_3, NUM_4, NUM_5, NUM_6, NUM_BONUS_1, NUM_BONUS_2, NUM_BONUS_3, NUM_BONUS_4, NUM_BONUS_5, NUM_BONUS_6) VALUES (1, '2020-05-07', 4, 1, 6, 2, 1, 3, 2, 2, 7, 8, 2, 3, 9);
>> Update query result: 1
Get Pension Win Number (Round: 2)
INSERT INTO PENSION_LOTTO (`ROUNDNUM`, `DRAWDATE`, NUM_GROUP, NUM_1, NUM_2, NUM_3, NUM_4, NUM_5, NUM_6, NUM_BONUS_1, NUM_BONUS_2, NUM_BONUS_3, NUM_BONUS_4, NUM_BONUS_5, NUM_BONUS_6) VALUES (2, '2020-05-14', 2, 4, 5, 0, 5, 5, 8, 1, 5, 4, 4, 5, 7);
>> Update query result: 1
...
Get Pension Win Number (Round: 42)
INSERT INTO PENSION_LOTTO (`ROUNDNUM`, `DRAWDATE`, NUM_GROUP, NUM_1, NUM_2, NUM_3, NUM_4, NUM_5, NUM_6, NUM_BONUS_1, NUM_BONUS_2, NUM_BONUS_3, NUM_BONUS_4, NUM_BONUS_5, NUM_BONUS_6) VALUES (42, '2021-02-18', 4, 9, 7, 4, 2, 3, 1, 8, 9, 2, 2, 4, 3);
>> Update query result: 1

마찬가지로 1주일에 한번씩 구동해주면 자동으로 DB가 업데이트된다

(개인적으로 구동하는 웹서버에 스케쥴 등록하는게 좋을 것 같다)

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

import pandas as pd

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

In [5]: df.head(5)
Out[5]:
   ROUNDNUM    DRAWDATE  NUM_GROUP  ...  NUM_BONUS_4  NUM_BONUS_5  NUM_BONUS_6
0         1  2020-05-07          4  ...            2            3            9
1         2  2020-05-14          2  ...            4            5            7
2         3  2020-05-21          4  ...            3            6            9
3         4  2020-05-28          4  ...            0            9            7
4         5  2020-06-04          4  ...            7            7            9

[5 rows x 15 columns]

In [6]: df.tail(5)
Out[6]:
    ROUNDNUM    DRAWDATE  NUM_GROUP  ...  NUM_BONUS_4  NUM_BONUS_5  NUM_BONUS_6
37        38  2021-01-21          3  ...            4            8            0
38        39  2021-01-28          1  ...            7            1            8
39        40  2021-02-04          2  ...            8            0            2
40        41  2021-02-11          1  ...            3            3            5
41        42  2021-02-18          4  ...            2            4            3

[5 rows x 15 columns]

In [7]: df.shape
Out[7]: (42, 15)

끝~!

반응형
Comments