코딩

[파이썬] 새로운 게시글이 등록되면 자동으로 카카오톡 보내주기

땡감 2022. 1. 6. 09:32
반응형

[파이썬] 카카오톡으로 나에게 메시지를 보내보자

 

[파이썬] 카카오톡으로 나에게 메시지를 보내보자

파이썬으로 사이트 키워드 챗봇을 만들고 있다. 가장 흔하게 쓰는 텔레그램이 아닌 카카오톡으로 하다보니 인증 방법부터 Json으로 처리하는 방법까지 알아야했다. 아직 내가 목표로 하는 프로

gam860720.tistory.com

[파이썬] 카카오톡 자동 토근 발행 후 나에게 메시지 보내기 (feat.refresh token)

 

[파이썬] 카카오톡 자동 토근 발행 후 나에게 메시지 보내기 (feat.refresh token)

며칠전 파이썬으로 카카오톡을 통해 자신에게 메시지를 보내는 법에 알아봤다. (해당 글은 아래 링크 참조) [파이썬] 카카오톡으로 나에게 메시지를 보내보자 하지만 단점으로 언급한것처럼 카

gam860720.tistory.com

 

위의 두 글을 작성하고 본격적으로 내가 하고자 했던 코딩을 시작했다.

목표는 내가 자주가는 사이트의 핫딜 정보를 카톡으로 보내주기.

사이트는 fmkorea라는 사이트로 3대 악마게임으로 유명한 FM을 하면서 알게된 사이트다.

아이를 낳고선 게임을 일체 하지않는 요즘은 핫딜때문에 더욱 더 자주 방문하게 되었는데 이를 자동화하고 싶어 만들었다.

백문이 불여일견이므로 먼저 코드부터 살펴보자.

import requests
import json
import time
import copy
from bs4 import BeautifulSoup
from datetime import datetime


# 코드 발급용 url  https://kauth.kakao.com/oauth/authorize?client_id=자신의 REST 키 값&redirect_uri=https://example.com/oauth&response_type=code
url = 'https://kauth.kakao.com/oauth/token'  # 카톡 인증 url
rest_api_key = '자신의 REST API'  # 카톡 rest 키
redirect_uri = 'https://example.com/oauth'  # 카톡 인증용 redirect url
authorize_code = '코드값'  # 카톡 인증용 코드 수행시마다 재발급 필요
site_url = 'https://m.fmkorea.com/hotdeal'
board_list = []  # 크롤링 결과 저장 리스트
p_board_list = []  # 이전 크롤링 결과 저장 리스트


def f_get_list():
    result_search = requests.get(site_url)
    # print(result_search)
    html = result_search.text
    soup = BeautifulSoup(html, 'html.parser')
    times = soup.select('.regdate') #글 작성시간 크롤링
    titles = soup.select('.hotdeal_var8') #글 제목 클롤링

    for idx in range(0, len(titles), 1):
        t = titles[idx].text.replace('\t', '') #제목 앞에 탭 제거
        loc = t.rfind('[') #댓글 수가 제목에 포함되어 댓글 수는 날려버리기 위해서 '[' 문자의 위치 값 찾음
        board_list.append('제목:' + t[0:loc] + ' 링크:' + site_url + titles[idx]['href'])
            #'작성시간:' + times[idx].text.strip() + '  제목:' + t[0:loc] + ' 링크:' + site_url + titles[idx]['href'])  #핫딜 종료되면 시간 순서랑 제목 순서가 어긋나서 작성시간 뺌


# 최초 토큰 발췌용 함수 refresh token을 리턴함 (refresh token은 한달정도 유효)
def f_auth():
    data = {
        'grant_type': 'authorization_code',
        'client_id': rest_api_key,
        'redirect_uri': redirect_uri,
        'code': authorize_code,
    }

    response = requests.post(url, data=data)
    tokens = response.json()

    with open("kakao_code.json", "w") as fp:
        json.dump(tokens, fp)
    with open("kakao_code.json", "r") as fp:
        ts = json.load(fp)
    r_token = ts["refresh_token"]
    return r_token


