์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿ•ท๏ธ ์ž๋ฐ” ์›น ํฌ๋กค๋ง๊ณผ Jsoup ์™„์ „์ •๋ณต

๐Ÿ•ท๏ธ ์ž๋ฐ” ์›น ํฌ๋กค๋ง๊ณผ Jsoup ์™„์ „์ •๋ณต

๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ์ž๋™ํ™”๋กœ ๊ฐœ๋ฐœ์ž์˜ ์‚ถ์„ ํŽธํ•˜๊ฒŒ ๋งŒ๋“ค์–ด๋ณด์ž!
Web Crawling HTML ๋ฌธ์„œ div class="content" h1 title /h1 p class="text" /p Jsoup โ˜• Data ๐Ÿ“Š

์•ˆ๋…•, ์นœ๊ตฌ! ๐Ÿ˜Š ์˜ค๋Š˜์€ ์ž๋ฐ” ๊ฐœ๋ฐœ์ž๋ผ๋ฉด ๊ผญ ์•Œ์•„์•ผ ํ•  ์›น ํฌ๋กค๋ง์— ๋Œ€ํ•ด ์ด์•ผ๊ธฐํ•ด๋ณผ๊นŒ ํ•ด. ์ธํ„ฐ๋„ท์—๋Š” ์—„์ฒญ๋‚œ ์–‘์˜ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์ž–์•„? ๋‰ด์Šค ๊ธฐ์‚ฌ, ์ƒํ’ˆ ์ •๋ณด, ์ฃผ์‹ ์‹œ์„ธ, ๋‚ ์”จ ์ •๋ณด ๋“ฑ๋“ฑ... ์ด๋Ÿฐ ๋ฐ์ดํ„ฐ๋“ค์„ ์ผ์ผ์ด ์†์œผ๋กœ ๋ณต์‚ฌ-๋ถ™์—ฌ๋„ฃ๊ธฐ ํ•˜๋ฉด ์ •๋ง ๋”์ฐํ•˜์ง€ ์•Š๊ฒ ์–ด? ๐Ÿคฏ

๋ฐ”๋กœ ์ด๋Ÿด ๋•Œ ํ•„์š”ํ•œ ๊ฒŒ ์›น ํฌ๋กค๋ง์ด์•ผ. ๊ทธ๋ฆฌ๊ณ  ์ž๋ฐ”์—์„œ ์›น ํฌ๋กค๋ง์„ ํ•  ๋•Œ ๊ฐ€์žฅ ๋งŽ์ด ์‚ฌ์šฉํ•˜๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ๋ฐ”๋กœ **Jsoup**์ด์ง€. ๋งˆ์น˜ ์›นํŽ˜์ด์ง€๋ฅผ ์š”๋ฆฌํ•˜๋Š” ์š”๋ฆฌ์‚ฌ์ฒ˜๋Ÿผ, Jsoup์€ HTML ๋ฌธ์„œ๋ฅผ ์•„์ฃผ ์‰ฝ๊ฒŒ ํŒŒ์‹ฑํ•˜๊ณ  ์›ํ•˜๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ์™์™ ๋ฝ‘์•„๋‚ผ ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ค˜. ๐Ÿณ


๐ŸŽฏ ์›น ํฌ๋กค๋ง์ด ๋ญ๊ธธ๋ž˜?

์›น ํฌ๋กค๋ง(Web Crawling)์€ ์‰ฝ๊ฒŒ ๋งํ•ด์„œ ์›น์‚ฌ์ดํŠธ๋ฅผ ์ž๋™์œผ๋กœ ๋Œ์•„๋‹ค๋‹ˆ๋ฉฐ ๋ฐ์ดํ„ฐ๋ฅผ ์ˆ˜์ง‘ํ•˜๋Š” ๊ธฐ์ˆ ์ด์•ผ. ๋งˆ์น˜ ๊ฑฐ๋ฏธ(Spider)๊ฐ€ ๊ฑฐ๋ฏธ์ค„์„ ํƒ€๊ณ  ์ด๋™ํ•˜๋“ฏ์ด, ํ”„๋กœ๊ทธ๋žจ์ด ์›นํŽ˜์ด์ง€๋ฅผ ๋”ฐ๋ผ๋‹ค๋‹ˆ๋ฉฐ ์ •๋ณด๋ฅผ ๊ธ์–ด๋ชจ์œผ๋Š” ๊ฑฐ์ง€. ๊ทธ๋ž˜์„œ ์›น ํฌ๋กค๋Ÿฌ๋ฅผ '์ŠคํŒŒ์ด๋”'๋ผ๊ณ  ๋ถ€๋ฅด๊ธฐ๋„ ํ•ด. ๐Ÿ•ท๏ธ

๐Ÿ” ์›น ํฌ๋กค๋ง vs ์›น ์Šคํฌ๋ž˜ํ•‘ ๋งŽ์€ ์‚ฌ๋žŒ๋“ค์ด ํ—ท๊ฐˆ๋ คํ•˜๋Š” ๋ถ€๋ถ„์ธ๋ฐ, ์‚ฌ์‹ค ์ด ๋‘˜์€ ์•ฝ๊ฐ„ ๋‹ค๋ฅธ ๊ฐœ๋…์ด์•ผ.

โ€ข ์›น ํฌ๋กค๋ง: ์›น์‚ฌ์ดํŠธ๋ฅผ ๊ด‘๋ฒ”์œ„ํ•˜๊ฒŒ ํƒ์ƒ‰ํ•˜๋ฉฐ ๋ฐ์ดํ„ฐ๋ฅผ ์ˆ˜์ง‘ (๊ฒ€์ƒ‰์—”์ง„์ด ํ•˜๋Š” ์ผ)
โ€ข ์›น ์Šคํฌ๋ž˜ํ•‘: ํŠน์ • ์›นํŽ˜์ด์ง€์—์„œ ์›ํ•˜๋Š” ๋ฐ์ดํ„ฐ๋งŒ ์ถ”์ถœ (์šฐ๋ฆฌ๊ฐ€ ์ฃผ๋กœ ํ•˜๋Š” ์ผ)

ํ•˜์ง€๋งŒ ์‹ค๋ฌด์—์„œ๋Š” ๋‘ ์šฉ์–ด๋ฅผ ๊ฑฐ์˜ ๊ฐ™์€ ์˜๋ฏธ๋กœ ์‚ฌ์šฉํ•˜๋‹ˆ๊นŒ ๋„ˆ๋ฌด ์‹ ๊ฒฝ ์“ฐ์ง€ ์•Š์•„๋„ ๋ผ! ๐Ÿ˜‰

๐Ÿ’ก ์™œ ์›น ํฌ๋กค๋ง์„ ๋ฐฐ์›Œ์•ผ ํ• ๊นŒ?

์ž, ์ด์ œ ์‹ค์งˆ์ ์ธ ์ด์•ผ๊ธฐ๋ฅผ ํ•ด๋ณด์ž. ์›น ํฌ๋กค๋ง์„ ๋ฐฐ์šฐ๋ฉด ๋ญ๊ฐ€ ์ข‹์„๊นŒ? ๐Ÿค”

1. ์‹œ์žฅ ์กฐ์‚ฌ ์ž๋™ํ™”
๊ฒฝ์Ÿ์‚ฌ ์ œํ’ˆ ๊ฐ€๊ฒฉ์„ ๋งค์ผ ์ฒดํฌํ•ด์•ผ ํ•œ๋‹ค๋ฉด? ์ˆ˜๋ฐฑ ๊ฐœ์˜ ์‚ฌ์ดํŠธ๋ฅผ ์ผ์ผ์ด ๋ฐฉ๋ฌธํ•  ํ•„์š” ์—†์ด ํฌ๋กค๋Ÿฌ๊ฐ€ ์ž๋™์œผ๋กœ ์ˆ˜์ง‘ํ•ด์ค˜. ์ด์ปค๋จธ์Šค ์—…๊ณ„์—์„œ๋Š” ํ•„์ˆ˜ ๊ธฐ์ˆ ์ด์ง€! ๐Ÿ’ฐ

2. ๋ฐ์ดํ„ฐ ๋ถ„์„ ํ”„๋กœ์ ํŠธ
๋น…๋ฐ์ดํ„ฐ ๋ถ„์„์„ ํ•˜๋ ค๋ฉด ๋จผ์ € ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์–ด์•ผ ํ•˜์ž–์•„? ์›น ํฌ๋กค๋ง์œผ๋กœ ๋‰ด์Šค ๊ธฐ์‚ฌ, SNS ๊ฒŒ์‹œ๊ธ€, ๋ฆฌ๋ทฐ ๋“ฑ์„ ์ˆ˜์ง‘ํ•ด์„œ ํŠธ๋ Œ๋“œ๋ฅผ ๋ถ„์„ํ•  ์ˆ˜ ์žˆ์–ด. ๐Ÿ“ˆ

3. ์ž๋™ํ™” ๋ด‡ ๊ฐœ๋ฐœ
๋ถ€๋™์‚ฐ ๋งค๋ฌผ ์•Œ๋ฆผ, ํ•œ์ •ํŒ ์ œํ’ˆ ์žฌ์ž…๊ณ  ์•Œ๋ฆผ, ์ฃผ์‹ ๋‰ด์Šค ๋ชจ๋‹ˆํ„ฐ๋ง ๋“ฑ ๋‹ค์–‘ํ•œ ์ž๋™ํ™” ์„œ๋น„์Šค๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด. ์‹ค์ œ๋กœ **์žฌ๋Šฅ๋„ท** ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ๋„ ์ด๋Ÿฐ ์ž๋™ํ™” ์„œ๋น„์Šค ๊ฐœ๋ฐœ ์žฌ๋Šฅ์ด ์ธ๊ธฐ๊ฐ€ ๋งŽ๋”๋ผ๊ณ ! ๐Ÿค–

4. ํฌํŠธํด๋ฆฌ์˜ค ๊ฐ•ํ™”
์ทจ์—… ์ค€๋น„์ƒ์ด๋ผ๋ฉด? ์›น ํฌ๋กค๋ง ํ”„๋กœ์ ํŠธ๋Š” ์ •๋ง ์ข‹์€ ํฌํŠธํด๋ฆฌ์˜ค๊ฐ€ ๋ผ. "์ €๋Š” ์ด๋Ÿฐ ๋ฐ์ดํ„ฐ๋ฅผ ์ˆ˜์ง‘ํ•ด์„œ ์ด๋Ÿฐ ์ธ์‚ฌ์ดํŠธ๋ฅผ ๋„์ถœํ–ˆ์Šต๋‹ˆ๋‹ค"๋ผ๊ณ  ๋งํ•  ์ˆ˜ ์žˆ์œผ๋‹ˆ๊นŒ. โœจ


โ˜• Jsoup, ๋„ˆ๋Š” ๋ˆ„๊ตฌ๋ƒ?

์ž, ์ด์ œ ๋ณธ๊ฒฉ์ ์œผ๋กœ Jsoup์— ๋Œ€ํ•ด ์•Œ์•„๋ณด์ž. Jsoup์€ ์ž๋ฐ”๋กœ ๋งŒ๋“ค์–ด์ง„ HTML ํŒŒ์‹ฑ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์•ผ. 2009๋…„์— ์ฒ˜์Œ ๋“ฑ์žฅํ–ˆ์œผ๋‹ˆ๊นŒ ๋ฒŒ์จ 15๋…„์ด ๋„˜์€ ๊ฒ€์ฆ๋œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ง€. ๐ŸŽ‚

๐ŸŒŸ Jsoup์˜ ์žฅ์ 

1. ์—„์ฒญ๋‚˜๊ฒŒ ์‰ฌ์šด ์‚ฌ์šฉ๋ฒ•
jQuery ์Šคํƒ€์ผ์˜ ์…€๋ ‰ํ„ฐ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด์„œ ์›น ๊ฐœ๋ฐœ ๊ฒฝํ—˜์ด ์žˆ๋‹ค๋ฉด ๋ฐ”๋กœ ์ต์ˆ™ํ•˜๊ฒŒ ๋А๊ปด์งˆ ๊ฑฐ์•ผ. CSS ์…€๋ ‰ํ„ฐ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•˜๋ฉด ๋ผ!

2. ๊ฐ•๋ ฅํ•œ HTML ํŒŒ์‹ฑ
๊นจ์ง„ HTML๋„ ์ž˜ ์ฒ˜๋ฆฌํ•ด์ค˜. ์‹ค์ œ ์›น์‚ฌ์ดํŠธ๋“ค์€ ์™„๋ฒฝํ•œ HTML์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์€๋ฐ, Jsoup์€ ์ด๋Ÿฐ ๊ฒƒ๋“ค๋„ ์ž˜ ํŒŒ์‹ฑํ•ด. ๐Ÿ‘

3. XSS ๊ณต๊ฒฉ ๋ฐฉ์–ด
์‚ฌ์šฉ์ž ์ž…๋ ฅ์„ ์•ˆ์ „ํ•˜๊ฒŒ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ๋„ ์ œ๊ณตํ•ด. ๋ณด์•ˆ๋„ ์ฑ™๊ธธ ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฑฐ์ง€!

4. ๊ฐ€๋ณ๊ณ  ๋น ๋ฆ„
์™ธ๋ถ€ ์˜์กด์„ฑ์ด ๊ฑฐ์˜ ์—†์–ด์„œ ํ”„๋กœ์ ํŠธ์— ๋ถ€๋‹ด ์—†์ด ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์–ด. ์šฉ๋Ÿ‰๋„ ์ž‘๊ณ  ์„ฑ๋Šฅ๋„ ์ข‹์•„! โšก

