이전 글을 먼저 읽고 이 글을 읽으시면 이해하는데 도움이 될 것 같습니다.
파이썬 크롤링 기법 선택 방법 - 셀레니움(Selenium), 스크래피(Scrapy), beautiful soup, lxml
Intro오늘은 업무 상 크롤링을 할 일이 있어서 파이썬으로 크롤링할 수 있는 대다수 기법을 활용한 테스트 결과로현명하게 파이썬 크롤링 기법을 선택하는 방법에 대해 글을 작성하고자 합니다.
rnasterofmysea.tistory.com
Intro
🔹요약
1. User agent 와 Sleep(시간차) 깔고가자.
2. IP를 우회 해야한다.(Proxy or VPN)
3. 무료 Proxy 사용시, Beautiful Soup 등 정적 크롤링은 가능하다,
셀레니움(Selenium)은 안된다.
4. 셀레니움의 최적의 방안 = 구글 Colab 또는 VPN 또는 유로 Proxy
크롤링 프로젝트를 하고 있는데 인터넷에 있는 한국어로 된 정보들의 대다수가 단순히 크롤링 실행만 실행하는 정도인 것 같아
직접글을 쓰게 되었습니다.
간단히 크로링을 맛보는 게 아니라
프로젝트를 한다던가 구조가 복잡한 프록시를 하려면
IP 차단을 방지하는 대책을 고려해야합니다.
크롤링하려는 웹 페이지 서버에 부하는 준다던가, 메크로가 돌아간다고 판단이 되면
사용하는 IP가 차단이 될 수 있습니다. 이땐 돌이킬 수 없겠죠.
여러 도메인을 돌면서 1회씩 크로링한다고 하면 사실 차단당하지 않겠지만
대다수의 크롤링 요구사항이 한 메인페이지에서 세부페이지 들어가고 또 세부페이지 들어가고 이럴 겁니다.
타고타고 내려가는 크롤링은 결국 레벨(뎁스)가 깊어지게 됩니다.
그럼 한 웹서버에 계속 부하는 준다는 것을 뜻하는데,
사람(user)처럼 보이기 위해 user-agent를 돌려서 쓴다던가, Time sleep을 랜덤으로 넣어는 방법도 있지만
이것은 한계가 있습니다. 결국 차단당합니다.
근본적으로 IP가 차단 되는 것을 방지할 수 없습니다.
그러니까 차단되도 되는 IP를 쓰자는 겁니다.
1. User agent 와 Sleep(시간차) 깔고가자.
User agent 와 Time sleep은 근본적인 해결책이 아니더라도 IP 차단되는 것을 늦춰주는 역할을합니다.
🔹 Time Sleep(시간차)
특히 Time Sleep은 중요합니다.
시간차 없이 같은 도메인을 1000페이지를 방문 한다고 했을 때, 속도가 빠른 Beutifulsoup이나 lxml 같은 경우 1초에 몇 번씩 HTML을 파싱하겠죠. 그럼 당연히 차단될 확률이 높습니다, 동적 크롤링(셀레니움)을 쓴다고 해도 피차 마찬가지입니다.
그래서 랜덤하게 시간차를 주어서 사람이 접속하는 척 해주는 겁니다.
아래는 셀레니움과 BeutifulSoup 에서 랜덤 Sleep을 준 예시 입니다.
✅ BeautifulSoup 예시
import requests
from bs4 import BeautifulSoup
import time
import random
urls = ["https://example.com/page1", "https://example.com/page2"]
for url in urls:
response = requests.get(url)
soup = BeautifulSoup(response.text, "html.parser")
print(soup.title.text)
sleep_time = random.uniform(2, 5) # 2~5초 사이 랜덤 대기
print(f"{sleep_time:.2f}초 동안 대기합니다.")
time.sleep(sleep_time)
✅ 셀레니움 예시
from selenium import webdriver
import time
import random
driver = webdriver.Chrome()
urls = ["https://example.com/page1", "https://example.com/page2"]
for url in urls:
driver.get(url)
print(driver.title)
sleep_time = random.uniform(2, 5) # 2~5초 사이 랜덤 대기
print(f"{sleep_time:.2f}초 동안 대기합니다.")
time.sleep(sleep_time)
driver.quit()
🔹User-Agent란?
User-Agent는 웹 브라우저가 HTTP 요청을 보낼 때 사용하는 헤더(header)로, 서버에게 "나는 이런 브라우저, 이런 운영체제를 사용하는 클라이언트입니다" 라는 정보를 알려주는 역할을 합니다.
- 기본적으로 제공되는 User-Agent가 없거나 비정상적일 경우, 웹사이트가 크롤링을 감지하여 IP를 차단할 가능성이 높습니다.
- 정상적인 브라우저처럼 User-Agent를 설정하면 자연스러운 사용자 요청처럼 보이게 되어 차단을 피할 수 있습니다.
일반적인 User-Agent 형식의 예시는 다음과 같습니다:
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36
이 예시가 뜻하는 바는 다음과 같습니다:
- 브라우저 종류: Chrome
- 브라우저 엔진: AppleWebKit (기반)
- 운영체제: Windows 10 (64비트)
✅ BeautifulSoup 예시 (Time Sleep + User Agent)
import requests
from bs4 import BeautifulSoup
import time
import random
urls = ["https://example.com/page1", "https://example.com/page2"]
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"
}
for url in urls:
response = requests.get(url, headers=headers)
soup = BeautifulSoup(response.text, "html.parser")
print(soup.title.text)
sleep_time = random.uniform(2, 5) # 2~5초 사이 랜덤 대기
print(f"{sleep_time:.2f}초 동안 대기합니다.")
time.sleep(sleep_time)
✅ 셀레니움 예시 (Time Sleep + User Agent)
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
import time
import random
options = Options()
options.add_argument("user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36")
driver = webdriver.Chrome(options=options)
urls = ["https://example.com/page1", "https://example.com/page2"]
for url in urls:
driver.get(url)
print(driver.title)
sleep_time = random.uniform(2, 5) # 2~5초 사이 랜덤 대기
print(f"{sleep_time:.2f}초 동안 대기합니다.")
time.sleep(sleep_time)
driver.quit()
한 가지 User-Agent를 고정해서 쓰는 것보다는 여러 User-Agent를 랜덤으로 번갈아가며 사용하는 방식이 웹 크롤링이나 스크레이핑에 더 효과적입니다. 매 요청마다 동일한 User-Agent로 접근하면, 웹사이트에서 봇 또는 크롤러로 판단하기 쉽습니다. 여러 User-Agent를 번갈아 쓰면 요청이 더 자연스럽게 보이며, 탐지 가능성을 낮출 수 있습니다.
몇 십번에서 몇 백번 하고 말꺼다, 이럼 상관없을 듯 하지만 IP 우회없이 본인 IP로 진행한다면 최대한 IP 차단을 방지해야겠죠.
이제 부터 Selenium만 기준으로 예를 들겠습니다.(결국 똑같아요)
✅ 셀레니움 예시 (Time Sleep + User Agent)
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
import random
import time
user_agents = [
# Chrome
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
# Firefox
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:120.0) Gecko/20100101 Firefox/120.0",
# Edge
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0",
# Safari (Mac)
"Mozilla/5.0 (Macintosh; Intel Mac OS X 13_3) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Safari/605.1.15",
]
urls = ["https://example.com/page1", "https://example.com/page2"]
for url in urls:
options = Options()
ua = random.choice(user_agents)
options.add_argument(f"user-agent={ua}")
driver = webdriver.Chrome(options=options)
driver.get(url)
print(driver.title)
sleep_time = random.uniform(2, 5)
print(f"{sleep_time:.2f}초 동안 대기합니다.")
time.sleep(sleep_time)
driver.quit()
2. IP 우회하기 (Proxy)
근본적으로 IP가 차단 되는 것을 방지할 수 없습니다.
그러니까 차단되도 되는 IP를 쓰자는 겁니다.
그럼 내가 프로그램을 실행시킬껀데 내 IP를 안쓰는 방법이 무엇인지 생각해봅시다.
가장 보편적인 방법은 Proxy나 VPN을 사용하는 겁니다.
유로 Proxy 사용하면 best이고 이 경우에는 사실 user agent고 timeout이고 뭐고 그냥 IP 바꾸면 되면 장땡이니까 뭐 이 글이 사실 필요가 없습니다. 근데 유로 Proxy 쓸 상황이 안된다? 그럼 무료 Proxy 인데
무료 프록시를 제공해주는 사이트는 많습니다,
하지만 그 중에 제 구실을 하는 IP는 적습니다 + 매우 불안정합니다 (심각한 이슈)
정적 크롤링을 할꺼면 상관없습니다. 되는 IP를 찾으면서 돌면 되니까요, 문제는 동적 크롤링입니다.
구글에 검색하면 여러 사이트가 나오는데, 예시로 아래 사이트를 보자면
https://fineproxy.org/ko/free-proxies/asia/south-korea/
무료 한국 IP 프록시 목록 - 매시간 업데이트됨
한국 무료 프록시 목록의 이점을 살펴보고, 보안, 신뢰성 및 FineProxy의 유료 프록시가 탁월한 이점을 제공하는 이유에 대해 알아보세요.
fineproxy.org
IP는 많이 제공해주는데 저기서 되는 Proxy IP를 찾아야합니다.
정적 크롤링(Beautiful soup) 일 경우 웹에 접속에서 HTML만 갖고 올 수 있으면 되니까, 파이썬 코드 작성해서 무료 Proxy 사이트를 크롤링해서 IP 가져온 다음에 크롤링 하려는 웹사이트와 연결이 되는지만 확인하면 됩니다.
아래는 제가 작성한 코드입니다. 그대로 가져다 써도 IP는 잘 찾아줍니다.
get_proxies 는 ip 리스트를 전체 돌려서 여러개 반환해주는 코드이고
get_proxy는 ip 리스트에서 연결되는 ip 하나를 즉시 반환해주는 코드입니다.
테스트 url이나 time sleep 조건은 입맛에 맞게 바꾸면 됩니다.
import requests
from bs4 import BeautifulSoup
def get_proxies():
url = "https://www.sslproxies.org/"
try:
response = requests.get(url, timeout=5)
response.raise_for_status()
except requests.RequestException as e:
print(f"Error fetching proxy list: {e}")
return []
soup = BeautifulSoup(response.text, 'html.parser')
table = soup.find('table', {'class': 'table table-striped table-bordered'})
if not table:
print("Failed to find proxy table.")
return []
rows = table.find_all('tr')
proxies = []
for row in rows[1:]:
cols = row.find_all('td')
if len(cols) >= 2:
ip = cols[0].text.strip()
port = cols[1].text.strip()
proxies.append(f"http://{ip}:{port}")
return proxies
def get_proxy(test_url="https://example.com"):
proxies = get_proxies()
print(f"{len(proxies)}개의 프록시 중에서 연결 가능한 프록시를 찾는 중입니다...")
for proxy in proxies:
try:
print(f"테스트 중: {proxy}")
response = requests.get(test_url, proxies={"http": proxy, "https": proxy}, timeout=5)
if response.status_code == 200:
print(f"✅ 연결 성공: {proxy}")
return proxy # 바로 반환
except requests.RequestException:
print(f"❌ 연결 실패: {proxy}")
continue
print("연결 가능한 프록시를 찾지 못했습니다.")
return None # 연결 가능한 프록시가 없는 경우
# ✅ 실행 예제
if __name__ == "__main__":
working_proxy = get_proxy()
print("\n연결된 프록시:", working_proxy)
다시 말하지만,
Beautiful Soup 일때는 문제가 없지만 동적 크롤링(셀레니움 등등) 에서는 무료 Proxy를 사용하기 힘듭니다.
그 이유는 Web driver에 있습니다.
정적 크롤링인 경우 페이지 리소스를 로딩하지 않기 때문에 web driver가 없지만, Selenium은 브라우저와 직접 소통하기 위해 WebDriver가 별도의 프로세스로 실행됩니다. 프록시 접속 실패 시 브라우저 세션이 끊기거나 응답이 멈춰버리면, WebDriver는 정상적으로 세션을 닫지 못하고 프로세스만 떠 있는 상태가 됩니다. 이렇게 되면 Python 코드에서는 WebDriver 종료를 호출했음에도 내부적으로는 프로세스가 종료되지 않는 상태로 남아있게 됩니다. 이러한 프로세스가 쌓이면 시스템 자원이 고갈되어 프로그램이 강제 종료됩니다.
프록시 연결이 끊기면 기존 driver 를 죽이고 새로 생성하면 되지 않을까 싶어도 프로세스 간의 충돌은 피할 수 없습니다.
단순히 driver.quit(), create driver 로 동작 시키면 안된다는 말입니다.
하단은 Web driver 프로세스 충돌시 발생하는 에러 코드입니다.
Stacktrace:
GetHandleVerifier [0x00007FF6A6C5FE45+26629]
(No symbol) [0x00007FF6A6BC6010]
(No symbol) [0x00007FF6A6A5931A]
(No symbol) [0x00007FF6A6AAF8E7]
(No symbol) [0x00007FF6A6AAFB1C]
(No symbol) [0x00007FF6A6B034A7]
(No symbol) [0x00007FF6A6AD7AEF]
(No symbol) [0x00007FF6A6B00169]
(No symbol) [0x00007FF6A6AD7883]
(No symbol) [0x00007FF6A6AA0550]
(No symbol) [0x00007FF6A6AA1803]
GetHandleVerifier [0x00007FF6A6FB72BD+3529853]
GetHandleVerifier [0x00007FF6A6FCDA22+3621858]
GetHandleVerifier [0x00007FF6A6FC24D3+3575443]
GetHandleVerifier [0x00007FF6A6D2B77A+860474]
(No symbol) [0x00007FF6A6BD088F]
(No symbol) [0x00007FF6A6BCCBC4]
(No symbol) [0x00007FF6A6BCCD66]
(No symbol) [0x00007FF6A6BBC2C9]
BaseThreadInitThunk [0x00007FFACC8DE8D7+23]
RtlUserThreadStart [0x00007FFACE5DBF6C+44]
그렇기 때문에 Proxy 연결이 끊기면 작동 중인 Driver의 프로세스를 죽이고 재생성하는 로직이 들어가야합니다.
import time
import psutil
from selenium import webdriver
from selenium.common.exceptions import WebDriverException
# 드라이버 옵션 설정 함수
def get_driver():
options = webdriver.ChromeOptions()
options.add_argument('--no-sandbox')
options.add_argument('--disable-dev-shm-usage')
options.add_argument('--disable-gpu')
options.add_argument('--headless') # 필요시 headless 제거
return webdriver.Chrome(options=options)
# 크롬/드라이버 강제 종료 함수
def kill_driver_processes():
for proc in psutil.process_iter(['pid', 'name']):
try:
name = proc.info['name']
if name and ('chrome' in name.lower() or 'chromedriver' in name.lower()):
proc.kill()
except (psutil.NoSuchProcess, psutil.AccessDenied):
continue
# 드라이버로 접속하는 함수 (오류시 재시도)
def visit_with_restart(url, max_retries=3):
for attempt in range(max_retries):
try:
print(f"[{attempt+1}차 시도] 드라이버 실행 중...")
driver = get_driver()
driver.set_page_load_timeout(10) # 페이지 로딩 제한
driver.get(url)
print(" 페이지 접속 성공")
driver.quit()
return
except WebDriverException as e:
print(f"오류 발생: {e}")
print(" 드라이버 종료 후 재시도 중...")
try:
driver.quit()
except:
pass
kill_driver_processes()
time.sleep(2)
print("최대 재시도 횟수 초과. 작업 종료.")
# 사용 예시
if __name__ == "__main__":
target_url = "https://example.com"
visit_with_restart(target_url)
하지만 이렇게 하더라도 문제는 불안한 Proxy IP에 드라이버를 죽였다살렸다가 하는게 맞을까요...?
되는 Proxy IP 찾는데도 테이블 뺑뻉이 돌리면서 찾아야하고(심지어 없을 수도 있음)
찾고 난뒤에 크롤링을 하려고하니까 하기도 전에 또 끊겨서 다시 Proxy 찾고 이걸 N번째 반복한다고 생각해보면 ㅎㅎ...
그러다 프로세스 뻑나면 강제종료 되고 ㅎㅎ...
그래서 제 결론은 가능이야 하지만 시간적으로나 기능적으로나 하자가 너무 많습니다.
"셀레니움"에서 무료 프록시를 사용할 수 없다 "입니다. (가능이야 하지만)
스크래피(Scrapy) 일 경우 가능은 합니다.
알아서 로드밸런싱을 해주기 때문에 Proxy IP가 죽어도 알아서 되는 IP 찾아다가 크롤링 해줍니다.
하지만..... 시간이 매우 오래걸린다는 문제는 동일하며
이를 해결하기 위해 멀티프로세싱을 돌려야하는 상황이 나옵니다, 실제로 멀티프로세싱 구현은 스크래피에서 아주 간단합니다.
하지만 여기서 문제는 멀티프로세싱을 돌리다가 웹드라이버가 꺼져버리면 해당 라인은 데이터가 다 날라갑니다.
예를 들어 대분류에서 중분류, 중분류에서 페이지로 레벨을 내려가면서 멀티프로세싱 크롤링을 돌리다가
어떤 한페이지 진행 중 IP 연결이 끊겨 드라이버나 프로세스에 이상이 생기는 등 예상치 않게 종료되면 해당 부모 레벨까지의 크롤링 프로세스가 죽어버립니다.
여기서 저는 스크래피로 작업 시작하다가 결국 셀레니움으로 돌아가서 재개발했습니다....
더 좋은 방안이나 이 문제 해결방안 있으시면 말씀해주시면 감사하겠습니다.
3. IP 우회하기 (VPN)
VPN 일 경우 제 경험상 무료 Proxy 보다 훨씬 낫습니다.
프록시 설정 부분을 다 치우고 VPN 프로그램 키고 돌리면 됩니다.
로직상 필요한 것은 크롤링 진행 완료된 것을 로그로 남기던지 크롤링에서 TIME OUT 에러가 생기면 그 지점부터 다시 시작할 수 있게 종료지점을 기억하기만 하면 VPN 재실행 후 다시 그 지점부터 돌려주면 됩니다.
저는 이렇게 크롤링 작업(수천 페이지)를 완료했습니다.
4. Google Colab 이용하기
Colab 환경 자체가 Google 서버를 통한 가상환경이기 때문에 IP가 우리 것이 아니죠 ㅎㅎ..
그래서 colab에서 실행 시키면 본인 IP가 차단 당할 경우는 없습니다.
구글 계정 여러개 있으면 여러개 한번에 돌리면 수공 멀티 프로세싱(Multi Processing)까지 ㄷㄷ...
하지만 세션이 죽어버리면 처음부터 세팅을 다시해줘야한다는 치명적인 단점이 있습니다.
경험 상 몇 백회까지는 문제 없습니다. 그 이상을 테스트 안해봐서 확답이 어렵네요.
Colab은 리눅스 기반 가상 환경이기 때문에 Window와는 Web driver 설정이 조금 다릅니다.
이 부분은 차후에 다른 포스트에 자세한 내용 올리겠습니다.
5. 결론
무료 Proxy와 셀레니움을 같이 써보려고 많은 시도를 했으나 성공하질 못했습니다.
대안으로 VPN을 사용했고 Colab으로도 개발해본 결과 대용량 크롤링에서는 한계가 있습니다.
최적의 방안은 당연히 유료 Proxy 일 것 같고 VPN이 그다음입니다. 차악으로 Colab.
희생으로 만들어진 포스트
'Computer Science > Python' 카테고리의 다른 글
[Python] re 모듈: 정규 표현식 (0) | 2025.04.04 |
---|---|
파이썬 크롤링 기법 선택 방법 - 셀레니움(Selenium), 스크래피(Scrapy), beautiful soup, lxml (1) | 2025.03.22 |
[Python] 파이썬 독학 자료 정리 (1) | 2025.03.01 |
[Python] 코딩 테스트 필수 STL 1탄 (중요도와 사용 빈도 기준) (0) | 2025.02.06 |