# 토큰을 갱신하는 함수로 refresh token을 인자로 받고 새로운 token을 리턴함 (주기마다 반복 발급 받음)
def f_auth_refresh(r_token):
    with open("kakao_code.json", "r") as fp:
        ts = json.load(fp)
    data = {
        "grant_type": "refresh_token",
        "client_id": rest_api_key,
        "refresh_token": r_token
    }
    response = requests.post(url, data=data)
    tokens = response.json()

    with open(r"kakao_code.json", "w") as fp:
        json.dump(tokens, fp)
    with open("kakao_code.json", "r") as fp:
        ts = json.load(fp)
    token = ts["access_token"]
    return token


def f_send_talk(token, text):
    header = {'Authorization': 'Bearer ' + token}
    url = 'https://kapi.kakao.com/v2/api/talk/memo/default/send'  # 나에게 보내기 주소
    post = {
        'object_type': 'text',
        'text': text,
        'link': {
            'web_url': 'https://developers.kakao.com',
            'mobile_web_url': 'https://developers.kakao.com'
        },
        'button_title': '키워드'
    }
    data = {'template_object': json.dumps(post)}
    return requests.post(url, headers=header, data=data)


r_token = f_auth()

while True:
    f_get_list()  #게시글 크롤링
    token = f_auth_refresh(r_token)  # 새로운 액세스 토큰을 발급 받음
    sms_list = list(set(board_list) - set(p_board_list))  # 이전 리스트와 비교하여 다른 값만 문자 보낼 리스트로 저장
    p_board_list = copy.deepcopy(board_list)  # 현재 게시글을 이전 게시글로 저장
    #p_board_list = board_list  #같은 메모리 주소 사용하여 같은 내용으로만 취급됌

    f_send_talk(token, '현재 시간 {} 기준 최신글은 총 {}개입니다.'.format(datetime.now().strftime('%H:%M:%S'),len(sms_list)))
    for i in range(0, len(sms_list), 1):
        f_send_talk(token, sms_list[i])

    board_list.clear()
    sms_list.clear()
    time.sleep(1800)  # 반복 주기

환경마다 다르게 설정해야할 부분은 REST API KEY값과 Code 값 부분, 그리고 반복 주기값이다.

이전 두 포스팅에서 언급을 했던 카카오톡 인증하는 부분과 다시 인증하여 메시지 전송하는 부분은 동일하다.

사이트에서 원하는 정보를 크롤링하는 f_get_list 함수와 추출한 list를 카카오톡으로 보내는 부분만 추가되었다.

보낼때는 현재 시간을 기준으로 이전 리스트들과 비교하여 새로운 게시글들만 보내도록 하였는데 처음에는 set 개념을 이용하지 않아 시간을 많이 허비했다.

추가로 리스트를 복사하는 부분은 다른 언어들과는 다르게 참조형식으로 복사가 되어 애를 많이 먹었다.

크롤링을 공부하면서 알게된 추가 팁은 PC버젼 URL 보단 모바일 버젼 URL에서 크롤링하는게 조금 더 수월하다는거다.

완성 후, 며칠 지켜보면서 확인된 문제도 있는데 가끔 동일한 제목의 게시글이 새로운글로 인식이 되어 다시 알림이 오는 경우와 카카오톡에서는 링크를 클릭하면 바로 페이지로 연결되는데 그 링크가 클릭이 되지 않는 일반 텍스트로 알림이 오는 경우 이 두가지였다.

일단 여기서 마무리 짓고 다른 사이트도 만들어봐야겠다.

 

###### 2022년 1월 03일 오후 14시 47분 수정 내용 #####

  • 앞으로 카카오톡으로 메시지를 자주 보낼 것 같아 카카오톡 관련 함수들을 별도의 파일로 모듈화를 함. (kakao.py)
  • 최초 token 발행 및 refresh token 가져오는 부분 수정
  • 기존 코드에서 모듈화 된 부분 수정 및 함수이름 변경
  • 테스트시 발생했던 에러 확인용 주석 제거

##### 2021년 1월 03일 오후 10시03분 수정 내용 #####

  • 가격 정보 크롤링 후 해당 내용도 같이 전달
  • 카카오톡에 보이는 메시지 내용 개행 처리 (제목, 가격, 링크)

1) kakao.py

import time
import requests
import json

# code url  https://kauth.kakao.com/oauth/authorize?client_id=자신의 rest api key 값&redirect_uri=https://example.com/oauth&response_type=code
url = 'https://kauth.kakao.com/oauth/token'  # 카톡 인증 url
rest_api_key = '자신의 rest api key 값'  # 카톡 rest 키
redirect_uri = 'https://example.com/oauth'  # 카톡 인증용 redirect url
authorize_code = 'code url을 통해 얻은 code 값'  # 카톡 인증용 코드 수행시마다 재발급 필요


