크롤링한 데이터를 분석하여 인사이트 도출
[문제상황]
[원인]
Retry-After: <http-date>
Retry-After: <delay-seconds>
# 예시
Retry-After: Wed, 21 Oct 2015 07:28:00 GMT
Retry-After: 120
"어떻게 Block이 된 걸까"
1. requests : 요청을 보내는 단계로, 클라이언트(나의 컴퓨터)가 서버로 보냄
2. response : 요청을 받은 서버가 데이터를 가져와 클라이언트에게 보내는 것
3. 크롤링 시, 이 requests를 아주 짧은 시간에 과도하게 보냄 (약 80개 상품에 10페이지 => 800 + @)*
4. 서버에서는 이러한 반복적이고 잦은 요청을 공격으로 인식 (서버 개발자가 설정한 기준에 의해)
5. 서버와 페이지, 데이터를 보호하기 위해 해당 클라이언트(=나)를 Block
[해결]
| 구분 | requests | selenium |
|---|---|---|
| 작동 방식 | 서버에 HTTP 요청만 보내고 응답을 받아옴 (HTML 코드만 가져옴) | 실제 브라우저를 제어함 (Chrome/Firefox 등 실행) |
| 렌더링 | ❌ 없음 (JavaScript는 실행되지 않음) | ✅ 있음 (브라우저가 JS를 렌더링) |
| 사용 대상 | 정적 페이지 (HTML에 정보가 있는 경우) | 동적 페이지 (JS로 데이터가 나중에 로딩되는 경우) |
| 항목 | requests | selenium |
|---|---|---|
| 속도 | 🚀 빠름 (단순 요청/응답만 함) | 🐢 느림 (브라우저 실행 + JS 처리 필요) |
| 리소스 사용 | 🧠 적음 (CPU/메모리 부담 적음) | 💥 큼 (브라우저 여러 개 뜨면 리소스 폭증) |
| 병렬성 | ✔ 다중 스레딩/비동기화 쉬움 | ✘ 브라우저 인스턴스 관리가 까다로움 |
| 상황 | 적합한 도구 |
|---|---|
| 뉴스 기사 HTML 긁기 | ✅ requests |
| 쇼핑몰에서 스크롤해야 상품 전체가 보이는 경우 | ✅ selenium |
| API 엔드포인트나 URL 분석해서 바로 JSON 가져올 수 있는 경우 | ✅ requests |
| 버튼 클릭 → AJAX로 데이터 갱신되는 웹 페이지 | ✅ selenium |
| requests | selenium |
|---|---|
| 간단하고 안정적 웹 페이지 구조 바뀌어도 적응이 쉬움 | 웹 요소 위치 지정이 불안정할 수 있음 (클래스 이름이 바뀌거나, DOM 구조 변화 시) 브라우저 업데이트, 드라이버 버전 등 외부 요인에 영향을 많이 받음 |
| 상황 | 추천 도구 |
|---|---|
| 빠르고 단순하게 HTML만 긁어오고 싶다 | requests |
| 동적으로 생성된 데이터를 수집해야 한다 | selenium |
| 대량으로 빠르게 크롤링하고 싶다 | requests + BeautifulSoup |
| 사용자처럼 직접 버튼 누르고 데이터를 얻어야 한다 | selenium |
from selenium import webdriver
from selenium.webdriver.common.by import By
import requests
from bs4 import BeautifulSoup
from tqdm import tqdm
import time
import random
import pyautogui
import pyperclip
import pandas as pd
import numpy as np
import re
driver = webdriver.Chrome()
# 크롤링
product_name_list = []
product_price_list = []
score_list = []
count_list = []
for i in tqdm(range(1, 10)):
sleep_time = random.uniform(3,5) # 3~5
time.sleep(sleep_time) # 3~5초 동안 쉼
page = 'https://emart.ssg.com/disp/category.ssg?dispCtgId=6000213534&page=' + str(i)
url = requests.get(page)
html = BeautifulSoup(url.text)
# '<Response [429]>'
if int(str(url).split(' ')[1][1:4]) >= 400: # 블락된 경우, 크롬 드라이버로 html 정보 받아옴
driver.get(page)
driver.implicitly_wait(10)
scr = driver.page_source
html = BeautifulSoup(scr)
# 상품명
product_name = html.find_all('div', attrs = {'class' : 'mnemitem_tit'})
for x in product_name:
x1 = x.get_text()
product_name_list.append(x1)
# 상품 가격
product_price = html.find_all('div', attrs = {'class' : 'new_price'})
for x in product_price:
x1 = x.get_text()
product_price_list.append(x1)
# 리뷰가 없는 상품 체크
products = html.find_all('li', attrs = {'class' : 'mnemitem_grid_item'})
for y in range(len(products)):
check = products[y].find_all('div', attrs = {'class' : 'mnemitem_review_info'})
if str(check) == '[]':
score_list.append(np.nan)
count_list.append(np.nan)
else: # 리뷰가 있는 경우
score = products[y].find_all('div', attrs = {'class':'mnemitem_review_info'})[0]\
.find_all('span', class_ = 'review_text')[0].get_text()
count = products[y].find_all('div', attrs = {'class':'mnemitem_review_info'})[0]\
.find_all('span', class_ = 'review_text')[1].get_text()
score_list.append(score)
count_list.append(count)
df1 = pd.DataFrame( {'상품명' : product_name_list, '상품가격' : product_price_list,
'리뷰점수' : score_list, '리뷰개수' : count_list})
df1.head()
# 상품명
def func1(x):
return x.replace('\n', '')
df1['상품명'] = df1['상품명'].apply(func1)
# 상품가격
def func2(x):
x1 = x.split('\n')
return int(x1[2].replace(',', '')[:-1])
df1['상품가격'] = df1['상품가격'].apply(func2)
df1.head()
# 리뷰점수
def func3(x):
return float(x)
df1['리뷰점수'] = df1['리뷰점수'].apply(func3)
# 리뷰 개수
def func4(x):
if pd.isna(x):
return np.nan
else:
pattern = r'\d+'
x1 = re.findall(pattern, str(x))[0]
return int(x1)
df1['리뷰개수'] = df1['리뷰개수'].apply(func4)
# 상품명을 기준으로 중복 제거
df2 = df1.drop_duplicates(subset = '상품명')
cond1 = df1['상품명'] == '동원참치 스프레드 체다치즈 100g*4'
df1.loc[cond1]
df2.to_csv('emart_product.csv')
# 두 개의 항목을 기준으로 정렬 -> 리스트 형식 활용
df2.sort_values(by = ['리뷰개수', '리뷰점수'],
ascending = [False, False]).head(50)
import matplotlib as mpl
import matplotlib.pyplot as plt
import seaborn as sns
import plotly.express as px
mpl.rc('font', family = 'Malgun Gothic')
# 전체 범위 시각화 (왼)
sns.histplot(df2, x = '상품가격')
# 데이터가 많이 분포하는 (0~25000) 부분만 시각화 (오)
plt.xlim([0,25000])
sns.histplot(df2, x='상품가격')
![]() | ![]() |
|---|
# 가장 높은 제품
df2.sort_values(by = '상품가격', ascending = False).head(1)
# 가장 낮은 제품
df2.sort_values(by = '상품가격', ascending = False).tail(1)
# 브랜드 구분
def func5(x):
x1 = x.split(' ')[0]
return x1
df2['브랜드'] = df2['상품명'].apply(func5)
# 확인할 브랜드 범위를 선정하여 재분류
br_list = ['동원', '덴마크', '그레이니', '매일', '죽염', '빙그레', '노브랜드',
'피코크', '서울우유', '남양']
def func6(x):
for br in br_list:
if br in x:
return br
else:
return '기타'
df2['브랜드2'] = df2['상품명'].apply(func6)
# 판매 제품이 가장 많은 브랜드 확인
df2['브랜드2'].value_counts()
df2.pivot_table(index = '브랜드2', values = '리뷰점수',
aggfunc = ['mean', 'max', 'min'])
df2.pivot_table(index = '브랜드2', values = '리뷰개수', aggfunc = 'sum')\
.sort_values(by = '리뷰개수', ascending = False)
text1 = 'Please contact us at contact@example.com.'
pattern = r'[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}'
# A-Z a-z 0-9 : 대/소문자 알파벳, 숫자
# ._%+- : 해당 특수문자가 들어갈 수 있다
# + : 앞의 [ ]가 연속됨
# {n, } : n번 이상 반복
# {n} : 정확히 n회 반복
# {n, m} : 최소 n번, 최대 m번 반복
re.findall(pattern, text1)
text9 = "Visit our website at http://example.com."
pattern = r'(?:https?://)\S+'
# \S+ : 공백이 아닌 문자 하나 이상이 연속됨
# (? .... ) : 논캡쳐 그룹 : 전체 링크가 필요한 것이기 때문에,
# http는 참조하지 않음
# http 뒤의 s? : s 가 있을 수도, 없을 수도 있다
re.findall(pattern, text9)[0]
text10 = "Our phone number is 123-456-7890."
pattern = r'\d{3}-\d{3}-\d{4}'
# pattern = r'\d+-\d+-\d+'
# \d : 숫자
# {n} : 정확히 n번 반복
re.findall(pattern, text10)
text11 = "London is the capital of England."
pattern = r'[A-Z][a-z]*'
# * 바로 앞의 패턴이 0번 이상 반복됨
# 대문자는 반드시 등장하되, 소문자는 없어도 됨
re.findall(pattern, text11)
# ['London', 'England']
text11 = "London is the capital of England. HAHAHAHA"
re.findall(pattern, text11)
# ['London', 'England', 'H', 'A', 'H', 'A', 'H', 'A', 'H', 'A']
text12 = "The event is scheduled on 2024-01-25."
pattern = r'\d{4}-\d{2}-\d{2}'
re.findall(pattern, text12)
# ['2024-01-25']
text13 = "The meeting starts at 14:30 and 17:10."
pattern = r'\d{2}:\d{2}'
re.findall(pattern, text13)
# ['14:30', '17:10']
text14 = "<title>Example Domain</title>"
pattern = r'<.*?>'
# . : 사용되는 모든 문자열
# * : 0 회 이상 반복
# ? : 있을수도 없을수도 있다
re.sub(pattern, '', text14)
# 'Example Domain'
예제 문자열: "Ping this IP 192.168.1.1"
text15 = "Ping this IP 192.168.1.1"
pattern = r'(?:\d{1,3}\.){3}\d{1,3}'
# (?: ... ) : 논캡쳐 그룹
# d{1,3} : 숫자가 1~3자리수다
# {3} : 3번 반복
# \. : .
re.findall(pattern, text15)
# ['192.168.1.1']
text16 = "I Love Python."
change = 'You'
re.sub(r'Python', change, text16)
def func17(email):
pattern = r'[A-Za-z0-9._%-+]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}'
if re.match(pattern, email): # match되는게 없으면 None -> False
return True
else:
return False
func17('korea123@123.3') # False
func17('korea123@gmail.com') # True