๐Ÿ’ก ๊ฟ€ํŒ: Jsoup์€ ๋‹จ์ˆœํžˆ ํฌ๋กค๋ง๋งŒ ํ•˜๋Š” ๊ฒŒ ์•„๋‹ˆ์•ผ. HTML ์ƒ์„ฑ, ์ˆ˜์ •, ์ •์ œ(sanitization)๋„ ๊ฐ€๋Šฅํ•ด. ๊ทธ๋ž˜์„œ ์›น ๊ฐœ๋ฐœ์—์„œ HTML์„ ๋‹ค๋ฃฐ ๋•Œ๋„ ์œ ์šฉํ•˜๊ฒŒ ์“ธ ์ˆ˜ ์žˆ์ง€!

๐Ÿ“ฆ Jsoup ์„ค์น˜ํ•˜๊ธฐ

Jsoup์„ ์‚ฌ์šฉํ•˜๋ ค๋ฉด ๋จผ์ € ํ”„๋กœ์ ํŠธ์— ์ถ”๊ฐ€ํ•ด์•ผ๊ฒ ์ง€? Maven์ด๋‚˜ Gradle์„ ์‚ฌ์šฉํ•œ๋‹ค๋ฉด ์ •๋ง ๊ฐ„๋‹จํ•ด.

Maven ์‚ฌ์šฉ์ž๋ผ๋ฉด:

<dependency>
    <groupId>org.jsoup</groupId>
    <artifactId>jsoup</artifactId>
    <version>1.17.2</version>
</dependency>

Gradle ์‚ฌ์šฉ์ž๋ผ๋ฉด:

implementation 'org.jsoup:jsoup:1.17.2'

๋ฒ„์ „์€ ๊ณ„์† ์—…๋ฐ์ดํŠธ๋˜๋‹ˆ๊นŒ Maven Central Repository์—์„œ ์ตœ์‹  ๋ฒ„์ „์„ ํ™•์ธํ•˜๋Š” ๊ฒŒ ์ข‹์•„. 2024๋…„ ๊ธฐ์ค€์œผ๋กœ๋Š” 1.17.2๊ฐ€ ์ตœ์‹ ์ด์•ผ. ๐Ÿ“…

โš ๏ธ ์ฃผ์˜์‚ฌํ•ญ: ๋งŒ์•ฝ Maven์ด๋‚˜ Gradle์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š”๋‹ค๋ฉด, Jsoup JAR ํŒŒ์ผ์„ ์ง์ ‘ ๋‹ค์šด๋กœ๋“œํ•ด์„œ ํ”„๋กœ์ ํŠธ์˜ classpath์— ์ถ”๊ฐ€ํ•ด์•ผ ํ•ด. ํ•˜์ง€๋งŒ ์š”์ฆ˜์€ ๊ฑฐ์˜ ๋ชจ๋“  ํ”„๋กœ์ ํŠธ๊ฐ€ ๋นŒ๋“œ ๋„๊ตฌ๋ฅผ ์‚ฌ์šฉํ•˜๋‹ˆ๊นŒ ์ด ๋ฐฉ๋ฒ•์€ ๊ถŒ์žฅํ•˜์ง€ ์•Š์•„!

๐Ÿš€ ์ฒซ ๋ฒˆ์งธ ํฌ๋กค๋ง ์ฝ”๋“œ ์ž‘์„ฑํ•˜๊ธฐ

์ด๋ก ์€ ์ด์ œ ์ถฉ๋ถ„ํ•ด! ์‹ค์ œ๋กœ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด๋ณด์ž. ๊ฐ€์žฅ ๊ธฐ๋ณธ์ ์ธ ์˜ˆ์ œ๋ถ€ํ„ฐ ์‹œ์ž‘ํ• ๊ฒŒ. ๐Ÿ˜Ž

๐ŸŽฌ Hello Jsoup!

๋จผ์ € ๊ฐ„๋‹จํ•˜๊ฒŒ ์›นํŽ˜์ด์ง€๋ฅผ ๊ฐ€์ ธ์™€์„œ ์ œ๋ชฉ์„ ์ถœ๋ ฅํ•˜๋Š” ์ฝ”๋“œ๋ฅผ ๋งŒ๋“ค์–ด๋ณผ๊นŒ?

import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;