# 최초 token들 발급하여 refresh token 저장
def f_get_refresh_token():
    data = {
        'grant_type': 'authorization_code',
        'client_id': rest_api_key,
        'redirect_uri': redirect_uri,
        'code': authorize_code,
    }

    response = requests.post(url, data=data)
    tokens = response.json()

    with open('refresh_token.json', 'w') as fd:
        json.dump(tokens, fd)


# refresh token을 이용하여 새로운 access token 발급
def f_reissue_token():
    with open('refresh_token.json', 'r') as fd:
        token = json.load(fd)
    refresh_token = token['refresh_token']
    data = {
        'grant_type': 'refresh_token',
        'client_id': rest_api_key,
        'refresh_token': refresh_token
    }
    response = requests.post(url, data=data)
    tokens = response.json()

    with open('access_token.json', 'w') as fd:
        json.dump(tokens, fd)
    with open('access_token.json', 'r') as fd:
        ts = json.load(fd)
    access_token = ts['access_token']
    return access_token


# 메시지 전송
def f_send_msg(access_token, msg):
    header = {'Authorization': 'Bearer ' + access_token}
    url = 'https://kapi.kakao.com/v2/api/talk/memo/default/send'  # 나에게 보내기 주소
    post = {
        'object_type': 'text',
        'text': msg,
        'link': {
            'web_url': 'https://developers.kakao.com',
            'mobile_web_url': 'https://developers.kakao.com'
        },
        'button_title': '키워드'
    }
    data = {'template_object': json.dumps(post)}
    return requests.post(url, headers=header, data=data)

 

2) fm.py

import requests
import json
import time
import copy
from bs4 import BeautifulSoup
from datetime import datetime

import kakao


site_url = 'https://m.fmkorea.com/hotdeal'
board_list = []  # 크롤링 결과 저장 리스트
p_board_list = []  # 이전 크롤링 결과 저장 리스트


def f_get_list():
    result_search = requests.get(site_url)
    html = result_search.text
    soup = BeautifulSoup(html, 'html.parser')
    times = soup.select('.regdate') #글 작성시간 크롤링
    titles = soup.select('.hotdeal_var8') #글 제목 클롤링
    prices = soup.select('.hotdeal_info > span:nth-child(2) > a.strong') #가격 크롤링

    for idx in range(0, len(titles), 1):
        t = titles[idx].text.replace('\t', '') #제목 앞에 탭 제거
        loc = t.rfind('[') #댓글 수가 제목에 포함되어 댓글 수는 날려버리기 위해서 '[' 문자의 위치 값 찾음
		board_list.append('제목: ' +t[0:loc] +'\n링크: ' +site_url +titles[idx]['href'])
        #board_list.append('제목: ' +t[0:loc] +'\n가격: ' +prices[idx].text +'\n링크: ' +site_url +titles[idx]['href'])


#kakao.f_get_refresh_token() #최초 refresh token 추출시에만 수행

while True:
    f_get_list()  #게시글 크롤링
    access_token = kakao.f_reissue_token()  # 새로운 액세스 토큰을 발급 받음
    sms_list = list(set(board_list) - set(p_board_list))  # 이전 리스트와 비교하여 다른 값만 문자 보낼 리스트로 저장
    p_board_list = copy.deepcopy(board_list)  # 현재 게시글을 이전 게시글로 저장

    kakao.f_send_msg(access_token, 'FM 결과\n'  +'현재 시간 {} 기준 최신글은 총 {}개입니다.'.format(datetime.now().strftime('%H:%M:%S'),len(sms_list)))

    for i in range(0, len(sms_list), 1):
        kakao.f_send_msg(access_token, sms_list[i])

    board_list.clear()
    sms_list.clear()
    time.sleep(1800)  # 반복 주기

 

##### 2022년 01월 05일 오후10시 30분 수정사항 #####

  • 가격 정보 제외

최초 작성시간을 가져왔을 때 문제가 된것처럼 가격정보도 핫딜이 종료되면 제목 크롤링 결과와 리스트 인덱스가 불일치하는 경우가 발생한다.

문제의 원인은 제목의 sytle 부분이 아래처럼 값이 추가되는 경우에만 발생하는데 크롤리을 할 때 해당 값을 제외하고 크롤링하는 방법을 찾고 있다.

