Notice
Recent Posts
Recent Comments
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 |
Tags
- 드라마
- 휴식
- mysql
- 충동억제
- 웹개발
- 부천공간대여
- 파티룸
- 서울파티룸
- 구로파티룸
- 그릭요거트
- 가장존경하는인물
- 광명파티룸
- 보드게임점수
- 착한코딩
- 옥길동파티룸
- 부천파티룸
- MBTI
- 스컬킹점수
- 취미
- 코딩
- 보드게임점수계산
- 개발자
- 스컬킹
- 77845
- 스페이스우일
- 일
- 스컬킹점수계산
- 옥길파티룸
- 존경하는위인
- 해외여행
Archives
- Today
- Total
SIMPLE & UNIQUE
[gpt와 함께하는 업무 자동화] 1. GPT가 코드 짜고, Gemini가 요약한 주식 알림 자동화 시스템 본문
1. autoGemini.py
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import (
StaleElementReferenceException,
TimeoutException,
ElementNotInteractableException
)
from selenium.webdriver.common.action_chains import ActionChains
from webdriver_manager.chrome import ChromeDriverManager
import time
# ✅ (추가) 결과를 파일로 저장하기 위한 라이브러리
import json
from datetime import datetime
# 디버그 로그 출력 여부 (True면 답변 추출 과정에서 상태를 계속 출력)
DEBUG = True # 필요 없으면 False
# ==============================
# Chrome 옵션
# ==============================
# Selenium이 크롬을 실행할 때 적용할 "실행 옵션"을 지정합니다.
# - binary_location: 크롬 실행 파일 위치
# - user-data-dir / profile-directory: 로그인 세션(쿠키 등)을 유지하기 위해 크롬 프로필을 사용
# - start-maximized: 처음부터 최대화
# - AutomationControlled 등: 자동화 탐지 완화 목적(100% 차단은 아님)
options = webdriver.ChromeOptions()
options.binary_location = r"C:\Program Files\Google\Chrome\Application\chrome.exe"
options.add_argument(r"--user-data-dir=C:\pythonInvest\chrome_profile_gemini")
options.add_argument("--profile-directory=Default")
options.add_argument("--start-maximized")
options.add_argument("--disable-blink-features=AutomationControlled")
options.add_argument("--disable-extensions")
options.add_argument("--remote-debugging-port=0")
# 실제 크롬 드라이버(ChromeDriver)를 생성합니다.
# webdriver_manager가 크롬 버전에 맞는 ChromeDriver를 자동 설치/사용합니다.
driver = webdriver.Chrome(
service=Service(ChromeDriverManager().install()),
options=options
)
# Gemini 사이트로 이동
driver.get("https://gemini.google.com/")
# WebDriverWait: 특정 요소가 나타날 때까지 최대 N초 기다리는 도구
# 여기서는 전체적으로 최대 180초까지 기다릴 수 있게 준비해 둠(필요시 사용)
wait = WebDriverWait(driver, 180)
# ==============================
# 컨텍스트(iframe/메인) 정렬
# ==============================
def switch_to_iframe_with_content(driver, timeout=20):
"""
[왜 필요한가?]
- 웹페이지는 "메인 문서" 안에 "iframe"이라는 하위 문서를 포함할 수 있습니다.
- Selenium은 현재 보고 있는 문서(컨텍스트)에서만 요소를 찾을 수 있습니다.
- Gemini UI는 어떤 경우에는 메인 문서에 입력창이 있고,
어떤 경우에는 iframe 내부에 입력창이 있을 수 있어요.
- 그래서 "입력창이 존재하는 문서(메인/iframe)"를 찾아서 그쪽으로 전환해주는 함수입니다.
[무엇을 하나?]
1) 우선 메인(document)으로 이동
2) 메인에 입력창이 있으면 종료
3) 없으면 모든 iframe을 하나씩 들어가면서 입력창이 있는지 확인
4) 제한 시간(timeout) 안에 찾지 못하면 TimeoutException 발생
"""
# 먼저 메인(최상위) 문서로 이동
driver.switch_to.default_content()
end = time.time() + timeout
# 현재 컨텍스트에 입력창 후보가 있는지 확인하는 내부 함수
def has_content():
# textarea 또는 contenteditable(채팅 입력 UI에 자주 쓰임) 요소가 있으면 True로 간주
return (
driver.find_elements(By.XPATH, "//textarea") or
driver.find_elements(By.XPATH, "//*[@contenteditable='true']")
)
while time.time() < end:
# 1) 메인 문서 확인
driver.switch_to.default_content()
if has_content():
# 메인 문서에 입력 UI가 있으면 여기서 끝(현재 컨텍스트 유지)
return
# 2) iframe들을 순회하면서 입력 UI가 있는지 확인
for iframe in driver.find_elements(By.TAG_NAME, "iframe"):
try:
# iframe 전환 전에 항상 메인으로 돌아갔다가(frame 참조 안정성 확보)
driver.switch_to.default_content()
driver.switch_to.frame(iframe)
# iframe 내부에서 입력창이 발견되면 그 컨텍스트에 머문 채로 종료
if has_content():
return
except Exception:
# 어떤 iframe은 접근이 막혀있거나 전환 시 오류가 날 수 있음 -> 무시하고 다음 iframe로
continue
# 잠깐 쉬었다가 다시 시도(페이지 로딩/렌더링을 기다리기 위함)
time.sleep(0.5)
# 제한 시간 내에 메인/iframe 어디에서도 입력 UI를 찾지 못한 경우
raise TimeoutException("Gemini UI 컨텍스트를 찾지 못했습니다.")
# ==============================
# 입력창 찾기
# ==============================
def get_input_box(driver, timeout=60):
"""
[목적]
- Gemini의 질문 입력칸 요소(웹 요소)를 찾아서 반환합니다.
[방법]
- textarea 또는 contenteditable 요소를 모두 찾은 다음,
화면에 실제로 보이는(is_displayed == True) 요소를 반환합니다.
[예외]
- DOM이 자주 바뀌면 StaleElementReferenceException이 날 수 있음
(요소를 찾았는데 그 순간 UI가 갱신되어 요소가 "낡은 참조"가 되는 현상)
- 제한 시간 내에 입력창을 못 찾으면 TimeoutException 발생
"""
end = time.time() + timeout
while time.time() < end:
# 입력창 후보들을 모두 찾음
for el in driver.find_elements(By.XPATH, "//textarea | //*[@contenteditable='true']"):
try:
# 화면에 보이는 요소만 사용(숨겨진 textarea 등 제외)
if el.is_displayed():
return el
except StaleElementReferenceException:
# 요소를 잡았는데 페이지가 갱신돼서 참조가 끊긴 경우 -> 다시 찾도록 continue
continue
time.sleep(0.3)
raise TimeoutException("입력창을 찾지 못했습니다.")
# ==============================
# 질문 전송
# ==============================
def send_question(driver, question):
"""
[목적]
- 입력창에 질문을 넣고 Enter를 눌러 전송합니다.
[흐름]
1) 입력창이 있는 컨텍스트(메인/iframe)로 이동
2) 입력창 요소 찾기
3) 마우스로 클릭해서 커서를 입력창에 둠
4) question 텍스트 입력
5) Enter 전송
[ActionChains를 쓰는 이유]
- 단순히 el.click()만으로 클릭이 안 되는 경우가 있어서,
실제 사용자처럼 "이동(move_to_element) -> 클릭(click)" 동작을 만들기 위해 사용합니다.
"""
switch_to_iframe_with_content(driver, 30)
box = get_input_box(driver, 60)
# 입력창으로 마우스 이동 후 클릭(커서 활성화)
ActionChains(driver).move_to_element(box).click().perform()
time.sleep(0.2)
# 질문 입력
box.send_keys(question)
time.sleep(0.3)
# Enter(전송)
ActionChains(driver).send_keys(Keys.ENTER).perform()
# ==============================
# 답변 후보 블록 수집 (멀티 셀렉터)
# ==============================
def get_message_blocks(driver):
"""
[왜 이렇게 복잡한가?]
- Gemini 웹 UI는 업데이트로 구조가 자주 바뀝니다.
- 어떤 날은 role='article' 안에 답변이 있고,
어떤 날은 class 이름에 markdown/response/message가 붙어있을 수 있어요.
- 그래서 여러 XPath 후보를 준비해 "답변처럼 보이는 텍스트 덩어리"를 최대한 수집합니다.
[노이즈 제거]
- 버튼/메뉴 같은 짧은 텍스트는 제외(예: '도구', '새 창에서 열기' 등)
- 중복 텍스트는 seen(set)으로 제거
"""
candidates = [
# 1) role 기반(있으면 가장 좋음)
"//*[@role='article']",
"//*[@role='main']//*[self::div or self::p]",
# 2) class / testid 기반 (UI 변경 대응)
"//*[contains(@class,'markdown') or contains(@class,'Markdown') or contains(@class,'message') or contains(@class,'response')]",
"//*[@data-testid and (contains(@data-testid,'response') or contains(@data-testid,'message') or contains(@data-testid,'assistant'))]",
# 3) aria-label 기반 (다국어/접근성 라벨 대응)
"//*[contains(@aria-label,'Response') or contains(@aria-label,'response') or contains(@aria-label,'답변')]",
# 4) 최후의 보루: main 내 텍스트 덩어리(너무 넓게 잡힘 -> 길이 필터로 거름)
"//main//*[self::div or self::p or self::span]"
]
elems = []
seen = set()
for xp in candidates:
try:
found = driver.find_elements(By.XPATH, xp)
except Exception:
continue
for el in found:
try:
# 화면에 보이는 요소만 사용
if not el.is_displayed():
continue
txt = (el.text or "").strip()
# 너무 짧은 텍스트는 버튼/메뉴/잡음 가능성이 큼 -> 제외
# (필요 시 40 값을 20~60 범위로 조절)
if len(txt) < 40:
continue
# 중복 제거용 key(앞부분+길이 조합)
key = (txt[:120], len(txt))
if key in seen:
continue
seen.add(key)
elems.append(el)
except (StaleElementReferenceException, Exception):
# DOM 변경 등으로 요소 참조가 깨지면 무시하고 계속
continue
return elems
# ==============================
# Gemini 답변 대기 + 추출
# ==============================
def wait_and_get_answer(driver, timeout=120):
"""
[핵심 아이디어]
- 질문을 보내기 "직후" 화면에 있던 텍스트들은 base_set(기준 스냅샷)으로 저장합니다.
- 이후 화면에서 발견되는 텍스트 중 base_set에 없던 것(=새로 생긴 텍스트)을 new_texts로 봅니다.
- new_texts 중 "가장 긴 텍스트"가 보통 실제 답변이므로 candidate로 선택합니다.
- 답변 생성 중에는 텍스트 길이가 계속 바뀌거나 내용이 조금씩 바뀔 수 있으니,
같은 candidate가 연속으로 관측되면 생성이 끝났다고 판단합니다(stable_count).
[반환]
- 안정화(stable_count >= 1)되면 답변(best)을 반환합니다.
- 시간 초과면 마지막 best가 있으면 반환, 없으면 TimeoutException 발생
"""
switch_to_iframe_with_content(driver, 30)
# 기준 스냅샷 확보(질문 직후 UI에 있던 텍스트들)
try:
base_blocks = [e.text.strip() for e in get_message_blocks(driver)]
except Exception:
base_blocks = []
base_set = set(base_blocks)
end = time.time() + timeout
best = "" # 현재까지 가장 그럴듯한 답변 텍스트
stable_count = 0 # 같은 답변이 연속 관측된 횟수(생성 완료 판단에 사용)
while time.time() < end:
try:
# UI가 바뀌어도 입력/출력 컨텍스트가 맞도록 주기적으로 정렬
switch_to_iframe_with_content(driver, 10)
# 화면에서 답변 후보 블록을 다시 수집
blocks = get_message_blocks(driver)
texts = [(b.text or "").strip() for b in blocks]
texts = [t for t in texts if t]
# base_set에 없던 텍스트만 추출 -> 새로 생긴 답변 후보
new_texts = [t for t in texts if t not in base_set and len(t) >= 40]
if DEBUG:
print(f"[DEBUG] blocks={len(blocks)} new_texts={len(new_texts)} best_len={len(best)}")
if new_texts:
# 가장 긴 텍스트를 답변 candidate로 선택
candidate = max(new_texts, key=len)
# 동일 candidate가 계속 나오면 생성이 멈췄다고 보고 stable_count 증가
if candidate == best:
stable_count += 1
else:
best = candidate
stable_count = 0
# 2회 연속으로 동일하면(=stable_count 1 이상) 답변 완료로 간주하고 반환
if stable_count >= 1:
return best
except StaleElementReferenceException:
# DOM이 바뀌면 종종 발생 -> 다시 루프 돌며 재시도
pass
except Exception:
pass
# 1초마다 다시 확인 (너무 빠르면 UI 갱신/렌더링을 못 따라갈 수 있음)
time.sleep(1)
# 시간 초과: 그래도 best가 있으면 반환(부분 답변이라도)
if best:
return best
raise TimeoutException("Gemini 답변을 시간 내에 가져오지 못했습니다.")
# ==============================
# 불필요 문구 제거 + JSON 저장
# ==============================
def clean_gemini_answer(raw_text: str, question: str) -> str:
"""
[목적]
- Gemini 페이지에 섞여 들어오는 UI 텍스트(메뉴/경고 문구 등) 제거
- 질문이 답변 블록에 같이 섞여 나온 경우 제거
- 빈 줄 제거
[주의]
- remove_exact는 "완전히 동일한 줄"만 제거합니다.
- ln.startswith("====") 등은 구분선 형태를 제거합니다.
"""
remove_exact = {
"Gemini와의 대화",
"도구",
"빠른 모드",
"새 창에서 열기",
"Gemini는 인물 등에 관한 정보 제공 시 실수를 할 수 있으니 다시 한번 확인하세요.",
"================ Gemini Answer ================",
"==============================================",
}
# 원문을 줄 단위로 분리하고 양끝 공백 제거
lines = [ln.strip() for ln in (raw_text or "").splitlines()]
cleaned = []
for ln in lines:
# 빈 줄 제거
if not ln:
continue
# 정확히 일치하는 UI 텍스트 제거
if ln in remove_exact:
continue
# 구분선 형태 제거
if ln.startswith("====") or ln.startswith("----"):
continue
cleaned.append(ln)
q = (question or "").strip()
if q:
# 질문이 답변에 그대로 포함되어 있는 경우(줄 전체가 질문인 경우) 제거
cleaned = [ln for ln in cleaned if ln != q]
# 질문이 문장 중간에 섞여 들어온 경우도 제거(보수적 처리)
cleaned = [ln for ln in cleaned if q not in ln]
return "\n".join(cleaned).strip()
def save_result_to_json(filepath: str, question: str, answer_clean: str):
"""
[목적]
- 질문/정리된 답변/타임스탬프를 JSON 파일로 저장
[파일 구조 예시]
{
"timestamp": "2025-12-12T17:50:43",
"question": "...",
"answer": "..."
}
"""
payload = {
"timestamp": datetime.now().isoformat(timespec="seconds"),
"question": question,
"answer": answer_clean
}
with open(filepath, "w", encoding="utf-8") as f:
# ensure_ascii=False: 한글이 \uXXXX로 깨지지 않게 저장
# indent=2: 사람이 읽기 좋게 보기(들여쓰기)
json.dump(payload, f, ensure_ascii=False, indent=2)
# ==============================
# 실행
# ==============================
print("⌛ 로그인/로딩 대기 중...")
# Gemini가 로딩되면서 DOM 구조가 잡힐 시간을 줌 + 입력 컨텍스트 확보
switch_to_iframe_with_content(driver, 60)
# 실제 보낼 질문(프롬프트)
question = (
"초보자들이 가독성 좋게 읽을 수 있도록 표 없이 텍스트로만 번호 붙여서 "
"국내, 미국 증시 흐름 깔끔하게 요약해줘. "
"추가로 내용에 대한 설명은 삭제해줘. "
"너가 대답하고 부연 설명하는 부분은 제거해줘. "
"마지막에 궁금한 거 있냐고 질문하는 것도 제거해줘."
"아래 양식을 지켜서 결과 만들어줘"
"국내 증시 흐름 (12월 12일)..다음 내용"
"미국 증시 흐름 (12월 11일)..다음 내용"
)
print(f"\n🧑 질문:\n{question}")
# 질문 전송
send_question(driver, question)
print("\n🤖 Gemini 답변 대기 중...")
# 답변 추출(대기 + 새 텍스트 탐지)
answer = wait_and_get_answer(driver, timeout=120)
# 원본 답변 출력(디버깅용)
print("\n================ Gemini Answer ================\n")
print(answer)
print("\n==============================================\n")
# 답변 정리(UI 문구 제거 등)
answer_clean = clean_gemini_answer(answer, question)
print("\n=========== Cleaned Gemini Answer ===========\n")
print(answer_clean)
print("\n===========================================\n")
# JSON 파일로 저장
save_result_to_json("gemini_result.json", question, answer_clean)
print("✅ gemini_result.json 저장 완료")
2. send_kakao.py
import subprocess
import time
import pyperclip
import json
import re
from pywinauto.keyboard import send_keys # 전역 키 입력(현재 포커스된 창에 키를 보냄)
# ==============================
# 설정값
# ==============================
# Gemini에서 생성된 결과(JSON 파일)를 읽어오기 위한 경로
JSON_PATH = "gemini_result.json"
# ==============================
# 1. JSON 파일에서 Gemini 답변 읽기
# ==============================
def load_answer(path: str) -> str:
"""
[목적]
- gemini_result.json 파일에서
Gemini가 생성한 'answer' 값만 읽어오기
[JSON 구조 예시]
{
"timestamp": "...",
"question": "...",
"answer": "여기에 실제 답변 텍스트"
}
[동작]
1) JSON 파일 열기
2) answer 키에 해당하는 문자열 추출
3) 앞뒤 공백 제거
4) 비어 있으면 에러 발생
"""
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
ans = (data.get("answer") or "").strip()
# answer가 없거나 비어 있으면 이후 카톡 전송 의미가 없으므로 즉시 에러
if not ans:
raise ValueError("JSON에 answer가 비어있습니다.")
return ans
# ==============================
# 2. Gemini 답변을 '번호 붙은 텍스트'로 정리
# ==============================
def format_numbered_text(answer: str) -> str:
"""
[목적]
- Gemini가 준 답변을
카카오톡에 보내기 좋게 '번호 붙은 텍스트' 형태로 정리
[정리 규칙]
- '국내 증시 흐름 (...)', '미국 증시 흐름 (...)' 같은
섹션 제목은 그대로 유지
- 섹션 제목 아래 문장들은
1), 2), 3) ... 형태로 번호 부여
- 빈 줄은 제거
[왜 필요한가?]
- Gemini 원문은 줄바꿈/서술 방식이 들쭉날쭉함
- 카카오톡에서는 간결한 번호형 요약이 가독성이 좋음
"""
# 줄 단위로 쪼갠 뒤, 앞뒤 공백 제거 + 빈 줄 제거
lines = [ln.strip() for ln in answer.splitlines() if ln.strip()]
out = [] # 최종 결과를 담을 리스트
idx = 1 # 번호 카운터 (1), 2), 3)...
for ln in lines:
# 섹션 헤더인지 확인
# 예: "국내 증시 흐름 (12월 12일)"
if re.match(r"^(국내|미국)\s*증시\s*흐름", ln):
# 이전 섹션과 시각적으로 구분하기 위해 한 줄 띄움
if out and out[-1] != "":
out.append("")
# 섹션 제목은 그대로 추가
out.append(ln)
# 번호는 새 섹션이므로 다시 1부터
idx = 1
continue
# 일반 문장은 번호를 붙여서 추가
out.append(f"{idx}) {ln}")
idx += 1
# 리스트를 다시 문자열로 합쳐 반환
return "\n".join(out).strip()
# ==============================
# 3. JSON → 카카오톡으로 보낼 메시지 준비
# ==============================
# 1) JSON에서 Gemini 답변 읽기
answer = load_answer(JSON_PATH)
# 2) 번호 붙은 텍스트로 정리
msg = format_numbered_text(answer)
# ==============================
# 4. 카카오톡 PC 실행
# ==============================
# subprocess.Popen:
# - 외부 프로그램(KakaoTalk.exe)을 실행
# - stdout/stderr를 버려서 콘솔 출력 방지
subprocess.Popen(
[r"C:\Program Files (x86)\Kakao\KakaoTalk\KakaoTalk.exe"],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
# 카카오톡 UI가 완전히 뜰 때까지 대기
time.sleep(5)
# ==============================
# 5. 카카오톡 검색창 열기
# ==============================
# Ctrl+F:
# - 카카오톡 PC 버전에서 채팅방/친구 검색창 열기
send_keys("^f")
time.sleep(0.6)
# 검색창이 열렸어도 포커스가 애매할 수 있어
# 한 번 더 Ctrl+F로 확실히 커서 위치 보정
send_keys("^f")
time.sleep(0.4)
# ==============================
# 5-1. 검색창 기존 내용 지우기 (추가)
# ==============================
send_keys("{HOME}")
time.sleep(0.05)
send_keys("+{END}") # Shift + End
time.sleep(0.05)
send_keys("{DELETE}")
time.sleep(0.2)
# ==============================
# 6. 채팅방 이름 입력
# ==============================
# 타이핑(send_keys("테스트")) 대신
# 클립보드 붙여넣기를 쓰는 이유:
# - 한글 IME 문제
# - 키 입력 누락 방지
pyperclip.copy("테스트")
time.sleep(0.05)
# Ctrl+V로 검색창에 붙여넣기
send_keys("^v")
time.sleep(0.3)
# ==============================
# 7. 엔터 → 채팅방 입장
# ==============================
# 검색 결과에서 첫 번째 방으로 입장
send_keys("{ENTER}")
# 채팅방 UI 로딩 대기
time.sleep(2)
# ==============================
# 8. 메시지 붙여넣기 + 전송
# ==============================
# 정리된 Gemini 답변(msg)을 클립보드에 복사
pyperclip.copy(msg)
time.sleep(2)
# 채팅 입력창에 붙여넣기
send_keys("^v")
time.sleep(0.2)
# Enter → 메시지 전송
send_keys("{ENTER}")
print("끝")
3. autoGemini.py 프롬프트
파이썬으로 제미나이(https://gemini.google.com/)에 접속해서 질문을 입력하고,
화면에 표시되는 답변을 기다렸다가 자동으로 가져오는 코드를 만들어줘.
크롬 로그인 상태를 유지한 채로 동작하게 해주고,
화면 구조가 바뀌어도 최대한 잘 동작하도록 안정적으로 만들어줘.
가져온 답변은 불필요한 문구를 제거해서 깔끔하게 정리해줘.
최종 결과는 시간 정보와 함께 파일로 저장되는 하나의 파이썬 파일(gemini_result.json)로 만들어줘.
* 제미나이에게 할 질문
초보자들이 가독성 좋게 읽을 수 있도록 표 없이 텍스트로만 번호 붙여서
국내, 미국 증시 흐름 깔끔하게 요약해줘
추가로 내용에 대한 설명은 삭제해줘
너가 대답하고 부연 설명하는 부분은 제거해줘
마지막에 궁금한 거 있냐고 질문하는 것도 제거해줘
아래 양식을 지켜서 결과 만들어줘
국내 증시 흐름 (12월 12일)..다음 내용
미국 증시 흐름 (12월 11일)..다음 내용
4. send_kakao.py 프롬프트
파이썬으로 gemini_result.json 파일을 읽어서, 그 안의 answer 내용을 가져오는 코드를 만들어줘.
가져온 내용을 카카오톡에 보내기 좋게 줄바꿈 정리하고, “국내/미국 증시 흐름” 같은 제목은 유지한 채 문장들에는 1), 2)처럼 번호를 붙여줘.
그 다음 카카오톡 PC를 실행해서 검색창을 열고, 기존 검색어를 지운 뒤 “테스트” 채팅방에 들어가게 해줘.
마지막으로 정리된 메시지를 붙여넣고 엔터로 전송까지 자동으로 하게 해줘.
초보자도 따라할 수 있게 주석을 자세히 달고, 실행 가능한 하나의 파이썬 파일로 전체 코드를 출력해줘.
'착한코딩 YouTube' 카테고리의 다른 글
| [gpt와 함께하는 업무 자동화] 2. GPT로 만드는 주식 자동화! 키움증권 API → 구글 시트 자동 저장 (0) | 2026.01.20 |
|---|---|
| 홈택스 공제/비공제 자동 선택 스크립트 만들기 javascript 소스 (4) | 2025.07.16 |
| etc. 파이썬 자동화 로또 구매, 당첨 확인 [예제소스] (0) | 2024.09.10 |
| SQL 쿼리_6강 SQL 데이터 중복 처리 예제 소스(REPLACE, DUPLICATE, MERGE) (0) | 2024.07.22 |
| 스컬킹 점수판_착한코딩.xlsx (0) | 2024.01.31 |
Comments