웹 크롤링에 대해서
웹 크롤링이란?
웹 크롤링(Web Crawling)은 웹사이트에서 데이터를 자동으로 수집하는 과정이다. 쉽게 말해, 사람이 웹사이트를 하나씩 방문해서 정보를 복사하는 대신, 프로그램이 자동으로 여러 웹페이지를 방문하여 필요한 정보를 수집하는 것이다.
간단한 비유: 책을 읽으면서 중요한 내용을 노트에 적는 것처럼, 크롤러는 웹사이트를 읽으면서 필요한 데이터를 자동으로 수집한다.
크롤러(Crawler) 또는 스파이더(Spider)라고 불리는 프로그램이 웹 페이지를 방문하여 정보를 추출하고 저장한다. 웹 스크래핑(Web Scraping)과 유사하지만, 크롤링은 여러 페이지를 체계적으로 탐색하는 것을 의미하고, 스크래핑은 특정 페이지에서 데이터를 추출하는 것을 의미한다.
크롤링의 용도
검색 엔진
- Google, Naver 같은 검색 엔진이 웹 페이지를 인덱싱
- 웹사이트의 구조와 내용을 분석
데이터 수집
- 가격 비교 사이트
- 뉴스 기사 수집
- 소셜 미디어 모니터링
시장 조사
- 경쟁사 분석
- 트렌드 파악
- 고객 리뷰 수집
연구 및 분석
- 학술 연구 데이터 수집
- 통계 분석
- 머신러닝 데이터셋 구축
크롤링 vs 스크래핑
크롤링과 스크래핑은 비슷해 보이지만 차이가 있다.
크롤링: 여러 페이지를 자동으로 돌아다니며 데이터를 수집 (예: 검색 엔진이 모든 웹페이지를 탐색) 스크래핑: 특정 페이지에서만 데이터를 추출 (예: 쇼핑몰에서 상품 가격만 가져오기)
| 항목 | 크롤링 (Crawling) | 스크래핑 (Scraping) |
|---|---|---|
| 범위 | 여러 페이지를 체계적으로 탐색 | 특정 페이지에서 데이터 추출 |
| 목적 | 웹사이트 구조 파악, 인덱싱 | 특정 데이터 추출 |
| 예시 | 검색 엔진 봇 | 상품 가격 추출 |
| 복잡도 | 높음 (링크 추적, 중복 방지 등) | 낮음 (단일 페이지 처리) |
초보자 팁: 처음에는 스크래핑(한 페이지에서만 데이터 추출)부터 시작하는 것이 좋다.
크롤링 도구와 라이브러리
크롤링을 하려면 프로그래밍 언어와 도구가 필요하다. Java를 사용한다면 아래 도구들을 사용할 수 있다.
Java
Jsoup
HTML을 파싱하고 데이터를 추출하는 가장 간단한 방법이다. **파싱(Parsing)**이란 HTML 코드를 읽어서 원하는 부분을 찾는 것을 의미한다.
언제 사용하나?: 정적인 HTML 페이지에서 데이터를 추출할 때 사용한다.
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
public class JsoupExample {
public static void main(String[] args) {
try {
// 웹 페이지 연결 및 HTML 가져오기
String url = "https://example.com";
Document doc = Jsoup.connect(url).get();
// 첫 번째 요소 찾기
Element title = doc.selectFirst("h1");
if (title != null) {
System.out.println("제목: " + title.text());
}
// 여러 요소 찾기 (class가 "item"인 div)
Elements items = doc.select("div.item");
for (Element item : items) {
System.out.println("아이템: " + item.text());
}
} catch (Exception e) {
System.err.println("오류: " + e.getMessage());
}
}
}
HttpClient (Java 11+)
Java에 내장된 HTTP 클라이언트로 별도 라이브러리 없이 사용할 수 있다. HTTP 클라이언트는 웹 서버에 요청을 보내고 응답을 받는 도구다.
언제 사용하나?: 웹페이지의 HTML을 가져올 때 사용한다. Jsoup과 함께 사용하면 완벽하다.
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;
public class HttpClientExample {
public static void main(String[] args) {
try {
// HTTP 클라이언트 생성
HttpClient client = HttpClient.newHttpClient();
// 요청 생성
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://example.com"))
.build();
// 요청 보내기 및 응답 받기
HttpResponse<String> response = client.send(
request,
HttpResponse.BodyHandlers.ofString()
);
// 응답 확인
System.out.println("상태 코드: " + response.statusCode());
System.out.println("HTML 길이: " + response.body().length() + " 문자");
} catch (Exception e) {
System.err.println("오류: " + e.getMessage());
}
}
}
Selenium
실제 브라우저를 자동으로 제어하여 JavaScript로 생성되는 동적 콘텐츠도 크롤링할 수 있다.
동적 콘텐츠란?: 페이지를 열었을 때 JavaScript가 실행되면서 나중에 나타나는 내용을 말한다. 예를 들어, 스크롤을 내려야 보이는 상품 목록 같은 것들이다.
언제 사용하나?:
- JavaScript로 데이터가 로드되는 페이지
- 버튼을 클릭해야 내용이 나타나는 페이지
- 스크롤해야 더 많은 데이터가 로드되는 페이지
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.By;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.openqa.selenium.support.ui.ExpectedConditions;
import java.time.Duration;
public class SeleniumExample {
public static void main(String[] args) {
WebDriver driver = null;
try {
// Chrome 브라우저 실행
driver = new ChromeDriver();
driver.get("https://example.com");
// 동적 콘텐츠 대기 (최대 10초)
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
wait.until(ExpectedConditions.presenceOfElementLocated(By.id("content")));
// 데이터 추출
WebElement title = driver.findElement(By.cssSelector("h1"));
System.out.println("제목: " + title.getText());
} catch (Exception e) {
System.err.println("오류: " + e.getMessage());
} finally {
if (driver != null) {
driver.quit(); // 브라우저 종료
}
}
}
}
JavaScript/Node.js
Puppeteer
const puppeteer = require("puppeteer");
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto("https://example.com");
// 데이터 추출
const title = await page.$eval("h1", (el) => el.textContent);
console.log(title);
// 스크린샷
await page.screenshot({ path: "screenshot.png" });
await browser.close();
})();
Cheerio
const cheerio = require("cheerio");
const axios = require("axios");
async function scrape() {
const response = await axios.get("https://example.com");
const $ = cheerio.load(response.data);
// jQuery와 유사한 문법
const title = $("h1").text();
const items = $(".item")
.map((i, el) => $(el).text())
.get();
console.log(title, items);
}
기타 도구
- HtmlUnit: Java 기반 헤드리스 브라우저
- OkHttp: HTTP 클라이언트 라이브러리
- WebDriver: Selenium WebDriver Java 바인딩
- Apache HttpClient: HTTP 통신 라이브러리
크롤링 기본 프로세스
크롤링은 크게 4단계로 이루어진다. 각 단계를 이해하면 크롤링의 전체 흐름을 파악할 수 있다.
1. 요청 전송
웹 페이지에 HTTP 요청을 보내서 HTML을 받아온다.
이해하기: 브라우저 주소창에 URL을 입력하고 엔터를 누르는 것과 같다. 프로그램이 대신 웹서버에 "이 페이지 주세요"라고 요청하는 것이다.
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;
public class RequestExample {
public static void main(String[] args) {
try {
// HTTP 클라이언트 생성
HttpClient client = HttpClient.newBuilder().build();
// 요청 생성 (User-Agent 설정으로 크롤러 차단 방지)
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://example.com"))
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
.build();
// 요청 보내기
HttpResponse<String> response = client.send(
request,
HttpResponse.BodyHandlers.ofString()
);
// 응답 확인
System.out.println("상태 코드: " + response.statusCode());
if (response.statusCode() == 200) {
System.out.println("HTML 길이: " + response.body().length() + " 문자");
}
} catch (Exception e) {
System.err.println("오류: " + e.getMessage());
}
}
}
2. HTML 파싱
HTML 문자열을 Jsoup으로 파싱하여 원하는 데이터를 찾을 수 있게 한다.
이해하기: 받아온 HTML은 그냥 긴 문자열이다. 파싱은 이 문자열을 분석해서 "여기가 제목이고, 여기가 본문이구나"라고 이해할 수 있게 만드는 과정이다.
CSS 선택자: HTML에서 원하는 요소를 찾기 위해 사용한다.
"h1"→ 모든 h1 태그"div.item"→ class가 "item"인 div 태그"#id"→ id가 "id"인 요소
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
public class ParsingExample {
public static void main(String[] args) {
try {
// HTML 문자열 (실제로는 response.body() 사용)
String html = "<html><body>" +
"<h1 class='title'>웹 크롤링 예제</h1>" +
"<div class='item'>아이템 1</div>" +
"<div class='item'>아이템 2</div>" +
"</body></html>";
// HTML 파싱
Document doc = Jsoup.parse(html);
// 첫 번째 요소 찾기 (class가 "title"인 h1)
Element title = doc.selectFirst("h1.title");
if (title != null) {
System.out.println("제목: " + title.text());
}
// 여러 요소 찾기 (class가 "item"인 div)
Elements items = doc.select("div.item");
for (Element item : items) {
System.out.println("아이템: " + item.text());
}
} catch (Exception e) {
System.err.println("오류: " + e.getMessage());
}
}
}
3. 데이터 추출
HTML에서 필요한 정보를 추출하여 구조화된 데이터로 만든다.
이해하기: 파싱한 HTML에서 "이 부분은 제목, 이 부분은 가격"처럼 필요한 정보만 골라내는 것이다. 추출한 데이터는 Map이나 List 같은 자료구조에 저장한다.
import java.util.ArrayList;
import java.util.List;
import java.util.HashMap;
import java.util.Map;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
public class DataExtractionExample {
public static void main(String[] args) {
try {
// 실제로는 doc.select("div.product-item") 등으로 가져옴
Elements items = null;
// 데이터 저장할 리스트
List<Map<String, String>> data = new ArrayList<>();
// 각 요소에서 정보 추출
for (Element item : items) {
// 제목, 가격, 링크 추출
Element titleElement = item.selectFirst("h2");
String title = (titleElement != null) ? titleElement.text().trim() : "";
Element priceElement = item.selectFirst("span.price");
String price = (priceElement != null) ? priceElement.text().trim() : "";
Element linkElement = item.selectFirst("a");
String link = (linkElement != null) ? linkElement.attr("href") : "";
// Map에 저장
Map<String, String> itemData = new HashMap<>();
itemData.put("title", title);
itemData.put("price", price);
itemData.put("link", link);
data.add(itemData);
}
// 결과 출력
System.out.println("추출 완료: " + data.size() + "개");
} catch (Exception e) {
System.err.println("오류: " + e.getMessage());
}
}
}
4. 데이터 저장
추출한 데이터를 JSON 또는 CSV 파일로 저장한다.
이해하기: 수집한 데이터를 파일로 저장하면 나중에 다시 사용하거나 분석할 수 있다.
JSON: 프로그래밍에서 많이 사용하는 데이터 형식 CSV: 엑셀에서 열 수 있는 쉼표로 구분된 형식
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import java.io.File;
import java.io.FileWriter;
import java.util.List;
import java.util.Map;
public class DataSavingExample {
public static void main(String[] args) {
List<Map<String, String>> data = createSampleData();
// JSON으로 저장
saveAsJson(data);
// CSV로 저장
saveAsCsv(data);
}
// JSON 저장
public static void saveAsJson(List<Map<String, String>> data) {
try {
ObjectMapper mapper = new ObjectMapper();
mapper.enable(SerializationFeature.INDENT_OUTPUT);
mapper.writeValue(new File("data.json"), data);
System.out.println("JSON 저장 완료");
} catch (Exception e) {
System.err.println("오류: " + e.getMessage());
}
}
// CSV 저장
public static void saveAsCsv(List<Map<String, String>> data) {
try (FileWriter writer = new FileWriter("data.csv")) {
writer.append("title,price,link\n");
for (Map<String, String> item : data) {
writer.append(item.get("title")).append(",")
.append(item.get("price")).append(",")
.append(item.get("link")).append("\n");
}
System.out.println("CSV 저장 완료");
} catch (Exception e) {
System.err.println("오류: " + e.getMessage());
}
}
private static List<Map<String, String>> createSampleData() {
List<Map<String, String>> data = new java.util.ArrayList<>();
Map<String, String> item = new java.util.HashMap<>();
item.put("title", "노트북");
item.put("price", "1,000,000원");
item.put("link", "https://example.com/laptop");
data.add(item);
return data;
}
}
고급 크롤링 기법
기본적인 크롤링을 넘어서 더 복잡한 상황을 다루는 방법들이다.
동적 콘텐츠 처리
일부 웹사이트는 페이지를 열었을 때 바로 모든 내용이 나타나지 않는다. 스크롤을 내리거나 버튼을 클릭해야 더 많은 내용이 보인다. 이런 경우 Selenium을 사용해야 한다.
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.By;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.openqa.selenium.support.ui.ExpectedConditions;
WebDriver driver = new ChromeDriver();
driver.get("https://example.com");
// JavaScript 실행 대기
WebDriverWait wait = new WebDriverWait(driver, 10);
WebElement element = wait.until(
ExpectedConditions.presenceOfElementLocated(By.id("dynamic-content"))
);
// 스크롤하여 더 많은 콘텐츠 로드
JavascriptExecutor js = (JavascriptExecutor) driver;
js.executeScript("window.scrollTo(0, document.body.scrollHeight);");
세션 관리 및 쿠키
로그인이 필요한 사이트를 크롤링할 때는 쿠키와 세션을 관리해야 한다.
쿠키란?: 웹사이트가 사용자를 기억하기 위해 저장하는 작은 정보 파일이다. 로그인 상태를 유지하는 데 사용된다.
세션이란?: 서버가 사용자의 상태를 기억하는 것이다. 로그인 후 쿠키를 통해 세션이 유지된다.
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;
import java.net.http.HttpClient.Builder;
import java.net.CookieManager;
import java.net.CookieHandler;
// CookieManager로 쿠키 관리
CookieManager cookieManager = new CookieManager();
CookieHandler.setDefault(cookieManager);
HttpClient client = HttpClient.newBuilder()
.cookieHandler(cookieManager)
.build();
// 로그인
String loginData = "username=user&password=pass";
HttpRequest loginRequest = HttpRequest.newBuilder()
.uri(URI.create("https://example.com/login"))
.header("Content-Type", "application/x-www-form-urlencoded")
.POST(HttpRequest.BodyPublishers.ofString(loginData))
.build();
client.send(loginRequest, HttpResponse.BodyHandlers.ofString());
// 쿠키가 자동으로 유지됨
HttpRequest dashboardRequest = HttpRequest.newBuilder()
.uri(URI.create("https://example.com/dashboard"))
.build();
HttpResponse<String> response = client.send(
dashboardRequest,
HttpResponse.BodyHandlers.ofString()
);
요청 간격 조절
서버 부담을 줄이기 위해 요청 사이에 간격을 둔다.
왜 필요한가?:
- 너무 빠르게 요청하면 서버에 부담이 간다
- 서버가 크롤러를 차단할 수 있다
- 윤리적인 크롤링을 위해 필요하다
권장 사항: 요청 사이에 1-3초 정도 대기하는 것이 좋다.
import java.util.List;
import java.util.Random;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;
public class CrawlWithDelayExample {
public void crawlWithDelay(List<String> urls) throws InterruptedException {
Random random = new Random();
HttpClient client = HttpClient.newHttpClient();
for (int i = 0; i < urls.size(); i++) {
String url = urls.get(i);
System.out.println("크롤링: " + url);
try {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.build();
HttpResponse<String> response = client.send(
request,
HttpResponse.BodyHandlers.ofString()
);
if (response.statusCode() == 200) {
System.out.println("성공: " + response.body().length() + " 문자");
}
} catch (Exception e) {
System.err.println("오류: " + e.getMessage());
}
// 마지막 URL이 아니면 1-3초 대기
if (i < urls.size() - 1) {
int delay = 1000 + random.nextInt(2000);
Thread.sleep(delay);
}
}
}
}
에러 처리 및 재시도
일시적인 오류에 대비해 재시도 로직을 구현한다.
왜 필요한가?:
- 네트워크가 일시적으로 불안정할 수 있다
- 서버가 일시적으로 바쁠 수 있다
- 이런 경우 다시 시도하면 성공할 수 있다
재시도 전략: 실패하면 잠시 기다렸다가 다시 시도한다. 시도 횟수가 늘어날수록 대기 시간도 늘린다(지수 백오프).
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;
import java.util.List;
public class RetryableHttpClient {
public HttpResponse<String> sendRequestWithRetry(String url, int maxRetries) {
// 재시도가 필요한 상태 코드
List<Integer> retryStatusCodes = List.of(429, 500, 502, 503, 504);
int attempt = 0;
while (attempt < maxRetries) {
try {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.timeout(java.time.Duration.ofSeconds(10))
.build();
HttpResponse<String> response = client.send(
request,
HttpResponse.BodyHandlers.ofString()
);
// 성공한 경우 응답 반환
if (!retryStatusCodes.contains(response.statusCode())) {
return response;
}
} catch (Exception e) {
System.err.println("오류: " + e.getMessage());
}
// 재시도 전 대기 (지수 백오프: 1초, 2초, 4초...)
attempt++;
if (attempt < maxRetries) {
int waitTime = (int) Math.pow(2, attempt - 1) * 1000;
try {
Thread.sleep(waitTime);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}
throw new RuntimeException("최대 재시도 횟수 초과");
}
}
robots.txt 준수
크롤링하기 전에 반드시 확인해야 하는 것이 robots.txt 파일이다.
robots.txt란?: 웹사이트 관리자가 크롤러에게 "이 경로는 크롤링하지 마세요"라고 알려주는 파일이다. 웹사이트 루트에 /robots.txt로 접근할 수 있다.
왜 중요한가?:
- 웹사이트의 규칙을 존중하는 것
- 법적 문제를 피하는 것
- 서버에 불필요한 부담을 주지 않는 것
robots.txt 확인
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import java.util.regex.Pattern;
import java.util.regex.Matcher;
public class RobotsTxtChecker {
public boolean canFetch(String userAgent, String url) {
try {
String robotsUrl = getBaseUrl(url) + "/robots.txt";
Document doc = Jsoup.connect(robotsUrl).get();
String robotsContent = doc.text();
// robots.txt 파싱 로직 구현
// 또는 crawler-commons 라이브러리 사용
return parseRobotsTxt(robotsContent, userAgent, url);
} catch (Exception e) {
// robots.txt가 없으면 기본적으로 허용
return true;
}
}
private String getBaseUrl(String url) {
// URL에서 기본 도메인 추출
Pattern pattern = Pattern.compile("https?://([^/]+)");
Matcher matcher = pattern.matcher(url);
if (matcher.find()) {
return "https://" + matcher.group(1);
}
return url;
}
}
// 사용 예시
RobotsTxtChecker checker = new RobotsTxtChecker();
boolean canFetch = checker.canFetch("MyBot", "https://example.com/page");
System.out.println(canFetch); // true or false
robots.txt 예시
User-agent: *
Disallow: /admin/
Disallow: /private/
Allow: /public/
Crawl-delay: 1
법적 및 윤리적 고려사항
크롤링은 강력한 도구이지만, 반드시 지켜야 할 규칙들이 있다. 이를 무시하면 법적 문제가 발생할 수 있다.
1. robots.txt 준수
- 웹사이트의 robots.txt 파일 확인
- 금지된 경로는 크롤링하지 않기
- 초보자 팁: 크롤링 전에 항상
https://example.com/robots.txt를 확인하자
2. 이용약관 확인
- 웹사이트의 이용약관 검토
- 크롤링이 금지되어 있는지 확인
- 초보자 팁: 사이트 하단의 "이용약관" 링크를 확인하자
3. 저작권 존중
- 수집한 데이터의 저작권 확인
- 상업적 이용 시 라이선스 확인
- 초보자 팁: 다른 사람의 글을 그대로 복사해서 사용하면 안 된다
4. 서버 부하 고려
- 적절한 요청 간격 유지 (1-3초 권장)
- 동시 요청 수 제한
- 캐싱 활용 (같은 페이지를 여러 번 요청하지 않기)
- 초보자 팁: 서버를 친구처럼 생각하고 예의를 지키자
5. 개인정보 보호
- 개인정보 수집 시 법적 요구사항 준수
- GDPR, 개인정보보호법 등 고려
- 초보자 팁: 개인정보가 포함된 데이터는 특히 주의하자
크롤링 방지 기법과 우회
일부 웹사이트는 크롤러를 차단하려고 한다. 이런 경우를 이해하고 대응하는 방법을 알아보자.
크롤링 방지 기법
웹사이트가 사용하는 크롤러 차단 방법들:
- User-Agent 검증: 브라우저가 아닌 프로그램을 감지
- IP 기반 차단: 특정 IP에서 너무 많은 요청이 오면 차단
- CAPTCHA: 사람인지 확인하는 퍼즐
- JavaScript 렌더링 필요: JavaScript 없이는 데이터가 안 보임
- Rate Limiting: 초당 요청 수를 제한
초보자 팁: 이런 차단을 우회하려고 하기보다는, 차단되지 않도록 예의를 지키는 것이 좋다.
우회 방법 (윤리적 범위 내)
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;
import java.net.InetSocketAddress;
import java.net.Proxy;
// User-Agent 설정
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
.build();
// 프록시 사용
Proxy proxy = new Proxy(
Proxy.Type.HTTP,
new InetSocketAddress("proxy.example.com", 8080)
);
HttpClient client = HttpClient.newBuilder()
.proxy(proxy)
.build();
HttpResponse<String> response = client.send(
request,
HttpResponse.BodyHandlers.ofString()
);
// 쿠키 및 세션 관리
CookieManager cookieManager = new CookieManager();
HttpClient sessionClient = HttpClient.newBuilder()
.cookieHandler(cookieManager)
.build();
실전 예제: 뉴스 기사 크롤링
뉴스 사이트에서 여러 페이지의 기사를 수집하는 완전한 예제다. 이 예제를 따라하면 실제로 동작하는 크롤러를 만들 수 있다.
이 예제에서 배우는 것:
- 여러 페이지를 순차적으로 크롤링하는 방법
- 각 페이지에서 기사 정보를 추출하는 방법
- 수집한 데이터를 JSON 파일로 저장하는 방법
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class NewsCrawler {
// 한 페이지의 기사 목록 크롤링
public List<Map<String, String>> crawlNews(String url) throws Exception {
Document doc = Jsoup.connect(url)
.userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
.timeout(10000)
.get();
List<Map<String, String>> articles = new ArrayList<>();
Elements newsItems = doc.select("div.news-item");
for (Element item : newsItems) {
try {
// 제목, 링크, 날짜 추출
Element titleElement = item.selectFirst("h2");
String title = (titleElement != null) ? titleElement.text().trim() : "제목 없음";
Element linkElement = item.selectFirst("a");
String link = (linkElement != null) ? linkElement.attr("href") : "";
if (link.startsWith("/")) {
link = "https://example.com" + link;
}
Element dateElement = item.selectFirst("span.date");
String date = (dateElement != null) ? dateElement.text().trim() : "날짜 없음";
// Map에 저장
Map<String, String> article = new HashMap<>();
article.put("title", title);
article.put("link", link);
article.put("date", date);
articles.add(article);
} catch (Exception e) {
System.err.println("기사 처리 오류: " + e.getMessage());
}
}
return articles;
}
public static void main(String[] args) {
try {
NewsCrawler crawler = new NewsCrawler();
List<Map<String, String>> allArticles = new ArrayList<>();
// 여러 페이지 크롤링
for (int page = 1; page <= 5; page++) {
String url = "https://example.com/news?page=" + page;
allArticles.addAll(crawler.crawlNews(url));
if (page < 5) {
Thread.sleep(1000); // 서버 부하 방지
}
}
// JSON으로 저장
ObjectMapper mapper = new ObjectMapper();
mapper.enable(SerializationFeature.INDENT_OUTPUT);
mapper.writeValue(new File("news.json"), allArticles);
System.out.println("저장 완료: " + allArticles.size() + "개 기사");
} catch (Exception e) {
System.err.println("오류: " + e.getMessage());
}
}
}
실행 방법
- 의존성 추가 (Maven의 경우
pom.xml에 추가):
<dependencies>
<!-- Jsoup: HTML 파싱 -->
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.17.1</version>
</dependency>
<!-- Jackson: JSON 처리 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.2</version>
</dependency>
</dependencies>
- 컴파일 및 실행:
javac -cp "jsoup-1.17.1.jar:jackson-databind-2.15.2.jar" NewsCrawler.java
java -cp ".:jsoup-1.17.1.jar:jackson-databind-2.15.2.jar" NewsCrawler
- 결과 확인:
news.json파일이 생성되고 수집한 기사 정보가 저장됩니다.
성능 최적화
여러 페이지를 하나씩 순차적으로 크롤링하면 시간이 오래 걸린다. 동시에 여러 페이지를 크롤링하면 속도가 크게 향상된다.
초보자 팁: 처음에는 순차적으로 크롤링하는 것이 안전하다. 서버에 부담을 주지 않고, 차단될 위험도 적다. 속도가 중요해지면 비동기나 멀티스레딩을 사용하자.
비동기 크롤링
여러 요청을 동시에 보내고 응답을 기다린다.
이해하기:
- 동기 방식: 요청1 → 응답1 대기 → 요청2 → 응답2 대기 (느림)
- 비동기 방식: 요청1, 요청2, 요청3 동시에 보냄 → 모든 응답 대기 (빠름)
주의: 너무 많은 요청을 동시에 보내면 서버에 부담이 가고 차단될 수 있다.
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.ArrayList;
public class AsyncCrawler {
public List<String> crawlAsync(List<String> urls) {
HttpClient client = HttpClient.newHttpClient();
List<CompletableFuture<String>> futures = new ArrayList<>();
// 각 URL에 대해 비동기 요청 생성
for (String url : urls) {
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
try {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.build();
return client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenApply(response -> response.body())
.join();
} catch (Exception e) {
return "";
}
});
futures.add(future);
}
// 모든 응답 대기
List<String> results = new ArrayList<>();
for (CompletableFuture<String> future : futures) {
results.add(future.join());
}
return results;
}
}
멀티스레딩
여러 스레드를 사용하여 동시에 여러 작업을 처리한다.
이해하기:
- 스레드: 작업을 수행하는 작은 단위
- 멀티스레딩: 여러 스레드가 동시에 일을 하는 것
- 예: 5명이 동시에 각각 다른 페이지를 크롤링하는 것과 같다
주의: 스레드 수가 너무 많으면 컴퓨터 리소스를 많이 사용한다.
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.ArrayList;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;
public class MultiThreadCrawler {
public List<String> crawlPages(List<String> urls) throws Exception {
// 스레드 풀 생성 (최대 5개 스레드)
ExecutorService executor = Executors.newFixedThreadPool(5);
List<Future<String>> futures = new ArrayList<>();
// 각 URL을 스레드 풀에 제출
for (String url : urls) {
Future<String> future = executor.submit(() -> {
try {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.build();
HttpResponse<String> response = client.send(
request,
HttpResponse.BodyHandlers.ofString()
);
return response.body();
} catch (Exception e) {
return "";
}
});
futures.add(future);
}
// 모든 작업 완료 대기
List<String> results = new ArrayList<>();
for (Future<String> future : futures) {
results.add(future.get());
}
executor.shutdown();
return results;
}
}
동기 vs 비동기 vs 멀티스레딩 비교
| 방식 | 설명 | 장점 | 단점 |
|---|---|---|---|
| 동기 | 하나씩 순차적으로 처리 | 구현이 간단, 서버 부담 적음 | 느림 |
| 비동기 | 여러 요청을 동시에 보냄 | 빠름, 단일 스레드 사용 | 복잡도 증가 |
| 멀티스레딩 | 여러 스레드로 동시 처리 | 빠름, 제어 가능 | 리소스 사용 많음, 동기화 필요 |
데이터 정제 및 처리
추출한 데이터를 정제하여 사용하기 쉽게 만든다.
왜 필요한가?: 웹에서 가져온 데이터에는 불필요한 HTML 태그, 공백, 특수문자 등이 섞여 있을 수 있다. 이런 것들을 제거하고 깔끔하게 만드는 것이 데이터 정제다.
정제하는 것들:
- HTML 태그 제거
- 불필요한 공백 제거
- 특수문자 제거
- 날짜 형식 통일
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
public class DataCleaner {
// HTML 태그 제거 및 텍스트 정리
public String cleanText(String text) {
text = text.replaceAll("<[^>]+>", ""); // HTML 태그 제거
text = text.replaceAll("\\s+", " ").trim(); // 공백 정리
text = text.replaceAll("[^\\w\\s]", ""); // 특수 문자 제거
return text.trim();
}
// 날짜 문자열 파싱
public LocalDate parseDate(String dateString) {
String[] formats = {"yyyy-MM-dd", "dd/MM/yyyy", "MMMM d, yyyy"};
for (String format : formats) {
try {
return LocalDate.parse(dateString, DateTimeFormatter.ofPattern(format));
} catch (Exception e) {
// 다음 형식 시도
}
}
return null;
}
}
모범 사례
크롤링을 안전하고 효율적으로 하기 위한 좋은 습관들이다.
요청 간격 유지
- 서버 부하를 줄이기 위해 적절한 딜레이 추가
Thread.sleep()사용- 초보자 팁: 최소 1초는 대기하자
에러 처리
- 네트워크 오류, 타임아웃 등 예외 상황 처리
- 재시도 로직 구현
- try-catch 블록으로 예외 처리
- 초보자 팁: 항상 try-catch로 감싸서 프로그램이 중단되지 않게 하자
캐싱 활용
- 동일한 페이지를 여러 번 요청하지 않도록 캐싱
Caffeine또는EhCache같은 캐싱 라이브러리 활용- 초보자 팁: 같은 URL을 여러 번 크롤링하지 않도록 주의하자
로깅
- 크롤링 과정을 로그로 기록
- SLF4J + Logback 같은 로깅 프레임워크 사용
- 디버깅과 모니터링에 유용
- 초보자 팁:
System.out.println()으로라도 진행 상황을 출력하자
데이터 검증
- 수집한 데이터의 유효성 검사
- 예상과 다른 구조에 대비
- null 체크 및 예외 처리
- 초보자 팁: 데이터를 추출할 때 null인지 확인하자
주의사항
크롤링을 시작하기 전에 반드시 알아야 할 것들이다.
법적 리스크
- 크롤링이 불법이 될 수 있는 경우가 있음
- 이용약관과 저작권법 확인 필수
- 초보자 팁: 확실하지 않으면 크롤링하지 않는 것이 안전하다
기술적 제한
- JavaScript로 렌더링되는 콘텐츠는 Selenium/WebDriver 필요
- CAPTCHA나 복잡한 인증은 우회 어려움
- HtmlUnit 같은 헤드리스 브라우저 사용 고려
- 초보자 팁: 처음에는 간단한 정적 페이지부터 시작하자
데이터 품질
- 웹사이트 구조 변경 시 크롤러 수정 필요
- 불완전하거나 오래된 데이터 가능성
- 초보자 팁: 웹사이트가 업데이트되면 크롤러도 수정해야 한다
서버 부하
- 과도한 요청은 서버에 부담
- IP 차단 위험
- 초보자 팁: 적절한 간격을 두고 요청하자. 서버를 배려하는 것이 중요하다
결론
웹 크롤링은 유용한 데이터 수집 방법이지만, 법적·윤리적 고려사항을 반드시 준수해야 한다.
초보자를 위한 요약:
- ✅ robots.txt를 확인하자
- ✅ 요청 사이에 적절한 간격(1-3초)을 두자
- ✅ 이용약관을 확인하자
- ✅ 서버에 부담을 주지 않도록 하자
- ✅ 처음에는 간단한 페이지부터 시작하자
올바른 도구와 기법을 사용하면 효율적으로 데이터를 수집할 수 있지만, 항상 책임감 있게 크롤링해야 한다. 크롤링은 강력한 도구이지만, 그만큼 책임도 크다. 웹사이트를 존중하고, 규칙을 지키며, 윤리적으로 사용하자.
댓글 0개
- 첫 댓글을 남겨보세요!