hotdeal_var8Y {
text-decoration: line-through!important;
}

일단 해결방법을 찾을 때까지 가격정보는 제외해야 할듯 하다.

 

##### 2022년 01월 06일 오전 09시 21분 수정사항 ######

  • 제목을 크롤링해오는 방식 변경 (2가지 중 선택)
    • select() 함수 대신 find_all() 함수로 대체하여 클래스명을 정규식으로 찾아 크롤링
    • select() 함수에 클래스명을 넘기는 대신 부모노드와 자신의 경로를 전달해 크롤링

위에서 언급했던 문제를 해결할 방법을 찾았다.

방법은 두가지인데 처음 작성시 조금만 더 꼼꼼하게 체크를 했어야 했다.

1.핫딜과 핫딜종료 클래스는 한글자 차이라 정규식을 이용해서 크롤링하는 방법

2.핫딜과 핫딜종료 클래스는 달라도 부모 노드의 순서가 동일하므로 해당 부모까지 크로링

먼저 1번 방법인 정규식을 사용하기 위해서는 select() 함수가 아닌 find_all() 함수를 사용해야 한다.

(select 함수는 정규식 미지원)

그리고 2번 방법은 select() 함수 인자로 바로 클래스명을 넘기는 것이 아니라 해당 클래스의 부모노드까지 넘기면 된다.

일단 수정이 이루어진 fm.py 코드를 첨부하며 이번이 제발 마지막이길 바래본다.

ps. 문제를 해결해서 기존의 작성시간, 가격도 같이 뿌려줄 수 있도록 수정했다.

import requests
import json
import time
import copy
import re
from bs4 import BeautifulSoup
from datetime import datetime

import kakao


site_url = 'https://m.fmkorea.com/hotdeal'
board_list = []  # 크롤링 결과 저장 리스트
p_board_list = []  # 이전 크롤링 결과 저장 리스트


def f_get_list():
    result_search = requests.get(site_url)
    html = result_search.text
    soup = BeautifulSoup(html, 'html.parser')
    times = soup.select('.regdate') #글 작성시간 크롤링
    titles = soup.find_all('a', re.compile('hotdeal_var8*'))    #딜 종료 게시글까지 가져오기 위해 정규식을 사용함, select 함수는 정규식 미지원 (딜 진행중 게시글은 hotdeal_var8, 딜 종료 게시글은 hotdeal_var8Y 클래스)
    #titles = soup.select('h3.title > a')    #핫딜과 핫딜종료 게시글의 제목을 가지고 있는 클래스명이 상이하여 부모노드를 명시하여 크롤링을 함
    prices = soup.select('.hotdeal_info > span:nth-child(2) > a.strong') #가격 크롤링

    for idx in range(0, len(titles), 1):
        t = titles[idx].text.replace('\t', '') #제목 앞에 탭 제거
        loc = t.rfind('[') #댓글 수가 제목에 포함되어 댓글 수는 날려버리기 위해서 '[' 문자의 위치 값 찾음
        board_list.append('제목: ' +t[0:loc] +'\n작성시간: '+times[idx].text.replace('\t','') +'\n가격: ' +prices[idx].text +'\n링크: ' +site_url +titles[idx]['href'])


#kakao.f_get_refresh_token() #최초 refresh token 추출시에만 수행

while True:
    f_get_list()  #게시글 크롤링
    access_token = kakao.f_reissue_token()  # 새로운 액세스 토큰을 발급 받음
    sms_list = list(set(board_list) - set(p_board_list))  # 이전 리스트와 비교하여 다른 값만 문자 보낼 리스트로 저장
    p_board_list = copy.deepcopy(board_list)  # 현재 게시글을 이전 게시글로 저장

    kakao.f_send_msg(access_token, 'FM 결과\n'  +'현재 시간 {} 최신글은 총 {}개입니다.'.format(datetime.now().strftime('%H:%M:%S'),len(sms_list)))

    for i in range(0, len(sms_list), 1):
        kakao.f_send_msg(access_token, sms_list[i])

    board_list.clear()
    sms_list.clear()
    time.sleep(1800)  # 반복 주기

위 코드는 2가지 방법 모두 기재되어 있으며 titles = soup.XXXX 부분만 자신의 기호에 맞게 사용하면 된다.

find_all 함수 사용시에는 import re를 해줘야 한다.

반응형