[KT Cloud TechUp] 보안뉴스 크롤링 - requests 사용
October 22, 2025
지금까지는 selenium을 사용해서 크롤링을 진행했는데, 셀레니움보다 좀 더 빠른 방식인 requests를 사용하는 크롤링 실습을 진행한다.
def extract_title_from_html(html_content, idx):
try:
soup = BeautifulSoup(html_content, 'html.parser')
meta_title = soup.find('meta', attrs={'name': 'title'})
if meta_title and meta_title.get('content'):
title = meta_title.get('content').strip()
title = title.replace("'", "").replace('"', '')
if title:
return title
if soup.title:
title = soup.title.get_text().strip()
if " | " in title:
title = title.split(" | ")[0]
elif " - " in title:
title = title.split(" - ")[0]
return title
return f"제목 없음 - idx {idx}"
except Exception as e:
return f"제목 추출 오류 - idx {idx}: {e}"
제목을 추출해준다.
def initialize_csv_file(filename):
with open(filename, 'w', newline='', encoding='utf-8') as csvfile:
csv_writer = csv.writer(csvfile)
csv_writer.writerow(['idx', 'title'])
def save_to_csv(filename, idx, title):
with open(filename, 'a', newline='', encoding='utf-8') as csvfile:
csv_writer = csv.writer(csvfile)
csv_writer.writerow([idx, title])
csv 초기화 및 저장 함수.
def crawl_boannews_articles():
base_url = "https://www.boannews.com/media/view.asp"
csv_filename = "boannews_articles.csv"
initialize_csv_file(csv_filename)
success_count = 0
fail_count = 0
start_idx = 139800
end_idx = 139839
print(f"크롤링을 시작합니다: idx {start_idx} 부터 idx {end_idx} 까지")
print("-" * 50)
for i in range(start_idx, end_idx + 1):
try:
params = {'idx': i, 'page': 1, 'kind': 3}
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language': 'ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7',
'Accept-Encoding': 'gzip, deflate, br',
'Connection': 'keep-alive',
'Upgrade-Insecure-Requests': '1',
}
response = requests.get(base_url, params=params, headers=headers, timeout=5)
if response.status_code == 200:
success_count += 1
title = extract_title_from_html(response.text, i)
print(f"idx {i}: 성공 - {title}")
save_to_csv(csv_filename, i, title)
else:
fail_count += 1
print(f"idx {i}: 실패 - 상태 코드 {response.status_code}")
except requests.exceptions.Timeout:
print(f"idx {i}: 타임아웃 발생")
fail_count += 1
except ConnectionError:
print(f"idx {i}: 연결 오류 발생")
fail_count += 1
except requests.exceptions.RequestException as e:
print(f"idx {i}: 요청 예외 발생 - {e}")
fail_count += 1
except Exception as e:
fail_count += 1
print(f"idx {i}: 예상치 못한 오류 - {e}")
time.sleep(0.5)
if (i - start_idx + 1) % 10 == 0:
processed = i - start_idx + 1
total = end_idx - start_idx + 1
progress = (processed / total) * 100
print(f"\n📊 진행상황: {processed}/{total} ({progress:.1f}%) - 성공: {success_count}, 실패: {fail_count}\n")
print("-" * 50)
print(f"크롤링 완료!")
print(f"총 요청 수: {end_idx - start_idx + 1}")
print(f"성공: {success_count}")
print(f"실패: {fail_count}")
if (success_count + fail_count) > 0:
print(f"성공률: {(success_count / (success_count + fail_count)) * 100:.1f}%")
print(f"📁 최종 결과가 '{csv_filename}'에 저장되었습니다.")
이렇게 제목, idx 번호를 추출해서 csv 파일에 저장하는 코드를 작성했다.
잘 저장되는 것을 확인.
소요시간은 39초이다.
멀티스레딩(동시 처리)를 위해 threading을 도입할려고 한다.
with ThreadPoolExecutor(max_workers=5) as executor:
# 모든 작업을 큐에 제출
future_to_idx = {
executor.submit(crawl_single_article, idx, csv_filename): idx
for idx in idx_list #40개 작업을 큐에 넣음
}
작업 큐: [139800, 139801, 139802, …, 139839] (40개)
스레드1: 139800 처리 → 완료 → 139805 가져가서 처리 → …
스레드2: 139801 처리 → 완료 → 139806 가져가서 처리 → …
스레드3: 139802 처리 → 완료 → 139807 가져가서 처리 → …
스레드4: 139803 처리 → 완료 → 139808 가져가서 처리 → …
스레드5: 139804 처리 → 완료 → 139809 가져가서 처리 → …
이런 식으로 작동하는 Work Stealing / Producer-Consumer 동적 할당 패턴이다.
단일 스레드 (기존 코드) for i in range(40): requests.get(…) # 40번의 순차적 I/O 대기 // CPU 사용률: 5%, 대부분 BLOCKED
멀티 스레드 (현재 코드)
ThreadPoolExecutor(max_workers=5):
5개 스레드가 동시에 requests.get()
// CPU 사용률: 20-30%, 컨텍스트 스위칭 증가
// 스케줄러 부하: 증가하지만 전체 성능 향상
스레딩을 도입하자 소요시간이 6.5초로 엄청나게 줄어들었다.
여기까지는 수업에서 진행한 내용이다. 하지만 기사를 스크랩하는데 제목만으로 파악할 수 있는 내용에는 한계가 있다고 생각되어서, 크롤링 하는 김에 내용까지 긁어서 llm에 집어넣은 뒤 요약해서 csv에 같이 붙여주면 좋겠다는 생각이 들었다. 이걸 매일 업데이트되는 뉴스를 모아서 팀원들에게 디스코드로 보내주면 좋겠다는 생각이 들었지만, 현실적으로 openai api 비용을 감당하기 힘들 것 같아서 일단 테스트용으로만 만들어 보기로 했다.