public class HelloJsoup {
    public static void main(String[] args) {
        try {
            // ์›นํŽ˜์ด์ง€ ์—ฐ๊ฒฐ ๋ฐ HTML ๊ฐ€์ ธ์˜ค๊ธฐ
            String url = "https://example.com";
            Document doc = Jsoup.connect(url).get();
            
            // ํŽ˜์ด์ง€ ์ œ๋ชฉ ์ถ”์ถœ
            String title = doc.title();
            System.out.println("ํŽ˜์ด์ง€ ์ œ๋ชฉ: " + title);
            
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

์™€! ์ •๋ง ๊ฐ„๋‹จํ•˜์ง€? ๐ŸŽ‰ ๋‹จ ๋ช‡ ์ค„์˜ ์ฝ”๋“œ๋กœ ์›นํŽ˜์ด์ง€๋ฅผ ๊ฐ€์ ธ์™€์„œ ์ œ๋ชฉ์„ ์ถ”์ถœํ–ˆ์–ด. ํ•˜๋‚˜์”ฉ ๋œฏ์–ด๋ณด์ž.

1. Jsoup.connect(url)
์ด ๋ฉ”์„œ๋“œ๋Š” ์ง€์ •ํ•œ URL์— ์—ฐ๊ฒฐ์„ ์ค€๋น„ํ•ด. ์•„์ง ์‹ค์ œ๋กœ ์š”์ฒญ์„ ๋ณด๋‚ด์ง€๋Š” ์•Š์•˜์–ด.

2. .get()
์ด์ œ ์‹ค์ œ๋กœ HTTP GET ์š”์ฒญ์„ ๋ณด๋‚ด๊ณ  HTML์„ ๊ฐ€์ ธ์™€. ๊ทธ๋ฆฌ๊ณ  Document ๊ฐ์ฒด๋กœ ๋ฐ˜ํ™˜ํ•ด์ค˜.

3. doc.title()
Document ๊ฐ์ฒด์—์„œ <title> ํƒœ๊ทธ์˜ ๋‚ด์šฉ์„ ์ถ”์ถœํ•ด. ์ •๋ง ์ง๊ด€์ ์ด์ง€?

๐Ÿค“ Document ๊ฐ์ฒด๊ฐ€ ๋ญ์•ผ?

Document๋Š” HTML ๋ฌธ์„œ ์ „์ฒด๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๊ฐ์ฒด์•ผ. DOM(Document Object Model) ๊ตฌ์กฐ๋กœ ๋˜์–ด ์žˆ์–ด์„œ, ์›นํŽ˜์ด์ง€์˜ ๋ชจ๋“  ์š”์†Œ์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์ง€. ๋งˆ์น˜ HTML ๋ฌธ์„œ๋ฅผ ์ž๋ฐ” ๊ฐ์ฒด๋กœ ๋งŒ๋“  ๊ฒƒ์ฒ˜๋Ÿผ ์ƒ๊ฐํ•˜๋ฉด ๋ผ!

ย 

๐ŸŽฏ CSS ์…€๋ ‰ํ„ฐ๋กœ ์š”์†Œ ์„ ํƒํ•˜๊ธฐ

์ด์ œ ์กฐ๊ธˆ ๋” ์‹ค์šฉ์ ์ธ ์˜ˆ์ œ๋ฅผ ๋ณผ๊นŒ? ๋‰ด์Šค ์‚ฌ์ดํŠธ์—์„œ ๊ธฐ์‚ฌ ์ œ๋ชฉ๋“ค์„ ๊ฐ€์ ธ์˜ค๋Š” ์ฝ”๋“œ์•ผ.

import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;

public class NewsCrawler {
    public static void main(String[] args) {
        try {
            String url = "https://news.example.com";
            Document doc = Jsoup.connect(url)
                    .userAgent("Mozilla/5.0") // User-Agent ์„ค์ •
                    .timeout(5000) // ํƒ€์ž„์•„์›ƒ 5์ดˆ
                    .get();
            
            // CSS ์…€๋ ‰ํ„ฐ๋กœ ๊ธฐ์‚ฌ ์ œ๋ชฉ ์„ ํƒ
            Elements headlines = doc.select("h2.article-title");
            
            System.out.println("์ด " + headlines.size() + "๊ฐœ์˜ ๊ธฐ์‚ฌ๋ฅผ ์ฐพ์•˜์Šต๋‹ˆ๋‹ค.\n");
            
            // ๊ฐ ์ œ๋ชฉ ์ถœ๋ ฅ
            for (Element headline : headlines) {
                String title = headline.text();
                String link = headline.select("a").attr("href");
                
                System.out.println("์ œ๋ชฉ: " + title);
                System.out.println("๋งํฌ: " + link);
                System.out.println("---");
            }
            
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

์˜ค! ์ฝ”๋“œ๊ฐ€ ์กฐ๊ธˆ ๊ธธ์–ด์กŒ๋„ค? ํ•˜์ง€๋งŒ ์—ฌ์ „ํžˆ ์ดํ•ดํ•˜๊ธฐ ์‰ฌ์›Œ. ํ•˜๋‚˜์”ฉ ์‚ดํŽด๋ณด์ž. ๐Ÿ”

userAgent() ๋ฉ”์„œ๋“œ
์›น ์„œ๋ฒ„์—๊ฒŒ "๋‚˜๋Š” ์ด๋Ÿฐ ๋ธŒ๋ผ์šฐ์ €์•ผ!"๋ผ๊ณ  ์•Œ๋ ค์ฃผ๋Š” ๊ฑฐ์•ผ. ์ผ๋ถ€ ์›น์‚ฌ์ดํŠธ๋Š” User-Agent๊ฐ€ ์—†์œผ๋ฉด ์ ‘๊ทผ์„ ์ฐจ๋‹จํ•˜๊ธฐ๋„ ํ•ด. ๊ทธ๋ž˜์„œ ์‹ค์ œ ๋ธŒ๋ผ์šฐ์ €์ฒ˜๋Ÿผ ์œ„์žฅํ•˜๋Š” ๊ฑฐ์ง€. ๐Ÿ˜

timeout() ๋ฉ”์„œ๋“œ
์„œ๋ฒ„ ์‘๋‹ต์„ ๊ธฐ๋‹ค๋ฆฌ๋Š” ์ตœ๋Œ€ ์‹œ๊ฐ„์„ ์„ค์ •ํ•ด. ๋ฐ€๋ฆฌ์ดˆ ๋‹จ์œ„์•ผ. 5000์ด๋ฉด 5์ดˆ๋ฅผ ์˜๋ฏธํ•˜์ง€. ๋„ˆ๋ฌด ๊ธธ๋ฉด ํ”„๋กœ๊ทธ๋žจ์ด ๋ฉˆ์ถ˜ ๊ฒƒ์ฒ˜๋Ÿผ ๋ณด์ผ ์ˆ˜ ์žˆ๊ณ , ๋„ˆ๋ฌด ์งง์œผ๋ฉด ๋А๋ฆฐ ์„œ๋ฒ„์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ชป ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์–ด.

select() ๋ฉ”์„œ๋“œ
์ด๊ฒŒ ํ•ต์‹ฌ์ด์•ผ! CSS ์…€๋ ‰ํ„ฐ๋ฅผ ์‚ฌ์šฉํ•ด์„œ ์›ํ•˜๋Š” ์š”์†Œ๋ฅผ ์„ ํƒํ•ด. "h2.article-title"์€ "article-title ํด๋ž˜์Šค๋ฅผ ๊ฐ€์ง„ h2 ํƒœ๊ทธ"๋ฅผ ์˜๋ฏธํ•ด. jQuery ์จ๋ดค๋‹ค๋ฉด ์ต์ˆ™ํ•  ๊ฑฐ์•ผ! ๐Ÿ’ซ

Elements vs Element
Elements๋Š” ์—ฌ๋Ÿฌ ๊ฐœ์˜ Element๋ฅผ ๋‹ด๋Š” ์ปฌ๋ ‰์…˜์ด์•ผ. ๋ฐ˜๋ณต๋ฌธ์œผ๋กœ ํ•˜๋‚˜์”ฉ ๊บผ๋‚ด์„œ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์ง€.


๐ŸŽจ ๋‹ค์–‘ํ•œ ์…€๋ ‰ํ„ฐ ํ™œ์šฉ๋ฒ•

CSS ์…€๋ ‰ํ„ฐ๋Š” ์ •๋ง ๊ฐ•๋ ฅํ•œ ๋„๊ตฌ์•ผ. Jsoup์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๋‹ค์–‘ํ•œ ์…€๋ ‰ํ„ฐ ํŒจํ„ด์„ ์•Œ์•„๋ณด์ž. ์ด๊ฑธ ๋งˆ์Šคํ„ฐํ•˜๋ฉด ์›ฌ๋งŒํ•œ ์›นํŽ˜์ด์ง€๋Š” ๋‹ค ํฌ๋กค๋งํ•  ์ˆ˜ ์žˆ์–ด! ๐Ÿ†

๐Ÿ“‹ ๊ธฐ๋ณธ ์…€๋ ‰ํ„ฐ

// ํƒœ๊ทธ ์„ ํƒ
Elements divs = doc.select("div");

// ํด๋ž˜์Šค ์„ ํƒ
Elements contents = doc.select(".content");

// ID ์„ ํƒ
Elements header = doc.select("#header");

// ์†์„ฑ ์„ ํƒ
Elements links = doc.select("a[href]");

// ์†์„ฑ ๊ฐ’์œผ๋กœ ์„ ํƒ
Elements pdfLinks = doc.select("a[href$=.pdf]");

// ์—ฌ๋Ÿฌ ์กฐ๊ฑด ์กฐํ•ฉ
Elements newsLinks = doc.select("div.news a[href]");

๐Ÿ”— ๊ณ„์ธต ๊ตฌ์กฐ ์…€๋ ‰ํ„ฐ

// ์ž์‹ ์„ ํƒ (์ง๊ณ„ ์ž์‹๋งŒ)
Elements directChildren = doc.select("div > p");

// ํ›„์† ์„ ํƒ (๋ชจ๋“  ํ•˜์œ„ ์š”์†Œ)
Elements allDescendants = doc.select("div p");

// ํ˜•์ œ ์„ ํƒ (๋ฐ”๋กœ ๋‹ค์Œ)
Elements nextSibling = doc.select("h2 + p");

// ํ˜•์ œ ์„ ํƒ (์ดํ›„ ๋ชจ๋“ )
Elements allSiblings = doc.select("h2 ~ p");

๐ŸŽฏ ๊ณ ๊ธ‰ ์…€๋ ‰ํ„ฐ

// ์ฒซ ๋ฒˆ์งธ ์š”์†Œ
Element first = doc.selectFirst("div.article");

// n๋ฒˆ์งธ ์š”์†Œ
Elements third = doc.select("li:nth-child(3)");

// ํ…์ŠคํŠธ ํฌํ•จ ์—ฌ๋ถ€๋กœ ์„ ํƒ
Elements matching = doc.select("p:contains(๊ฒ€์ƒ‰์–ด)");

// ์ •๊ทœ์‹์œผ๋กœ ์„ ํƒ
Elements regex = doc.select("a[href~=(?i)\\.(png|jpe?g)]");

// ๋ถ€์ • ์„ ํƒ
Elements notAds = doc.select("div:not(.advertisement)");

์™€์šฐ! ์ •๋ง ๋‹ค์–‘ํ•˜์ง€? ๐Ÿ˜ฒ ์ด ์ •๋„๋งŒ ์•Œ์•„๋„ ์›ฌ๋งŒํ•œ ์›นํŽ˜์ด์ง€๋Š” ๋‹ค ํŒŒ์‹ฑํ•  ์ˆ˜ ์žˆ์–ด. ์‹ค์ œ๋กœ ์‚ฌ์šฉํ•  ๋•Œ๋Š” ๋ธŒ๋ผ์šฐ์ €์˜ ๊ฐœ๋ฐœ์ž ๋„๊ตฌ(F12)๋ฅผ ์—ด์–ด์„œ HTML ๊ตฌ์กฐ๋ฅผ ํ™•์ธํ•˜๊ณ , ์ ์ ˆํ•œ ์…€๋ ‰ํ„ฐ๋ฅผ ์ฐพ๋Š” ๊ฒŒ ์ค‘์š”ํ•ด.

๐Ÿ’ก ๊ฐœ๋ฐœ์ž ๋„๊ตฌ ํ™œ์šฉ ํŒ: 1. ํฌ๋กฌ ๊ฐœ๋ฐœ์ž ๋„๊ตฌ์—์„œ ์›ํ•˜๋Š” ์š”์†Œ๋ฅผ ์šฐํด๋ฆญ
2. "Copy" โ†’ "Copy selector" ์„ ํƒ
3. ๋ณต์‚ฌ๋œ ์…€๋ ‰ํ„ฐ๋ฅผ Jsoup ์ฝ”๋“œ์— ๋ถ™์—ฌ๋„ฃ๊ธฐ

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ๋ณต์žกํ•œ ์…€๋ ‰ํ„ฐ๋„ ์‰ฝ๊ฒŒ ์–ป์„ ์ˆ˜ ์žˆ์–ด! ๋‹ค๋งŒ, ์ž๋™ ์ƒ์„ฑ๋œ ์…€๋ ‰ํ„ฐ๋Š” ๋„ˆ๋ฌด ๊ตฌ์ฒด์ ์ผ ์ˆ˜ ์žˆ์œผ๋‹ˆ ์ ์ ˆํžˆ ๋‹จ์ˆœํ™”ํ•˜๋Š” ๊ฒŒ ์ข‹์•„. ๐ŸŽฏ

๐Ÿ› ๏ธ ์‹ค์ „ ํฌ๋กค๋ง ํ…Œํฌ๋‹‰

์ด์ œ ์‹ค์ „์—์„œ ์œ ์šฉํ•œ ํ…Œํฌ๋‹‰๋“ค์„ ์•Œ์•„๋ณด์ž. ๋‹จ์ˆœํžˆ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๊ฒƒ์„ ๋„˜์–ด์„œ, ์•ˆ์ •์ ์ด๊ณ  ํšจ์œจ์ ์ธ ํฌ๋กค๋Ÿฌ๋ฅผ ๋งŒ๋“œ๋Š” ๋ฐฉ๋ฒ•์ด์•ผ. ๐ŸŽ“

โฑ๏ธ ์˜ˆ์˜ ๋ฐ”๋ฅธ ํฌ๋กค๋ง: ๋”œ๋ ˆ์ด ์ถ”๊ฐ€ํ•˜๊ธฐ

์„œ๋ฒ„์— ๋ถ€๋‹ด์„ ์ฃผ์ง€ ์•Š์œผ๋ ค๋ฉด ์š”์ฒญ ์‚ฌ์ด์— ์ ์ ˆํ•œ ๋”œ๋ ˆ์ด๋ฅผ ๋„ฃ์–ด์•ผ ํ•ด. ๋„ˆ๋ฌด ๋น ๋ฅด๊ฒŒ ์š”์ฒญํ•˜๋ฉด ์„œ๋ฒ„๊ฐ€ ๊ณผ๋ถ€ํ•˜๋˜๊ฑฐ๋‚˜, ์‹ฌํ•˜๋ฉด IP๊ฐ€ ์ฐจ๋‹จ๋  ์ˆ˜๋„ ์žˆ์–ด! ๐Ÿ˜ฑ

import java.util.concurrent.TimeUnit;

public class PoliteCrawler {
    public static void main(String[] args) {
        String[] urls = {
            "https://example.com/page1",
            "https://example.com/page2",
            "https://example.com/page3"
        };
        
        for (String url : urls) {
            try {
                System.out.println("ํฌ๋กค๋ง ์ค‘: " + url);
                Document doc = Jsoup.connect(url)
                        .userAgent("Mozilla/5.0")
                        .timeout(5000)
                        .get();
                
                // ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ
                processData(doc);
                
                // 2์ดˆ ๋Œ€๊ธฐ (์˜ˆ์˜ ๋ฐ”๋ฅธ ํฌ๋กค๋ง!)
                TimeUnit.SECONDS.sleep(2);
                
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                break;
            } catch (Exception e) {
                System.err.println("์—๋Ÿฌ ๋ฐœ์ƒ: " + e.getMessage());
            }
        }
    }
    
    private static void processData(Document doc) {
        // ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ๋กœ์ง
        System.out.println("์ œ๋ชฉ: " + doc.title());
    }
}

๋ณดํ†ต 1~3์ดˆ ์ •๋„์˜ ๋”œ๋ ˆ์ด๊ฐ€ ์ ๋‹นํ•ด. ๋ฌผ๋ก  ์‚ฌ์ดํŠธ๋งˆ๋‹ค ๋‹ค๋ฅด๋‹ˆ๊นŒ robots.txt ํŒŒ์ผ์„ ํ™•์ธํ•˜๋Š” ๊ฒŒ ์ข‹์•„. ์˜ˆ๋ฅผ ๋“ค์–ด https://example.com/robots.txt์— ์ ‘์†ํ•˜๋ฉด ํฌ๋กค๋ง ๊ทœ์น™์„ ๋ณผ ์ˆ˜ ์žˆ์ง€. ๐Ÿค–

๐Ÿ”„ ์žฌ์‹œ๋„ ๋กœ์ง ๊ตฌํ˜„ํ•˜๊ธฐ

๋„คํŠธ์›Œํฌ๋Š” ๋ถˆ์•ˆ์ •ํ•  ์ˆ˜ ์žˆ์–ด. ์ผ์‹œ์ ์ธ ์˜ค๋ฅ˜๋กœ ํฌ๋กค๋ง์ด ์‹คํŒจํ•˜๋ฉด ์žฌ์‹œ๋„ํ•˜๋Š” ๋กœ์ง์„ ์ถ”๊ฐ€ํ•˜์ž.

public class RetryableCrawler {
    private static final int MAX_RETRIES = 3;
    private static final int RETRY_DELAY = 2000; // 2์ดˆ
    
    public static Document fetchWithRetry(String url) {
        int attempts = 0;
        
        while (attempts < MAX_RETRIES) {
            try {
                return Jsoup.connect(url)
                        .userAgent("Mozilla/5.0")
                        .timeout(5000)
                        .get();
                        
            } catch (IOException e) {
                attempts++;
                System.err.println("์‹œ๋„ " + attempts + " ์‹คํŒจ: " + e.getMessage());
                
                if (attempts >= MAX_RETRIES) {
                    System.err.println("์ตœ๋Œ€ ์žฌ์‹œ๋„ ํšŸ์ˆ˜ ์ดˆ๊ณผ");
                    throw new RuntimeException("ํฌ๋กค๋ง ์‹คํŒจ: " + url, e);
                }
                
                try {
                    Thread.sleep(RETRY_DELAY);
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                    throw new RuntimeException("์žฌ์‹œ๋„ ์ค‘ ์ธํ„ฐ๋ŸฝํŠธ ๋ฐœ์ƒ", ie);
                }
            }
        }
        
        return null; // ๋„๋‹ฌํ•˜์ง€ ์•Š์Œ
    }
}

๐Ÿช ์ฟ ํ‚ค์™€ ์„ธ์…˜ ์ฒ˜๋ฆฌํ•˜๊ธฐ

๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•œ ์‚ฌ์ดํŠธ๋ฅผ ํฌ๋กค๋งํ•˜๋ ค๋ฉด? ์ฟ ํ‚ค์™€ ์„ธ์…˜์„ ๊ด€๋ฆฌํ•ด์•ผ ํ•ด. Jsoup์€ ์ด๊ฒƒ๋„ ์ง€์›ํ•ด! ๐Ÿช

import org.jsoup.Connection;
import java.util.Map;

public class LoginCrawler {
    public static void main(String[] args) {
        try {
            // 1๋‹จ๊ณ„: ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€ ์ ‘์† ๋ฐ ์ฟ ํ‚ค ํš๋“
            Connection.Response loginForm = Jsoup.connect("https://example.com/login")
                    .method(Connection.Method.GET)
                    .execute();
            
            Map<string, string=""> cookies = loginForm.cookies();
            
            // 2๋‹จ๊ณ„: ๋กœ๊ทธ์ธ ์š”์ฒญ (POST)
            Connection.Response loginResponse = Jsoup.connect("https://example.com/login")
                    .data("username", "myuser")
                    .data("password", "mypass")
                    .cookies(cookies)
                    .method(Connection.Method.POST)
                    .execute();
            
            // ๋กœ๊ทธ์ธ ํ›„ ์ฟ ํ‚ค ์—…๋ฐ์ดํŠธ
            cookies.putAll(loginResponse.cookies());
            
            // 3๋‹จ๊ณ„: ๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•œ ํŽ˜์ด์ง€ ์ ‘์†
            Document doc = Jsoup.connect("https://example.com/protected-page")
                    .cookies(cookies)
                    .get();
            
            System.out.println("๋กœ๊ทธ์ธ ์„ฑ๊ณต! ํŽ˜์ด์ง€ ์ œ๋ชฉ: " + doc.title());
            
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}</string,>

์ฟ ํ‚ค๋ฅผ ์œ ์ง€ํ•˜๋ฉด์„œ ์—ฌ๋Ÿฌ ํŽ˜์ด์ง€๋ฅผ ํƒ์ƒ‰ํ•  ์ˆ˜ ์žˆ์–ด. ๋งˆ์น˜ ์‹ค์ œ ์‚ฌ์šฉ์ž์ฒ˜๋Ÿผ ํ–‰๋™ํ•˜๋Š” ๊ฑฐ์ง€! ๐Ÿ˜Ž

โš ๏ธ ๋ฒ•์  ์ฃผ์˜์‚ฌํ•ญ: ๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•œ ์‚ฌ์ดํŠธ๋ฅผ ํฌ๋กค๋งํ•  ๋•Œ๋Š” ํŠนํžˆ ์กฐ์‹ฌํ•ด์•ผ ํ•ด. ์„œ๋น„์Šค ์ด์šฉ์•ฝ๊ด€์„ ์œ„๋ฐ˜ํ•  ์ˆ˜ ์žˆ๊ณ , ๊ฒฝ์šฐ์— ๋”ฐ๋ผ ๋ฒ•์  ๋ฌธ์ œ๊ฐ€ ๋  ์ˆ˜๋„ ์žˆ์–ด. ํ•ญ์ƒ ํ•ด๋‹น ์‚ฌ์ดํŠธ์˜ ์ •์ฑ…์„ ํ™•์ธํ•˜๊ณ , ํ•„์š”ํ•˜๋‹ค๋ฉด ๊ณต์‹ API๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒŒ ์ข‹์•„. ๐Ÿšจ

๐Ÿ“„ ํŽ˜์ด์ง€๋„ค์ด์…˜ ์ฒ˜๋ฆฌํ•˜๊ธฐ

๊ฒŒ์‹œํŒ์ด๋‚˜ ์ƒํ’ˆ ๋ชฉ๋ก์ฒ˜๋Ÿผ ์—ฌ๋Ÿฌ ํŽ˜์ด์ง€์— ๊ฑธ์ณ ์žˆ๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ์ˆ˜์ง‘ํ•˜๋ ค๋ฉด? ํŽ˜์ด์ง€๋„ค์ด์…˜์„ ์ฒ˜๋ฆฌํ•ด์•ผ์ง€!

public class PaginationCrawler {
    public static void main(String[] args) {
        String baseUrl = "https://example.com/articles?page=";
        int maxPages = 10;
        
        for (int page = 1; page <= maxPages; page++) {
            try {
                String url = baseUrl + page;
                System.out.println("\n=== ํŽ˜์ด์ง€ " + page + " ํฌ๋กค๋ง ===");
                
                Document doc = Jsoup.connect(url)
                        .userAgent("Mozilla/5.0")
                        .timeout(5000)
                        .get();
                
                // ๊ธฐ์‚ฌ ๋ชฉ๋ก ์ถ”์ถœ
                Elements articles = doc.select("div.article");
                
                if (articles.isEmpty()) {
                    System.out.println("๋” ์ด์ƒ ๊ธฐ์‚ฌ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.");
                    break; // ๋นˆ ํŽ˜์ด์ง€๋ฉด ์ข…๋ฃŒ
                }
                
                for (Element article : articles) {
                    String title = article.select("h2").text();
                    String author = article.select(".author").text();
                    String date = article.select(".date").text();
                    
                    System.out.println("์ œ๋ชฉ: " + title);
                    System.out.println("์ž‘์„ฑ์ž: " + author);
                    System.out.println("๋‚ ์งœ: " + date);
                    System.out.println("---");
                }
                
                // ์˜ˆ์˜ ๋ฐ”๋ฅธ ํฌ๋กค๋ง์„ ์œ„ํ•œ ๋”œ๋ ˆ์ด
                Thread.sleep(2000);
                
            } catch (Exception e) {
                System.err.println("ํŽ˜์ด์ง€ " + page + " ํฌ๋กค๋ง ์‹คํŒจ: " + e.getMessage());
            }
        }
    }
}

๐Ÿ’พ ๋ฐ์ดํ„ฐ ์ €์žฅํ•˜๊ธฐ

ํฌ๋กค๋งํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ์–ด๋–ป๊ฒŒ ์ €์žฅํ• ๊นŒ? ์—ฌ๋Ÿฌ ๋ฐฉ๋ฒ•์ด ์žˆ๋Š”๋ฐ, ์ƒํ™ฉ์— ๋งž๊ฒŒ ์„ ํƒํ•˜๋ฉด ๋ผ. ๐Ÿ“Š

๐Ÿ“ CSV ํŒŒ์ผ๋กœ ์ €์žฅํ•˜๊ธฐ

๊ฐ€์žฅ ๊ฐ„๋‹จํ•˜๊ณ  ๋ฒ”์šฉ์ ์ธ ๋ฐฉ๋ฒ•์ด์•ผ. ์—‘์…€์—์„œ๋„ ๋ฐ”๋กœ ์—ด ์ˆ˜ ์žˆ์–ด์„œ ํŽธ๋ฆฌํ•˜์ง€!

import java.io.FileWriter;
import java.io.IOException;

public class CsvSaver {
    public static void saveToCSV(List</code><article><code> articles, String filename) {
        try (FileWriter writer = new FileWriter(filename, StandardCharsets.UTF_8)) {
            // CSV ํ—ค๋” ์ž‘์„ฑ
            writer.append("์ œ๋ชฉ,์ž‘์„ฑ์ž,๋‚ ์งœ,URL\n");
            
            // ๋ฐ์ดํ„ฐ ์ž‘์„ฑ
            for (Article article : articles) {
                writer.append(escapeCsv(article.getTitle())).append(",");
                writer.append(escapeCsv(article.getAuthor())).append(",");
                writer.append(escapeCsv(article.getDate())).append(",");
                writer.append(escapeCsv(article.getUrl())).append("\n");
            }
            
            System.out.println("CSV ํŒŒ์ผ ์ €์žฅ ์™„๋ฃŒ: " + filename);
            
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    // CSV ํŠน์ˆ˜๋ฌธ์ž ์ด์Šค์ผ€์ดํ”„ ์ฒ˜๋ฆฌ
    private static String escapeCsv(String value) {
        if (value == null) return "";
        
        if (value.contains(",") || value.contains("\"") || value.contains("\n")) {
            value = "\"" + value.replace("\"", "\"\"") + "\"";
        }
        
        return value;
    }
}

// Article ํด๋ž˜์Šค
class Article {
    private String title;
    private String author;
    private String date;
    private String url;
    
    // ์ƒ์„ฑ์ž, getter, setter ์ƒ๋žต
}</code></article></pre>
</div>

<h3>๐Ÿ—„๏ธ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ €์žฅํ•˜๊ธฐ</h3>

<p>
๋Œ€๋Ÿ‰์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์ฒด๊ณ„์ ์œผ๋กœ ๊ด€๋ฆฌํ•˜๋ ค๋ฉด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๊ฐ€ ์ตœ๊ณ ์•ผ. JDBC๋ฅผ ์‚ฌ์šฉํ•œ ๊ฐ„๋‹จํ•œ ์˜ˆ์ œ๋ฅผ ๋ณผ๊นŒ?
</p>

<div class="tx-code-block">
<pre><code>import java.sql.*;

public class DatabaseSaver {
    private static final String DB_URL = "jdbc:mysql://localhost:3306/crawling_db";
    private static final String USER = "username";
    private static final String PASS = "password";
    
    public static void saveToDatabase(List</code><article><code> articles) {
        String sql = "INSERT INTO articles (title, author, date, url, created_at) VALUES (?, ?, ?, ?, NOW())";
        
        try (Connection conn = DriverManager.getConnection(DB_URL, USER, PASS);
             PreparedStatement pstmt = conn.prepareStatement(sql)) {
            
            conn.setAutoCommit(false); // ํŠธ๋žœ์žญ์…˜ ์‹œ์ž‘
            
            for (Article article : articles) {
                pstmt.setString(1, article.getTitle());
                pstmt.setString(2, article.getAuthor());
                pstmt.setString(3, article.getDate());
                pstmt.setString(4, article.getUrl());
                pstmt.addBatch();
            }
            
            int[] results = pstmt.executeBatch();
            conn.commit(); // ํŠธ๋žœ์žญ์…˜ ์ปค๋ฐ‹
            
            System.out.println(results.length + "๊ฐœ์˜ ๊ธฐ์‚ฌ๊ฐ€ ์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.");
            
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}</code></article></pre>
</div>

<h3>๐Ÿ“‹ JSON ํŒŒ์ผ๋กœ ์ €์žฅํ•˜๊ธฐ</h3>

<p>
API ํ˜•ํƒœ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ํ™œ์šฉํ•˜๋ ค๋ฉด JSON์ด ์ข‹์•„. Gson ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•œ ์˜ˆ์ œ์•ผ.
</p>

<div class="tx-code-block">
<pre><code>import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import java.io.FileWriter;

public class JsonSaver {
    public static void saveToJson(List</code><article><code> articles, String filename) {
        Gson gson = new GsonBuilder()
                .setPrettyPrinting() // ๋ณด๊ธฐ ์ข‹๊ฒŒ ํฌ๋งทํŒ…
                .create();
        
        try (FileWriter writer = new FileWriter(filename, StandardCharsets.UTF_8)) {
            gson.toJson(articles, writer);
            System.out.println("JSON ํŒŒ์ผ ์ €์žฅ ์™„๋ฃŒ: " + filename);
            
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}</code></article></pre>
</div>

<p>
Gson์„ ์‚ฌ์šฉํ•˜๋ ค๋ฉด Maven ์˜์กด์„ฑ์„ ์ถ”๊ฐ€ํ•ด์•ผ ํ•ด:
</p>

<div class="tx-code-block">
<pre><code><dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    <version>2.10.1</version>
</dependency>

๐ŸŽญ ๋™์  ์›นํŽ˜์ด์ง€ ํฌ๋กค๋ง

์—ฌ๊ธฐ์„œ ์ค‘์š”ํ•œ ์ด์•ผ๊ธฐ๋ฅผ ํ•ด์•ผ ํ•ด. Jsoup์€ ์ •์  HTML๋งŒ ํŒŒ์‹ฑํ•  ์ˆ˜ ์žˆ์–ด. ๋ฌด์Šจ ๋ง์ด๋ƒ๋ฉด, JavaScript๋กœ ๋™์ ์œผ๋กœ ์ƒ์„ฑ๋˜๋Š” ์ฝ˜ํ…์ธ ๋Š” ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์—†๋‹ค๋Š” ๊ฑฐ์•ผ. ๐Ÿ˜ฅ

์š”์ฆ˜ ๋งŽ์€ ์›น์‚ฌ์ดํŠธ๋“ค์ด React, Vue, Angular ๊ฐ™์€ ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ์‚ฌ์šฉํ•ด์„œ ์ฝ˜ํ…์ธ ๋ฅผ ๋™์ ์œผ๋กœ ๋ Œ๋”๋งํ•˜์ž–์•„? ์ด๋Ÿฐ ์‚ฌ์ดํŠธ๋“ค์€ Jsoup๋งŒ์œผ๋กœ๋Š” ํฌ๋กค๋ง์ด ์–ด๋ ค์›Œ.

๐Ÿค” ๋™์  ํŽ˜์ด์ง€์ธ์ง€ ํ™•์ธํ•˜๋Š” ๋ฐฉ๋ฒ•

1. ๋ธŒ๋ผ์šฐ์ €์—์„œ ํŽ˜์ด์ง€ ์†Œ์Šค ๋ณด๊ธฐ (Ctrl+U)
2. ์›ํ•˜๋Š” ๋ฐ์ดํ„ฐ๊ฐ€ ์†Œ์Šค์— ์žˆ๋Š”์ง€ ํ™•์ธ
3. ์—†๋‹ค๋ฉด? ๋™์  ํŽ˜์ด์ง€์•ผ! ๐ŸŽช

๐Ÿ”ง ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

๋ฐฉ๋ฒ• 1: Selenium ์‚ฌ์šฉํ•˜๊ธฐ
Selenium์€ ์‹ค์ œ ๋ธŒ๋ผ์šฐ์ €๋ฅผ ์ œ์–ดํ•ด์„œ JavaScript๊ฐ€ ์‹คํ–‰๋œ ํ›„์˜ HTML์„ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์–ด. ํ•˜์ง€๋งŒ Jsoup๋ณด๋‹ค ๋А๋ฆฌ๊ณ  ๋ฌด๊ฑฐ์›Œ.

// Selenium ์˜ˆ์ œ (์ฐธ๊ณ ์šฉ)
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;

WebDriver driver = new ChromeDriver();
driver.get("https://dynamic-site.com");

// JavaScript ์‹คํ–‰ ๋Œ€๊ธฐ
Thread.sleep(3000);

// ๋ Œ๋”๋ง๋œ HTML ๊ฐ€์ ธ์˜ค๊ธฐ
String html = driver.getPageSource();

// Jsoup์œผ๋กœ ํŒŒ์‹ฑ
Document doc = Jsoup.parse(html);

driver.quit();

๋ฐฉ๋ฒ• 2: API ์ง์ ‘ ํ˜ธ์ถœํ•˜๊ธฐ
๋งŽ์€ ๋™์  ์‚ฌ์ดํŠธ๋“ค์ด ๋‚ด๋ถ€์ ์œผ๋กœ API๋ฅผ ์‚ฌ์šฉํ•ด. ๋ธŒ๋ผ์šฐ์ € ๊ฐœ๋ฐœ์ž ๋„๊ตฌ์˜ Network ํƒญ์„ ์—ด์–ด์„œ API ์—”๋“œํฌ์ธํŠธ๋ฅผ ์ฐพ์•„๋ณด์ž. API๋ฅผ ์ง์ ‘ ํ˜ธ์ถœํ•˜๋ฉด ํ›จ์”ฌ ํšจ์œจ์ ์ด์•ผ! ๐Ÿš€

// API ์ง์ ‘ ํ˜ธ์ถœ ์˜ˆ์ œ
import java.net.http.*;

HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
        .uri(URI.create("https://api.example.com/articles"))
        .header("User-Agent", "Mozilla/5.0")
        .build();

HttpResponse<string> response = client.send(request, 
        HttpResponse.BodyHandlers.ofString());

// JSON ํŒŒ์‹ฑ
String jsonData = response.body();
// Gson์ด๋‚˜ Jackson์œผ๋กœ ํŒŒ์‹ฑ...</string>
๐Ÿ’ก ํ”„๋กœ ํŒ: API๋ฅผ ์ฐพ๋Š” ๊ฒŒ ๊ฐ€์žฅ ์ข‹์€ ๋ฐฉ๋ฒ•์ด์•ผ. ํฌ๋กค๋ง๋ณด๋‹ค ๋น ๋ฅด๊ณ , ์„œ๋ฒ„ ๋ถ€๋‹ด๋„ ์ ๊ณ , ๋ฐ์ดํ„ฐ๋„ ๊น”๋”ํ•˜๊ฒŒ ์ •๋ฆฌ๋˜์–ด ์žˆ๊ฑฐ๋“ . ๊ฐœ๋ฐœ์ž ๋„๊ตฌ์˜ Network ํƒญ์—์„œ XHR์ด๋‚˜ Fetch ์š”์ฒญ์„ ํ™•์ธํ•ด๋ด! ๐ŸŽฏ

โš–๏ธ ํฌ๋กค๋ง ์œค๋ฆฌ์™€ ๋ฒ•์  ๊ณ ๋ ค์‚ฌํ•ญ

๊ธฐ์ˆ ์ ์œผ๋กœ ๊ฐ€๋Šฅํ•˜๋‹ค๊ณ  ํ•ด์„œ ๋‹ค ํ•ด๋„ ๋˜๋Š” ๊ฑด ์•„๋‹ˆ์•ผ. ํฌ๋กค๋งํ•  ๋•Œ ๊ผญ ์ง€์ผœ์•ผ ํ•  ์œค๋ฆฌ์™€ ๋ฒ•์  ์‚ฌํ•ญ๋“ค์„ ์•Œ์•„๋ณด์ž. ๐ŸŽ“

๐Ÿ“œ robots.txt ์ค€์ˆ˜ํ•˜๊ธฐ

robots.txt๋Š” ์›น์‚ฌ์ดํŠธ๊ฐ€ ํฌ๋กค๋Ÿฌ์—๊ฒŒ ๋ณด๋‚ด๋Š” ์ผ์ข…์˜ ๊ฐ€์ด๋“œ๋ผ์ธ์ด์•ผ. ์˜ˆ๋ฅผ ๋“ค์–ด:

User-agent: *
Disallow: /admin/
Disallow: /private/
Crawl-delay: 10

User-agent: Googlebot
Allow: /

์ด ํŒŒ์ผ์€ "admin๊ณผ private ํด๋”๋Š” ํฌ๋กค๋งํ•˜์ง€ ๋งˆ์„ธ์š”. ๊ทธ๋ฆฌ๊ณ  ์š”์ฒญ ์‚ฌ์ด์— 10์ดˆ์”ฉ ๊ธฐ๋‹ค๋ ค์ฃผ์„ธ์š”"๋ผ๊ณ  ๋งํ•˜๊ณ  ์žˆ์–ด. ์ด๊ฑธ ๋ฌด์‹œํ•˜๋ฉด ๋ฒ•์  ๋ฌธ์ œ๊ฐ€ ๋  ์ˆ˜ ์žˆ์–ด! โš ๏ธ

๐Ÿšซ ํ•˜์ง€ ๋ง์•„์•ผ ํ•  ๊ฒƒ๋“ค

1. ๊ฐœ์ธ์ •๋ณด ์ˆ˜์ง‘ ๊ธˆ์ง€
์ด๋ฉ”์ผ, ์ „ํ™”๋ฒˆํ˜ธ, ์ฃผ์†Œ ๊ฐ™์€ ๊ฐœ์ธ์ •๋ณด๋ฅผ ๋ฌด๋‹จ์œผ๋กœ ์ˆ˜์ง‘ํ•˜๋ฉด ๊ฐœ์ธ์ •๋ณด๋ณดํ˜ธ๋ฒ• ์œ„๋ฐ˜์ด์•ผ. ์ ˆ๋Œ€ ํ•˜๋ฉด ์•ˆ ๋ผ! ๐Ÿ™…

2. ์ €์ž‘๊ถŒ ์นจํ•ด ์ฃผ์˜
๋‰ด์Šค ๊ธฐ์‚ฌ, ์ด๋ฏธ์ง€, ๋™์˜์ƒ ๋“ฑ์€ ์ €์ž‘๊ถŒ์ด ์žˆ์–ด. ๋‹จ์ˆœํžˆ ๋งํฌ๋ฅผ ๋ชจ์œผ๋Š” ๊ฑด ๊ดœ์ฐฎ์ง€๋งŒ, ์ฝ˜ํ…์ธ  ์ž์ฒด๋ฅผ ๋ณต์‚ฌํ•ด์„œ ์žฌ๋ฐฐํฌํ•˜๋ฉด ๋ฌธ์ œ๊ฐ€ ๋ผ.

3. ์„œ๋ฒ„ ๊ณผ๋ถ€ํ•˜ ์œ ๋ฐœ ๊ธˆ์ง€
์ดˆ๋‹น ์ˆ˜๋ฐฑ ๊ฐœ์˜ ์š”์ฒญ์„ ๋ณด๋‚ด๋ฉด ์„œ๋ฒ„๊ฐ€ ๋‹ค์šด๋  ์ˆ˜ ์žˆ์–ด. ์ด๊ฑด DDoS ๊ณต๊ฒฉ์œผ๋กœ ๊ฐ„์ฃผ๋  ์ˆ˜ ์žˆ์–ด. ํ•ญ์ƒ ์ ์ ˆํ•œ ๋”œ๋ ˆ์ด๋ฅผ ๋„ฃ์ž! โฑ๏ธ

4. ์ด์šฉ์•ฝ๊ด€ ํ™•์ธ
๋งŽ์€ ์›น์‚ฌ์ดํŠธ๊ฐ€ ์ด์šฉ์•ฝ๊ด€์—์„œ ํฌ๋กค๋ง์„ ๊ธˆ์ง€ํ•˜๊ณ  ์žˆ์–ด. ํŠนํžˆ ์†Œ์…œ๋ฏธ๋””์–ด ํ”Œ๋žซํผ๋“ค์€ ์—„๊ฒฉํ•ด. ๊ณต์‹ API๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒŒ ์•ˆ์ „ํ•ด.

โœ… ์•ˆ์ „ํ•œ ํฌ๋กค๋ง ์ฒดํฌ๋ฆฌ์ŠคํŠธ โ–ก robots.txt ํ™•์ธํ–ˆ๋‚˜์š”?
โ–ก ์ด์šฉ์•ฝ๊ด€์„ ์ฝ์–ด๋ดค๋‚˜์š”?
โ–ก ์ ์ ˆํ•œ ๋”œ๋ ˆ์ด๋ฅผ ์„ค์ •ํ–ˆ๋‚˜์š”? (์ตœ์†Œ 1์ดˆ)
โ–ก User-Agent๋ฅผ ์„ค์ •ํ–ˆ๋‚˜์š”?
โ–ก ๊ฐœ์ธ์ •๋ณด๋ฅผ ์ˆ˜์ง‘ํ•˜์ง€ ์•Š๋‚˜์š”?
โ–ก ์ƒ์—…์  ๋ชฉ์ ์ด๋ผ๋ฉด ๋ฒ•๋ฅ  ์ž๋ฌธ์„ ๋ฐ›์•˜๋‚˜์š”?
โ–ก ๊ณต์‹ API๊ฐ€ ์žˆ๋Š”์ง€ ํ™•์ธํ–ˆ๋‚˜์š”?

๋ชจ๋‘ ์ฒดํฌํ–ˆ๋‹ค๋ฉด ์•ˆ์ „ํ•˜๊ฒŒ ํฌ๋กค๋งํ•  ์ค€๋น„๊ฐ€ ๋œ ๊ฑฐ์•ผ! โœจ

๐Ÿ—๏ธ ์‹ค์ „ ํ”„๋กœ์ ํŠธ: ๋‰ด์Šค ํฌ๋กค๋Ÿฌ ๋งŒ๋“ค๊ธฐ

์ด์ œ ๋ฐฐ์šด ๋‚ด์šฉ์„ ์ข…ํ•ฉํ•ด์„œ ์‹ค์ „ ํ”„๋กœ์ ํŠธ๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž! ์—ฌ๋Ÿฌ ๋‰ด์Šค ์‚ฌ์ดํŠธ์—์„œ ๊ธฐ์‚ฌ๋ฅผ ์ˆ˜์ง‘ํ•˜๋Š” ํฌ๋กค๋Ÿฌ์•ผ. ๐Ÿ“ฐ

๐Ÿ“ ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ

news-crawler/
โ”œโ”€โ”€ src/
โ”‚   โ”œโ”€โ”€ crawler/
โ”‚   โ”‚   โ”œโ”€โ”€ NewsCrawler.java
โ”‚   โ”‚   โ”œโ”€โ”€ CrawlerConfig.java
โ”‚   โ”‚   โ””โ”€โ”€ RateLimiter.java
โ”‚   โ”œโ”€โ”€ model/
โ”‚   โ”‚   โ””โ”€โ”€ Article.java
โ”‚   โ”œโ”€โ”€ parser/
โ”‚   โ”‚   โ”œโ”€โ”€ Parser.java
โ”‚   โ”‚   โ””โ”€โ”€ SiteParser.java
โ”‚   โ”œโ”€โ”€ storage/
โ”‚   โ”‚   โ”œโ”€โ”€ CsvStorage.java
โ”‚   โ”‚   โ””โ”€โ”€ DatabaseStorage.java
โ”‚   โ””โ”€โ”€ Main.java
โ””โ”€โ”€ pom.xml

๐Ÿ“ Article ๋ชจ๋ธ

package model;

import java.time.LocalDateTime;

public class Article {
    private String title;
    private String content;
    private String author;
    private String url;
    private String source;
    private LocalDateTime publishedDate;
    private LocalDateTime crawledDate;
    
    public Article() {
        this.crawledDate = LocalDateTime.now();
    }
    
    // Getters and Setters
    public String getTitle() { return title; }
    public void setTitle(String title) { this.title = title; }
    
    public String getContent() { return content; }
    public void setContent(String content) { this.content = content; }
    
    public String getAuthor() { return author; }
    public void setAuthor(String author) { this.author = author; }
    
    public String getUrl() { return url; }
    public void setUrl(String url) { this.url = url; }
    
    public String getSource() { return source; }
    public void setSource(String source) { this.source = source; }
    
    public LocalDateTime getPublishedDate() { return publishedDate; }
    public void setPublishedDate(LocalDateTime publishedDate) { 
        this.publishedDate = publishedDate; 
    }
    
    public LocalDateTime getCrawledDate() { return crawledDate; }
    
    @Override
    public String toString() {
        return String.format("[%s] %s - %s", source, title, author);
    }
}

โš™๏ธ CrawlerConfig

package crawler;

public class CrawlerConfig {
    private int timeout = 5000;
    private int retryCount = 3;
    private int delayBetweenRequests = 2000;
    private String userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36";
    
    public int getTimeout() { return timeout; }
    public void setTimeout(int timeout) { this.timeout = timeout; }
    
    public int getRetryCount() { return retryCount; }
    public void setRetryCount(int retryCount) { this.retryCount = retryCount; }
    
    public int getDelayBetweenRequests() { return delayBetweenRequests; }
    public void setDelayBetweenRequests(int delay) { 
        this.delayBetweenRequests = delay; 
    }
    
    public String getUserAgent() { return userAgent; }
    public void setUserAgent(String userAgent) { this.userAgent = userAgent; }
}

๐ŸŽฏ SiteParser ์ธํ„ฐํŽ˜์ด์Šค

package parser;

import model.Article;
import org.jsoup.nodes.Document;
import java.util.List;

public interface SiteParser {
    String getSiteName();
    List<string> getArticleUrls(Document doc);
    Article parseArticle(Document doc);
}</string>

๐Ÿ“ฐ ๊ตฌ์ฒด์ ์ธ ํŒŒ์„œ ๊ตฌํ˜„

package parser;

import model.Article;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import java.util.ArrayList;
import java.util.List;

public class ExampleNewsParser implements SiteParser {
    
    @Override
    public String getSiteName() {
        return "Example News";
    }
    
    @Override
    public List<string> getArticleUrls(Document doc) {
        List<string> urls = new ArrayList<>();
        Elements links = doc.select("div.article-list a.article-link");
        
        for (Element link : links) {
            String url = link.absUrl("href");
            if (!url.isEmpty()) {
                urls.add(url);
            }
        }
        
        return urls;
    }
    
    @Override
    public Article parseArticle(Document doc) {
        Article article = new Article();
        article.setSource(getSiteName());
        
        // ์ œ๋ชฉ ์ถ”์ถœ
        Element titleElement = doc.selectFirst("h1.article-title");
        if (titleElement != null) {
            article.setTitle(titleElement.text());
        }
        
        // ์ž‘์„ฑ์ž ์ถ”์ถœ
        Element authorElement = doc.selectFirst("span.author-name");
        if (authorElement != null) {
            article.setAuthor(authorElement.text());
        }
        
        // ๋ณธ๋ฌธ ์ถ”์ถœ
        Element contentElement = doc.selectFirst("div.article-content");
        if (contentElement != null) {
            article.setContent(contentElement.text());
        }
        
        // URL ์„ค์ •
        article.setUrl(doc.location());
        
        return article;
    }
}</string></string>

๐Ÿš€ ๋ฉ”์ธ ํฌ๋กค๋Ÿฌ

package crawler;

import model.Article;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import parser.SiteParser;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

public class NewsCrawler {
    private CrawlerConfig config;
    private SiteParser parser;
    
    public NewsCrawler(CrawlerConfig config, SiteParser parser) {
        this.config = config;
        this.parser = parser;
    }
    
    public List</code><article><code> crawl(String listPageUrl) {
        List</code><article><code> articles = new ArrayList<>();
        
        try {
            System.out.println("๋ชฉ๋ก ํŽ˜์ด์ง€ ํฌ๋กค๋ง: " + listPageUrl);
            Document listDoc = fetchDocument(listPageUrl);
            List<string> articleUrls = parser.getArticleUrls(listDoc);
            
            System.out.println(articleUrls.size() + "๊ฐœ์˜ ๊ธฐ์‚ฌ URL ๋ฐœ๊ฒฌ");
            
            for (String url : articleUrls) {
                try {
                    System.out.println("๊ธฐ์‚ฌ ํฌ๋กค๋ง: " + url);
                    Document articleDoc = fetchDocument(url);
                    Article article = parser.parseArticle(articleDoc);
                    articles.add(article);
                    
                    // ๋”œ๋ ˆ์ด
                    TimeUnit.MILLISECONDS.sleep(config.getDelayBetweenRequests());
                    
                } catch (Exception e) {
                    System.err.println("๊ธฐ์‚ฌ ํฌ๋กค๋ง ์‹คํŒจ: " + url);
                    e.printStackTrace();
                }
            }
            
        } catch (Exception e) {
            System.err.println("๋ชฉ๋ก ํŽ˜์ด์ง€ ํฌ๋กค๋ง ์‹คํŒจ");
            e.printStackTrace();
        }
        
        return articles;
    }
    
    private Document fetchDocument(String url) throws IOException {
        int attempts = 0;
        
        while (attempts < config.getRetryCount()) {
            try {
                return Jsoup.connect(url)
                        .userAgent(config.getUserAgent())
                        .timeout(config.getTimeout())
                        .get();
                        
            } catch (IOException e) {
                attempts++;
                if (attempts >= config.getRetryCount()) {
                    throw e;
                }
                
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                    throw new IOException("์žฌ์‹œ๋„ ์ค‘ ์ธํ„ฐ๋ŸฝํŠธ", ie);
                }
            }
        }
        
        throw new IOException("๋ฌธ์„œ ๊ฐ€์ ธ์˜ค๊ธฐ ์‹คํŒจ");
    }
}</string></code></article></article></pre>
</div>

<h3>๐ŸŽฌ Main ํด๋ž˜์Šค</h3>

<div class="tx-code-block">
<pre><code>import crawler.CrawlerConfig;
import crawler.NewsCrawler;
import model.Article;
import parser.ExampleNewsParser;
import storage.CsvStorage;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        System.out.println("=== ๋‰ด์Šค ํฌ๋กค๋Ÿฌ ์‹œ์ž‘ ===\n");
        
        // ์„ค์ •
        CrawlerConfig config = new CrawlerConfig();
        config.setTimeout(5000);
        config.setDelayBetweenRequests(2000);
        
        // ํŒŒ์„œ ์„ค์ •
        ExampleNewsParser parser = new ExampleNewsParser();
        
        // ํฌ๋กค๋Ÿฌ ์ƒ์„ฑ
        NewsCrawler crawler = new NewsCrawler(config, parser);
        
        // ํฌ๋กค๋ง ์‹คํ–‰
        String targetUrl = "https://example-news.com/latest";
        List</code><article><code> articles = crawler.crawl(targetUrl);
        
        // ๊ฒฐ๊ณผ ์ถœ๋ ฅ
        System.out.println("\n=== ํฌ๋กค๋ง ์™„๋ฃŒ ===");
        System.out.println("์ด " + articles.size() + "๊ฐœ์˜ ๊ธฐ์‚ฌ ์ˆ˜์ง‘");
        
        for (Article article : articles) {
            System.out.println(article);
        }
        
        // CSV๋กœ ์ €์žฅ
        CsvStorage storage = new CsvStorage();
        storage.save(articles, "news_articles.csv");
        
        System.out.println("\nํ”„๋กœ๊ทธ๋žจ ์ข…๋ฃŒ");
    }
}</code></article></pre>
</div>

<p>
์™€! ๊ฝค ๋ณธ๊ฒฉ์ ์ธ ํ”„๋กœ์ ํŠธ๊ฐ€ ๋์ง€? ๐Ÿ˜Ž ์ด ๊ตฌ์กฐ๋Š” ํ™•์žฅํ•˜๊ธฐ๋„ ์‰ฌ์›Œ. ์ƒˆ๋กœ์šด ๋‰ด์Šค ์‚ฌ์ดํŠธ๋ฅผ ์ถ”๊ฐ€ํ•˜๋ ค๋ฉด SiteParser ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•˜๋Š” ์ƒˆ ํด๋ž˜์Šค๋งŒ ๋งŒ๋“ค๋ฉด ๋ผ!
</p>

<hr class="tx-divider">

<h2>๐Ÿ› ๋””๋ฒ„๊น…๊ณผ ๋ฌธ์ œ ํ•ด๊ฒฐ</h2>

<p>
ํฌ๋กค๋งํ•˜๋‹ค ๋ณด๋ฉด ์—ฌ๋Ÿฌ ๋ฌธ์ œ๋ฅผ ๋งŒ๋‚˜๊ฒŒ ๋ผ. ์ž์ฃผ ๋ฐœ์ƒํ•˜๋Š” ๋ฌธ์ œ๋“ค๊ณผ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ณด์ž. ๐Ÿ”ง
</p>

<h3>โŒ ์ž์ฃผ ๋งŒ๋‚˜๋Š” ์—๋Ÿฌ๋“ค</h3>

<p>
<strong>1. HTTP 403 Forbidden</strong><br>
์„œ๋ฒ„๊ฐ€ ์ ‘๊ทผ์„ ๊ฑฐ๋ถ€ํ•˜๋Š” ๊ฑฐ์•ผ. User-Agent๋ฅผ ์„ค์ •ํ•˜์ง€ ์•Š์•˜๊ฑฐ๋‚˜, ๋ด‡์œผ๋กœ ์ธ์‹๋์„ ๊ฐ€๋Šฅ์„ฑ์ด ๋†’์•„.
</p>

<div class="tx-code-block">
<pre><code>// ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•
Document doc = Jsoup.connect(url)
    .userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
    .referrer("https://www.google.com") // Referrer๋„ ์ถ”๊ฐ€
    .get();

2. SocketTimeoutException
์„œ๋ฒ„ ์‘๋‹ต์ด ๋„ˆ๋ฌด ๋А๋ ค์„œ ํƒ€์ž„์•„์›ƒ์ด ๋ฐœ์ƒํ•œ ๊ฑฐ์•ผ. ํƒ€์ž„์•„์›ƒ ์‹œ๊ฐ„์„ ๋Š˜๋ ค๋ณด์ž.

// ํƒ€์ž„์•„์›ƒ 10์ดˆ๋กœ ์ฆ๊ฐ€
Document doc = Jsoup.connect(url)
    .timeout(10000)
    .get();

3. ์›ํ•˜๋Š” ์š”์†Œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Œ
์…€๋ ‰ํ„ฐ๊ฐ€ ์ž˜๋ชป๋๊ฑฐ๋‚˜, ํŽ˜์ด์ง€ ๊ตฌ์กฐ๊ฐ€ ๋ณ€๊ฒฝ๋์„ ์ˆ˜ ์žˆ์–ด. ๋ธŒ๋ผ์šฐ์ € ๊ฐœ๋ฐœ์ž ๋„๊ตฌ๋กœ ๋‹ค์‹œ ํ™•์ธํ•ด๋ด!

// ๋ฐฉ์–ด์  ์ฝ”๋”ฉ
Element titleElement = doc.selectFirst("h1.title");
if (titleElement != null) {
    String title = titleElement.text();
} else {
    System.err.println("์ œ๋ชฉ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.");
    // ๋Œ€์ฒด ์…€๋ ‰ํ„ฐ ์‹œ๋„
    titleElement = doc.selectFirst("h1");
}

4. ํ•œ๊ธ€ ๊นจ์ง ํ˜„์ƒ
์ธ์ฝ”๋”ฉ ๋ฌธ์ œ์•ผ. UTF-8๋กœ ๋ช…์‹œ์ ์œผ๋กœ ์„ค์ •ํ•ด์ฃผ์ž.

Document doc = Jsoup.connect(url)
    .userAgent("Mozilla/5.0")
    .get();

// ๋˜๋Š” HTML ๋ฌธ์ž์—ด์„ ํŒŒ์‹ฑํ•  ๋•Œ
Document doc = Jsoup.parse(html, "UTF-8");

๐Ÿ” ๋””๋ฒ„๊น… ํŒ

1. HTML ๊ตฌ์กฐ ์ถœ๋ ฅํ•˜๊ธฐ
๋ฌธ์ œ๊ฐ€ ์ƒ๊ธฐ๋ฉด ์‹ค์ œ HTML์„ ํ™•์ธํ•ด๋ด.

// ์ „์ฒด HTML ์ถœ๋ ฅ
System.out.println(doc.html());

// ํŠน์ • ์š”์†Œ๋งŒ ์ถœ๋ ฅ
Element content = doc.selectFirst("div.content");
if (content != null) {
    System.out.println(content.html());
}

2. ๋กœ๊น… ์ถ”๊ฐ€ํ•˜๊ธฐ
์–ด๋””์„œ ๋ฌธ์ œ๊ฐ€ ์ƒ๊ธฐ๋Š”์ง€ ์ถ”์ ํ•˜์ž.

import java.util.logging.*;

Logger logger = Logger.getLogger(NewsCrawler.class.getName());

logger.info("ํฌ๋กค๋ง ์‹œ์ž‘: " + url);
logger.warning("์š”์†Œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Œ: " + selector);
logger.severe("ํฌ๋กค๋ง ์‹คํŒจ: " + e.getMessage());

3. HTML ํŒŒ์ผ๋กœ ์ €์žฅํ•˜๊ธฐ
๋‚˜์ค‘์— ๋ถ„์„ํ•˜๊ธฐ ์œ„ํ•ด HTML์„ ํŒŒ์ผ๋กœ ์ €์žฅํ•ด๋‘๋ฉด ํŽธํ•ด.

import java.nio.file.*;

String html = doc.html();
Files.writeString(
    Paths.get("debug_page.html"), 
    html, 
    StandardCharsets.UTF_8
);

โšก ์„ฑ๋Šฅ ์ตœ์ ํ™”

๋Œ€๋Ÿ‰์˜ ๋ฐ์ดํ„ฐ๋ฅผ ํฌ๋กค๋งํ•  ๋•Œ๋Š” ์„ฑ๋Šฅ์ด ์ค‘์š”ํ•ด. ๋ช‡ ๊ฐ€์ง€ ์ตœ์ ํ™” ๊ธฐ๋ฒ•์„ ์•Œ์•„๋ณด์ž! ๐Ÿš€

๐Ÿ”€ ๋ฉ€ํ‹ฐ์Šค๋ ˆ๋”ฉ ํ™œ์šฉํ•˜๊ธฐ

์—ฌ๋Ÿฌ ํŽ˜์ด์ง€๋ฅผ ๋™์‹œ์— ํฌ๋กค๋งํ•˜๋ฉด ์‹œ๊ฐ„์„ ํฌ๊ฒŒ ๋‹จ์ถ•ํ•  ์ˆ˜ ์žˆ์–ด. ExecutorService๋ฅผ ์‚ฌ์šฉํ•ด๋ณด์ž.

import java.util.concurrent.*;
import java.util.List;
import java.util.ArrayList;

public class ParallelCrawler {
    private static final int THREAD_POOL_SIZE = 5;
    
    public List</code><article><code> crawlParallel(List<string> urls) {
        ExecutorService executor = Executors.newFixedThreadPool(THREAD_POOL_SIZE);
        List<future<article>> futures = new ArrayList<>();
        
        // ๊ฐ URL์„ ๋ณ„๋„ ์Šค๋ ˆ๋“œ์—์„œ ์ฒ˜๋ฆฌ
        for (String url : urls) {
            Future</future<article></string></code><article><code> future = executor.submit(() -> {
                try {
                    Document doc = Jsoup.connect(url)
                            .userAgent("Mozilla/5.0")
                            .timeout(5000)
                            .get();
                    return parseArticle(doc);
                } catch (Exception e) {
                    System.err.println("ํฌ๋กค๋ง ์‹คํŒจ: " + url);
                    return null;
                }
            });
            futures.add(future);
        }
        
        // ๊ฒฐ๊ณผ ์ˆ˜์ง‘
        List</code><article><code> articles = new ArrayList<>();
        for (Future</code><article><code> future : futures) {
            try {
                Article article = future.get(10, TimeUnit.SECONDS);
                if (article != null) {
                    articles.add(article);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        
        executor.shutdown();
        return articles;
    }
    
    private Article parseArticle(Document doc) {
        // ํŒŒ์‹ฑ ๋กœ์ง
        return new Article();
    }
}</code></article></article></article></article></pre>
</div>

<div class="tx-warning">
<strong>โš ๏ธ ์ฃผ์˜:</strong> ๋ฉ€ํ‹ฐ์Šค๋ ˆ๋”ฉ์„ ์‚ฌ์šฉํ•  ๋•Œ๋Š” ์„œ๋ฒ„์— ๊ณผ๋ถ€ํ•˜๋ฅผ ์ฃผ์ง€ ์•Š๋„๋ก ์Šค๋ ˆ๋“œ ์ˆ˜๋ฅผ ์ ์ ˆํžˆ ์ œํ•œํ•ด์•ผ ํ•ด. ๋ณดํ†ต 5~10๊ฐœ ์ •๋„๊ฐ€ ์ ๋‹นํ•ด. ๊ทธ๋ฆฌ๊ณ  ๊ฐ ์Šค๋ ˆ๋“œ์—์„œ๋„ ๋”œ๋ ˆ์ด๋ฅผ ๋„ฃ๋Š” ๊ฑธ ์žŠ์ง€ ๋งˆ!
</div>

<h3>๐Ÿ’พ ์บ์‹ฑ ํ™œ์šฉํ•˜๊ธฐ</h3>

<p>
๊ฐ™์€ ํŽ˜์ด์ง€๋ฅผ ์—ฌ๋Ÿฌ ๋ฒˆ ์š”์ฒญํ•˜์ง€ ์•Š๋„๋ก ์บ์‹ฑ์„ ๊ตฌํ˜„ํ•˜์ž.
</p>

<div class="tx-code-block">
<pre><code>import java.util.concurrent.ConcurrentHashMap;
import java.util.Map;

public class CachedCrawler {
    private Map<string, document=""> cache = new ConcurrentHashMap<>();
    
    public Document fetchWithCache(String url) throws IOException {
        // ์บ์‹œ์— ์žˆ์œผ๋ฉด ๋ฐ˜ํ™˜
        if (cache.containsKey(url)) {
            System.out.println("์บ์‹œ์—์„œ ๋กœ๋“œ: " + url);
            return cache.get(url);
        }
        
        // ์—†์œผ๋ฉด ํฌ๋กค๋ง
        System.out.println("์ƒˆ๋กœ ํฌ๋กค๋ง: " + url);
        Document doc = Jsoup.connect(url)
                .userAgent("Mozilla/5.0")
                .timeout(5000)
                .get();
        
        // ์บ์‹œ์— ์ €์žฅ
        cache.put(url, doc);
        return doc;
    }
    
    public void clearCache() {
        cache.clear();
    }
}</string,>

๐Ÿ“Š ์ง„ํ–‰ ์ƒํ™ฉ ๋ชจ๋‹ˆํ„ฐ๋ง

๋Œ€๋Ÿ‰ ํฌ๋กค๋งํ•  ๋•Œ๋Š” ์ง„ํ–‰ ์ƒํ™ฉ์„ ๋ณผ ์ˆ˜ ์žˆ์œผ๋ฉด ์ข‹์•„.

public class ProgressTracker {
    private int total;
    private int completed;
    private int failed;
    
    public ProgressTracker(int total) {
        this.total = total;
        this.completed = 0;
        this.failed = 0;
    }
    
    public synchronized void incrementCompleted() {
        completed++;
        printProgress();
    }
    
    public synchronized void incrementFailed() {
        failed++;
        printProgress();
    }
    
    private void printProgress() {
        int percentage = (int) ((completed + failed) * 100.0 / total);
        System.out.printf("\r์ง„ํ–‰๋ฅ : %d%% (%d/%d ์™„๋ฃŒ, %d ์‹คํŒจ)", 
                percentage, completed, total, failed);
    }
}

๐ŸŽ“ ๊ณ ๊ธ‰ ํ…Œํฌ๋‹‰

์ด์ œ ์ข€ ๋” ๊ณ ๊ธ‰ ๊ธฐ์ˆ ๋“ค์„ ์•Œ์•„๋ณด์ž. ํ”„๋กœ ๊ฐœ๋ฐœ์ž๋“ค์ด ์‚ฌ์šฉํ•˜๋Š” ํ…Œํฌ๋‹‰์ด์•ผ! ๐ŸŒŸ

๐Ÿ” ํ”„๋ก์‹œ ์‚ฌ์šฉํ•˜๊ธฐ

IP ์ฐจ๋‹จ์„ ํ”ผํ•˜๊ฑฐ๋‚˜ ์ง€์—ญ ์ œํ•œ์„ ์šฐํšŒํ•˜๋ ค๋ฉด ํ”„๋ก์‹œ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด.

import java.net.Proxy;
import java.net.InetSocketAddress;

public class ProxyCrawler {
    public Document fetchWithProxy(String url, String proxyHost, int proxyPort) 
            throws IOException {
        
        Proxy proxy = new Proxy(
            Proxy.Type.HTTP, 
            new InetSocketAddress(proxyHost, proxyPort)
        );
        
        Document doc = Jsoup.connect(url)
                .proxy(proxy)
                .userAgent("Mozilla/5.0")
                .timeout(10000)
                .get();
        
        return doc;
    }
}

๐ŸŽญ User-Agent ๋กœํ…Œ์ด์…˜

๋‹ค์–‘ํ•œ User-Agent๋ฅผ ๋ฒˆ๊ฐˆ์•„ ์‚ฌ์šฉํ•˜๋ฉด ๋ด‡ ํƒ์ง€๋ฅผ ํ”ผํ•  ์ˆ˜ ์žˆ์–ด.

import java.util.Random;

public class UserAgentRotator {
    private static final String[] USER_AGENTS = {
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
        "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
        "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36",
        "Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15",
        "Mozilla/5.0 (iPad; CPU OS 14_0 like Mac OS X) AppleWebKit/605.1.15"
    };
    
    private Random random = new Random();
    
    public String getRandomUserAgent() {
        return USER_AGENTS[random.nextInt(USER_AGENTS.length)];
    }
    
    public Document fetch(String url) throws IOException {
        return Jsoup.connect(url)
                .userAgent(getRandomUserAgent())
                .timeout(5000)
                .get();
    }
}

๐Ÿ“ ์ •๊ทœํ‘œํ˜„์‹์œผ๋กœ ๋ฐ์ดํ„ฐ ์ •์ œํ•˜๊ธฐ

ํฌ๋กค๋งํ•œ ๋ฐ์ดํ„ฐ์—๋Š” ๋ถˆํ•„์š”ํ•œ ๊ณต๋ฐฑ์ด๋‚˜ ํŠน์ˆ˜๋ฌธ์ž๊ฐ€ ๋งŽ์•„. ์ •๊ทœํ‘œํ˜„์‹์œผ๋กœ ๊น”๋”ํ•˜๊ฒŒ ์ •์ œํ•˜์ž.

import java.util.regex.*;

public class DataCleaner {
    
    // ์—ฌ๋Ÿฌ ๊ณต๋ฐฑ์„ ํ•˜๋‚˜๋กœ
    public static String normalizeWhitespace(String text) {
        return text.replaceAll("\\s+", " ").trim();
    }
    
    // HTML ํƒœ๊ทธ ์ œ๊ฑฐ
    public static String removeHtmlTags(String text) {
        return text.replaceAll("<[^>]*>", "");
    }
    
    // ์ด๋ฉ”์ผ ์ถ”์ถœ
    public static List<string> extractEmails(String text) {
        List<string> emails = new ArrayList<>();
        Pattern pattern = Pattern.compile(
            "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}"
        );
        Matcher matcher = pattern.matcher(text);
        
        while (matcher.find()) {
            emails.add(matcher.group());
        }
        
        return emails;
    }
    
    // ์ „ํ™”๋ฒˆํ˜ธ ์ถ”์ถœ (ํ•œ๊ตญ)
    public static List<string> extractPhoneNumbers(String text) {
        List<string> phones = new ArrayList<>();
        Pattern pattern = Pattern.compile(
            "0\\d{1,2}-\\d{3,4}-\\d{4}"
        );
        Matcher matcher = pattern.matcher(text);
        
        while (matcher.find()) {
            phones.add(matcher.group());
        }
        
        return phones;
    }
    
    // ๊ฐ€๊ฒฉ ์ถ”์ถœ
    public static List<integer> extractPrices(String text) {
        List<integer> prices = new ArrayList<>();
        Pattern pattern = Pattern.compile(
            "(\\d{1,3}(,\\d{3})*|\\d+)์›"
        );
        Matcher matcher = pattern.matcher(text);
        
        while (matcher.find()) {
            String priceStr = matcher.group(1).replace(",", "");
            prices.add(Integer.parseInt(priceStr));
        }
        
        return prices;
    }
}</integer></integer></string></string></string></string>

๐Ÿ”„ ์ฆ๋ถ„ ํฌ๋กค๋ง ๊ตฌํ˜„ํ•˜๊ธฐ

๋งค๋ฒˆ ์ „์ฒด๋ฅผ ํฌ๋กค๋งํ•˜์ง€ ๋ง๊ณ , ์ƒˆ๋กœ์šด ๊ฒƒ๋งŒ ๊ฐ€์ ธ์˜ค๋Š” ์ฆ๋ถ„ ํฌ๋กค๋ง์„ ๊ตฌํ˜„ํ•ด๋ณด์ž.

import java.time.LocalDateTime;
import java.util.Set;
import java.util.HashSet;

public class IncrementalCrawler {
    private Set<string> crawledUrls = new HashSet<>();
    private LocalDateTime lastCrawlTime;
    
    public List</string></code><article><code> crawlNew(String listPageUrl) {
        List</code><article><code> newArticles = new ArrayList<>();
        
        try {
            Document doc = Jsoup.connect(listPageUrl).get();
            Elements articleLinks = doc.select("a.article-link");
            
            for (Element link : articleLinks) {
                String url = link.absUrl("href");
                
                // ์ด๋ฏธ ํฌ๋กค๋งํ•œ URL์€ ๊ฑด๋„ˆ๋›ฐ๊ธฐ
                if (crawledUrls.contains(url)) {
                    System.out.println("์ด๋ฏธ ํฌ๋กค๋งํ•จ: " + url);
                    continue;
                }
                
                // ์ƒˆ ๊ธฐ์‚ฌ ํฌ๋กค๋ง
                Document articleDoc = Jsoup.connect(url).get();
                Article article = parseArticle(articleDoc);
                
                // ๋งˆ์ง€๋ง‰ ํฌ๋กค๋ง ์ดํ›„์˜ ๊ธฐ์‚ฌ๋งŒ
                if (lastCrawlTime == null || 
                    article.getPublishedDate().isAfter(lastCrawlTime)) {
                    newArticles.add(article);
                    crawledUrls.add(url);
                }
            }
            
            lastCrawlTime = LocalDateTime.now();
            
        } catch (Exception e) {
            e.printStackTrace();
        }
        
        return newArticles;
    }
    
    private Article parseArticle(Document doc) {
        // ํŒŒ์‹ฑ ๋กœ์ง
        return new Article();
    }
    
    public void saveCrawledUrls(String filename) throws IOException {
        Files.write(
            Paths.get(filename), 
            crawledUrls, 
            StandardCharsets.UTF_8
        );
    }
    
    public void loadCrawledUrls(String filename) throws IOException {
        if (Files.exists(Paths.get(filename))) {
            crawledUrls = new HashSet<>(
                Files.readAllLines(Paths.get(filename), StandardCharsets.UTF_8)
            );
        }
    }
}</code></article></article></pre>
</div>

<hr class="tx-divider">

<h2>๐ŸŒ ์‹ค์ œ ์‚ฌ์šฉ ์‚ฌ๋ก€</h2>

<p>
์ด๋ก ๋งŒ ๋ฐฐ์šฐ๋ฉด ์žฌ๋ฏธ์—†์ž–์•„? ์‹ค์ œ๋กœ ์–ด๋–ป๊ฒŒ ํ™œ์šฉ๋˜๋Š”์ง€ ๋ช‡ ๊ฐ€์ง€ ์‚ฌ๋ก€๋ฅผ ๋ณด์ž. ๐Ÿ’ผ
</p>

<h3>๐Ÿ’ฐ ๊ฐ€๊ฒฉ ๋น„๊ต ์„œ๋น„์Šค</h3>

<p>
์—ฌ๋Ÿฌ ์‡ผํ•‘๋ชฐ์˜ ์ƒํ’ˆ ๊ฐ€๊ฒฉ์„ ์ˆ˜์ง‘ํ•ด์„œ ์ตœ์ €๊ฐ€๋ฅผ ์ฐพ์•„์ฃผ๋Š” ์„œ๋น„์Šค์•ผ. ์‹ค์ œ๋กœ ๋งŽ์€ ๊ฐ€๊ฒฉ ๋น„๊ต ์‚ฌ์ดํŠธ๋“ค์ด ์ด๋Ÿฐ ๋ฐฉ์‹์œผ๋กœ ์šด์˜๋ผ.
</p>

<div class="tx-code-block">
<pre><code>public class PriceComparison {
    public List<productprice> comparePrice(String productName) {
        List<productprice> prices = new ArrayList<>();
        
        // ์—ฌ๋Ÿฌ ์‡ผํ•‘๋ชฐ ํฌ๋กค๋ง
        prices.addAll(crawlSiteA(productName));
        prices.addAll(crawlSiteB(productName));
        prices.addAll(crawlSiteC(productName));
        
        // ๊ฐ€๊ฒฉ์ˆœ ์ •๋ ฌ
        prices.sort(Comparator.comparing(ProductPrice::getPrice));
        
        return prices;
    }
    
    private List<productprice> crawlSiteA(String productName) {
        // ์‚ฌ์ดํŠธ A ํฌ๋กค๋ง ๋กœ์ง
        return new ArrayList<>();
    }
}

class ProductPrice {
    private String siteName;
    private String productName;
    private int price;
    private String url;
    
    // getters, setters...
}</productprice></productprice></productprice>

๐Ÿ“Š ๋ถ€๋™์‚ฐ ์‹œ์„ธ ๋ถ„์„

๋ถ€๋™์‚ฐ ์‚ฌ์ดํŠธ์—์„œ ๋งค๋ฌผ ์ •๋ณด๋ฅผ ์ˆ˜์ง‘ํ•ด์„œ ์ง€์—ญ๋ณ„ ์‹œ์„ธ๋ฅผ ๋ถ„์„ํ•  ์ˆ˜ ์žˆ์–ด.

public class RealEstateCrawler {
    public List<property> crawlProperties(String region) {
        List<property> properties = new ArrayList<>();
        
        try {
            String url = "https://realestate-site.com/search?region=" + region;
            Document doc = Jsoup.connect(url).get();
            
            Elements listings = doc.select("div.property-listing");
            
            for (Element listing : listings) {
                Property property = new Property();
                property.setRegion(region);
                property.setType(listing.select(".property-type").text());
                property.setPrice(parsePrice(listing.select(".price").text()));
                property.setArea(parseArea(listing.select(".area").text()));
                property.setFloor(listing.select(".floor").text());
                
                properties.add(property);
            }
            
        } catch (Exception e) {
            e.printStackTrace();
        }
        
        return properties;
    }
    
    private int parsePrice(String priceText) {
        // "3์–ต 5์ฒœ๋งŒ์›" -> 350000000
        // ํŒŒ์‹ฑ ๋กœ์ง...
        return 0;
    }
    
    private double parseArea(String areaText) {
        // "85ใŽก" -> 85.0
        return 0.0;
    }
}</property></property>

๐Ÿ“ฐ ๋‰ด์Šค ๋ชจ๋‹ˆํ„ฐ๋ง ๋ด‡

ํŠน์ • ํ‚ค์›Œ๋“œ๊ฐ€ ํฌํ•จ๋œ ๋‰ด์Šค๋ฅผ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋ชจ๋‹ˆํ„ฐ๋งํ•˜๊ณ  ์•Œ๋ฆผ์„ ๋ณด๋‚ด๋Š” ๋ด‡์ด์•ผ.

import java.util.Timer;
import java.util.TimerTask;

public class NewsMonitorBot {
    private Set notifiedArticles = new HashSet<>();
    private List keywords;
    
    public NewsMonitorBot(List keywords) {
        this.keywords = keywords;
    }
    
    public void startMonitoring(int intervalMinutes) {
        Timer timer = new Timer();
        
        timer.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                checkForNewArticles();
            }
        }, 0, intervalMinutes * 60 * 1000);
    }
    
    private void checkForNewArticles() {
        try {
            List
articles = crawlNews(); for (Article article : articles) { if (notifiedArticles.contains(article.getUrl())) { continue; } // ํ‚ค์›Œ๋“œ ํฌํ•จ ์—ฌ๋ถ€ ํ™•์ธ for (String keyword : keywords) { if (article.getTitle().contains(keyword) || article.getContent().contains(keyword)) { sendNotification(article, keyword); notifiedArticles.add(article.getUrl()); break; } } } } catch (Exception e) { e.printStackTrace(); } } private List
crawlNews() { // ๋‰ด์Šค ํฌ๋กค๋ง ๋กœ์ง return new ArrayList<>(); } private void sendNotification(Article article, String keyword) { System.out.println("๐Ÿ”” ํ‚ค์›Œ๋“œ '" + keyword + "' ๋ฐœ๊ฒฌ!"); System.out.println("์ œ๋ชฉ: " + article.getTitle()); System.out.println("URL: " + article.getUrl()); // ์ด๋ฉ”์ผ, ์Šฌ๋ž™, ํ…”๋ ˆ๊ทธ๋žจ ๋“ฑ์œผ๋กœ ์•Œ๋ฆผ ์ „์†ก } }

์ด๋Ÿฐ ํ”„๋กœ์ ํŠธ๋“ค์„ ๋งŒ๋“ค์–ด์„œ **์žฌ๋Šฅ๋„ท** ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ ์„œ๋น„์Šค๋กœ ์ œ๊ณตํ•  ์ˆ˜๋„ ์žˆ์–ด. ์‹ค์ œ๋กœ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ์ž๋™ํ™” ์„œ๋น„์Šค๋Š” ์ˆ˜์š”๊ฐ€ ๋งŽ๊ฑฐ๋“ ! ๐Ÿ’ก


๐ŸŽฏ ๋งˆ๋ฌด๋ฆฌํ•˜๋ฉฐ

์™€! ์ •๋ง ๊ธด ์—ฌ์ •์ด์—ˆ์ง€? ๐Ÿ˜… ์šฐ๋ฆฌ๋Š” ์›น ํฌ๋กค๋ง์˜ ๊ธฐ์ดˆ๋ถ€ํ„ฐ ๊ณ ๊ธ‰ ํ…Œํฌ๋‹‰๊นŒ์ง€ ๋ชจ๋‘ ์‚ดํŽด๋ดค์–ด. Jsoup์„ ์‚ฌ์šฉํ•˜๋ฉด ์ž๋ฐ”๋กœ๋„ ์‰ฝ๊ณ  ๊ฐ•๋ ฅํ•˜๊ฒŒ ์›น ํฌ๋กค๋ง์„ ํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฑธ ์•Œ์•˜์„ ๊ฑฐ์•ผ.

๐ŸŽ“ ํ•ต์‹ฌ ์ •๋ฆฌ 1. Jsoup ๊ธฐ๋ณธ
โ€ข HTML ํŒŒ์‹ฑ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ
โ€ข CSS ์…€๋ ‰ํ„ฐ ์ง€์›
โ€ข ๊ฐ„๋‹จํ•œ API

2. ํฌ๋กค๋ง ๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค
โ€ข robots.txt ์ค€์ˆ˜
โ€ข ์ ์ ˆํ•œ ๋”œ๋ ˆ์ด ์„ค์ •
โ€ข User-Agent ์„ค์ •
โ€ข ์—๋Ÿฌ ์ฒ˜๋ฆฌ ๋ฐ ์žฌ์‹œ๋„

3. ๊ณ ๊ธ‰ ํ…Œํฌ๋‹‰
โ€ข ๋ฉ€ํ‹ฐ์Šค๋ ˆ๋”ฉ
โ€ข ์บ์‹ฑ
โ€ข ํ”„๋ก์‹œ ์‚ฌ์šฉ
โ€ข ์ฆ๋ถ„ ํฌ๋กค๋ง

4. ๋ฒ•์ /์œค๋ฆฌ์  ๊ณ ๋ ค์‚ฌํ•ญ
โ€ข ๊ฐœ์ธ์ •๋ณด ๋ณดํ˜ธ
โ€ข ์ €์ž‘๊ถŒ ์กด์ค‘
โ€ข ์„œ๋ฒ„ ๋ถ€ํ•˜ ์ตœ์†Œํ™”
โ€ข ์ด์šฉ์•ฝ๊ด€ ์ค€์ˆ˜

๐Ÿš€ ๋‹ค์Œ ๋‹จ๊ณ„

์ด์ œ ์—ฌ๋Ÿฌ๋ถ„์€ ์›น ํฌ๋กค๋ง์˜ ๊ธฐ๋ณธ์„ ๋งˆ์Šคํ„ฐํ–ˆ์–ด! ๋‹ค์Œ ๋‹จ๊ณ„๋กœ ๋‚˜์•„๊ฐ€๋ ค๋ฉด:

1. ์‹ค์ „ ํ”„๋กœ์ ํŠธ ๋งŒ๋“ค๊ธฐ
๋ฐฐ์šด ๋‚ด์šฉ์„ ํ™œ์šฉํ•ด์„œ ์‹ค์ œ ํ”„๋กœ์ ํŠธ๋ฅผ ๋งŒ๋“ค์–ด๋ด. ํฌํŠธํด๋ฆฌ์˜ค๋กœ๋„ ์ข‹๊ณ , ์‹ค์ œ ์„œ๋น„์Šค๋กœ ๋งŒ๋“ค ์ˆ˜๋„ ์žˆ์–ด! ๐ŸŽจ

2. Selenium ๋ฐฐ์šฐ๊ธฐ
๋™์  ์›นํŽ˜์ด์ง€๋ฅผ ํฌ๋กค๋งํ•˜๋ ค๋ฉด Selenium์„ ๋ฐฐ์›Œ์•ผ ํ•ด. Jsoup๊ณผ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๋ฉด ์™„๋ฒฝํ•˜์ง€! ๐Ÿค

3. ์Šค์ผ€์ค„๋ง ์ถ”๊ฐ€ํ•˜๊ธฐ
Quartz๋‚˜ Spring Scheduler๋ฅผ ์‚ฌ์šฉํ•ด์„œ ์ •๊ธฐ์ ์œผ๋กœ ํฌ๋กค๋งํ•˜๋Š” ์‹œ์Šคํ…œ์„ ๋งŒ๋“ค์–ด๋ด. โฐ

4. ๋ฐ์ดํ„ฐ ๋ถ„์„ ์—ฐ๊ณ„
์ˆ˜์ง‘ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ถ„์„ํ•ด์„œ ์ธ์‚ฌ์ดํŠธ๋ฅผ ๋„์ถœํ•ด๋ด. Python์˜ pandas๋‚˜ ์ž๋ฐ”์˜ Apache Spark๋ฅผ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ์–ด. ๐Ÿ“Š

5. API ๊ฐœ๋ฐœ
ํฌ๋กค๋งํ•œ ๋ฐ์ดํ„ฐ๋ฅผ REST API๋กœ ์ œ๊ณตํ•˜๋Š” ์„œ๋น„์Šค๋ฅผ ๋งŒ๋“ค์–ด๋ด. Spring Boot์™€ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๋ฉด ์ข‹์•„! ๐ŸŒ

๐Ÿ’ฌ ๋งˆ์ง€๋ง‰ ์กฐ์–ธ

์›น ํฌ๋กค๋ง์€ ์ •๋ง ์œ ์šฉํ•œ ๊ธฐ์ˆ ์ด์ง€๋งŒ, ์ฑ…์ž„๊ฐ ์žˆ๊ฒŒ ์‚ฌ์šฉํ•ด์•ผ ํ•ด. ํ•ญ์ƒ ๋ฒ•๊ณผ ์œค๋ฆฌ๋ฅผ ์ง€ํ‚ค๊ณ , ๋‹ค๋ฅธ ์‚ฌ๋žŒ์˜ ์„œ๋ฒ„์— ํ”ผํ•ด๋ฅผ ์ฃผ์ง€ ์•Š๋„๋ก ์ฃผ์˜ํ•˜์ž. ๊ทธ๋ฆฌ๊ณ  ๊ฐ€๋Šฅํ•˜๋ฉด ๊ณต์‹ API๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒŒ ๊ฐ€์žฅ ์ข‹๋‹ค๋Š” ๊ฑธ ์žŠ์ง€ ๋งˆ! ๐Ÿ˜Š

ํฌ๋กค๋ง์€ ๊ณ„์† ์—ฐ์Šตํ•˜๋ฉด์„œ ์‹ค๋ ฅ์„ ์Œ“์•„์•ผ ํ•ด. ์›น์‚ฌ์ดํŠธ๋งˆ๋‹ค ๊ตฌ์กฐ๊ฐ€ ๋‹ค๋ฅด๊ณ , ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ๋ฌธ์ œ๋“ค์ด ์ƒ๊ธฐ๊ฑฐ๋“ . ํ•˜์ง€๋งŒ ๊ทธ ๊ณผ์ •์—์„œ ๋งŽ์ด ๋ฐฐ์šฐ๊ฒŒ ๋  ๊ฑฐ์•ผ. ํฌ๊ธฐํ•˜์ง€ ๋ง๊ณ  ๊ณ„์† ๋„์ „ํ•ด๋ด! ๐Ÿ’ช

ํ˜น์‹œ ํฌ๋กค๋ง ํ”„๋กœ์ ํŠธ๋ฅผ ์ง„ํ–‰ํ•˜๋‹ค๊ฐ€ ๋ง‰ํžˆ๋Š” ๋ถ€๋ถ„์ด ์žˆ๋‹ค๋ฉด, ๊ฐœ๋ฐœ์ž ์ปค๋ฎค๋‹ˆํ‹ฐ๋‚˜ **์žฌ๋Šฅ๋„ท** ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ ๋„์›€์„ ๊ตฌํ•  ์ˆ˜๋„ ์žˆ์–ด. ํ•จ๊ป˜ ๋ฐฐ์šฐ๊ณ  ์„ฑ์žฅํ•˜๋Š” ๊ฒŒ ์ค‘์š”ํ•˜๋‹ˆ๊นŒ! ๐Ÿค—

์ž, ์ด์ œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์ฒซ ํฌ๋กค๋Ÿฌ๋ฅผ ๋งŒ๋“ค์–ด๋ณผ ์‹œ๊ฐ„์ด์•ผ! ํ–‰์šด์„ ๋นŒ์–ด! ๐Ÿ€โœจ

Happy Crawling! ์ฆ๊ฑฐ์šด ํฌ๋กค๋ง ๋˜์„ธ์š”! ๐Ÿš€ โ˜• ๐Ÿ•ท๏ธ
๋Œ“๊ธ€ ์ž‘์„ฑ

์ด ๊ธ€์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์ƒ๊ฐ์„ ๋“ค๋ ค์ฃผ์„ธ์š”

๋Œ“๊ธ€ 0
๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค.

์•„์ง ๋Œ“๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค.