WiseCard는 사용자가 보유한 카드의 혜택 정보를 기반으로, 현재 위치에서의 최적의 할인 매장과 사용자의 소비 계획에 따른 최적의 혜택 카드를 안내해주는 카드 혜택 추천 모바일 애플리케이션입니다.
📍 핵심 기능
- 카드사 혜택 정보 크롤링 🔍
- Naver Map 연동으로 주변 매장 시각화 및 매장 추천 🗺️
- 사용자의 결제 상황에 맞춘 최적의 카드 자동 추천 💡
카드사의 혜택 정보를 자동으로 수집하고 정리하는 크롤링 시스템을 구현하였습니다. 전체 과정은 다음과 같습니다:
카드 목록 수집
extract_card_info_from_url(list_url, card_url_prefix)함수를 사용하여 카드 목록을 수집합니다.수집된 정보는
_creditcardInfos.csv파일에 저장됩니다.CSV 파일에는 다음 항목이 포함됩니다:
- 카드 이름
- 카드 이미지 URL
- 카드 상세정보 URL
카드 상세 혜택 정보 크롤링
_creditcardInfos.csv에 저장된 각 카드의 상세 페이지 URL을 기반으로, 혜택 정보를 추가로 크롤링합니다.크롤링된 정보는
credit_benefit.csv에 저장되며, 아래와 같은 항목을 포함합니다:- 카드 ID
- 카드 이름
- 카드 이미지 URL
- 혜택 정보
- 크롤링 일자 (생성 일자)
- 카드 종류 (신용/체크 여부)
- 카드 이름 수집
- 카드 코드 추출 → 이미지 URL 및 상세 페이지 URL 생성
- 크롤링한 정보를 각각
card_names,card_imgs,card_urls리스트에 저장
# 카드 이름 추출name=li.find('span', class_='h4_b_lt')
ifname:
card_names.append(name.text.strip())
# 카드 코드 추출 → 이미지 및 상세 페이지 URL 생성a_tag=card_div.find('a')
ifa_tagand'onclick'ina_tag.attrs:
onclick_text=a_tag['onclick']
match=re.search(r"goCardDetail\('([^']+)'\)", onclick_text)
ifmatch:
card_code=match.group(1)
card_imgs.append(f'https://img.hyundaicard.com/img/com/card/card_{card_code}_h.png')
card_urls.append(f'{card_url_prefix}{card_code}&eventCode=00000')- 각 카드 상세 URL에 접속 카드 혜택 관련 텍스트만 선별 추출 (카드사마다 상이함)
- 혜택 텍스트를
benefits리스트에 저장
# 웹 페이지 접속 및 파싱driver.get(card_urls[i])
html=driver.page_sourcesoup=BeautifulSoup(html, 'html.parser')
# 팝업 컨테이너 중 혜택 관련된 모달만 선별popup_containers=soup.find_all('div', class_='modal_pop')
exclude_ids= {'popup_call_reservation', 'cardApplyPop', ...}
forcontainerinpopup_containers:
ifcontainer.get('id') inexclude_ids:
continueforelincontainer.find_all(['h1', 'p', 'li']):
texts.append(el.get_text(strip=True))
benefits.append(texts)selenium,BeautifulSoup,pandas를 활용하여 카드사 웹페이지를 자동으로 순회하며 상세 정보를 수집합니다.- 팝업 ID 필터링을 통해 혜택과 무관한 요소는 제외합니다.
- Google AI - Gemini 2.0 Flash 모델
웹 크롤링을 통해 수집한 카드 혜택 문장을 입력값으로 활용하여, Gemini가 해당 문장을 이해한 후 카테고리, 할인율, 포인트, 캐시백, 해당 매장 등으로 자동 분류하고 JSON 형태로 구조화합니다.
for_, cardindf.iterrows():
benefit_text=card["benefits"]
prompt= (
f"{benefit_text}\n\n"''' JSON 예시를 포함한 프롬프트 '''
)
try:
response=model.generate_content(prompt)
json_response=response.text.strip().replace("```json","").replace("```","")
updated_benefits.append(json_response)
print(json_response)
exceptExceptionase:
print(f"Error processing card {card['name']}: {e}")
updated_benefits.append("")사용자의 GPS 기반 위치를 기준으로, 크롤링을 통해 수집한 카드 혜택의 적용 매장이 주변에 존재하는지를 확인하는 용도로 사용하였습니다.
검색 키워드와 위치 좌표를 함께 전송하면, 결과로 해당 장소의 이름, 주소, 거리, 위도/경도 등의 정보를 반환합니다.
x_coord="126.94"y_coord="37.55"results= []
forcafeincafe_targets_list:
params= {
"query": cafe,
"x": x_coord,
"y": y_coord,
"radius": 1000,
"size": 5
}
response=requests.get(url, headers=headers, params=params)
ifresponse.status_code!=200:
print(f"{cafe} 검색 실패: {response.status_code}")
continueplaces=response.json().get("documents", [])
forplaceinplaces:
place_info= {
"place_name": place.get("place_name"),
"road_address_name": place.get("road_address_name"),
"x": place.get("x"),
"y": place.get("y"),
"카페명": cafe,
"거리": int(place.get("distance", "0")), # 문자열을 정수로 변환"카드별_혜택": {}
}
# 3. 이 place의 카페명으로 어떤 카드에서 혜택 있는지 확인 후 혜택 첨부forcard_name, infoincard_cafe_benefits.items():
ifcafeininfo["대상"]:
place_info["카드별_혜택"][card_name] =deepcopy(info["혜택"]) # 깊은 복사results.append(place_info)사용자 카드 내역 파일(카드내역.csv)을 불러와, 결제된 매장의 카테고리를 자동으로 추출하는 과정에서 Kakao Map API를 사용합니다.
추출한 카테고리 정보를 기반으로 해당 분야(예: 카페)에서의 결제 건별 평균 소비 금액을 도출할 수 있으며, 이는 이후 예상 할인 금액을 계산하고 카드 추천 시스템에 활용하는 과정에 적용할 수 있습니다.
card_history_df=pd.read_csv('카드내역2.csv')headers= {"Authorization": "KakaoAK API-KEY"}
url="https://dapi.kakao.com/v2/local/search/keyword.json"category_map= {
"MT1": "대형마트", "CS2": "편의점", "PS3": "어린이집, 유치원", "SC4": "학교",
"AC5": "학원", "PK6": "주차장", "OL7": "주유소, 충전소", "SW8": "지하철역",
"BK9": "은행", "CT1": "문화시설", "AG2": "중개업소", "PO3": "공공기관",
"AT4": "관광명소", "AD5": "숙박", "FD6": "음식점", "CE7": "카페",
"HP8": "병원", "PM9": "약국"
}category_stats= {}foridx, rowincard_history_df.iterrows():
store_name=row['가맹점명']
try:
amount=int(str(row['승인금액']).replace(",", "").replace("원", "").strip())
except:
amount=0store_name=store_name.replace("( 주 )", "").replace("주식회사", "")
params= {"query": store_name}
try:
response=requests.get(url, headers=headers, params=params)
data=response.json()
print(f"\n[{idx}] 가맹점명: {store_name} | 승인금액: {amount}원")
ifdata.get("documents"):
doc=data["documents"][0]
category_code=doc.get("category_group_code", "")
category_name=category_map.get(category_code, "기타")
print(f" • 이름: {doc['place_name']}")
print(f" 카테고리: {category_name} ({category_code})")
print(f" ----------------------")
ifcategory_namenotincategory_stats:
category_stats[category_name] = {"total_amount": 0, "count": 0}
category_stats[category_name]["total_amount"] +=amountcategory_stats[category_name]["count"] +=1else:
print(" → 검색 결과 없음")
time.sleep(0.3)
exceptExceptionase:
print(f" 오류 발생: {e}")print("\n카테고리별 총합 및 평균 금액:")
forcategory, statsincategory_stats.items():
total=stats["total_amount"]
count=stats["count"]
avg=total/countifcountelse0print(f" - {category}: 총 {total:,}원 / {count}회 / 평균 {avg:,.0f}원")사용자가 받을 수 있는 카드 혜택과 앞으로 받을 수 있는 혜택 한도를 계산합니다.
사용자의 카드 결제 내역 하나를 기준으로, 해당 결제에 대해 혜택을 받을 수 있는 카드들을 식별한 뒤, 할인 > 적립 > 캐시백 순의 우선 순위에 따라 가장 먼저 적용된 하나만 사용합니다.
혜택이 적용될 수 있는 최소 조건이 있다면 해당 조건을 만족하는지 우선 판단하고, 조건을 만족했을 때 혜택 계산을 진행합니다.
importre# 혜택이 적용되는 최소 조건(결제 금액 등)을 만족하는 지 판단하는 함수defextract_minimum_amount(condition_str):
""" '5만원 이상', '1만 원 이상', '5천원 이상' 등의 조건에서 최소 금액을 원 단위로 추출 """ifnotcondition_str:
returnNonepattern=r'(\d+)(만|천)?\s*원?\s*이상'# 숫자 + (만|천) + 원 + 이상 패턴match=re.search(pattern, condition_str)
ifmatch:
num=int(match.group(1))
unit=match.group(2)
ifunit=='만':
returnnum*10000elifunit=='천':
returnnum*1000else:
returnnum# 원 단위returnNone# 한 건의 결제내역에 대해 어떤 혜택이 적용되는지를 계산하는 함수defapply_benefit(row_data):
store_name=str(row_data["가맹점명"]) # 가맹점명을 문자열로 가져옴amount=row_data["이용금액"]
applied_amount=amountbenefit_detail=None# 어떤 혜택이 적용됐는지 (예: 할인, 적립, 캐시백)point_or_cashback=0.0# 적립/캐시백 금액 기본값 0원foridx, benefit_rowindf.iterrows():
benefits=benefit_row['benefits_dict']
forcategory, infoinbenefits.items():
ifinfoisnotNoneand"대상"ininfo:
targets=info["대상"] # 가맹점명이 혜택 대상에 포함되는지 확인iftargetsandany(targetinstore_namefortargetintargets):
# 현재 가맹점명이 혜택 대상에 포함되는 경우# ───── 할인 처리 시작 ─────할인_목록=info.get("혜택", {}).get("할인", [])
fordin할인_목록:
조건문=d.get("조건", "")
최소_금액=extract_minimum_amount(조건문) # 혜택이 적용되는 조건에서 최소금액 추출if최소_금액andamount<최소_금액: # 금액이 조건보다 작으면 건너뜀continueifd.get("할인율(%)"): # 할인율이 있는 경우rate=d["할인율(%)"]
ifrate:
applied_amount=amount* (1-rate/100)
benefit_detail="할인"returnapplied_amount, benefit_detail, 0.0elifd.get("금액(원)"): # 정액 할인 금액이 있는 경우discount_amt=d["금액(원)"]
ifdiscount_amt:
applied_amount=max(0, amount-discount_amt)
benefit_detail="할인"returnapplied_amount, benefit_detail, 0.0# ───── 적립 처리 시작 ─────적립_목록=info.get("혜택", {}).get("적립", [])
fordin적립_목록:
ifd.get("적립율(%)"): # 적립율이 있는 경우rate=d["적립율(%)"]
ifrate:
benefit_detail="적립"point_or_cashback=amount*rate/100returnamount, benefit_detail, point_or_cashback# 지불 금액은 변함없음# ───── 캐시백 처리 시작 ─────캐시백_목록=info.get("혜택", {}).get("캐시백", [])
fordin캐시백_목록:
ifd.get("캐시백 비율(%)"): # 비율 캐시백cashback_rate=d["캐시백 비율(%)"]
ifcashback_rate:
benefit_detail="캐시백"point_or_cashback=amount*cashback_rate/100returnamount, benefit_detail, point_or_cashbackelifd.get("금액(원)"): # 정액 캐시백cashback_amt=d["금액(원)"]
ifcashback_amt:
benefit_detail="캐시백"point_or_cashback=cashback_amtreturnamount, benefit_detail, point_or_cashback# 어떤 혜택도 적용되지 않은 경우returnamount, None, 0.0