일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- 국내주식
- 해외주식
- homebridge
- Python
- 매터
- 월패드
- ConnectedHomeIP
- 애플
- Espressif
- Bestin
- 미국주식
- SK텔레콤
- Home Assistant
- 현대통신
- 코스피
- 오블완
- matter
- RS-485
- MQTT
- 티스토리챌린지
- 배당
- 공모주
- 파이썬
- 홈네트워크
- raspberry pi
- 나스닥
- cluster
- 힐스테이트 광교산
- esp32
- Apple
- Today
- Total
YOGYUI
Python::BeautifulSoup - 동행복권 로또 6/45 당첨번호 크롤링 본문
[ Web Crawling (Python) ]
동행복권 사이트에서 로또 6/45 역대 당첨번호들을 크롤링한 뒤 DB에 저장해보자
1. 최신 회차 크롤링
동행복권 메인 페이지에 접속하면 좌측 상단에 최신 회차 및 당첨번호를 확인할 수 있다
고민할 것 없이 바로 requests 사용해서 GET method로 HTTP 요청을 넣은 후 html 코드를 읽어보자
import requests
url = "https://dhlottery.co.kr/common.do?method=main"
html = requests.get(url).text
주석들이 한글로 달려있는게 뭔가 정겹다
크롬 개발자 도구 (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&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=회차번호
'회차번호'를 숫자로 변경 후 주소창에 입력하면 다음과 같이 조회가 된다
크롬 개발자 도구를 열어서 당첨번호 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"로 생성)
회차(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)
끝~!
'Data Analysis > Data Engineering' 카테고리의 다른 글
공공데이터포털::공휴일 데이터 조회 (REST API) (10) | 2021.04.03 |
---|---|
Python::folium - 빅데이터분석기사 필기시험 고사장 위치 지도시각화 (0) | 2021.02.26 |
공공데이터포털::코로나19 감염현황 데이터 조회 (REST API) (0) | 2021.02.22 |
Python::BeautifulSoup - 동행복권 연금복권720+ 당첨번호 크롤링 (0) | 2021.02.21 |
Python::BeautifulSoup - 기상청 '도시별 현재날씨' 크롤링 (0) | 2021.02.07 |