์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿš€ ์›น ํฌ๋กค๋ง๊ณผ ์Šคํฌ๋ž˜ํ•‘ ์™„์ „์ •๋ณต: ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ์ž๋™ํ™”์˜ ๋ชจ๋“  ๊ฒƒ ๐Ÿš€

๐Ÿš€ ์›น ํฌ๋กค๋ง๊ณผ ์Šคํฌ๋ž˜ํ•‘ ์™„์ „์ •๋ณต: ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ์ž๋™ํ™”์˜ ๋ชจ๋“  ๊ฒƒ ๐Ÿš€

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

์š”์ฆ˜ ๊ฐ™์€ ๋ฐ์ดํ„ฐ ์‹œ๋Œ€์— ์ด๋Ÿฐ ๊ธฐ์ˆ ์€ ๊ฐœ์ธ ํ”„๋กœ์ ํŠธ๋ถ€ํ„ฐ ๋น„์ฆˆ๋‹ˆ์Šค๊นŒ์ง€ ๋‹ค์–‘ํ•œ ๋ถ„์•ผ์—์„œ ํ™œ์šฉ๋˜๊ณ  ์žˆ์–ด. ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ๋„ ์›น ํฌ๋กค๋ง ์„œ๋น„์Šค๋ฅผ ์ œ๊ณตํ•˜๋Š” ์ „๋ฌธ๊ฐ€๋“ค์ด ๋งŽ์ด ํ™œ๋™ํ•˜๊ณ  ์žˆ์ง€. ์ด ๊ธ€์„ ํ†ตํ•ด ๋„Œ ์›น ํฌ๋กค๋ง์˜ ๊ธฐ์ดˆ๋ถ€ํ„ฐ ๊ณ ๊ธ‰ ๊ธฐ๋ฒ•๊นŒ์ง€ ์‰ฝ๊ณ  ์žฌ๋ฏธ์žˆ๊ฒŒ ๋ฐฐ์šธ ์ˆ˜ ์žˆ์„ ๊ฑฐ์•ผ! ๐Ÿ˜Š

๐Ÿ“š ๋ชฉ์ฐจ

  1. ์›น ํฌ๋กค๋ง๊ณผ ์Šคํฌ๋ž˜ํ•‘ ์ดํ•ดํ•˜๊ธฐ
  2. ํฌ๋กค๋ง์˜ ๊ธฐ๋ณธ ์›๋ฆฌ์™€ ์ž‘๋™ ๋ฐฉ์‹
  3. ํŒŒ์ด์ฌ์œผ๋กœ ์‹œ์ž‘ํ•˜๋Š” ์›น ํฌ๋กค๋ง
  4. BeautifulSoup์œผ๋กœ HTML ํŒŒ์‹ฑํ•˜๊ธฐ
  5. Selenium์œผ๋กœ ๋™์  ์›นํŽ˜์ด์ง€ ๋‹ค๋ฃจ๊ธฐ
  6. Scrapy ํ”„๋ ˆ์ž„์›Œํฌ ํ™œ์šฉํ•˜๊ธฐ
  7. API๋ฅผ ํ™œ์šฉํ•œ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘
  8. ํฌ๋กค๋ง ์‹œ ์ฃผ์˜ํ•ด์•ผ ํ•  ๋ฒ•์ /์œค๋ฆฌ์  ๋ฌธ์ œ
  9. ๋ฐ์ดํ„ฐ ์ €์žฅ ๋ฐ ๊ด€๋ฆฌ ๋ฐฉ๋ฒ•
  10. ์‹ค์ „ ํ”„๋กœ์ ํŠธ: ๋‚˜๋งŒ์˜ ํฌ๋กค๋Ÿฌ ๋งŒ๋“ค๊ธฐ

1. ์›น ํฌ๋กค๋ง๊ณผ ์Šคํฌ๋ž˜ํ•‘ ์ดํ•ดํ•˜๊ธฐ ๐Ÿ•ธ๏ธ

์›น ํฌ๋กค๋ง(Web Crawling)๊ณผ ์Šคํฌ๋ž˜ํ•‘(Scraping)์ด๋ผ๋Š” ๋ง, ๋“ค์–ด๋ณธ ์  ์žˆ์ง€? ์ด ๋‘ ์šฉ์–ด๋Š” ์ข…์ข… ํ˜ผ์šฉ๋˜์ง€๋งŒ ์‚ฌ์‹ค ์•ฝ๊ฐ„์˜ ์ฐจ์ด๊ฐ€ ์žˆ์–ด.

์›น ํฌ๋กค๋ง(Web Crawling): ์›น ํŽ˜์ด์ง€๋ฅผ ์ž๋™์œผ๋กœ ํƒ์ƒ‰ํ•˜๋ฉด์„œ ๋งํฌ๋ฅผ ๋”ฐ๋ผ ์ด๋™ํ•˜๋ฉฐ ์›น์„ '๊ธฐ์–ด ๋‹ค๋‹ˆ๋Š”' ๊ณผ์ •์ด์•ผ. ๊ตฌ๊ธ€ ๊ฐ™์€ ๊ฒ€์ƒ‰ ์—”์ง„์ด ์ •๋ณด๋ฅผ ์ˆ˜์ง‘ํ•  ๋•Œ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ์‹์ด์ง€.

์›น ์Šคํฌ๋ž˜ํ•‘(Web Scraping): ํŠน์ • ์›น ํŽ˜์ด์ง€์—์„œ ์›ํ•˜๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ์ถ”์ถœํ•˜๋Š” ๊ณผ์ •์ด์•ผ. ํฌ๋กค๋ง์ด '์ด๋™'์— ์ดˆ์ ์„ ๋‘”๋‹ค๋ฉด, ์Šคํฌ๋ž˜ํ•‘์€ '์ถ”์ถœ'์— ์ดˆ์ ์„ ๋‘๋Š” ๊ฑฐ์ง€.

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

์›น ํฌ๋กค๋ง๊ณผ ์Šคํฌ๋ž˜ํ•‘์ด ํ•„์š”ํ•œ ์ด์œ  ๐Ÿค”

์™œ ์ด๋Ÿฐ ๊ธฐ์ˆ ์ด ํ•„์š”ํ• ๊นŒ? ๋ช‡ ๊ฐ€์ง€ ์˜ˆ๋ฅผ ๋“ค์–ด๋ณผ๊ฒŒ:

  1. ๋ฐ์ดํ„ฐ ๋ถ„์„: ์‹œ์žฅ ๋™ํ–ฅ, ๊ฐ€๊ฒฉ ๋น„๊ต, ์†Œ๋น„์ž ๋ฆฌ๋ทฐ ๋“ฑ์„ ๋ถ„์„ํ•˜๊ธฐ ์œ„ํ•œ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘
  2. ์ฝ˜ํ…์ธ  ๋ชจ๋‹ˆํ„ฐ๋ง: ๊ฒฝ์Ÿ์‚ฌ ์›น์‚ฌ์ดํŠธ๋‚˜ ๋‰ด์Šค ์‚ฌ์ดํŠธ์˜ ์—…๋ฐ์ดํŠธ ์ถ”์ 
  3. ์—ฐ๊ตฌ ๋ชฉ์ : ํ•™์ˆ  ์—ฐ๊ตฌ๋‚˜ ํŠธ๋ Œ๋“œ ๋ถ„์„์„ ์œ„ํ•œ ๋Œ€๋Ÿ‰์˜ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘
  4. ์ž๋™ํ™”: ๋ฐ˜๋ณต์ ์ธ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ์ž‘์—…์˜ ์ž๋™ํ™”
  5. ๊ฐœ์ธ ํ”„๋กœ์ ํŠธ: ์ทจ๋ฏธ๋‚˜ ํ•™์Šต ๋ชฉ์ ์˜ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘

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

ํฌ๋กค๋ง ๋ด‡ ์›นํŽ˜์ด์ง€ ๋ฐ์ดํ„ฐ ์ถ”์ถœ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค

์›น ํฌ๋กค๋ง์˜ ๊ธฐ๋ณธ ์›๋ฆฌ ๐Ÿงฉ

์›น ํฌ๋กค๋ง์€ ๊ธฐ๋ณธ์ ์œผ๋กœ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋‹จ๊ณ„๋กœ ์ด๋ฃจ์–ด์ ธ:

  1. URL ์š”์ฒญ: ์›น ํŽ˜์ด์ง€์— HTTP ์š”์ฒญ์„ ๋ณด๋‚ด HTML ๋ฌธ์„œ๋ฅผ ๊ฐ€์ ธ์˜ด
  2. HTML ํŒŒ์‹ฑ: ๊ฐ€์ ธ์˜จ HTML ๋ฌธ์„œ๋ฅผ ๋ถ„์„ํ•˜์—ฌ ๊ตฌ์กฐํ™”๋œ ๋ฐ์ดํ„ฐ๋กœ ๋ณ€ํ™˜
  3. ๋ฐ์ดํ„ฐ ์ถ”์ถœ: ์›ํ•˜๋Š” ์ •๋ณด๋ฅผ ์„ ํƒ์ ์œผ๋กœ ์ถ”์ถœ
  4. ๋‹ค์Œ ํŽ˜์ด์ง€ ํƒ์ƒ‰: ๋งํฌ๋ฅผ ๋”ฐ๋ผ ๋‹ค๋ฅธ ํŽ˜์ด์ง€๋กœ ์ด๋™ํ•˜๊ฑฐ๋‚˜ ํŽ˜์ด์ง€๋„ค์ด์…˜ ์ฒ˜๋ฆฌ
  5. ๋ฐ์ดํ„ฐ ์ €์žฅ: ์ถ”์ถœํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ํŒŒ์ผ์ด๋‚˜ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ €์žฅ

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

2. ํฌ๋กค๋ง์˜ ๊ธฐ๋ณธ ์›๋ฆฌ์™€ ์ž‘๋™ ๋ฐฉ์‹ ๐Ÿ”

์ด์ œ ํฌ๋กค๋ง์ด ์–ด๋–ป๊ฒŒ ์ž‘๋™ํ•˜๋Š”์ง€ ์ข€ ๋” ์ž์„ธํžˆ ์•Œ์•„๋ณผ๊นŒ? ์›น ํฌ๋กค๋ง์„ ์ดํ•ดํ•˜๋ ค๋ฉด ๋จผ์ € ์›น์˜ ๊ธฐ๋ณธ ๊ตฌ์กฐ๋ฅผ ์•Œ์•„์•ผ ํ•ด.

์›น์˜ ๊ธฐ๋ณธ ๊ตฌ์กฐ ์ดํ•ดํ•˜๊ธฐ ๐ŸŒ

์›น ํŽ˜์ด์ง€๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ HTML(HyperText Markup Language)๋กœ ์ž‘์„ฑ๋˜์–ด ์žˆ์–ด. ์—ฌ๊ธฐ์— CSS(Cascading Style Sheets)๋กœ ์Šคํƒ€์ผ์„ ์ž…ํžˆ๊ณ , JavaScript๋กœ ๋™์ ์ธ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜์ง€. ํฌ๋กค๋ง์€ ์ด ์ค‘์—์„œ ์ฃผ๋กœ HTML์„ ๋‹ค๋ฃจ๊ฒŒ ๋ผ.

๊ฐ„๋‹จํ•œ HTML ๊ตฌ์กฐ ์˜ˆ์‹œ:

<!DOCTYPE html>
<html>
<head>
    <title>์ƒ˜ํ”Œ ํŽ˜์ด์ง€</title>
</head>
<body>
    <h1>์•ˆ๋…•ํ•˜์„ธ์š”!</h1>
    <p>์ด๊ฒƒ์€ <a href="https://example.com">๋งํฌ</a>์ž…๋‹ˆ๋‹ค.</p>
    <div class="product">
        <h2>์ œํ’ˆ๋ช…</h2>
        <p class="price">10,000์›</p>
    </div>
</body>
</html>

ํฌ๋กค๋Ÿฌ๋Š” ์ด๋Ÿฐ HTML ๊ตฌ์กฐ๋ฅผ ๋ถ„์„ํ•ด์„œ ์›ํ•˜๋Š” ์ •๋ณด(์˜ˆ: ์ œํ’ˆ๋ช…, ๊ฐ€๊ฒฉ)๋ฅผ ์ถ”์ถœํ•˜๊ฑฐ๋‚˜, a ํƒœ๊ทธ์˜ href ์†์„ฑ์„ ํ†ตํ•ด ๋‹ค๋ฅธ ํŽ˜์ด์ง€๋กœ ์ด๋™ํ•  ์ˆ˜ ์žˆ์–ด. ์ด๊ฑธ HTML ํŒŒ์‹ฑ(Parsing)์ด๋ผ๊ณ  ํ•ด. ๐Ÿง

HTTP ์š”์ฒญ๊ณผ ์‘๋‹ต ์ดํ•ดํ•˜๊ธฐ ๐Ÿ“ก

ํฌ๋กค๋ง์˜ ์ฒซ ๋‹จ๊ณ„๋Š” ์›น ์„œ๋ฒ„์— HTTP ์š”์ฒญ์„ ๋ณด๋‚ด๋Š” ๊ฑฐ์•ผ. ์ด ๊ณผ์ •์„ ์ข€ ๋” ์ž์„ธํžˆ ์‚ดํŽด๋ณผ๊ฒŒ:

  1. HTTP ์š”์ฒญ(Request): ํฌ๋กค๋Ÿฌ๊ฐ€ ์›น ์„œ๋ฒ„์— ํŠน์ • URL์˜ ํŽ˜์ด์ง€๋ฅผ ์š”์ฒญํ•จ
  2. HTTP ์‘๋‹ต(Response): ์„œ๋ฒ„๊ฐ€ ์š”์ฒญ๋œ ํŽ˜์ด์ง€์˜ HTML ๋‚ด์šฉ์„ ์‘๋‹ต์œผ๋กœ ๋ณด๋ƒ„
  3. ์ƒํƒœ ์ฝ”๋“œ: ์„œ๋ฒ„๋Š” ์‘๋‹ต๊ณผ ํ•จ๊ป˜ ์ƒํƒœ ์ฝ”๋“œ(์˜ˆ: 200 ์„ฑ๊ณต, 404 ํŽ˜์ด์ง€ ์—†์Œ)๋ฅผ ๋ณด๋ƒ„

์•Œ์•„๋‘๋ฉด ์ข‹์€ HTTP ์ƒํƒœ ์ฝ”๋“œ:

  • โœ… 200 OK: ์š”์ฒญ ์„ฑ๊ณต
  • ๐Ÿ”„ 301/302: ํŽ˜์ด์ง€ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ
  • โŒ 403 Forbidden: ์ ‘๊ทผ ๊ถŒํ•œ ์—†์Œ
  • ๐Ÿ” 404 Not Found: ํŽ˜์ด์ง€๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Œ
  • โš ๏ธ 429 Too Many Requests: ๋„ˆ๋ฌด ๋งŽ์€ ์š”์ฒญ (ํฌ๋กค๋ง ์ œํ•œ)
  • ๐Ÿ›‘ 500 Internal Server Error: ์„œ๋ฒ„ ์˜ค๋ฅ˜

ํฌ๋กค๋งํ•  ๋•Œ 429 ์—๋Ÿฌ๋ฅผ ์ž์ฃผ ๋งŒ๋‚˜๊ฒŒ ๋œ๋‹ค๋ฉด, ๋„ˆ๋ฌด ๋น ๋ฅธ ์†๋„๋กœ ์š”์ฒญ์„ ๋ณด๋‚ด๊ณ  ์žˆ๋‹ค๋Š” ์‹ ํ˜ธ์•ผ. ์ด๋Ÿด ๋•Œ๋Š” ์š”์ฒญ ์‚ฌ์ด์— ์‹œ๊ฐ„ ๊ฐ„๊ฒฉ์„ ๋‘๋Š” ๊ฒƒ์ด ์ข‹์•„! ๐Ÿข

์›น ํฌ๋กค๋Ÿฌ์˜ ํ•ต์‹ฌ ์ปดํฌ๋„ŒํŠธ ๐Ÿงฉ

์›น ํฌ๋กค๋Ÿฌ๋Š” ๋ณดํ†ต ๋‹ค์Œ๊ณผ ๊ฐ™์€ ํ•ต์‹ฌ ์ปดํฌ๋„ŒํŠธ๋กœ ๊ตฌ์„ฑ๋ผ:

ํฌ๋กค๋Ÿฌ ์—”์ง„ Core Controller URL ํ”„๋ก ํ‹ฐ์–ด ๋ฐฉ๋ฌธํ•  URL ๋Œ€๊ธฐ์—ด HTML ๋‹ค์šด๋กœ๋” HTTP ์š”์ฒญ ์ฒ˜๋ฆฌ ์ฝ˜ํ…์ธ  ํŒŒ์„œ HTML ๊ตฌ์กฐ ๋ถ„์„ ๋ฐ์ดํ„ฐ ์ €์žฅ์†Œ ์ถ”์ถœ ๋ฐ์ดํ„ฐ ์ €์žฅ ์›น ํฌ๋กค๋Ÿฌ์˜ ํ•ต์‹ฌ ์ปดํฌ๋„ŒํŠธ
  1. URL ํ”„๋ก ํ‹ฐ์–ด(Frontier): ๋ฐฉ๋ฌธํ•  URL๋“ค์˜ ๋Œ€๊ธฐ์—ด
  2. HTML ๋‹ค์šด๋กœ๋”: ์›น ํŽ˜์ด์ง€๋ฅผ ๋‹ค์šด๋กœ๋“œํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ
  3. ์ฝ˜ํ…์ธ  ํŒŒ์„œ: HTML์„ ๋ถ„์„ํ•˜๊ณ  ๊ตฌ์กฐํ™”ํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ
  4. ๋ฐ์ดํ„ฐ ์ถ”์ถœ๊ธฐ: ์›ํ•˜๋Š” ์ •๋ณด๋ฅผ ์ถ”์ถœํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ
  5. ๋ฐ์ดํ„ฐ ์ €์žฅ์†Œ: ์ถ”์ถœํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ

์ด ์ปดํฌ๋„ŒํŠธ๋“ค์ด ์œ ๊ธฐ์ ์œผ๋กœ ์ž‘๋™ํ•˜๋ฉด์„œ ์›น์„ ๋Œ์•„๋‹ค๋‹ˆ๋ฉฐ ๋ฐ์ดํ„ฐ๋ฅผ ์ˆ˜์ง‘ํ•˜๊ฒŒ ๋˜๋Š” ๊ฑฐ์•ผ. ๋งˆ์น˜ ์ธํ„ฐ๋„ท์„ ํƒํ—˜ํ•˜๋Š” ๋กœ๋ด‡์ด๋ผ๊ณ  ์ƒ๊ฐํ•˜๋ฉด ์ดํ•ดํ•˜๊ธฐ ์‰ฌ์šธ ๊ฑฐ์•ผ! ๐Ÿค–

ํฌ๋กค๋ง ์ „๋žต ๐Ÿ“Š

ํฌ๋กค๋ง์„ ํ•  ๋•Œ๋Š” ์–ด๋–ค ์ˆœ์„œ๋กœ ํŽ˜์ด์ง€๋ฅผ ๋ฐฉ๋ฌธํ• ์ง€ ์ „๋žต์ด ํ•„์š”ํ•ด:

  1. ๋„ˆ๋น„ ์šฐ์„  ํƒ์ƒ‰(BFS): ํ˜„์žฌ ํŽ˜์ด์ง€์˜ ๋ชจ๋“  ๋งํฌ๋ฅผ ๋จผ์ € ๋ฐฉ๋ฌธํ•œ ํ›„ ๋‹ค์Œ ๋‹จ๊ณ„๋กœ ์ด๋™
  2. ๊นŠ์ด ์šฐ์„  ํƒ์ƒ‰(DFS): ํ•œ ๊ฒฝ๋กœ๋ฅผ ๋๊นŒ์ง€ ํƒ์ƒ‰ํ•œ ํ›„ ๋‹ค๋ฅธ ๊ฒฝ๋กœ๋กœ ์ด๋™
  3. ์šฐ์„ ์ˆœ์œ„ ๊ธฐ๋ฐ˜ ํƒ์ƒ‰: ์ค‘์š”๋„๋‚˜ ๊ด€๋ จ์„ฑ์— ๋”ฐ๋ผ ํŽ˜์ด์ง€ ๋ฐฉ๋ฌธ ์ˆœ์„œ ๊ฒฐ์ •

์–ด๋–ค ์ „๋žต์„ ์„ ํƒํ• ์ง€๋Š” ํฌ๋กค๋ง ๋ชฉ์ ์— ๋”ฐ๋ผ ๋‹ฌ๋ผ์ ธ. ์˜ˆ๋ฅผ ๋“ค์–ด, ํŠน์ • ์นดํ…Œ๊ณ ๋ฆฌ์˜ ๋ชจ๋“  ์ œํ’ˆ ์ •๋ณด๋ฅผ ์ˆ˜์ง‘ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด BFS๊ฐ€ ์ ํ•ฉํ•  ์ˆ˜ ์žˆ๊ณ , ํŠน์ • ์ฃผ์ œ์— ๊ด€ํ•œ ๊นŠ์€ ์ •๋ณด๋ฅผ ์ฐพ๊ณ  ์‹ถ๋‹ค๋ฉด DFS๊ฐ€ ์ข‹์„ ์ˆ˜ ์žˆ์–ด. ๐Ÿงญ

๐Ÿ” ํฌ๋กค๋ง ํŒ: ์›น์‚ฌ์ดํŠธ๋ฅผ ํฌ๋กค๋งํ•  ๋•Œ๋Š” robots.txt ํŒŒ์ผ์„ ํ•ญ์ƒ ํ™•์ธํ•˜์ž! ์ด ํŒŒ์ผ์€ ์›น์‚ฌ์ดํŠธ ์šด์˜์ž๊ฐ€ ํฌ๋กค๋Ÿฌ์—๊ฒŒ ์–ด๋–ค ํŽ˜์ด์ง€๋ฅผ ๋ฐฉ๋ฌธํ•ด๋„ ๋˜๊ณ , ์–ด๋–ค ํŽ˜์ด์ง€๋Š” ๋ฐฉ๋ฌธํ•˜๋ฉด ์•ˆ ๋˜๋Š”์ง€ ์•Œ๋ ค์ฃผ๋Š” ๊ฐ€์ด๋“œ๋ผ์ธ์ด์•ผ.

์˜ˆ: https://example.com/robots.txt

3. ํŒŒ์ด์ฌ์œผ๋กœ ์‹œ์ž‘ํ•˜๋Š” ์›น ํฌ๋กค๋ง ๐Ÿ

์ด์ œ ์‹ค์ œ๋กœ ํฌ๋กค๋ง์„ ์‹œ์ž‘ํ•ด๋ณผ๊นŒ? ํŒŒ์ด์ฌ์€ ์›น ํฌ๋กค๋ง์— ๊ฐ€์žฅ ๋งŽ์ด ์‚ฌ์šฉ๋˜๋Š” ์–ธ์–ด ์ค‘ ํ•˜๋‚˜์•ผ. ๋ฌธ๋ฒ•์ด ๊ฐ„๊ฒฐํ•˜๊ณ , ๋‹ค์–‘ํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์ง€์›ํ•˜๊ธฐ ๋•Œ๋ฌธ์ด์ง€. ํŒŒ์ด์ฌ์œผ๋กœ ํฌ๋กค๋ง์„ ์‹œ์ž‘ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ณด์ž! ๐Ÿš€

ํ•„์š”ํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์„ค์น˜ํ•˜๊ธฐ ๐Ÿ“š

ํŒŒ์ด์ฌ์œผ๋กœ ํฌ๋กค๋ง์„ ํ•˜๋ ค๋ฉด ๋ช‡ ๊ฐ€์ง€ ํ•ต์‹ฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ํ•„์š”ํ•ด:

# pip๋ฅผ ์‚ฌ์šฉํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์„ค์น˜
pip install requests
pip install beautifulsoup4
pip install selenium
pip install scrapy

๊ฐ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ ์—ญํ• ์„ ๊ฐ„๋‹จํžˆ ์„ค๋ช…ํ• ๊ฒŒ:

  1. requests: HTTP ์š”์ฒญ์„ ๋ณด๋‚ด๊ณ  ์‘๋‹ต์„ ๋ฐ›๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ
  2. BeautifulSoup: HTML๊ณผ XML ํŒŒ์ผ์„ ํŒŒ์‹ฑํ•˜๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ
  3. Selenium: ์›น ๋ธŒ๋ผ์šฐ์ €๋ฅผ ์ž๋™ํ™”ํ•˜๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ (JavaScript๊ฐ€ ํ•„์š”ํ•œ ๋™์  ํŽ˜์ด์ง€ ์ฒ˜๋ฆฌ)
  4. Scrapy: ๋Œ€๊ทœ๋ชจ ํฌ๋กค๋ง์„ ์œ„ํ•œ ํ”„๋ ˆ์ž„์›Œํฌ

์ฒ˜์Œ์—๋Š” requests์™€ BeautifulSoup๋งŒ์œผ๋กœ๋„ ์ถฉ๋ถ„ํžˆ ๋งŽ์€ ๊ฒƒ์„ ํ•  ์ˆ˜ ์žˆ์–ด. ์ด ๋‘ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋กœ ๊ธฐ๋ณธ์ ์ธ ํฌ๋กค๋ง์„ ์‹œ์ž‘ํ•ด๋ณด์ž! ๐Ÿ› ๏ธ

์ฒซ ๋ฒˆ์งธ ํฌ๋กค๋ง: requests์™€ BeautifulSoup ์‚ฌ์šฉํ•˜๊ธฐ ๐Ÿฒ

๊ฐ„๋‹จํ•œ ์˜ˆ์ œ๋กœ ์‹œ์ž‘ํ•ด๋ณผ๊ฒŒ. ์›น ํŽ˜์ด์ง€์˜ ์ œ๋ชฉ๊ณผ ๋ชจ๋“  ๋งํฌ๋ฅผ ์ถ”์ถœํ•˜๋Š” ์ฝ”๋“œ์•ผ:

import requests
from bs4 import BeautifulSoup

# ์›น ํŽ˜์ด์ง€ ๊ฐ€์ ธ์˜ค๊ธฐ
url = 'https://example.com'
response = requests.get(url)

# ์‘๋‹ต ์ƒํƒœ ํ™•์ธ
if response.status_code == 200:
    # HTML ํŒŒ์‹ฑ
    soup = BeautifulSoup(response.text, 'html.parser')
    
    # ํŽ˜์ด์ง€ ์ œ๋ชฉ ์ถ”์ถœ
    title = soup.title.text
    print(f'ํŽ˜์ด์ง€ ์ œ๋ชฉ: {title}')
    
    # ๋ชจ๋“  ๋งํฌ ์ถ”์ถœ
    links = soup.find_all('a')
    print(f'์ด {len(links)}๊ฐœ์˜ ๋งํฌ๋ฅผ ์ฐพ์•˜์Šต๋‹ˆ๋‹ค:')
    
    for i, link in enumerate(links, 1):
        href = link.get('href')
        link_text = link.text.strip()
        print(f'{i}. {link_text} -> {href}')
else:
    print(f'์˜ค๋ฅ˜ ๋ฐœ์ƒ: {response.status_code}')

์ด ์ฝ”๋“œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋‹จ๊ณ„๋กœ ์ž‘๋™ํ•ด:

  1. requests๋ฅผ ์‚ฌ์šฉํ•ด ์›น ํŽ˜์ด์ง€์˜ HTML์„ ๊ฐ€์ ธ์˜ด
  2. BeautifulSoup์„ ์‚ฌ์šฉํ•ด HTML์„ ํŒŒ์‹ฑ
  3. soup.title.text๋กœ ํŽ˜์ด์ง€ ์ œ๋ชฉ์„ ์ถ”์ถœ
  4. soup.find_all('a')๋กœ ๋ชจ๋“  ๋งํฌ(a ํƒœ๊ทธ)๋ฅผ ์ฐพ์Œ
  5. ๊ฐ ๋งํฌ์˜ href ์†์„ฑ๊ณผ ํ…์ŠคํŠธ๋ฅผ ์ถœ๋ ฅ

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

ํ—ค๋” ์„ค์ •๊ณผ ์‚ฌ์šฉ์ž ์—์ด์ „ํŠธ ๐Ÿ•ต๏ธ

ํฌ๋กค๋ง์„ ํ•  ๋•Œ ์ค‘์š”ํ•œ ๊ฒƒ ์ค‘ ํ•˜๋‚˜๋Š” '์˜ˆ์˜'๋ฅผ ์ง€ํ‚ค๋Š” ๊ฑฐ์•ผ. ๋งŽ์€ ์›น์‚ฌ์ดํŠธ๋Š” ๋ด‡์˜ ์ ‘๊ทผ์„ ์ œํ•œํ•˜๊ธฐ๋„ ํ•˜๊ฑฐ๋“ . ๊ทธ๋ž˜์„œ ์šฐ๋ฆฌ๋Š” '์‚ฌ์šฉ์ž ์—์ด์ „ํŠธ(User-Agent)'๋ฅผ ์„ค์ •ํ•ด์„œ ์ •์ƒ์ ์ธ ๋ธŒ๋ผ์šฐ์ €์ฒ˜๋Ÿผ ๋ณด์ด๊ฒŒ ํ•  ์ˆ˜ ์žˆ์–ด:

import requests
from bs4 import BeautifulSoup

# ํ—ค๋” ์„ค์ •
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-Language': 'ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7',
}

url = 'https://example.com'
response = requests.get(url, headers=headers)

# ์ดํ›„ ์ฝ”๋“œ๋Š” ๋™์ผ...

์ด๋ ‡๊ฒŒ ํ—ค๋”๋ฅผ ์„ค์ •ํ•˜๋ฉด ์›น์‚ฌ์ดํŠธ๊ฐ€ ์šฐ๋ฆฌ์˜ ํฌ๋กค๋Ÿฌ๋ฅผ ์ผ๋ฐ˜ ๋ธŒ๋ผ์šฐ์ €๋กœ ์ธ์‹ํ•  ๊ฐ€๋Šฅ์„ฑ์ด ๋†’์•„์ ธ. ํ•˜์ง€๋งŒ ์ด๊ฒƒ์ด ๋ชจ๋“  ์ œํ•œ์„ ์šฐํšŒํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ์˜๋ฏธ๋Š” ์•„๋‹ˆ๋‹ˆ ์ฃผ์˜ํ•ด์•ผ ํ•ด! ๐Ÿšจ

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

๋งŽ์€ ์›น์‚ฌ์ดํŠธ๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ์—ฌ๋Ÿฌ ํŽ˜์ด์ง€๋กœ ๋‚˜๋ˆ ์„œ ๋ณด์—ฌ์ฃผ์ง€. ์ด๋Ÿฐ ๊ฒฝ์šฐ ํŽ˜์ด์ง€๋„ค์ด์…˜(pagination)์„ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•ด:

import requests
from bs4 import BeautifulSoup
import time

headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'}

# ์—ฌ๋Ÿฌ ํŽ˜์ด์ง€ ํฌ๋กค๋ง
base_url = 'https://example.com/page/'
all_items = []

for page_num in range(1, 6):  # 1ํŽ˜์ด์ง€๋ถ€ํ„ฐ 5ํŽ˜์ด์ง€๊นŒ์ง€
    url = f'{base_url}{page_num}'
    print(f'ํฌ๋กค๋ง ์ค‘: {url}')
    
    response = requests.get(url, headers=headers)
    
    if response.status_code == 200:
        soup = BeautifulSoup(response.text, 'html.parser')
        
        # ํŽ˜์ด์ง€์—์„œ ์•„์ดํ…œ ์ถ”์ถœ (์˜ˆ: ์ƒํ’ˆ ๋ชฉ๋ก)
        items = soup.select('.item')  # CSS ์„ ํƒ์ž๋กœ ์•„์ดํ…œ ์„ ํƒ
        
        for item in items:
            title = item.select_one('.title').text.strip()
            price = item.select_one('.price').text.strip()
            all_items.append({'title': title, 'price': price})
        
        # ์„œ๋ฒ„์— ๋ถ€๋‹ด์„ ์ฃผ์ง€ ์•Š๊ธฐ ์œ„ํ•ด ์ž ์‹œ ๋Œ€๊ธฐ
        time.sleep(2)
    else:
        print(f'ํŽ˜์ด์ง€ {page_num} ์ ‘๊ทผ ์‹คํŒจ: {response.status_code}')

print(f'์ด {len(all_items)}๊ฐœ์˜ ์•„์ดํ…œ์„ ์ˆ˜์ง‘ํ–ˆ์Šต๋‹ˆ๋‹ค.')

์ด ์ฝ”๋“œ๋Š” ์—ฌ๋Ÿฌ ํŽ˜์ด์ง€๋ฅผ ์ˆœํšŒํ•˜๋ฉด์„œ ๊ฐ ํŽ˜์ด์ง€์—์„œ ์•„์ดํ…œ(์˜ˆ: ์ƒํ’ˆ)์„ ์ถ”์ถœํ•ด. ๋ช‡ ๊ฐ€์ง€ ์ค‘์š”ํ•œ ํฌ์ธํŠธ๊ฐ€ ์žˆ์–ด:

  1. for ๋ฃจํ”„๋กœ ์—ฌ๋Ÿฌ ํŽ˜์ด์ง€๋ฅผ ์ˆœํšŒ
  2. CSS ์„ ํƒ์ž(.item, .title, .price)๋กœ ์›ํ•˜๋Š” ์š”์†Œ๋ฅผ ์ •ํ™•ํžˆ ์„ ํƒ
  3. time.sleep()์œผ๋กœ ์š”์ฒญ ์‚ฌ์ด์— ์‹œ๊ฐ„ ๊ฐ„๊ฒฉ์„ ๋‘  (์„œ๋ฒ„ ๋ถ€ํ•˜ ๋ฐฉ์ง€)

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

๐ŸŒฑ ์ดˆ๋ณด์ž ํŒ: ํฌ๋กค๋ง ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•  ๋•Œ๋Š” ๋จผ์ € ์†Œ๊ทœ๋ชจ๋กœ ํ…Œ์ŠคํŠธํ•˜๋Š” ๊ฒƒ์ด ์ข‹์•„. ์˜ˆ๋ฅผ ๋“ค์–ด, ๋ชจ๋“  ํŽ˜์ด์ง€๊ฐ€ ์•„๋‹Œ ์ฒ˜์Œ 2-3ํŽ˜์ด์ง€๋งŒ ํฌ๋กค๋งํ•ด๋ณด๊ณ , ๋ฐ์ดํ„ฐ๊ฐ€ ์ œ๋Œ€๋กœ ์ถ”์ถœ๋˜๋Š”์ง€ ํ™•์ธํ•œ ํ›„ ๊ทœ๋ชจ๋ฅผ ํ™•์žฅํ•˜์ž!

4. BeautifulSoup์œผ๋กœ HTML ํŒŒ์‹ฑํ•˜๊ธฐ ๐Ÿœ

BeautifulSoup์€ HTML๊ณผ XML ํŒŒ์ผ์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์ถ”์ถœํ•˜๋Š” ํŒŒ์ด์ฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์•ผ. ์ด๋ฆ„์ฒ˜๋Ÿผ '์•„๋ฆ„๋‹ค์šด ์ˆ˜ํ”„'์ฒ˜๋Ÿผ HTML์˜ ๋ณต์žกํ•œ ๊ตฌ์กฐ๋ฅผ ๋ง›์žˆ๊ฒŒ ์š”๋ฆฌํ•ด์ฃผ์ง€! ๐Ÿ˜‹ ์ด์ œ BeautifulSoup์˜ ๊ฐ•๋ ฅํ•œ ๊ธฐ๋Šฅ๋“ค์„ ์ž์„ธํžˆ ์•Œ์•„๋ณด์ž.

BeautifulSoup ๊ธฐ๋ณธ ์‚ฌ์šฉ๋ฒ• ๐Ÿฅ„

BeautifulSoup์˜ ๊ธฐ๋ณธ์ ์ธ ์‚ฌ์šฉ๋ฒ•์„ ์•Œ์•„๋ณผ๊ฒŒ:

from bs4 import BeautifulSoup

# HTML ๋ฌธ์ž์—ด
html_doc = """
<html>
    <head>
        <title>BeautifulSoup ์˜ˆ์ œ</title>
    </head>
    <body>
        <h1 id="main-title" class="header">์•ˆ๋…•ํ•˜์„ธ์š”!</h1>
        <p class="content">์ด๊ฒƒ์€ <b>BeautifulSoup</b>์˜ ์˜ˆ์ œ์ž…๋‹ˆ๋‹ค.</p>
        <div class="products">
            <div class="product">
                <h2>์ƒํ’ˆ 1</h2>
                <p class="price">10,000์›</p>
            </div>
            <div class="product">
                <h2>์ƒํ’ˆ 2</h2>
                <p class="price">20,000์›</p>
            </div>
        </div>
        <a href="https://example.com">๋งํฌ</a>
    </body>
</html>
"""

# HTML ํŒŒ์‹ฑ
soup = BeautifulSoup(html_doc, 'html.parser')

# ๊ธฐ๋ณธ์ ์ธ ์ •๋ณด ์ถ”์ถœ
print(soup.title.text)  # 'BeautifulSoup ์˜ˆ์ œ'
print(soup.h1.text)     # '์•ˆ๋…•ํ•˜์„ธ์š”!'
print(soup.p.text)      # '์ด๊ฒƒ์€ BeautifulSoup์˜ ์˜ˆ์ œ์ž…๋‹ˆ๋‹ค.'

BeautifulSoup ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•˜๋ฉด, HTML ๋ฌธ์„œ์˜ ์š”์†Œ๋“ค์„ ์‰ฝ๊ฒŒ ํƒ์ƒ‰ํ•  ์ˆ˜ ์žˆ์–ด. soup.title, soup.h1 ๊ฐ™์€ ๋ฐฉ์‹์œผ๋กœ ์ฒซ ๋ฒˆ์งธ ํ•ด๋‹น ํƒœ๊ทธ์— ๋ฐ”๋กœ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์ง€! ๐Ÿงญ

์š”์†Œ ์ฐพ๊ธฐ: find()์™€ find_all() ๐Ÿ”

BeautifulSoup์—์„œ ๊ฐ€์žฅ ๋งŽ์ด ์‚ฌ์šฉํ•˜๋Š” ๋ฉ”์„œ๋“œ๋Š” find()์™€ find_all()์ด์•ผ:

# ์œ„์˜ HTML ์˜ˆ์ œ์— ์ด์–ด์„œ...

# find() - ์ฒซ ๋ฒˆ์งธ ์ผ์น˜ํ•˜๋Š” ์š”์†Œ ์ฐพ๊ธฐ
title = soup.find('h1')
print(title.text)  # '์•ˆ๋…•ํ•˜์„ธ์š”!'

# find_all() - ๋ชจ๋“  ์ผ์น˜ํ•˜๋Š” ์š”์†Œ ์ฐพ๊ธฐ
all_products = soup.find_all('div', class_='product')
print(f'์ƒํ’ˆ ๊ฐœ์ˆ˜: {len(all_products)}')  # '์ƒํ’ˆ ๊ฐœ์ˆ˜: 2'

# ์†์„ฑ์œผ๋กœ ์š”์†Œ ์ฐพ๊ธฐ
main_title = soup.find(id='main-title')
print(main_title.text)  # '์•ˆ๋…•ํ•˜์„ธ์š”!'

# ์—ฌ๋Ÿฌ ์กฐ๊ฑด ์กฐํ•ฉํ•˜๊ธฐ
prices = soup.find_all('p', class_='price')
for price in prices:
    print(price.text)  # '10,000์›', '20,000์›'

find()๋Š” ์ฒซ ๋ฒˆ์งธ๋กœ ์ผ์น˜ํ•˜๋Š” ์š”์†Œ๋งŒ ๋ฐ˜ํ™˜ํ•˜๊ณ , find_all()์€ ์ผ์น˜ํ•˜๋Š” ๋ชจ๋“  ์š”์†Œ๋ฅผ ๋ฆฌ์ŠคํŠธ๋กœ ๋ฐ˜ํ™˜ํ•ด. ํƒœ๊ทธ ์ด๋ฆ„, class, id ๋“ฑ ๋‹ค์–‘ํ•œ ์กฐ๊ฑด์œผ๋กœ ์š”์†Œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์žˆ์–ด! ๐ŸŽฏ

CSS ์„ ํƒ์ž ์‚ฌ์šฉํ•˜๊ธฐ: select()์™€ select_one() ๐ŸŽจ

CSS ์„ ํƒ์ž๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋” ์ง๊ด€์ ์œผ๋กœ ์š”์†Œ๋ฅผ ์„ ํƒํ•  ์ˆ˜ ์žˆ์–ด:

# select_one() - CSS ์„ ํƒ์ž๋กœ ์ฒซ ๋ฒˆ์งธ ์š”์†Œ ์ฐพ๊ธฐ
title = soup.select_one('#main-title')  # id๊ฐ€ main-title์ธ ์š”์†Œ
print(title.text)  # '์•ˆ๋…•ํ•˜์„ธ์š”!'

# select() - CSS ์„ ํƒ์ž๋กœ ๋ชจ๋“  ์š”์†Œ ์ฐพ๊ธฐ
products = soup.select('.product')  # class๊ฐ€ product์ธ ๋ชจ๋“  ์š”์†Œ
print(f'์ƒํ’ˆ ๊ฐœ์ˆ˜: {len(products)}')  # '์ƒํ’ˆ ๊ฐœ์ˆ˜: 2'

# ๋ณต์žกํ•œ CSS ์„ ํƒ์ž ์‚ฌ์šฉํ•˜๊ธฐ
product_prices = soup.select('.product .price')
for price in product_prices:
    print(price.text)  # '10,000์›', '20,000์›'

# ์ž์‹ ์„ ํƒ์ž ์‚ฌ์šฉํ•˜๊ธฐ
first_product_title = soup.select_one('.products > .product > h2')
print(first_product_title.text)  # '์ƒํ’ˆ 1'

CSS ์„ ํƒ์ž๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋ณต์žกํ•œ ๊ตฌ์กฐ์—์„œ๋„ ์›ํ•˜๋Š” ์š”์†Œ๋ฅผ ์ •ํ™•ํ•˜๊ฒŒ ์„ ํƒํ•  ์ˆ˜ ์žˆ์–ด. ์›น ๊ฐœ๋ฐœ ๊ฒฝํ—˜์ด ์žˆ๋‹ค๋ฉด ์ด ๋ฐฉ์‹์ด ๋” ์ต์ˆ™ํ•  ๊ฑฐ์•ผ! ๐ŸŽญ

์š”์†Œ์˜ ์†์„ฑ๊ณผ ๋‚ด์šฉ ๋‹ค๋ฃจ๊ธฐ ๐Ÿงฉ

์š”์†Œ๋ฅผ ์ฐพ์€ ํ›„์—๋Š” ๊ทธ ์†์„ฑ๊ณผ ๋‚ด์šฉ์„ ๋‹ค์–‘ํ•œ ๋ฐฉ๋ฒ•์œผ๋กœ ์ถ”์ถœํ•  ์ˆ˜ ์žˆ์–ด:

# ์š”์†Œ์˜ ํ…์ŠคํŠธ ๋‚ด์šฉ ๊ฐ€์ ธ์˜ค๊ธฐ
h1 = soup.find('h1')
print(h1.text)  # '์•ˆ๋…•ํ•˜์„ธ์š”!'
print(h1.string)  # '์•ˆ๋…•ํ•˜์„ธ์š”!' (์ž์‹ ํƒœ๊ทธ๊ฐ€ ์—†์„ ๋•Œ๋งŒ ์ž‘๋™)

# ์š”์†Œ์˜ ์†์„ฑ ๊ฐ€์ ธ์˜ค๊ธฐ
link = soup.find('a')
print(link['href'])  # 'https://example.com'
print(link.get('href'))  # 'https://example.com' (๋” ์•ˆ์ „ํ•œ ๋ฐฉ๋ฒ•)

# ๋ชจ๋“  ์†์„ฑ ํ™•์ธํ•˜๊ธฐ
print(h1.attrs)  # {'id': 'main-title', 'class': ['header']}

# ๋‚ด์šฉ ๋ณ€๊ฒฝํ•˜๊ธฐ (ํŒŒ์‹ฑ๋œ ๊ฐ์ฒด ์ˆ˜์ •)
h1.string = '์ œ๋ชฉ์ด ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!'
print(h1)  # <h1 id="main-title" class="header">์ œ๋ชฉ์ด ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!</h1>

BeautifulSoup์—์„œ๋Š” ์š”์†Œ์˜ ํ…์ŠคํŠธ(.text, .string)์™€ ์†์„ฑ(['์†์„ฑ๋ช…'], .get('์†์„ฑ๋ช…'))์„ ์‰ฝ๊ฒŒ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์–ด. ์‹ฌ์ง€์–ด ํŒŒ์‹ฑ๋œ ๊ฐ์ฒด๋ฅผ ์ˆ˜์ •ํ•  ์ˆ˜๋„ ์žˆ์ง€! ๐Ÿ”ง

ํ˜•์ œ, ๋ถ€๋ชจ, ์ž์‹ ์š”์†Œ ํƒ์ƒ‰ํ•˜๊ธฐ ๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ

HTML ๊ตฌ์กฐ๋ฅผ ํƒ์ƒ‰ํ•  ๋•Œ๋Š” ์š”์†Œ ๊ฐ„์˜ ๊ด€๊ณ„๋ฅผ ์ด์šฉํ•  ์ˆ˜๋„ ์žˆ์–ด:

# ์ฒซ ๋ฒˆ์งธ ์ƒํ’ˆ ์š”์†Œ ์ฐพ๊ธฐ
first_product = soup.select_one('.product')

# ์ž์‹ ์š”์†Œ ์ฐพ๊ธฐ
product_title = first_product.find('h2')
print(product_title.text)  # '์ƒํ’ˆ 1'

# ๋ชจ๋“  ์ž์‹ ์š”์†Œ ๋ฆฌ์ŠคํŠธ
children = list(first_product.children)
print(len(children))  # ์ž์‹ ์š”์†Œ ๊ฐœ์ˆ˜ (ํ…์ŠคํŠธ ๋…ธ๋“œ ํฌํ•จ)

# ๋ถ€๋ชจ ์š”์†Œ ์ฐพ๊ธฐ
parent = product_title.parent
print(parent['class'])  # ['product']

# ๋‹ค์Œ ํ˜•์ œ ์š”์†Œ ์ฐพ๊ธฐ
next_sibling = product_title.find_next_sibling('p')
print(next_sibling.text)  # '10,000์›'

# ์ด์ „ ํ˜•์ œ ์š”์†Œ ์ฐพ๊ธฐ
prev_sibling = next_sibling.find_previous_sibling()
print(prev_sibling.text)  # '์ƒํ’ˆ 1'

์ด๋Ÿฐ ๋ฐฉ์‹์œผ๋กœ HTML ๊ตฌ์กฐ๋ฅผ ์ž์œ ๋กญ๊ฒŒ ํƒ์ƒ‰ํ•  ์ˆ˜ ์žˆ์–ด. ํŠนํžˆ ๋ณต์žกํ•œ ๊ตฌ์กฐ์—์„œ ์›ํ•˜๋Š” ์š”์†Œ๋ฅผ ์ •ํ™•ํžˆ ์ฐพ์•„๋‚ด๋Š” ๋ฐ ์œ ์šฉํ•˜์ง€! ๐Ÿงฉ

๐ŸŒŸ ์‹ค์ „ ์˜ˆ์ œ: ๋‰ด์Šค ๊ธฐ์‚ฌ ์ถ”์ถœํ•˜๊ธฐ

import requests
from bs4 import BeautifulSoup

url = 'https://example-news.com/technology'
response = requests.get(url, headers={'User-Agent': 'Mozilla/5.0'})

if response.status_code == 200:
    soup = BeautifulSoup(response.text, 'html.parser')
    
    # ๋‰ด์Šค ๊ธฐ์‚ฌ ๋ชฉ๋ก ์ฐพ๊ธฐ
    articles = soup.select('.article-item')
    
    for article in articles:
        # ์ œ๋ชฉ ์ถ”์ถœ
        title = article.select_one('.article-title').text.strip()
        
        # ์š”์•ฝ ์ถ”์ถœ
        summary = article.select_one('.article-summary').text.strip()
        
        # ๋‚ ์งœ ์ถ”์ถœ
        date = article.select_one('.article-date').text.strip()
        
        # ๋งํฌ ์ถ”์ถœ
        link = article.select_one('a').get('href')
        if not link.startswith('http'):
            link = 'https://example-news.com' + link
        
        print(f'์ œ๋ชฉ: {title}')
        print(f'์š”์•ฝ: {summary}')
        print(f'๋‚ ์งœ: {date}')
        print(f'๋งํฌ: {link}')
        print('-' * 50)

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

5. Selenium์œผ๋กœ ๋™์  ์›นํŽ˜์ด์ง€ ๋‹ค๋ฃจ๊ธฐ ๐Ÿš—

์š”์ฆ˜ ๋Œ€๋ถ€๋ถ„์˜ ์›น์‚ฌ์ดํŠธ๋Š” JavaScript๋ฅผ ์‚ฌ์šฉํ•ด ๋™์ ์œผ๋กœ ์ฝ˜ํ…์ธ ๋ฅผ ๋กœ๋“œํ•ด. ์ด๋Ÿฐ ์‚ฌ์ดํŠธ๋Š” requests์™€ BeautifulSoup๋งŒ์œผ๋กœ๋Š” ํฌ๋กค๋งํ•˜๊ธฐ ์–ด๋ ค์›Œ. ์™œ๋ƒํ•˜๋ฉด ์ด ๋„๊ตฌ๋“ค์€ ์ดˆ๊ธฐ HTML๋งŒ ๊ฐ€์ ธ์˜ค๊ณ , JavaScript๊ฐ€ ์‹คํ–‰๋œ ํ›„์˜ ์ƒํƒœ๋ฅผ ๋ณผ ์ˆ˜ ์—†๊ฑฐ๋“ . ์ด๋Ÿด ๋•Œ ํ•„์š”ํ•œ ๊ฒƒ์ด ๋ฐ”๋กœ Selenium์ด์•ผ! ๐Ÿฆพ

Selenium ์†Œ๊ฐœ ๋ฐ ์„ค์น˜ ๐Ÿ”ง

Selenium์€ ์›น ๋ธŒ๋ผ์šฐ์ €๋ฅผ ์ž๋™ํ™”ํ•˜๋Š” ๋„๊ตฌ์•ผ. ์‹ค์ œ ๋ธŒ๋ผ์šฐ์ €๋ฅผ ์—ด๊ณ  ์‚ฌ๋žŒ์ด ํ•˜๋Š” ๊ฒƒ์ฒ˜๋Ÿผ ํด๋ฆญ, ์Šคํฌ๋กค, ์ž…๋ ฅ ๋“ฑ์˜ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์–ด. ๋จผ์ € ์„ค์น˜๋ถ€ํ„ฐ ํ•ด๋ณผ๊ฒŒ:

# Selenium ์„ค์น˜
pip install selenium

# ์›น ๋“œ๋ผ์ด๋ฒ„ ์„ค์น˜ (Chrome ์‚ฌ์šฉ ์˜ˆ์‹œ)
# 1. Chrome ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์„ค์น˜๋˜์–ด ์žˆ์–ด์•ผ ํ•จ
# 2. Chrome ๋ฒ„์ „์— ๋งž๋Š” ChromeDriver ๋‹ค์šด๋กœ๋“œ: https://sites.google.com/a/chromium.org/chromedriver/downloads
# 3. ๋‹ค์šด๋กœ๋“œํ•œ ๋“œ๋ผ์ด๋ฒ„๋ฅผ ๊ฒฝ๋กœ์— ์ถ”๊ฐ€ํ•˜๊ฑฐ๋‚˜ ์ฝ”๋“œ์—์„œ ๊ฒฝ๋กœ ์ง€์ •

์ตœ์‹  ๋ฒ„์ „์˜ Selenium์—์„œ๋Š” ์›น ๋“œ๋ผ์ด๋ฒ„ ์ž๋™ ์„ค์น˜ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ด:

from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager

# ์ž๋™์œผ๋กœ ์ ์ ˆํ•œ ChromeDriver ์„ค์น˜ ๋ฐ ์‚ฌ์šฉ
driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()))

์ด์ œ Selenium์„ ์‚ฌ์šฉํ•  ์ค€๋น„๊ฐ€ ๋์–ด! ๐ŸŽฎ

๊ธฐ๋ณธ์ ์ธ Selenium ์‚ฌ์šฉ๋ฒ• ๐Ÿš€

Selenium์˜ ๊ธฐ๋ณธ์ ์ธ ์‚ฌ์šฉ๋ฒ•์„ ์•Œ์•„๋ณด์ž:

from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.common.by import By
import time

# ๋“œ๋ผ์ด๋ฒ„ ์ดˆ๊ธฐํ™”
driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()))

try:
    # ์›น ํŽ˜์ด์ง€ ์—ด๊ธฐ
    driver.get('https://example.com')
    
    # ํŽ˜์ด์ง€ ํƒ€์ดํ‹€ ์ถœ๋ ฅ
    print(f'ํŽ˜์ด์ง€ ์ œ๋ชฉ: {driver.title}')
    
    # ์š”์†Œ ์ฐพ๊ธฐ (By.ID, By.CLASS_NAME, By.CSS_SELECTOR ๋“ฑ ์‚ฌ์šฉ ๊ฐ€๋Šฅ)
    header = driver.find_element(By.TAG_NAME, 'h1')
    print(f'ํ—ค๋” ํ…์ŠคํŠธ: {header.text}')
    
    # ์—ฌ๋Ÿฌ ์š”์†Œ ์ฐพ๊ธฐ
    links = driver.find_elements(By.TAG_NAME, 'a')
    print(f'๋งํฌ ๊ฐœ์ˆ˜: {len(links)}')
    
    # ์ฒซ ๋ฒˆ์งธ ๋งํฌ ํด๋ฆญ
    if links:
        links[0].click()
        time.sleep(2)  # ํŽ˜์ด์ง€ ๋กœ๋”ฉ ๋Œ€๊ธฐ
        print(f'์ƒˆ ํŽ˜์ด์ง€ ์ œ๋ชฉ: {driver.title}')
    
    # ๋’ค๋กœ ๊ฐ€๊ธฐ
    driver.back()
    time.sleep(1)
    
    # ํŽ˜์ด์ง€ ์†Œ์Šค ๊ฐ€์ ธ์˜ค๊ธฐ
    page_source = driver.page_source
    print(f'ํŽ˜์ด์ง€ ์†Œ์Šค ๊ธธ์ด: {len(page_source)}')
    
finally:
    # ๋ธŒ๋ผ์šฐ์ € ๋‹ซ๊ธฐ
    driver.quit()

์ด ์ฝ”๋“œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•ด:

  1. Chrome ๋ธŒ๋ผ์šฐ์ € ์—ด๊ธฐ
  2. ์ง€์ •๋œ URL๋กœ ์ด๋™
  3. ํŽ˜์ด์ง€ ์ œ๋ชฉ ์ถœ๋ ฅ
  4. h1 ํƒœ๊ทธ ์š”์†Œ ์ฐพ์•„์„œ ํ…์ŠคํŠธ ์ถœ๋ ฅ
  5. ๋ชจ๋“  ๋งํฌ(a ํƒœ๊ทธ) ์ฐพ๊ธฐ
  6. ์ฒซ ๋ฒˆ์งธ ๋งํฌ ํด๋ฆญ ๋ฐ ์ƒˆ ํŽ˜์ด์ง€ ์ œ๋ชฉ ์ถœ๋ ฅ
  7. ๋’ค๋กœ ๊ฐ€๊ธฐ
  8. ํŽ˜์ด์ง€ ์†Œ์Šค ๊ฐ€์ ธ์˜ค๊ธฐ
  9. ๋ธŒ๋ผ์šฐ์ € ๋‹ซ๊ธฐ

Selenium์€ ์‹ค์ œ ๋ธŒ๋ผ์šฐ์ €๋ฅผ ์ œ์–ดํ•˜๊ธฐ ๋•Œ๋ฌธ์— JavaScript๊ฐ€ ์‹คํ–‰๋œ ํ›„์˜ ํŽ˜์ด์ง€ ์ƒํƒœ๋ฅผ ๋ณผ ์ˆ˜ ์žˆ์–ด! ์ด๊ฒŒ ๋ฐ”๋กœ ๋™์  ์›นํŽ˜์ด์ง€ ํฌ๋กค๋ง์˜ ํ•ต์‹ฌ์ด์ง€. ๐Ÿ”‘

๊ณ ๊ธ‰ Selenium ๊ธฐ๋Šฅ: ๋Œ€๊ธฐ ๋ฐ ์ƒํ˜ธ์ž‘์šฉ โฑ๏ธ

์›นํŽ˜์ด์ง€๋Š” ๋กœ๋”ฉ ์‹œ๊ฐ„์ด ํ•„์š”ํ•˜๊ณ , ํŠนํžˆ JavaScript๋กœ ๋™์  ์ฝ˜ํ…์ธ ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ๊ฒฝ์šฐ ๋” ๊ทธ๋ž˜. Selenium์—์„œ๋Š” ์ด๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์œ„ํ•œ ๋Œ€๊ธฐ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ด:

from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.keys import Keys

driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()))

try:
    # ์›น ํŽ˜์ด์ง€ ์—ด๊ธฐ
    driver.get('https://example.com')
    
    # ๋ช…์‹œ์  ๋Œ€๊ธฐ: ํŠน์ • ์š”์†Œ๊ฐ€ ๋‚˜ํƒ€๋‚  ๋•Œ๊นŒ์ง€ ์ตœ๋Œ€ 10์ดˆ ๋Œ€๊ธฐ
    search_box = WebDriverWait(driver, 10).until(
        EC.presence_of_element_located((By.ID, 'search'))
    )
    
    # ํ…์ŠคํŠธ ์ž…๋ ฅ
    search_box.send_keys('Selenium ์ž๋™ํ™”')
    
    # Enter ํ‚ค ๋ˆ„๋ฅด๊ธฐ
    search_box.send_keys(Keys.RETURN)
    
    # ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ๋กœ๋“œ๋  ๋•Œ๊นŒ์ง€ ๋Œ€๊ธฐ
    results = WebDriverWait(driver, 10).until(
        EC.presence_of_all_elements_located((By.CSS_SELECTOR, '.result-item'))
    )
    
    print(f'๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ๊ฐœ์ˆ˜: {len(results)}')
    
    # ์Šคํฌ๋กค ๋‹ค์šด
    driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
    
    # ์ž ์‹œ ๋Œ€๊ธฐ
    import time
    time.sleep(2)
    
    # ๋” ๋งŽ์€ ๊ฒฐ๊ณผ๊ฐ€ ๋กœ๋“œ๋  ๋•Œ๊นŒ์ง€ ๋Œ€๊ธฐ
    more_results = WebDriverWait(driver, 10).until(
        EC.presence_of_all_elements_located((By.CSS_SELECTOR, '.result-item'))
    )
    
    print(f'์Šคํฌ๋กค ํ›„ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ๊ฐœ์ˆ˜: {len(more_results)}')
    
finally:
    driver.quit()

์ด ์ฝ”๋“œ์—์„œ ์ฃผ๋ชฉํ•  ์ ์€:

  1. ๋ช…์‹œ์  ๋Œ€๊ธฐ(Explicit Wait): WebDriverWait์™€ expected_conditions๋ฅผ ์‚ฌ์šฉํ•ด ํŠน์ • ์กฐ๊ฑด์ด ์ถฉ์กฑ๋  ๋•Œ๊นŒ์ง€ ๋Œ€๊ธฐ
  2. ์š”์†Œ ์ƒํ˜ธ์ž‘์šฉ: send_keys()๋กœ ํ…์ŠคํŠธ ์ž…๋ ฅ, Keys.RETURN์œผ๋กœ Enter ํ‚ค ๋ˆ„๋ฅด๊ธฐ
  3. JavaScript ์‹คํ–‰: execute_script()๋กœ ์Šคํฌ๋กค ๋‹ค์šด ๊ฐ™์€ JavaScript ์ฝ”๋“œ ์‹คํ–‰

์ด๋Ÿฐ ๊ธฐ๋Šฅ๋“ค์„ ํ™œ์šฉํ•˜๋ฉด ๋ณต์žกํ•œ ๋™์  ์›นํŽ˜์ด์ง€๋„ ํšจ๊ณผ์ ์œผ๋กœ ํฌ๋กค๋งํ•  ์ˆ˜ ์žˆ์–ด! ๐ŸŒŸ

Headless ๋ชจ๋“œ: ๋ธŒ๋ผ์šฐ์ € UI ์—†์ด ์‹คํ–‰ํ•˜๊ธฐ ๐Ÿ‘ป

Selenium์€ ๊ธฐ๋ณธ์ ์œผ๋กœ ๋ธŒ๋ผ์šฐ์ € ์ฐฝ์„ ์—ด์–ด์„œ ์ž‘์—…ํ•˜์ง€๋งŒ, ์„œ๋ฒ„ ํ™˜๊ฒฝ์ด๋‚˜ ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์‹คํ–‰ํ•  ๋•Œ๋Š” UI ์—†์ด ์‹คํ–‰ํ•˜๋Š” 'Headless' ๋ชจ๋“œ๊ฐ€ ์œ ์šฉํ•ด:

from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.chrome.options import Options

# ํ—ค๋“œ๋ฆฌ์Šค ๋ชจ๋“œ ์„ค์ •
chrome_options = Options()
chrome_options.add_argument("--headless")
chrome_options.add_argument("--disable-gpu")  # Windows์—์„œ ํ•„์š”ํ•  ์ˆ˜ ์žˆ์Œ
chrome_options.add_argument("--window-size=1920,1080")  # ์ฐฝ ํฌ๊ธฐ ์„ค์ •

# ๋“œ๋ผ์ด๋ฒ„ ์ดˆ๊ธฐํ™”
driver = webdriver.Chrome(
    service=Service(ChromeDriverManager().install()),
    options=chrome_options
)

try:
    # ์›น ํŽ˜์ด์ง€ ์—ด๊ธฐ
    driver.get('https://example.com')
    print(f'ํŽ˜์ด์ง€ ์ œ๋ชฉ: {driver.title}')
    
    # ์Šคํฌ๋ฆฐ์ƒท ์ €์žฅ (ํ—ค๋“œ๋ฆฌ์Šค ๋ชจ๋“œ์—์„œ๋„ ์ž‘๋™)
    driver.save_screenshot('screenshot.png')
    print('์Šคํฌ๋ฆฐ์ƒท์ด ์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.')
    
finally:
    driver.quit()

ํ—ค๋“œ๋ฆฌ์Šค ๋ชจ๋“œ๋Š” UI๊ฐ€ ํ•„์š” ์—†๋Š” ์„œ๋ฒ„ ํ™˜๊ฒฝ์ด๋‚˜ ์ž๋™ํ™” ์Šคํฌ๋ฆฝํŠธ์—์„œ ํŠนํžˆ ์œ ์šฉํ•ด. ๋ฉ”๋ชจ๋ฆฌ๋„ ์ ๊ฒŒ ์‚ฌ์šฉํ•˜๊ณ  ๋” ๋น ๋ฅด๊ฒŒ ์‹คํ–‰๋˜์ง€! ๐Ÿš€

์‹ค์ „ ์˜ˆ์ œ: ๋ฌดํ•œ ์Šคํฌ๋กค ํŽ˜์ด์ง€ ํฌ๋กค๋ง ๐Ÿ“œ

์š”์ฆ˜ ๋งŽ์€ ์›น์‚ฌ์ดํŠธ๋Š” ํŽ˜์ด์ง€๋„ค์ด์…˜ ๋Œ€์‹  ๋ฌดํ•œ ์Šคํฌ๋กค์„ ์‚ฌ์šฉํ•ด. ์ด๋Ÿฐ ํŽ˜์ด์ง€๋ฅผ ํฌ๋กค๋งํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ณด์ž:

from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import time

driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()))

try:
    # ๋ฌดํ•œ ์Šคํฌ๋กค ํŽ˜์ด์ง€ ์—ด๊ธฐ
    driver.get('https://example-infinite-scroll.com')
    
    # ์•„์ดํ…œ์„ ์ €์žฅํ•  ๋ฆฌ์ŠคํŠธ
    all_items = []
    
    # ์ด์ „ ์•„์ดํ…œ ์ˆ˜
    prev_items_count = 0
    
    # ์ตœ๋Œ€ ์Šคํฌ๋กค ํšŸ์ˆ˜
    max_scrolls = 5
    scrolls = 0
    
    while scrolls < max_scrolls:
        # ํ˜„์žฌ ์•„์ดํ…œ ๊ฐ€์ ธ์˜ค๊ธฐ
        items = driver.find_elements(By.CSS_SELECTOR, '.item')
        items_count = len(items)
        
        print(f'ํ˜„์žฌ ์•„์ดํ…œ ์ˆ˜: {items_count}')
        
        # ์ƒˆ๋กœ์šด ์•„์ดํ…œ๋งŒ ์ฒ˜๋ฆฌ
        for i in range(prev_items_count, items_count):
            item_text = items[i].text
            all_items.append(item_text)
            print(f'์•„์ดํ…œ {i+1}: {item_text[:50]}...')
        
        # ์ด์ „ ์นด์šดํŠธ ์—…๋ฐ์ดํŠธ
        prev_items_count = items_count
        
        # ํŽ˜์ด์ง€ ๋์œผ๋กœ ์Šคํฌ๋กค
        driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
        
        # ์ž ์‹œ ๋Œ€๊ธฐ
        time.sleep(2)
        
        # ์ƒˆ ์ฝ˜ํ…์ธ ๊ฐ€ ๋กœ๋“œ๋  ๋•Œ๊นŒ์ง€ ๋Œ€๊ธฐ
        try:
            WebDriverWait(driver, 5).until(
                lambda d: len(d.find_elements(By.CSS_SELECTOR, '.item')) > prev_items_count
            )
        except:
            print("๋” ์ด์ƒ ์ƒˆ๋กœ์šด ์•„์ดํ…œ์ด ๋กœ๋“œ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.")
            break
        
        scrolls += 1
    
    print(f'์ด {len(all_items)}๊ฐœ์˜ ์•„์ดํ…œ์„ ์ˆ˜์ง‘ํ–ˆ์Šต๋‹ˆ๋‹ค.')
    
finally:
    driver.quit()

์ด ์ฝ”๋“œ๋Š” ๋ฌดํ•œ ์Šคํฌ๋กค ํŽ˜์ด์ง€์—์„œ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋ฐฉ์‹์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์ˆ˜์ง‘ํ•ด:

  1. ํ˜„์žฌ ํŽ˜์ด์ง€์— ์žˆ๋Š” ์•„์ดํ…œ ์ˆ˜์ง‘
  2. ํŽ˜์ด์ง€ ๋๊นŒ์ง€ ์Šคํฌ๋กค
  3. ์ƒˆ๋กœ์šด ์•„์ดํ…œ์ด ๋กœ๋“œ๋  ๋•Œ๊นŒ์ง€ ๋Œ€๊ธฐ
  4. ์ƒˆ๋กœ ๋กœ๋“œ๋œ ์•„์ดํ…œ๋งŒ ์ฒ˜๋ฆฌ
  5. ์ง€์ •๋œ ํšŸ์ˆ˜๋งŒํผ ๋ฐ˜๋ณตํ•˜๊ฑฐ๋‚˜ ๋” ์ด์ƒ ์ƒˆ ์•„์ดํ…œ์ด ๋กœ๋“œ๋˜์ง€ ์•Š์„ ๋•Œ๊นŒ์ง€ ๋ฐ˜๋ณต

๋ฌดํ•œ ์Šคํฌ๋กค, ๋™์  ๋กœ๋”ฉ, ํŒ์—… ์ฒ˜๋ฆฌ ๋“ฑ ๋ณต์žกํ•œ ์›นํŽ˜์ด์ง€๋„ Selenium์„ ์‚ฌ์šฉํ•˜๋ฉด ํšจ๊ณผ์ ์œผ๋กœ ํฌ๋กค๋งํ•  ์ˆ˜ ์žˆ์–ด! ์ด๊ฒƒ์ด Selenium์˜ ๊ฐ€์žฅ ํฐ ์žฅ์ ์ด์ง€. ๐Ÿฆธโ€โ™‚๏ธ

โš ๏ธ ์ฃผ์˜์‚ฌํ•ญ: Selenium์€ ๊ฐ•๋ ฅํ•˜์ง€๋งŒ, ์‹ค์ œ ๋ธŒ๋ผ์šฐ์ €๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ์— requests๋ณด๋‹ค ๋А๋ฆฌ๊ณ  ๋ฆฌ์†Œ์Šค๋ฅผ ๋งŽ์ด ์‚ฌ์šฉํ•ด. ๊ฐ€๋Šฅํ•˜๋ฉด API๋‚˜ requests๋ฅผ ๋จผ์ € ์‹œ๋„ํ•˜๊ณ , ๊ผญ ํ•„์š”ํ•œ ๊ฒฝ์šฐ์—๋งŒ Selenium์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ข‹์•„!

6. Scrapy ํ”„๋ ˆ์ž„์›Œํฌ ํ™œ์šฉํ•˜๊ธฐ ๐Ÿ•ท๏ธ

์ง€๊ธˆ๊นŒ์ง€ requests, BeautifulSoup, Selenium ๊ฐ™์€ ๋„๊ตฌ๋“ค์„ ์‚ดํŽด๋ดค์–ด. ์ด ๋„๊ตฌ๋“ค์€ ๊ฐ๊ฐ ์œ ์šฉํ•˜์ง€๋งŒ, ๋Œ€๊ทœ๋ชจ ํฌ๋กค๋ง ํ”„๋กœ์ ํŠธ๋ฅผ ์ง„ํ–‰ํ•  ๋•Œ๋Š” ๋” ์ฒด๊ณ„์ ์ธ ์ ‘๊ทผ์ด ํ•„์š”ํ•ด. ์—ฌ๊ธฐ์„œ ๋“ฑ์žฅํ•˜๋Š” ๊ฒƒ์ด ๋ฐ”๋กœ Scrapy๋ผ๋Š” ๊ฐ•๋ ฅํ•œ ์›น ํฌ๋กค๋ง ํ”„๋ ˆ์ž„์›Œํฌ์•ผ! ๐Ÿš€

Scrapy ์†Œ๊ฐœ ๋ฐ ์„ค์น˜ ๐Ÿ“ฆ

Scrapy๋Š” ํŒŒ์ด์ฌ์œผ๋กœ ์ž‘์„ฑ๋œ ์˜คํ”ˆ ์†Œ์Šค ์›น ํฌ๋กค๋ง ํ”„๋ ˆ์ž„์›Œํฌ์•ผ. ๋Œ€๊ทœ๋ชจ ํฌ๋กค๋ง ํ”„๋กœ์ ํŠธ๋ฅผ ์œ„ํ•œ ์™„์ „ํ•œ ์ธํ”„๋ผ๋ฅผ ์ œ๊ณตํ•˜๋ฉฐ, ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์žฅ์ ์ด ์žˆ์–ด:

  1. ๋น„๋™๊ธฐ ๋„คํŠธ์›Œํ‚น์œผ๋กœ ๋น ๋ฅธ ์„ฑ๋Šฅ
  2. ๋‚ด์žฅ๋œ ์„ ํƒ๊ธฐ(selector)๋กœ ์‰ฌ์šด ๋ฐ์ดํ„ฐ ์ถ”์ถœ
  3. ๋ฏธ๋“ค์›จ์–ด์™€ ํŒŒ์ดํ”„๋ผ์ธ์„ ํ†ตํ•œ ์œ ์—ฐํ•œ ์ฒ˜๋ฆฌ
  4. ์ž๋™ ์žฌ์‹œ๋„, ์ฟ ํ‚ค ๋ฐ ์„ธ์…˜ ์ฒ˜๋ฆฌ ๋“ฑ ๋‚ด์žฅ ๊ธฐ๋Šฅ
  5. ๋‹ค์–‘ํ•œ ์ถœ๋ ฅ ํ˜•์‹(JSON, CSV, XML ๋“ฑ) ์ง€์›

๋จผ์ € Scrapy๋ฅผ ์„ค์น˜ํ•ด๋ณผ๊ฒŒ:

# Scrapy ์„ค์น˜
pip install scrapy

Scrapy ํ”„๋กœ์ ํŠธ ์‹œ์ž‘ํ•˜๊ธฐ ๐Ÿ

Scrapy๋Š” ํ”„๋กœ์ ํŠธ ๊ธฐ๋ฐ˜์œผ๋กœ ์ž‘๋™ํ•ด. ์ƒˆ ํ”„๋กœ์ ํŠธ๋ฅผ ๋งŒ๋“ค๊ณ  ์ŠคํŒŒ์ด๋”(spider)๋ฅผ ์ž‘์„ฑํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ณด์ž:

# ์ƒˆ Scrapy ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ
scrapy startproject bookscrapy

# ํ”„๋กœ์ ํŠธ ๋””๋ ‰ํ† ๋ฆฌ๋กœ ์ด๋™
cd bookscrapy

# ์ŠคํŒŒ์ด๋” ์ƒ์„ฑ
scrapy genspider bookspider example-bookstore.com

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ๊ฐ€ ์ƒ์„ฑ๋ผ:

bookscrapy/
    โ”œโ”€โ”€ bookscrapy/
    โ”‚   โ”œโ”€โ”€ __init__.py
    โ”‚   โ”œโ”€โ”€ items.py          # ์•„์ดํ…œ ์ •์˜
    โ”‚   โ”œโ”€โ”€ middlewares.py    # ๋ฏธ๋“ค์›จ์–ด ์ •์˜
    โ”‚   โ”œโ”€โ”€ pipelines.py      # ํŒŒ์ดํ”„๋ผ์ธ ์ •์˜
    โ”‚   โ”œโ”€โ”€ settings.py       # ์„ค์ • ํŒŒ์ผ
    โ”‚   โ””โ”€โ”€ spiders/          # ์ŠคํŒŒ์ด๋” ๋””๋ ‰ํ† ๋ฆฌ
    โ”‚       โ”œโ”€โ”€ __init__.py
    โ”‚       โ””โ”€โ”€ bookspider.py # ์ƒ์„ฑ๋œ ์ŠคํŒŒ์ด๋”
    โ””โ”€โ”€ scrapy.cfg            # ํ”„๋กœ์ ํŠธ ์„ค์ • ํŒŒ์ผ

์ฒซ ๋ฒˆ์งธ Scrapy ์ŠคํŒŒ์ด๋” ์ž‘์„ฑํ•˜๊ธฐ ๐Ÿ•ธ๏ธ

์ด์ œ bookspider.py ํŒŒ์ผ์„ ํŽธ์ง‘ํ•ด์„œ ์ฒซ ๋ฒˆ์งธ ์ŠคํŒŒ์ด๋”๋ฅผ ์ž‘์„ฑํ•ด๋ณด์ž:

import scrapy

class BookspiderSpider(scrapy.Spider):
    name = 'bookspider'
    allowed_domains = ['example-bookstore.com']
    start_urls = ['https://example-bookstore.com/books']

    def parse(self, response):
        # ๋ชจ๋“  ์ฑ… ํ•ญ๋ชฉ ์„ ํƒ
        books = response.css('div.book-item')
        
        for book in books:
            # ๊ฐ ์ฑ…์˜ ์ •๋ณด ์ถ”์ถœ
            yield {
                'title': book.css('h2.title::text').get().strip(),
                'author': book.css('p.author::text').get().strip(),
                'price': book.css('span.price::text').get().strip(),
                'rating': book.css('div.rating::attr(data-rating)').get(),
                'url': book.css('a.book-link::attr(href)').get()
            }
        
        # ๋‹ค์Œ ํŽ˜์ด์ง€ ๋งํฌ ์ฐพ๊ธฐ
        next_page = response.css('a.next-page::attr(href)').get()
        if next_page:
            # ์ ˆ๋Œ€ URL๋กœ ๋ณ€ํ™˜
            next_page_url = response.urljoin(next_page)
            # ๋‹ค์Œ ํŽ˜์ด์ง€ ํฌ๋กค๋ง
            yield scrapy.Request(next_page_url, callback=self.parse)

์ด ์ŠคํŒŒ์ด๋”๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•ด:

  1. start_urls์— ์ง€์ •๋œ URL์—์„œ ์‹œ์ž‘
  2. ๊ฐ ์ฑ… ํ•ญ๋ชฉ(div.book-item)์„ ์ฐพ์•„์„œ ์ •๋ณด ์ถ”์ถœ
  3. ๋‹ค์Œ ํŽ˜์ด์ง€ ๋งํฌ๋ฅผ ์ฐพ์•„์„œ ์žฌ๊ท€์ ์œผ๋กœ ํฌ๋กค๋ง

์ŠคํŒŒ์ด๋”๋ฅผ ์‹คํ–‰ํ•˜๋ ค๋ฉด ๋‹ค์Œ ๋ช…๋ น์–ด๋ฅผ ์‚ฌ์šฉํ•ด:

# ์ŠคํŒŒ์ด๋” ์‹คํ–‰ ๋ฐ ๊ฒฐ๊ณผ๋ฅผ JSON ํŒŒ์ผ๋กœ ์ €์žฅ
scrapy crawl bookspider -o books.json

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ํฌ๋กค๋ง ๊ฒฐ๊ณผ๊ฐ€ books.json ํŒŒ์ผ์— ์ €์žฅ๋ผ. CSV๋‚˜ XML ๊ฐ™์€ ๋‹ค๋ฅธ ํ˜•์‹์œผ๋กœ๋„ ์ €์žฅํ•  ์ˆ˜ ์žˆ์–ด! ๐Ÿ“

์•„์ดํ…œ(Items) ์ •์˜ํ•˜๊ธฐ ๐Ÿ“

Scrapy์—์„œ๋Š” ์ถ”์ถœํ•  ๋ฐ์ดํ„ฐ์˜ ๊ตฌ์กฐ๋ฅผ ๋ฏธ๋ฆฌ ์ •์˜ํ•  ์ˆ˜ ์žˆ์–ด. items.py ํŒŒ์ผ์„ ํŽธ์ง‘ํ•ด๋ณด์ž:

import scrapy

class BookItem(scrapy.Item):
    title = scrapy.Field()
    author = scrapy.Field()
    price = scrapy.Field()
    rating = scrapy.Field()
    url = scrapy.Field()
    description = scrapy.Field()
    published_date = scrapy.Field()
    isbn = scrapy.Field()

์ด์ œ ์ŠคํŒŒ์ด๋”์—์„œ ์ด ์•„์ดํ…œ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด:

import scrapy
from bookscrapy.items import BookItem

class BookspiderSpider(scrapy.Spider):
    name = 'bookspider'
    allowed_domains = ['example-bookstore.com']
    start_urls = ['https://example-bookstore.com/books']

    def parse(self, response):
        books = response.css('div.book-item')
        
        for book in books:
            # ์ฑ… ์ƒ์„ธ ํŽ˜์ด์ง€ URL ๊ฐ€์ ธ์˜ค๊ธฐ
            book_url = response.urljoin(book.css('a.book-link::attr(href)').get())
            
            # ์ƒ์„ธ ํŽ˜์ด์ง€๋กœ ์ด๋™ํ•˜์—ฌ ์ถ”๊ฐ€ ์ •๋ณด ์ˆ˜์ง‘
            yield scrapy.Request(
                book_url,
                callback=self.parse_book_details,
                meta={'title': book.css('h2.title::text').get().strip(),
                      'author': book.css('p.author::text').get().strip(),
                      'price': book.css('span.price::text').get().strip(),
                      'rating': book.css('div.rating::attr(data-rating)').get()}
            )
        
        # ๋‹ค์Œ ํŽ˜์ด์ง€ ์ฒ˜๋ฆฌ
        next_page = response.css('a.next-page::attr(href)').get()
        if next_page:
            yield scrapy.Request(response.urljoin(next_page), callback=self.parse)
    
    def parse_book_details(self, response):
        # ์•„์ดํ…œ ์ƒ์„ฑ
        book_item = BookItem()
        
        # ์ด์ „ ํŽ˜์ด์ง€์—์„œ ๊ฐ€์ ธ์˜จ ๊ธฐ๋ณธ ์ •๋ณด ์„ค์ •
        book_item['title'] = response.meta['title']
        book_item['author'] = response.meta['author']
        book_item['price'] = response.meta['price']
        book_item['rating'] = response.meta['rating']
        book_item['url'] = response.url
        
        # ์ƒ์„ธ ํŽ˜์ด์ง€์—์„œ ์ถ”๊ฐ€ ์ •๋ณด ์ถ”์ถœ
        book_item['description'] = response.css('div.description::text').get().strip()
        book_item['published_date'] = response.css('span.published-date::text').get().strip()
        book_item['isbn'] = response.css('span.isbn::text').get().strip()
        
        yield book_item

์ด ์ฝ”๋“œ๋Š” ์ฑ… ๋ชฉ๋ก ํŽ˜์ด์ง€์—์„œ ๊ธฐ๋ณธ ์ •๋ณด๋ฅผ ์ˆ˜์ง‘ํ•œ ํ›„, ๊ฐ ์ฑ…์˜ ์ƒ์„ธ ํŽ˜์ด์ง€๋กœ ์ด๋™ํ•ด์„œ ์ถ”๊ฐ€ ์ •๋ณด๋ฅผ ์ˆ˜์ง‘ํ•ด. ์ด๋ ‡๊ฒŒ ์—ฌ๋Ÿฌ ๋‹จ๊ณ„์˜ ํฌ๋กค๋ง์„ ์ฒด๊ณ„์ ์œผ๋กœ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ์ด Scrapy์˜ ๊ฐ•์ ์ด์•ผ! ๐Ÿงฉ

ํŒŒ์ดํ”„๋ผ์ธ(Pipelines) ํ™œ์šฉํ•˜๊ธฐ ๐Ÿ”„

Scrapy์˜ ํŒŒ์ดํ”„๋ผ์ธ์„ ์‚ฌ์šฉํ•˜๋ฉด ์ถ”์ถœํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ณ  ์ €์žฅํ•˜๋Š” ๊ณผ์ •์„ ์ž๋™ํ™”ํ•  ์ˆ˜ ์žˆ์–ด. pipelines.py ํŒŒ์ผ์„ ํŽธ์ง‘ํ•ด๋ณด์ž:

import re
import sqlite3

class PriceCleaningPipeline:
    def process_item(self, item, spider):
        # ๊ฐ€๊ฒฉ์—์„œ ํ†ตํ™” ๊ธฐํ˜ธ ์ œ๊ฑฐํ•˜๊ณ  ์ˆซ์ž๋งŒ ์ถ”์ถœ
        if 'price' in item:
            item['price'] = re.sub(r'[^\d.]', '', item['price'])
            # ๋ฌธ์ž์—ด์„ float๋กœ ๋ณ€ํ™˜
            try:
                item['price'] = float(item['price'])
            except ValueError:
                item['price'] = 0.0
        return item

class SQLitePipeline:
    def __init__(self):
        # ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ๋ฐ ํ…Œ์ด๋ธ” ์ƒ์„ฑ
        self.conn = sqlite3.connect('books.db')
        self.cur = self.conn.cursor()
        self.cur.execute('''
            CREATE TABLE IF NOT EXISTS books
            (title TEXT, author TEXT, price REAL, rating TEXT,
             url TEXT, description TEXT, published_date TEXT, isbn TEXT)
        ''')
    
    def process_item(self, item, spider):
        # ์•„์ดํ…œ์„ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์‚ฝ์ž…
        self.cur.execute('''
            INSERT INTO books VALUES (?, ?, ?, ?, ?, ?, ?, ?)
        ''', (
            item.get('title', ''),
            item.get('author', ''),
            item.get('price', 0.0),
            item.get('rating', ''),
            item.get('url', ''),
            item.get('description', ''),
            item.get('published_date', ''),
            item.get('isbn', '')
        ))
        self.conn.commit()
        return item
    
    def close_spider(self, spider):
        # ์ŠคํŒŒ์ด๋”๊ฐ€ ์ข…๋ฃŒ๋  ๋•Œ ์—ฐ๊ฒฐ ๋‹ซ๊ธฐ
        self.conn.close()

ํŒŒ์ดํ”„๋ผ์ธ์„ ํ™œ์„ฑํ™”ํ•˜๋ ค๋ฉด settings.py ํŒŒ์ผ์— ๋‹ค์Œ ๋‚ด์šฉ์„ ์ถ”๊ฐ€ํ•ด:

ITEM_PIPELINES = {
    'bookscrapy.pipelines.PriceCleaningPipeline': 300,
    'bookscrapy.pipelines.SQLitePipeline': 400,
}

์ˆซ์ž๋Š” ํŒŒ์ดํ”„๋ผ์ธ์˜ ์‹คํ–‰ ์ˆœ์„œ๋ฅผ ๋‚˜ํƒ€๋‚ด. ๋‚ฎ์€ ์ˆซ์ž๊ฐ€ ๋จผ์ € ์‹คํ–‰๋ผ. ์ด ์˜ˆ์ œ์—์„œ๋Š” ๋จผ์ € ๊ฐ€๊ฒฉ ๋ฐ์ดํ„ฐ๋ฅผ ์ •์ œํ•œ ํ›„, SQLite ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ €์žฅํ•˜๊ฒŒ ๋ผ. ๐Ÿ”„

๋ฏธ๋“ค์›จ์–ด(Middlewares) ํ™œ์šฉํ•˜๊ธฐ ๐Ÿ”„

Scrapy์˜ ๋ฏธ๋“ค์›จ์–ด๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์š”์ฒญ๊ณผ ์‘๋‹ต์„ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ณผ์ •์„ ์ปค์Šคํ„ฐ๋งˆ์ด์ฆˆํ•  ์ˆ˜ ์žˆ์–ด. ์˜ˆ๋ฅผ ๋“ค์–ด, ์š”์ฒญ์— ์‚ฌ์šฉ์ž ์—์ด์ „ํŠธ๋ฅผ ์ถ”๊ฐ€ํ•˜๊ฑฐ๋‚˜, ์‘๋‹ต์„ ํ•„ํ„ฐ๋งํ•˜๋Š” ๋“ฑ์˜ ์ž‘์—…์„ ํ•  ์ˆ˜ ์žˆ์ง€. middlewares.py ํŒŒ์ผ์„ ํŽธ์ง‘ํ•ด๋ณด์ž:

import random
from scrapy import signals

class RandomUserAgentMiddleware:
    def __init__(self):
        self.user_agents = [
            'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
            'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Safari/605.1.15',
            'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0'
        ]
    
    def process_request(self, request, spider):
        # ๋žœ๋ค ์‚ฌ์šฉ์ž ์—์ด์ „ํŠธ ์„ค์ •
        request.headers['User-Agent'] = random.choice(self.user_agents)
        return None

class DelayedRequestsMiddleware:
    def process_request(self, request, spider):
        # ์š”์ฒญ ์‚ฌ์ด์— ๋žœ๋ค ์ง€์—ฐ ์ถ”๊ฐ€
        import time
        time.sleep(random.uniform(1, 3))
        return None

๋ฏธ๋“ค์›จ์–ด๋ฅผ ํ™œ์„ฑํ™”ํ•˜๋ ค๋ฉด settings.py ํŒŒ์ผ์— ๋‹ค์Œ ๋‚ด์šฉ์„ ์ถ”๊ฐ€ํ•ด:

DOWNLOADER_MIDDLEWARES = {
    'bookscrapy.middlewares.RandomUserAgentMiddleware': 400,
    'bookscrapy.middlewares.DelayedRequestsMiddleware': 500,
}

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ๊ฐ ์š”์ฒญ๋งˆ๋‹ค ๋žœ๋คํ•œ ์‚ฌ์šฉ์ž ์—์ด์ „ํŠธ๊ฐ€ ์„ค์ •๋˜๊ณ , ์š”์ฒญ ์‚ฌ์ด์— 1~3์ดˆ์˜ ๋žœ๋ค ์ง€์—ฐ์ด ์ถ”๊ฐ€๋ผ. ์ด๋Š” ์›น์‚ฌ์ดํŠธ์— ๊ณผ๋„ํ•œ ๋ถ€ํ•˜๋ฅผ ์ฃผ์ง€ ์•Š๊ณ  ์ฐจ๋‹จ์„ ๋ฐฉ์ง€ํ•˜๋Š” ๋ฐ ๋„์›€์ด ๋ผ! ๐Ÿ›ก๏ธ

Scrapy Shell๋กœ ๋””๋ฒ„๊น…ํ•˜๊ธฐ ๐Ÿš

Scrapy Shell์€ ์›นํŽ˜์ด์ง€๋ฅผ ๋Œ€ํ™”ํ˜•์œผ๋กœ ํƒ์ƒ‰ํ•˜๊ณ  ์„ ํƒ์ž๋ฅผ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ๋Š” ๊ฐ•๋ ฅํ•œ ๋„๊ตฌ์•ผ:

# Scrapy Shell ์‹คํ–‰
scrapy shell "https://example-bookstore.com/books"

# ์‘๋‹ต ๊ฐ์ฒด ํ™•์ธ
response

# CSS ์„ ํƒ์ž ํ…Œ์ŠคํŠธ
response.css('div.book-item')
response.css('h2.title::text').getall()

# XPath ์„ ํƒ์ž ํ…Œ์ŠคํŠธ
response.xpath('//div[@class="book-item"]')
response.xpath('//h2[@class="title"]/text()').getall()

# ํŠน์ • ์š”์†Œ ์„ ํƒ ๋ฐ ํƒ์ƒ‰
book = response.css('div.book-item')[0]
book.css('h2.title::text').get()
book.css('p.author::text').get()

Scrapy Shell์„ ์‚ฌ์šฉํ•˜๋ฉด ์‹ค์ œ ํฌ๋กค๋Ÿฌ๋ฅผ ์ž‘์„ฑํ•˜๊ธฐ ์ „์— ์„ ํƒ์ž๋ฅผ ํ…Œ์ŠคํŠธํ•˜๊ณ  ๋””๋ฒ„๊น…ํ•  ์ˆ˜ ์žˆ์–ด. ์ด๋Š” ํฌ๋กค๋Ÿฌ ๊ฐœ๋ฐœ ์‹œ๊ฐ„์„ ํฌ๊ฒŒ ๋‹จ์ถ•์‹œ์ผœ์ค˜! ๐Ÿ”

๐Ÿ’ก Scrapy ํŒ: Scrapy๋Š” ๋Œ€๊ทœ๋ชจ ํฌ๋กค๋ง ํ”„๋กœ์ ํŠธ์— ์ ํ•ฉํ•ด. ํ•˜์ง€๋งŒ ๊ฐ„๋‹จํ•œ ํฌ๋กค๋ง ์ž‘์—…์—๋Š” requests์™€ BeautifulSoup์˜ ์กฐํ•ฉ์ด ๋” ๊ฐ„๋‹จํ•˜๊ณ  ์ง๊ด€์ ์ผ ์ˆ˜ ์žˆ์–ด. ํ”„๋กœ์ ํŠธ์˜ ๊ทœ๋ชจ์™€ ๋ณต์žก์„ฑ์— ๋งž๋Š” ๋„๊ตฌ๋ฅผ ์„ ํƒํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ด!

7. API๋ฅผ ํ™œ์šฉํ•œ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ๐Ÿ”Œ

์ง€๊ธˆ๊นŒ์ง€ HTML ํŽ˜์ด์ง€๋ฅผ ํฌ๋กค๋งํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์‚ดํŽด๋ดค์–ด. ํ•˜์ง€๋งŒ ๋งŽ์€ ์›น์‚ฌ์ดํŠธ๋Š” API(Application Programming Interface)๋ฅผ ์ œ๊ณตํ•ด์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๋” ์‰ฝ๊ฒŒ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ค˜. API๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด HTML ํŒŒ์‹ฑ ์—†์ด๋„ ๊ตฌ์กฐํ™”๋œ ๋ฐ์ดํ„ฐ๋ฅผ ์ง์ ‘ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์–ด์„œ ํฌ๋กค๋ง๋ณด๋‹ค ํ›จ์”ฌ ํšจ์œจ์ ์ด๊ณ  ์•ˆ์ •์ ์ด์•ผ! ๐Ÿš€

API๋ž€ ๋ฌด์—‡์ธ๊ฐ€? ๐Ÿค”

API๋Š” ๋‘ ์†Œํ”„ํŠธ์›จ์–ด ๊ตฌ์„ฑ ์š”์†Œ๊ฐ€ ์„œ๋กœ ํ†ต์‹ ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋Š” ์ธํ„ฐํŽ˜์ด์Šค์•ผ. ์›น API๋Š” ๋ณดํ†ต HTTP ์š”์ฒญ์„ ํ†ตํ•ด ๋ฐ์ดํ„ฐ๋ฅผ ์ฃผ๊ณ ๋ฐ›์œผ๋ฉฐ, JSON์ด๋‚˜ XML ํ˜•์‹์œผ๋กœ ์‘๋‹ต์„ ์ œ๊ณตํ•ด.

API๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์˜ ์žฅ์ ์€ ๋‹ค์Œ๊ณผ ๊ฐ™์•„:

  1. ๊ตฌ์กฐํ™”๋œ ๋ฐ์ดํ„ฐ๋ฅผ ์ง์ ‘ ๋ฐ›์„ ์ˆ˜ ์žˆ์Œ
  2. HTML ๊ตฌ์กฐ ๋ณ€๊ฒฝ์— ์˜ํ–ฅ์„ ๋ฐ›์ง€ ์•Š์Œ
  3. ์„œ๋ฒ„ ๋ถ€ํ•˜๊ฐ€ ์ ๊ณ  ํšจ์œจ์ 
  4. ๋Œ€๋ถ€๋ถ„ ๊ณต์‹์ ์œผ๋กœ ์ง€์›๋˜๋ฏ€๋กœ ๋ฒ•์ /์œค๋ฆฌ์  ๋ฌธ์ œ๊ฐ€ ์ ์Œ
ํด๋ผ์ด์–ธํŠธ API ์„œ๋ฒ„ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค HTTP ์š”์ฒญ JSON/XML ์‘๋‹ต ์ฟผ๋ฆฌ ๋ฐ์ดํ„ฐ API ์ž‘๋™ ๋ฐฉ์‹

REST API ๊ธฐ๋ณธ ์ดํ•ดํ•˜๊ธฐ ๐ŸŒ

๊ฐ€์žฅ ํ”ํ•œ ์›น API ์œ ํ˜•์€ REST(Representational State Transfer) API์•ผ. REST API๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ํŠน์ง•์ด ์žˆ์–ด:

  1. ์ž์›(Resource)์„ URI๋กœ ํ‘œํ˜„
  2. HTTP ๋ฉ”์„œ๋“œ(GET, POST, PUT, DELETE ๋“ฑ)๋กœ ์ž์›์— ๋Œ€ํ•œ ํ–‰๋™ ์ •์˜
  3. ํด๋ผ์ด์–ธํŠธ์™€ ์„œ๋ฒ„์˜ ์ƒํƒœ๋ฅผ ๋ถ„๋ฆฌ(Stateless)

์˜ˆ๋ฅผ ๋“ค์–ด, ์ฑ… ์ •๋ณด๋ฅผ ์ œ๊ณตํ•˜๋Š” REST API๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์—”๋“œํฌ์ธํŠธ๋ฅผ ๊ฐ€์งˆ ์ˆ˜ ์žˆ์–ด:

GET /api/books           # ๋ชจ๋“  ์ฑ… ๋ชฉ๋ก ์กฐํšŒ
GET /api/books/123       # ID๊ฐ€ 123์ธ ์ฑ… ์กฐํšŒ
GET /api/books?author=๊น€์ž‘๊ฐ€  # ์ €์ž๊ฐ€ '๊น€์ž‘๊ฐ€'์ธ ์ฑ… ์กฐํšŒ
POST /api/books          # ์ƒˆ ์ฑ… ์ถ”๊ฐ€
PUT /api/books/123       # ID๊ฐ€ 123์ธ ์ฑ… ์ •๋ณด ์ˆ˜์ •
DELETE /api/books/123    # ID๊ฐ€ 123์ธ ์ฑ… ์‚ญ์ œ

requests๋กœ API ํ˜ธ์ถœํ•˜๊ธฐ ๐Ÿ“ž

ํŒŒ์ด์ฌ์—์„œ API๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ๊ฐ€์žฅ ๊ฐ„๋‹จํ•œ ๋ฐฉ๋ฒ•์€ requests ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฑฐ์•ผ:

import requests
import json

# GET ์š”์ฒญ ์˜ˆ์‹œ
response = requests.get('https://api.example.com/books')

# ์‘๋‹ต ์ƒํƒœ ํ™•์ธ
if response.status_code == 200:
    # JSON ์‘๋‹ต ํŒŒ์‹ฑ
    data = response.json()
    
    # ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ
    for book in data['books']:
        print(f"์ œ๋ชฉ: {book['title']}")
        print(f"์ €์ž: {book['author']}")
        print(f"๊ฐ€๊ฒฉ: {book['price']}")
        print('-' * 30)
else:
    print(f"์˜ค๋ฅ˜ ๋ฐœ์ƒ: {response.status_code}")

API๋Š” ๋ณดํ†ต JSON ํ˜•์‹์œผ๋กœ ์‘๋‹ต์„ ์ œ๊ณตํ•ด. response.json() ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด JSON ์‘๋‹ต์„ ํŒŒ์ด์ฌ ๋”•์…”๋„ˆ๋ฆฌ๋กœ ๋ณ€ํ™˜ํ•  ์ˆ˜ ์žˆ์–ด. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด HTML ํŒŒ์‹ฑ ์—†์ด๋„ ๊ตฌ์กฐํ™”๋œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ”๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์ง€! ๐ŸŽ

๋งค๊ฐœ๋ณ€์ˆ˜์™€ ํ—ค๋” ์ „์†กํ•˜๊ธฐ ๐Ÿ“

API ํ˜ธ์ถœ ์‹œ ๋งค๊ฐœ๋ณ€์ˆ˜(ํŒŒ๋ผ๋ฏธํ„ฐ)์™€ ํ—ค๋”๋ฅผ ์ „์†กํ•ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์•„:

import requests

# ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ ์ „์†ก
params = {
    'author': '๊น€์ž‘๊ฐ€',
    'min_price': 10000,
    'max_price': 30000,
    'sort': 'price_asc'
}

# ํ—ค๋” ์„ค์ •
headers = {
    'User-Agent': 'MyApp/1.0',
    'Accept': 'application/json',
    'Authorization': 'Bearer YOUR_API_KEY'
}

# GET ์š”์ฒญ with ํŒŒ๋ผ๋ฏธํ„ฐ ๋ฐ ํ—ค๋”
response = requests.get(
    'https://api.example.com/books',
    params=params,
    headers=headers
)

# ์‘๋‹ต ํ™•์ธ
print(f"URL: {response.url}")  # ์‹ค์ œ ์š”์ฒญ๋œ URL ํ™•์ธ
print(f"์ƒํƒœ ์ฝ”๋“œ: {response.status_code}")

if response.status_code == 200:
    data = response.json()
    print(f"์ด {len(data['books'])}๊ฐœ์˜ ์ฑ…์„ ์ฐพ์•˜์Šต๋‹ˆ๋‹ค.")

params ๋งค๊ฐœ๋ณ€์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด URL์— ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ์ž๋™์œผ๋กœ ์ถ”๊ฐ€๋ผ. ๋˜ํ•œ headers ๋งค๊ฐœ๋ณ€์ˆ˜๋ฅผ ์‚ฌ์šฉํ•ด ์š”์ฒญ ํ—ค๋”๋ฅผ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์–ด. ํŠนํžˆ API ํ‚ค๋‚˜ ์ธ์ฆ ํ† ํฐ์€ ๋ณดํ†ต ํ—ค๋”๋ฅผ ํ†ตํ•ด ์ „์†กํ•ด! ๐Ÿ”‘

POST ์š”์ฒญ์œผ๋กœ ๋ฐ์ดํ„ฐ ์ „์†กํ•˜๊ธฐ ๐Ÿ“ค

๋ฐ์ดํ„ฐ๋ฅผ ์„œ๋ฒ„์— ์ „์†กํ•˜๋ ค๋ฉด POST ์š”์ฒญ์„ ์‚ฌ์šฉํ•ด:

import requests

# ์ „์†กํ•  ๋ฐ์ดํ„ฐ
new_book = {
    'title': 'ํŒŒ์ด์ฌ์œผ๋กœ ๋ฐฐ์šฐ๋Š” ์›น ํฌ๋กค๋ง',
    'author': 'ํ™๊ธธ๋™',
    'price': 25000,
    'description': '์›น ํฌ๋กค๋ง๊ณผ ์Šคํฌ๋ž˜ํ•‘์˜ ๋ชจ๋“  ๊ฒƒ์„ ๋‹ค๋ฃฌ ์ฑ…์ž…๋‹ˆ๋‹ค.'
}

# POST ์š”์ฒญ
response = requests.post(
    'https://api.example.com/books',
    json=new_book,  # JSON ํ˜•์‹์œผ๋กœ ๋ฐ์ดํ„ฐ ์ „์†ก
    headers={'Authorization': 'Bearer YOUR_API_KEY'}
)

# ์‘๋‹ต ํ™•์ธ
if response.status_code == 201:  # 201 Created
    created_book = response.json()
    print(f"์ƒˆ ์ฑ…์ด ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ID: {created_book['id']}")
else:
    print(f"์˜ค๋ฅ˜ ๋ฐœ์ƒ: {response.status_code}")
    print(response.text)

json ๋งค๊ฐœ๋ณ€์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ํŒŒ์ด์ฌ ๋”•์…”๋„ˆ๋ฆฌ๊ฐ€ ์ž๋™์œผ๋กœ JSON ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜๋ผ. ๋˜๋Š” data ๋งค๊ฐœ๋ณ€์ˆ˜๋ฅผ ์‚ฌ์šฉํ•ด ํผ ๋ฐ์ดํ„ฐ๋ฅผ ์ „์†กํ•  ์ˆ˜๋„ ์žˆ์–ด. ๐Ÿ”„

์ธ์ฆ ์ฒ˜๋ฆฌํ•˜๊ธฐ ๐Ÿ”

๋งŽ์€ API๋Š” ์ธ์ฆ์ด ํ•„์š”ํ•ด. ์ธ์ฆ ๋ฐฉ์‹์—๋Š” ์—ฌ๋Ÿฌ ๊ฐ€์ง€๊ฐ€ ์žˆ์–ด:

import requests

# ๊ธฐ๋ณธ ์ธ์ฆ (์‚ฌ์šฉ์ž๋ช…/๋น„๋ฐ€๋ฒˆํ˜ธ)
response = requests.get(
    'https://api.example.com/books',
    auth=('username', 'password')
)

# API ํ‚ค ์ธ์ฆ (์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ)
response = requests.get(
    'https://api.example.com/books',
    params={'api_key': 'YOUR_API_KEY'}
)

# API ํ‚ค ์ธ์ฆ (ํ—ค๋”)
response = requests.get(
    'https://api.example.com/books',
    headers={'X-API-Key': 'YOUR_API_KEY'}
)

# OAuth 2.0 ํ† ํฐ ์ธ์ฆ
response = requests.get(
    'https://api.example.com/books',
    headers={'Authorization': 'Bearer YOUR_ACCESS_TOKEN'}
)

API ๋ฌธ์„œ๋ฅผ ํ™•์ธํ•ด์„œ ์–ด๋–ค ์ธ์ฆ ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•ด์•ผ ํ•˜๋Š”์ง€ ์•Œ์•„๋ณด๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ด. ์ธ์ฆ ์ •๋ณด๋Š” ๋ฏผ๊ฐํ•œ ์ •๋ณด์ด๋ฏ€๋กœ ์ฝ”๋“œ์— ์ง์ ‘ ํ•˜๋“œ์ฝ”๋”ฉํ•˜์ง€ ๋ง๊ณ  ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋‚˜ ์„ค์ • ํŒŒ์ผ์—์„œ ๋ถˆ๋Ÿฌ์˜ค๋Š” ๊ฒƒ์ด ์ข‹์•„! ๐Ÿ”’

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

API์—์„œ ๋Œ€๋Ÿ‰์˜ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ฌ ๋•Œ๋Š” ํŽ˜์ด์ง€๋„ค์ด์…˜์„ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•ด:

import requests

url = 'https://api.example.com/books'
all_books = []
page = 1

while True:
    # ํŽ˜์ด์ง€ ํŒŒ๋ผ๋ฏธํ„ฐ ์„ค์ •
    params = {
        'page': page,
        'per_page': 100  # ํ•œ ํŽ˜์ด์ง€๋‹น ํ•ญ๋ชฉ ์ˆ˜
    }
    
    response = requests.get(url, params=params)
    
    if response.status_code == 200:
        data = response.json()
        books = data['books']
        
        # ๊ฒฐ๊ณผ๊ฐ€ ์—†์œผ๋ฉด ์ข…๋ฃŒ
        if not books:
            break
        
        all_books.extend(books)
        print(f"ํŽ˜์ด์ง€ {page} ์ฒ˜๋ฆฌ ์™„๋ฃŒ: {len(books)}๊ฐœ ํ•ญ๋ชฉ")
        
        # ๋‹ค์Œ ํŽ˜์ด์ง€๋กœ
        page += 1
        
        # API ์ œํ•œ์„ ๊ณ ๋ คํ•œ ์ง€์—ฐ
        import time
        time.sleep(1)
    else:
        print(f"์˜ค๋ฅ˜ ๋ฐœ์ƒ: {response.status_code}")
        break

print(f"์ด {len(all_books)}๊ฐœ์˜ ์ฑ…์„ ์ˆ˜์ง‘ํ–ˆ์Šต๋‹ˆ๋‹ค.")

API๋งˆ๋‹ค ํŽ˜์ด์ง€๋„ค์ด์…˜ ๋ฐฉ์‹์ด ๋‹ค๋ฅผ ์ˆ˜ ์žˆ์–ด. ์ผ๋ถ€ API๋Š” page/per_page ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์‚ฌ์šฉํ•˜๊ณ , ๋‹ค๋ฅธ API๋Š” offset/limit ๋˜๋Š” cursor ๊ธฐ๋ฐ˜ ํŽ˜์ด์ง€๋„ค์ด์…˜์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด. API ๋ฌธ์„œ๋ฅผ ํ™•์ธํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ด! ๐Ÿ“š

์‹ค์ „ ์˜ˆ์ œ: ๊ณต๊ณต API ํ™œ์šฉํ•˜๊ธฐ ๐Ÿ›๏ธ

ํ•œ๊ตญ์˜ ๊ณต๊ณต ๋ฐ์ดํ„ฐ ํฌํ„ธ(data.go.kr)์—์„œ ์ œ๊ณตํ•˜๋Š” API๋ฅผ ํ™œ์šฉํ•˜๋Š” ์˜ˆ์ œ๋ฅผ ์‚ดํŽด๋ณด์ž:

import requests
import pandas as pd
from datetime import datetime

# ๊ณต๊ณต ๋ฐ์ดํ„ฐ ํฌํ„ธ API ํ‚ค (์‹ค์ œ ์‚ฌ์šฉ ์‹œ ๋ฐœ๊ธ‰ ํ•„์š”)
api_key = 'YOUR_API_KEY'

# ์ฝ”๋กœ๋‚˜19 ๊ฐ์—ผ ํ˜„ํ™ฉ API ํ˜ธ์ถœ
url = 'http://openapi.data.go.kr/openapi/service/rest/Covid19/getCovid19InfStateJson'
params = {
    'serviceKey': api_key,
    'pageNo': 1,
    'numOfRows': 10,
    'startCreateDt': '20210101',
    'endCreateDt': '20210131'
}

response = requests.get(url, params=params)

# XML ์‘๋‹ต ํŒŒ์‹ฑ (์ด API๋Š” XML ํ˜•์‹์œผ๋กœ ์‘๋‹ต)
from xml.etree import ElementTree as ET
root = ET.fromstring(response.content)

# ๋ฐ์ดํ„ฐ ์ถ”์ถœ
items = root.findall('.//item')
covid_data = []

for item in items:
    data = {
        '๋‚ ์งœ': item.find('stateDt').text,
        'ํ™•์ง„์ž': item.find('decideCnt').text,
        '์‚ฌ๋ง์ž': item.find('deathCnt').text,
        '๊ฒ€์‚ฌ์ค‘': item.find('examCnt').text,
        '๊ฒฉ๋ฆฌํ•ด์ œ': item.find('clearCnt').text
    }
    covid_data.append(data)

# ๋ฐ์ดํ„ฐํ”„๋ ˆ์ž„์œผ๋กœ ๋ณ€ํ™˜
df = pd.DataFrame(covid_data)

# ๋‚ ์งœ ํ˜•์‹ ๋ณ€ํ™˜ ๋ฐ ์ •๋ ฌ
df['๋‚ ์งœ'] = pd.to_datetime(df['๋‚ ์งœ'])
df = df.sort_values('๋‚ ์งœ')

# ์ˆซ์ž ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜
numeric_columns = ['ํ™•์ง„์ž', '์‚ฌ๋ง์ž', '๊ฒ€์‚ฌ์ค‘', '๊ฒฉ๋ฆฌํ•ด์ œ']
for col in numeric_columns:
    df[col] = pd.to_numeric(df[col])

# ์ผ๋ณ„ ์‹ ๊ทœ ํ™•์ง„์ž ๊ณ„์‚ฐ
df['์‹ ๊ทœํ™•์ง„์ž'] = df['ํ™•์ง„์ž'].diff()

print(df.head())

# CSV ํŒŒ์ผ๋กœ ์ €์žฅ
df.to_csv('covid19_data_202101.csv', index=False, encoding='utf-8-sig')

์ด ์˜ˆ์ œ๋Š” ๊ณต๊ณต ๋ฐ์ดํ„ฐ ํฌํ„ธ์˜ ์ฝ”๋กœ๋‚˜19 API๋ฅผ ํ˜ธ์ถœํ•ด์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์ˆ˜์ง‘ํ•˜๊ณ , pandas๋ฅผ ์‚ฌ์šฉํ•ด ๋ฐ์ดํ„ฐ๋ฅผ ์ฒ˜๋ฆฌํ•œ ํ›„ CSV ํŒŒ์ผ๋กœ ์ €์žฅํ•ด. API๋ฅผ ํ†ตํ•ด ์ˆ˜์ง‘ํ•œ ๋ฐ์ดํ„ฐ๋Š” ๋ฐ”๋กœ ๋ฐ์ดํ„ฐ ๋ถ„์„์— ํ™œ์šฉํ•  ์ˆ˜ ์žˆ์–ด! ๐Ÿ“Š

๐Ÿ’ก API ํ™œ์šฉ ํŒ: ๊ฐ€๋Šฅํ•˜๋ฉด ํฌ๋กค๋ง๋ณด๋‹ค API๋ฅผ ์šฐ์„ ์ ์œผ๋กœ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ข‹์•„. API๋Š” ๋” ์•ˆ์ •์ ์ด๊ณ , ๊ตฌ์กฐํ™”๋œ ๋ฐ์ดํ„ฐ๋ฅผ ์ œ๊ณตํ•˜๋ฉฐ, ์›น์‚ฌ์ดํŠธ ๋ณ€๊ฒฝ์— ์˜ํ–ฅ์„ ๋ฐ›์ง€ ์•Š์•„. ๋˜ํ•œ ๋Œ€๋ถ€๋ถ„์˜ API๋Š” ์‚ฌ์šฉ๋Ÿ‰ ์ œํ•œ(rate limit)์ด ์žˆ์œผ๋‹ˆ API ๋ฌธ์„œ๋ฅผ ๊ผผ๊ผผํžˆ ํ™•์ธํ•˜๊ณ  ์ œํ•œ์„ ์ค€์ˆ˜ํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ด!

9. ๋ฐ์ดํ„ฐ ์ €์žฅ ๋ฐ ๊ด€๋ฆฌ ๋ฐฉ๋ฒ• ๐Ÿ’พ

ํฌ๋กค๋ง์„ ํ†ตํ•ด ๋ฐ์ดํ„ฐ๋ฅผ ์ˆ˜์ง‘ํ–ˆ๋‹ค๋ฉด, ์ด์ œ ๊ทธ ๋ฐ์ดํ„ฐ๋ฅผ ์–ด๋–ป๊ฒŒ ์ €์žฅํ•˜๊ณ  ๊ด€๋ฆฌํ• ์ง€ ์ƒ๊ฐํ•ด๋ด์•ผ ํ•ด. ํšจ์œจ์ ์ธ ๋ฐ์ดํ„ฐ ์ €์žฅ๊ณผ ๊ด€๋ฆฌ๋Š” ํฌ๋กค๋ง ํ”„๋กœ์ ํŠธ์˜ ์„ฑ๊ณต์— ์ค‘์š”ํ•œ ๋ถ€๋ถ„์ด์•ผ! ์ด ์„น์…˜์—์„œ๋Š” ๋‹ค์–‘ํ•œ ๋ฐ์ดํ„ฐ ์ €์žฅ ๋ฐฉ๋ฒ•๊ณผ ๊ฐ๊ฐ์˜ ์žฅ๋‹จ์ ์„ ์•Œ์•„๋ณผ๊ฒŒ. ๐Ÿ“Š

ํŒŒ์ผ ๊ธฐ๋ฐ˜ ์ €์žฅ ๋ฐฉ์‹ ๐Ÿ“„

๊ฐ€์žฅ ๊ฐ„๋‹จํ•œ ๋ฐฉ๋ฒ•์€ ์ˆ˜์ง‘ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ํŒŒ์ผ๋กœ ์ €์žฅํ•˜๋Š” ๊ฑฐ์•ผ. ์ฃผ๋กœ ์‚ฌ์šฉ๋˜๋Š” ํŒŒ์ผ ํ˜•์‹์€ ๋‹ค์Œ๊ณผ ๊ฐ™์•„:

1. CSV (Comma-Separated Values) ํŒŒ์ผ

import csv

# ๋ฐ์ดํ„ฐ ์˜ˆ์‹œ
data = [
    {'title': 'ํŒŒ์ด์ฌ ๊ธฐ์ดˆ', 'author': '๊น€ํŒŒ์ด', 'price': 25000},
    {'title': '์›น ํฌ๋กค๋ง ๋งˆ์Šคํ„ฐ', 'author': '์ดํฌ๋กค', 'price': 30000},
    {'title': '๋ฐ์ดํ„ฐ ๋ถ„์„ ์ž…๋ฌธ', 'author': '๋ฐ•๋ฐ์ดํ„ฐ', 'price': 28000}
]

# CSV ํŒŒ์ผ๋กœ ์ €์žฅ
with open('books.csv', 'w', newline='', encoding='utf-8-sig') as f:
    # ํ•„๋“œ ์ด๋ฆ„ ์ •์˜
    fieldnames = ['title', 'author', 'price']
    writer = csv.DictWriter(f, fieldnames=fieldnames)
    
    # ํ—ค๋” ์“ฐ๊ธฐ
    writer.writeheader()
    
    # ๋ฐ์ดํ„ฐ ์“ฐ๊ธฐ
    for item in data:
        writer.writerow(item)

print("CSV ํŒŒ์ผ ์ €์žฅ ์™„๋ฃŒ!")

์žฅ์ : ๊ฐ„๋‹จํ•˜๊ณ , ์—‘์…€ ๋“ฑ์˜ ํ”„๋กœ๊ทธ๋žจ์—์„œ ์‰ฝ๊ฒŒ ์—ด ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์šฉ๋Ÿ‰์ด ์ž‘์•„.
๋‹จ์ : ๋ณต์žกํ•œ ๊ตฌ์กฐ์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๊ธฐ ์–ด๋ ต๊ณ , ๋Œ€์šฉ๋Ÿ‰ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ์— ๋น„ํšจ์œจ์ ์ผ ์ˆ˜ ์žˆ์–ด.

2. JSON (JavaScript Object Notation) ํŒŒ์ผ

import json

# ๋ฐ์ดํ„ฐ ์˜ˆ์‹œ (์ค‘์ฒฉ ๊ตฌ์กฐ ํฌํ•จ)
data = [
    {
        'title': 'ํŒŒ์ด์ฌ ๊ธฐ์ดˆ',
        'author': '๊น€ํŒŒ์ด',
        'price': 25000,
        'categories': ['ํ”„๋กœ๊ทธ๋ž˜๋ฐ', 'ํŒŒ์ด์ฌ'],
        'reviews': [
            {'user': '๋…์ž1', 'rating': 5, 'comment': '์ข‹์€ ์ฑ…์ด์—์š”!'},
            {'user': '๋…์ž2', 'rating': 4, 'comment': '์ดˆ๋ณด์ž์—๊ฒŒ ์ถ”์ฒœํ•ฉ๋‹ˆ๋‹ค.'}
        ]
    },
    # ๋‹ค๋ฅธ ์ฑ… ๋ฐ์ดํ„ฐ...
]

# JSON ํŒŒ์ผ๋กœ ์ €์žฅ
with open('books.json', 'w', encoding='utf-8') as f:
    json.dump(data, f, ensure_ascii=False, indent=4)

print("JSON ํŒŒ์ผ ์ €์žฅ ์™„๋ฃŒ!")

์žฅ์ : ๋ณต์žกํ•œ ์ค‘์ฒฉ ๊ตฌ์กฐ๋ฅผ ํ‘œํ˜„ํ•  ์ˆ˜ ์žˆ๊ณ , ์›น ํ™˜๊ฒฝ๊ณผ ํ˜ธํ™˜์„ฑ์ด ์ข‹์œผ๋ฉฐ, ์‚ฌ๋žŒ์ด ์ฝ๊ธฐ ์‰ฌ์›Œ.
๋‹จ์ : CSV๋ณด๋‹ค ํŒŒ์ผ ํฌ๊ธฐ๊ฐ€ ํฌ๊ณ , ๋Œ€์šฉ๋Ÿ‰ ๋ฐ์ดํ„ฐ์˜ ๊ฒฝ์šฐ ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰์ด ๋งŽ์„ ์ˆ˜ ์žˆ์–ด.

3. XML (eXtensible Markup Language) ํŒŒ์ผ

import xml.etree.ElementTree as ET
from xml.dom import minidom

# XML ๋ฃจํŠธ ์š”์†Œ ์ƒ์„ฑ
root = ET.Element('books')

# ์ฑ… ๋ฐ์ดํ„ฐ ์ถ”๊ฐ€
book = ET.SubElement(root, 'book')
ET.SubElement(book, 'title').text = 'ํŒŒ์ด์ฌ ๊ธฐ์ดˆ'
ET.SubElement(book, 'author').text = '๊น€ํŒŒ์ด'
ET.SubElement(book, 'price').text = '25000'

# ์นดํ…Œ๊ณ ๋ฆฌ ์ถ”๊ฐ€ (์ค‘์ฒฉ ๊ตฌ์กฐ)
categories = ET.SubElement(book, 'categories')
ET.SubElement(categories, 'category').text = 'ํ”„๋กœ๊ทธ๋ž˜๋ฐ'
ET.SubElement(categories, 'category').text = 'ํŒŒ์ด์ฌ'

# XML ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ (์˜ˆ์˜๊ฒŒ ์ถœ๋ ฅ)
xml_str = minidom.parseString(ET.tostring(root)).toprettyxml(indent="  ")

# ํŒŒ์ผ๋กœ ์ €์žฅ
with open('books.xml', 'w', encoding='utf-8') as f:
    f.write(xml_str)

print("XML ํŒŒ์ผ ์ €์žฅ ์™„๋ฃŒ!")

์žฅ์ : ๊ตฌ์กฐํ™”๋œ ๋ฐ์ดํ„ฐ๋ฅผ ํ‘œํ˜„ํ•˜๊ธฐ ์ข‹๊ณ , ๋งŽ์€ ์‹œ์Šคํ…œ์—์„œ ์ง€์›ํ•ด.
๋‹จ์ : JSON๋ณด๋‹ค ๋” ๋ณต์žกํ•˜๊ณ  ํŒŒ์ผ ํฌ๊ธฐ๊ฐ€ ํฌ๋ฉฐ, ํŒŒ์‹ฑ์ด ๋” ์–ด๋ ค์šธ ์ˆ˜ ์žˆ์–ด.

4. Excel ํŒŒ์ผ

import pandas as pd

# ๋ฐ์ดํ„ฐ ์˜ˆ์‹œ
data = [
    {'title': 'ํŒŒ์ด์ฌ ๊ธฐ์ดˆ', 'author': '๊น€ํŒŒ์ด', 'price': 25000},
    {'title': '์›น ํฌ๋กค๋ง ๋งˆ์Šคํ„ฐ', 'author': '์ดํฌ๋กค', 'price': 30000},
    {'title': '๋ฐ์ดํ„ฐ ๋ถ„์„ ์ž…๋ฌธ', 'author': '๋ฐ•๋ฐ์ดํ„ฐ', 'price': 28000}
]

# DataFrame ์ƒ์„ฑ
df = pd.DataFrame(data)

# Excel ํŒŒ์ผ๋กœ ์ €์žฅ
df.to_excel('books.xlsx', index=False)

print("Excel ํŒŒ์ผ ์ €์žฅ ์™„๋ฃŒ!")

์žฅ์ : ์—‘์…€์—์„œ ๋ฐ”๋กœ ์—ด์–ด์„œ ์ž‘์—…ํ•  ์ˆ˜ ์žˆ๊ณ , ์„œ์‹ ์ง€์ •์ด ๊ฐ€๋Šฅํ•ด.
๋‹จ์ : ํŒŒ์ผ ํฌ๊ธฐ๊ฐ€ ํฌ๊ณ , ํ”„๋กœ๊ทธ๋ž˜๋ฐ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๊ธฐ์—๋Š” ๋‹ค๋ฅธ ํ˜•์‹๋ณด๋‹ค ๋ณต์žกํ•  ์ˆ˜ ์žˆ์–ด.

๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ €์žฅ ๋ฐฉ์‹ ๐Ÿ—„๏ธ

๋Œ€์šฉ๋Ÿ‰ ๋ฐ์ดํ„ฐ๋‚˜ ๊ตฌ์กฐํ™”๋œ ๋ฐ์ดํ„ฐ๋ฅผ ํšจ์œจ์ ์œผ๋กœ ๊ด€๋ฆฌํ•˜๋ ค๋ฉด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ข‹์•„:

1. SQLite ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค

import sqlite3

# ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ
conn = sqlite3.connect('books.db')
cursor = conn.cursor()

# ํ…Œ์ด๋ธ” ์ƒ์„ฑ
cursor.execute('''
CREATE TABLE IF NOT EXISTS books (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    title TEXT NOT NULL,
    author TEXT NOT NULL,
    price INTEGER,
    category TEXT,
    published_date TEXT
)
''')

# ๋ฐ์ดํ„ฐ ์‚ฝ์ž…
books = [
    ('ํŒŒ์ด์ฌ ๊ธฐ์ดˆ', '๊น€ํŒŒ์ด', 25000, 'ํ”„๋กœ๊ทธ๋ž˜๋ฐ', '2021-01-15'),
    ('์›น ํฌ๋กค๋ง ๋งˆ์Šคํ„ฐ', '์ดํฌ๋กค', 30000, 'ํ”„๋กœ๊ทธ๋ž˜๋ฐ', '2021-03-20'),
    ('๋ฐ์ดํ„ฐ ๋ถ„์„ ์ž…๋ฌธ', '๋ฐ•๋ฐ์ดํ„ฐ', 28000, '๋ฐ์ดํ„ฐ ๊ณผํ•™', '2021-05-10')
]

cursor.executemany('''
INSERT INTO books (title, author, price, category, published_date)
VALUES (?, ?, ?, ?, ?)
''', books)

# ๋ณ€๊ฒฝ์‚ฌํ•ญ ์ €์žฅ
conn.commit()

# ๋ฐ์ดํ„ฐ ์กฐํšŒ
cursor.execute('SELECT * FROM books')
for row in cursor.fetchall():
    print(row)

# ์—ฐ๊ฒฐ ์ข…๋ฃŒ
conn.close()

print("SQLite ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ž‘์—… ์™„๋ฃŒ!")

์žฅ์ : ํŒŒ์ผ ๊ธฐ๋ฐ˜์ด๋ผ ์„ค์น˜๊ฐ€ ํ•„์š” ์—†๊ณ , ๊ฐ„๋‹จํ•œ ํ”„๋กœ์ ํŠธ์— ์ ํ•ฉํ•˜๋ฉฐ, SQL์˜ ๋ชจ๋“  ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด.
๋‹จ์ : ๋™์‹œ ์ ‘๊ทผ์— ์ œํ•œ์ด ์žˆ๊ณ , ๋Œ€๊ทœ๋ชจ ๋ฐ์ดํ„ฐ์—๋Š” ์„ฑ๋Šฅ์ด ๋–จ์–ด์งˆ ์ˆ˜ ์žˆ์–ด.

2. MySQL/PostgreSQL ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค

import pymysql

# ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ
conn = pymysql.connect(
    host='localhost',
    user='username',
    password='password',
    database='crawling_db'
)

try:
    with conn.cursor() as cursor:
        # ํ…Œ์ด๋ธ” ์ƒ์„ฑ
        cursor.execute('''
        CREATE TABLE IF NOT EXISTS books (
            id INT AUTO_INCREMENT PRIMARY KEY,
            title VARCHAR(255) NOT NULL,
            author VARCHAR(255) NOT NULL,
            price INT,
            category VARCHAR(100),
            published_date DATE
        )
        ''')
        
        # ๋ฐ์ดํ„ฐ ์‚ฝ์ž…
        sql = '''
        INSERT INTO books (title, author, price, category, published_date)
        VALUES (%s, %s, %s, %s, %s)
        '''
        books = [
            ('ํŒŒ์ด์ฌ ๊ธฐ์ดˆ', '๊น€ํŒŒ์ด', 25000, 'ํ”„๋กœ๊ทธ๋ž˜๋ฐ', '2021-01-15'),
            ('์›น ํฌ๋กค๋ง ๋งˆ์Šคํ„ฐ', '์ดํฌ๋กค', 30000, 'ํ”„๋กœ๊ทธ๋ž˜๋ฐ', '2021-03-20'),
            ('๋ฐ์ดํ„ฐ ๋ถ„์„ ์ž…๋ฌธ', '๋ฐ•๋ฐ์ดํ„ฐ', 28000, '๋ฐ์ดํ„ฐ ๊ณผํ•™', '2021-05-10')
        ]
        
        cursor.executemany(sql, books)
        
        # ๋ณ€๊ฒฝ์‚ฌํ•ญ ์ €์žฅ
        conn.commit()
        
        # ๋ฐ์ดํ„ฐ ์กฐํšŒ
        cursor.execute('SELECT * FROM books')
        for row in cursor.fetchall():
            print(row)
            
finally:
    conn.close()
    
print("MySQL ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ž‘์—… ์™„๋ฃŒ!")

์žฅ์ : ๋Œ€์šฉ๋Ÿ‰ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ์— ์ ํ•ฉํ•˜๊ณ , ๋™์‹œ ์ ‘๊ทผ์„ ์ง€์›ํ•˜๋ฉฐ, ๊ฐ•๋ ฅํ•œ ์ฟผ๋ฆฌ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ด.
๋‹จ์ : ์„ค์น˜์™€ ์„ค์ •์ด ํ•„์š”ํ•˜๊ณ , ๊ฐ„๋‹จํ•œ ํ”„๋กœ์ ํŠธ์—๋Š” ๊ณผ๋„ํ•  ์ˆ˜ ์žˆ์–ด.

3. MongoDB (NoSQL ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค)

from pymongo import MongoClient

# MongoDB ์—ฐ๊ฒฐ
client = MongoClient('mongodb://localhost:27017/')
db = client['crawling_db']
collection = db['books']

# ๋ฐ์ดํ„ฐ ์‚ฝ์ž… (๋ณต์žกํ•œ ์ค‘์ฒฉ ๊ตฌ์กฐ ๊ฐ€๋Šฅ)
books = [
    {
        'title': 'ํŒŒ์ด์ฌ ๊ธฐ์ดˆ',
        'author': '๊น€ํŒŒ์ด',
        'price': 25000,
        'categories': ['ํ”„๋กœ๊ทธ๋ž˜๋ฐ', 'ํŒŒ์ด์ฌ'],
        'reviews': [
            {'user': '๋…์ž1', 'rating': 5, 'comment': '์ข‹์€ ์ฑ…์ด์—์š”!'},
            {'user': '๋…์ž2', 'rating': 4, 'comment': '์ดˆ๋ณด์ž์—๊ฒŒ ์ถ”์ฒœํ•ฉ๋‹ˆ๋‹ค.'}
        ],
        'published_date': '2021-01-15'
    },
    {
        'title': '์›น ํฌ๋กค๋ง ๋งˆ์Šคํ„ฐ',
        'author': '์ดํฌ๋กค',
        'price': 30000,
        'categories': ['ํ”„๋กœ๊ทธ๋ž˜๋ฐ', '์›น', 'ํฌ๋กค๋ง'],
        'reviews': [],
        'published_date': '2021-03-20'
    }
]

# ์—ฌ๋Ÿฌ ๋ฌธ์„œ ์‚ฝ์ž…
result = collection.insert_many(books)
print(f"{len(result.inserted_ids)}๊ฐœ์˜ ๋ฌธ์„œ๊ฐ€ ์‚ฝ์ž…๋˜์—ˆ์Šต๋‹ˆ๋‹ค.")

# ๋ฐ์ดํ„ฐ ์กฐํšŒ
for book in collection.find():
    print(f"์ œ๋ชฉ: {book['title']}, ์ €์ž: {book['author']}, ๊ฐ€๊ฒฉ: {book['price']}")

# ํ•„ํ„ฐ๋ง ์กฐํšŒ
for book in collection.find({'price': {'$gt': 25000}}):
    print(f"25,000์› ์ดˆ๊ณผ ๋„์„œ: {book['title']}")

print("MongoDB ์ž‘์—… ์™„๋ฃŒ!")

์žฅ์ : ์œ ์—ฐํ•œ ์Šคํ‚ค๋งˆ๋กœ ๋ณต์žกํ•œ ์ค‘์ฒฉ ๊ตฌ์กฐ๋ฅผ ์‰ฝ๊ฒŒ ์ €์žฅํ•  ์ˆ˜ ์žˆ๊ณ , ๋Œ€์šฉ๋Ÿ‰ ๋ฐ์ดํ„ฐ์— ์ ํ•ฉํ•˜๋ฉฐ, ํ™•์žฅ์„ฑ์ด ์ข‹์•„.
๋‹จ์ : ๊ด€๊ณ„ํ˜• ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ณด๋‹ค ์กฐ์ธ ์—ฐ์‚ฐ์ด ๋ณต์žกํ•˜๊ณ , ์„ค์น˜์™€ ์„ค์ •์ด ํ•„์š”ํ•ด.

๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ๋ฐ ๋ถ„์„ ๋„๊ตฌ ๐Ÿ“Š

์ˆ˜์ง‘ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ณ  ๋ถ„์„ํ•˜๋Š” ๋ฐ ์œ ์šฉํ•œ ๋„๊ตฌ๋“ค์„ ์•Œ์•„๋ณด์ž:

1. pandas๋ฅผ ์‚ฌ์šฉํ•œ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ

import pandas as pd

# CSV ํŒŒ์ผ ์ฝ๊ธฐ
df = pd.read_csv('books.csv')

# ๊ธฐ๋ณธ ์ •๋ณด ํ™•์ธ
print(df.info())
print(df.describe())

# ๋ฐ์ดํ„ฐ ํ•„ํ„ฐ๋ง
programming_books = df[df['category'] == 'ํ”„๋กœ๊ทธ๋ž˜๋ฐ']
print(f"ํ”„๋กœ๊ทธ๋ž˜๋ฐ ๋„์„œ ์ˆ˜: {len(programming_books)}")

# ๋ฐ์ดํ„ฐ ์ •๋ ฌ
sorted_df = df.sort_values('price', ascending=False)
print("๊ฐ€๊ฒฉ ๋†’์€ ์ˆœ Top 3:")
print(sorted_df.head(3)[['title', 'price']])

# ๊ทธ๋ฃนํ™” ๋ฐ ์ง‘๊ณ„
category_stats = df.groupby('category').agg({
    'price': ['mean', 'min', 'max', 'count']
})
print("\n์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ํ†ต๊ณ„:")
print(category_stats)

# ๋ฐ์ดํ„ฐ ๋ณ€ํ™˜
df['discount_price'] = df['price'] * 0.9  # 10% ํ• ์ธ ๊ฐ€๊ฒฉ ์ถ”๊ฐ€

# ๋ณ€๊ฒฝ๋œ ๋ฐ์ดํ„ฐ ์ €์žฅ
df.to_csv('books_processed.csv', index=False)

pandas๋Š” ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ์™€ ๋ถ„์„์„ ์œ„ํ•œ ๊ฐ•๋ ฅํ•œ ๋„๊ตฌ์•ผ. CSV, Excel, JSON, SQL ๋“ฑ ๋‹ค์–‘ํ•œ ํ˜•์‹์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์‰ฝ๊ฒŒ ์ฝ๊ณ  ์“ธ ์ˆ˜ ์žˆ์œผ๋ฉฐ, ๋ฐ์ดํ„ฐ ํ•„ํ„ฐ๋ง, ์ •๋ ฌ, ๊ทธ๋ฃนํ™”, ๋ณ€ํ™˜ ๋“ฑ ๋‹ค์–‘ํ•œ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์–ด. ํฌ๋กค๋งํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ถ„์„ํ•˜๋Š” ๋ฐ ํ•„์ˆ˜์ ์ธ ๋„๊ตฌ๋ผ๊ณ  ํ•  ์ˆ˜ ์žˆ์ง€! ๐Ÿ“ˆ

2. ๋ฐ์ดํ„ฐ ์‹œ๊ฐํ™”

import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

# ๋ฐ์ดํ„ฐ ๋กœ๋“œ
df = pd.read_csv('books.csv')

# ๊ธฐ๋ณธ ์„ค์ •
plt.figure(figsize=(12, 6))
sns.set_style('whitegrid')

# ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ๋„์„œ ์ˆ˜ ๋ง‰๋Œ€ ๊ทธ๋ž˜ํ”„
plt.subplot(1, 2, 1)
category_counts = df['category'].value_counts()
sns.barplot(x=category_counts.index, y=category_counts.values)
plt.title('์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ๋„์„œ ์ˆ˜')
plt.xticks(rotation=45)

# ๊ฐ€๊ฒฉ ๋ถ„ํฌ ํžˆ์Šคํ† ๊ทธ๋žจ
plt.subplot(1, 2, 2)
sns.histplot(df['price'], bins=10, kde=True)
plt.title('๋„์„œ ๊ฐ€๊ฒฉ ๋ถ„ํฌ')
plt.xlabel('๊ฐ€๊ฒฉ')
plt.ylabel('๋„์„œ ์ˆ˜')

plt.tight_layout()
plt.savefig('book_analysis.png')
plt.show()

# ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ํ‰๊ท  ๊ฐ€๊ฒฉ ๋ง‰๋Œ€ ๊ทธ๋ž˜ํ”„
plt.figure(figsize=(10, 5))
avg_price = df.groupby('category')['price'].mean().sort_values(ascending=False)
sns.barplot(x=avg_price.index, y=avg_price.values)
plt.title('์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ํ‰๊ท  ๊ฐ€๊ฒฉ')
plt.xticks(rotation=45)
plt.ylabel('ํ‰๊ท  ๊ฐ€๊ฒฉ')
plt.tight_layout()
plt.savefig('category_avg_price.png')
plt.show()

matplotlib๊ณผ seaborn ๊ฐ™์€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ํฌ๋กค๋งํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ์‹œ๊ฐ์ ์œผ๋กœ ํ‘œํ˜„ํ•  ์ˆ˜ ์žˆ์–ด. ์ด๋ฅผ ํ†ตํ•ด ๋ฐ์ดํ„ฐ์˜ ํŒจํ„ด, ์ถ”์„ธ, ์ด์ƒ์น˜ ๋“ฑ์„ ์‰ฝ๊ฒŒ ํŒŒ์•…ํ•  ์ˆ˜ ์žˆ์ง€. ๋ฐ์ดํ„ฐ ์‹œ๊ฐํ™”๋Š” ๋ณต์žกํ•œ ์ •๋ณด๋ฅผ ์ง๊ด€์ ์œผ๋กœ ์ดํ•ดํ•˜๋Š” ๋ฐ ํฐ ๋„์›€์ด ๋ผ! ๐Ÿ“Š

๋ฐ์ดํ„ฐ ์ €์žฅ ์ „๋žต ๋ฐ ํŒ ๐Ÿ’ก

ํฌ๋กค๋ง ๋ฐ์ดํ„ฐ๋ฅผ ํšจ๊ณผ์ ์œผ๋กœ ์ €์žฅํ•˜๊ณ  ๊ด€๋ฆฌํ•˜๊ธฐ ์œ„ํ•œ ๋ช‡ ๊ฐ€์ง€ ํŒ์„ ์†Œ๊ฐœํ• ๊ฒŒ:

  1. ์ฆ๋ถ„ ํฌ๋กค๋ง ๋ฐ ์ €์žฅ: ์ด๋ฏธ ์ˆ˜์ง‘ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค์‹œ ์ˆ˜์ง‘ํ•˜์ง€ ์•Š๋„๋ก ์ฆ๋ถ„ ํฌ๋กค๋ง์„ ๊ตฌํ˜„ํ•˜๊ณ , ์ƒˆ๋กœ์šด ๋ฐ์ดํ„ฐ๋งŒ ์ถ”๊ฐ€ํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ์ €์žฅํ•ด.
  2. ๋ฐ์ดํ„ฐ ์ •๊ทœํ™”: ์ค‘๋ณต ๋ฐ์ดํ„ฐ๋ฅผ ์ œ๊ฑฐํ•˜๊ณ  ์ผ๊ด€๋œ ํ˜•์‹์œผ๋กœ ์ €์žฅํ•ด. ํŠนํžˆ ๊ด€๊ณ„ํ˜• ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ๋Š” ์ •๊ทœํ™”๊ฐ€ ์ค‘์š”ํ•ด.
  3. ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ํฌํ•จ: ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ์‹œ๊ฐ„, ์ถœ์ฒ˜ URL, ํฌ๋กค๋Ÿฌ ๋ฒ„์ „ ๋“ฑ์˜ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ํ•จ๊ป˜ ์ €์žฅํ•ด. ์ด๋Š” ๋‚˜์ค‘์— ๋ฐ์ดํ„ฐ์˜ ์‹ ๋ขฐ์„ฑ๊ณผ ์ถ”์ ์„ฑ์„ ํ™•๋ณดํ•˜๋Š” ๋ฐ ๋„์›€์ด ๋ผ.
  4. ๋ฐฑ์—… ์ „๋žต: ์ค‘์š”ํ•œ ๋ฐ์ดํ„ฐ๋Š” ์ •๊ธฐ์ ์œผ๋กœ ๋ฐฑ์—…ํ•˜๊ณ , ๊ฐ€๋Šฅํ•˜๋ฉด ๋‹ค๋ฅธ ์œ„์น˜์— ๋ณต์‚ฌ๋ณธ์„ ์ €์žฅํ•ด.
  5. ๋ฐ์ดํ„ฐ ์••์ถ•: ๋Œ€์šฉ๋Ÿ‰ ๋ฐ์ดํ„ฐ์˜ ๊ฒฝ์šฐ ์••์ถ• ๊ธฐ์ˆ ์„ ์‚ฌ์šฉํ•ด ์ €์žฅ ๊ณต๊ฐ„์„ ์ ˆ์•ฝํ•ด.
import json
import gzip
import datetime
import os

def save_crawled_data(data, filename_prefix, compress=True):
    """ํฌ๋กค๋ง ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๋Š” ํ•จ์ˆ˜
    
    Args:
        data: ์ €์žฅํ•  ๋ฐ์ดํ„ฐ
        filename_prefix: ํŒŒ์ผ๋ช… ์ ‘๋‘์‚ฌ
        compress: ์••์ถ• ์—ฌ๋ถ€
    """
    # ํ˜„์žฌ ์‹œ๊ฐ„์„ ํŒŒ์ผ๋ช…์— ํฌํ•จ
    timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
    filename = f"{filename_prefix}_{timestamp}"
    
    # ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ถ”๊ฐ€
    metadata = {
        'crawled_at': datetime.datetime.now().isoformat(),
        'crawler_version': '1.0',
        'item_count': len(data) if isinstance(data, list) else 1
    }
    
    # ๋ฐ์ดํ„ฐ์™€ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ๊ฒฐํ•ฉ
    full_data = {
        'metadata': metadata,
        'data': data
    }
    
    # ๋””๋ ‰ํ† ๋ฆฌ ์ƒ์„ฑ
    os.makedirs('crawled_data', exist_ok=True)
    
    if compress:
        # gzip์œผ๋กœ ์••์ถ•ํ•˜์—ฌ ์ €์žฅ
        filename = f"crawled_data/{filename}.json.gz"
        with gzip.open(filename, 'wt', encoding='utf-8') as f:
            json.dump(full_data, f, ensure_ascii=False, indent=2)
    else:
        # ์ผ๋ฐ˜ JSON ํŒŒ์ผ๋กœ ์ €์žฅ
        filename = f"crawled_data/{filename}.json"
        with open(filename, 'w', encoding='utf-8') as f:
            json.dump(full_data, f, ensure_ascii=False, indent=2)
    
    print(f"๋ฐ์ดํ„ฐ๊ฐ€ {filename}์— ์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.")
    return filename

# ์‚ฌ์šฉ ์˜ˆ์‹œ
books_data = [
    {'title': 'ํŒŒ์ด์ฌ ๊ธฐ์ดˆ', 'author': '๊น€ํŒŒ์ด', 'price': 25000},
    {'title': '์›น ํฌ๋กค๋ง ๋งˆ์Šคํ„ฐ', 'author': '์ดํฌ๋กค', 'price': 30000}
]

save_crawled_data(books_data, 'books', compress=True)

ํšจ์œจ์ ์ธ ๋ฐ์ดํ„ฐ ์ €์žฅ ๋ฐ ๊ด€๋ฆฌ๋Š” ํฌ๋กค๋ง ํ”„๋กœ์ ํŠธ์˜ ์„ฑ๊ณต์— ์ค‘์š”ํ•œ ์š”์†Œ์•ผ. ํ”„๋กœ์ ํŠธ์˜ ๊ทœ๋ชจ์™€ ๋ชฉ์ ์— ๋งž๋Š” ์ €์žฅ ๋ฐฉ์‹์„ ์„ ํƒํ•˜๊ณ , ๋ฐ์ดํ„ฐ์˜ ์ผ๊ด€์„ฑ๊ณผ ๋ฌด๊ฒฐ์„ฑ์„ ์œ ์ง€ํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ด! ๐Ÿ—„๏ธ

๐Ÿ’ก ๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ ํŒ: ํฌ๋กค๋ง ํ”„๋กœ์ ํŠธ๊ฐ€ ์ปค์งˆ์ˆ˜๋ก ๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ์˜ ์ค‘์š”์„ฑ๋„ ์ปค์ ธ. ์ฒ˜์Œ๋ถ€ํ„ฐ ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ์ €์žฅ ๊ตฌ์กฐ๋ฅผ ์„ค๊ณ„ํ•˜๊ณ , ๋ฐ์ดํ„ฐ ํ’ˆ์งˆ์„ ์œ ์ง€ํ•˜๊ธฐ ์œ„ํ•œ ๊ฒ€์ฆ ์ ˆ์ฐจ๋ฅผ ๋งˆ๋ จํ•˜๋Š” ๊ฒƒ์ด ์ข‹์•„. ๋˜ํ•œ ๋ฐ์ดํ„ฐ ์‚ฌ์šฉ ๋ชฉ์ ์— ๋งž๊ฒŒ ์ ์ ˆํ•œ ์ธ๋ฑ์‹ฑ๊ณผ ์ตœ์ ํ™”๋ฅผ ์ ์šฉํ•˜๋ฉด ๊ฒ€์ƒ‰ ๋ฐ ๋ถ„์„ ์„ฑ๋Šฅ์„ ํฌ๊ฒŒ ํ–ฅ์ƒ์‹œํ‚ฌ ์ˆ˜ ์žˆ์–ด!

10. ์‹ค์ „ ํ”„๋กœ์ ํŠธ: ๋‚˜๋งŒ์˜ ํฌ๋กค๋Ÿฌ ๋งŒ๋“ค๊ธฐ ๐Ÿš€

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

ํ”„๋กœ์ ํŠธ ๊ฐœ์š” ๐Ÿ“‹

์šฐ๋ฆฌ๊ฐ€ ๋งŒ๋“ค ํฌ๋กค๋Ÿฌ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๊ธฐ๋Šฅ์„ ๊ฐ€์งˆ ๊ฑฐ์•ผ:

  1. ์—ฌ๋Ÿฌ ์‡ผํ•‘๋ชฐ์—์„œ ํŠน์ • ์ œํ’ˆ์˜ ๊ฐ€๊ฒฉ ์ •๋ณด๋ฅผ ์ˆ˜์ง‘
  2. ์ˆ˜์ง‘ํ•œ ๊ฐ€๊ฒฉ์„ ๋น„๊ตํ•˜์—ฌ ์ตœ์ €๊ฐ€ ์ฐพ๊ธฐ
  3. ๊ฐ€๊ฒฉ ๋ณ€๋™ ์ถ”์  ๋ฐ ์•Œ๋ฆผ ๊ธฐ๋Šฅ
  4. ๊ฒฐ๊ณผ๋ฅผ CSV ํŒŒ์ผ๊ณผ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ €์žฅ
  5. ๊ฐ„๋‹จํ•œ ์›น ๋Œ€์‹œ๋ณด๋“œ๋กœ ๊ฒฐ๊ณผ ์‹œ๊ฐํ™”

์ด ํ”„๋กœ์ ํŠธ๋Š” ์‹ค์ œ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๋„๊ตฌ์ด๋ฉด์„œ, ์ง€๊ธˆ๊นŒ์ง€ ๋ฐฐ์šด ๋‹ค์–‘ํ•œ ๊ธฐ์ˆ ์„ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์ข‹์€ ์˜ˆ์ œ์•ผ. ๊ทธ๋Ÿผ ์‹œ์ž‘ํ•ด๋ณผ๊นŒ? ๐Ÿš€

1๋‹จ๊ณ„: ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ ์„ค๊ณ„ ๐Ÿ—๏ธ

๋จผ์ € ํ”„๋กœ์ ํŠธ์˜ ๊ตฌ์กฐ๋ฅผ ์„ค๊ณ„ํ•ด๋ณด์ž:

price_tracker/
โ”œโ”€โ”€ scrapers/
โ”‚   โ”œโ”€โ”€ __init__.py
โ”‚   โ”œโ”€โ”€ base_scraper.py      # ๊ธฐ๋ณธ ์Šคํฌ๋ž˜ํผ ํด๋ž˜์Šค
โ”‚   โ”œโ”€โ”€ shop_a_scraper.py    # ์‡ผํ•‘๋ชฐ A ์Šคํฌ๋ž˜ํผ
โ”‚   โ”œโ”€โ”€ shop_b_scraper.py    # ์‡ผํ•‘๋ชฐ B ์Šคํฌ๋ž˜ํผ
โ”‚   โ””โ”€โ”€ shop_c_scraper.py    # ์‡ผํ•‘๋ชฐ C ์Šคํฌ๋ž˜ํผ
โ”œโ”€โ”€ database/
โ”‚   โ”œโ”€โ”€ __init__.py
โ”‚   โ”œโ”€โ”€ db_manager.py        # ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ด€๋ฆฌ
โ”‚   โ””โ”€โ”€ models.py            # ๋ฐ์ดํ„ฐ ๋ชจ๋ธ
โ”œโ”€โ”€ utils/
โ”‚   โ”œโ”€โ”€ __init__.py
โ”‚   โ”œโ”€โ”€ config.py            # ์„ค์ • ํŒŒ์ผ
โ”‚   โ”œโ”€โ”€ logger.py            # ๋กœ๊น… ์œ ํ‹ธ๋ฆฌํ‹ฐ
โ”‚   โ””โ”€โ”€ notifier.py          # ์•Œ๋ฆผ ๊ธฐ๋Šฅ
โ”œโ”€โ”€ web/
โ”‚   โ”œโ”€โ”€ __init__.py
โ”‚   โ”œโ”€โ”€ app.py               # ์›น ๋Œ€์‹œ๋ณด๋“œ
โ”‚   โ””โ”€โ”€ templates/           # HTML ํ…œํ”Œ๋ฆฟ
โ”œโ”€โ”€ main.py                  # ๋ฉ”์ธ ์‹คํ–‰ ํŒŒ์ผ
โ”œโ”€โ”€ requirements.txt         # ํ•„์š”ํ•œ ํŒจํ‚ค์ง€ ๋ชฉ๋ก
โ””โ”€โ”€ README.md                # ํ”„๋กœ์ ํŠธ ์„ค๋ช…

์ด๋Ÿฐ ๊ตฌ์กฐ๋กœ ์„ค๊ณ„ํ•˜๋ฉด ์ฝ”๋“œ์˜ ๋ชจ๋“ˆํ™”์™€ ์œ ์ง€๋ณด์ˆ˜๊ฐ€ ์‰ฌ์›Œ์ ธ. ๊ฐ ์‡ผํ•‘๋ชฐ๋ณ„๋กœ ์Šคํฌ๋ž˜ํผ๋ฅผ ๋ถ„๋ฆฌํ•˜๊ณ , ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ด€๋ฆฌ์™€ ์œ ํ‹ธ๋ฆฌํ‹ฐ ๊ธฐ๋Šฅ์„ ๋ณ„๋„์˜ ๋ชจ๋“ˆ๋กœ ๊ตฌ์„ฑํ–ˆ์–ด. ๐Ÿงฉ

2๋‹จ๊ณ„: ๊ธฐ๋ณธ ์Šคํฌ๋ž˜ํผ ํด๋ž˜์Šค ๊ตฌํ˜„ ๐Ÿงฌ

๋ชจ๋“  ์‡ผํ•‘๋ชฐ ์Šคํฌ๋ž˜ํผ์˜ ๊ธฐ๋ณธ์ด ๋˜๋Š” ํด๋ž˜์Šค๋ฅผ ๋จผ์ € ๊ตฌํ˜„ํ•ด๋ณด์ž:

# base_scraper.py
import requests
from abc import ABC, abstractmethod
from bs4 import BeautifulSoup
import logging
from utils.logger import setup_logger

class BaseScraper(ABC):
    """๋ชจ๋“  ์Šคํฌ๋ž˜ํผ์˜ ๊ธฐ๋ณธ ํด๋ž˜์Šค"""
    
    def __init__(self, shop_name):
        self.shop_name = shop_name
        self.logger = setup_logger(f"{shop_name}_scraper")
        self.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-Language': 'ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7'
        }
    
    def get_page(self, url):
        """์›น ํŽ˜์ด์ง€ ๊ฐ€์ ธ์˜ค๊ธฐ"""
        try:
            response = requests.get(url, headers=self.headers, timeout=10)
            response.raise_for_status()  # 4XX, 5XX ์—๋Ÿฌ ์ฒดํฌ
            return response.text
        except requests.RequestException as e:
            self.logger.error(f"ํŽ˜์ด์ง€ ์š”์ฒญ ์‹คํŒจ: {url}, ์˜ค๋ฅ˜: {e}")
            return None
    
    def parse_html(self, html):
        """HTML ํŒŒ์‹ฑ"""
        if html:
            return BeautifulSoup(html, 'html.parser')
        return None
    
    @abstractmethod
    def search_product(self, keyword):
        """์ œํ’ˆ ๊ฒ€์ƒ‰ (์ž์‹ ํด๋ž˜์Šค์—์„œ ๊ตฌํ˜„)"""
        pass
    
    @abstractmethod
    def get_product_details(self, product_url):
        """์ œํ’ˆ ์ƒ์„ธ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ (์ž์‹ ํด๋ž˜์Šค์—์„œ ๊ตฌํ˜„)"""
        pass
    
    def clean_price(self, price_text):
        """๊ฐ€๊ฒฉ ํ…์ŠคํŠธ ์ •๋ฆฌ (์ˆซ์ž๋งŒ ์ถ”์ถœ)"""
        import re
        if price_text:
            # ์ˆซ์ž๋งŒ ์ถ”์ถœ
            price = re.sub(r'[^\d]', '', price_text)
            try:
                return int(price)
            except ValueError:
                self.logger.warning(f"๊ฐ€๊ฒฉ ๋ณ€ํ™˜ ์‹คํŒจ: {price_text}")
        return None
    
    def format_result(self, product_data):
        """๊ฒฐ๊ณผ ํฌ๋งทํŒ…"""
        return {
            'shop': self.shop_name,
            'title': product_data.get('title', ''),
            'price': product_data.get('price', 0),
            'url': product_data.get('url', ''),
            'image_url': product_data.get('image_url', ''),
            'availability': product_data.get('availability', False),
            'timestamp': product_data.get('timestamp', None)
        }

์ด ๊ธฐ๋ณธ ํด๋ž˜์Šค๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ด:

  1. ์›น ํŽ˜์ด์ง€ ์š”์ฒญ ๋ฐ HTML ํŒŒ์‹ฑ
  2. ๋กœ๊น… ์„ค์ •
  3. ๊ฐ€๊ฒฉ ์ •๋ณด ์ •์ œ
  4. ๊ฒฐ๊ณผ ํฌ๋งทํŒ…

์ถ”์ƒ ๋ฉ”์„œ๋“œ(abstractmethod)๋ฅผ ์‚ฌ์šฉํ•ด ์ž์‹ ํด๋ž˜์Šค์—์„œ ๋ฐ˜๋“œ์‹œ ๊ตฌํ˜„ํ•ด์•ผ ํ•  ๋ฉ”์„œ๋“œ๋ฅผ ์ •์˜ํ–ˆ์–ด. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ๊ฐ ์‡ผํ•‘๋ชฐ๋ณ„ ์Šคํฌ๋ž˜ํผ๊ฐ€ ์ผ๊ด€๋œ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ฐ€์ง€๊ฒŒ ๋ผ. ๐Ÿงฉ

3๋‹จ๊ณ„: ์‡ผํ•‘๋ชฐ๋ณ„ ์Šคํฌ๋ž˜ํผ ๊ตฌํ˜„ ๐Ÿช

์ด์ œ ํŠน์ • ์‡ผํ•‘๋ชฐ์„ ์œ„ํ•œ ์Šคํฌ๋ž˜ํผ๋ฅผ ๊ตฌํ˜„ํ•ด๋ณด์ž:

# shop_a_scraper.py
from scrapers.base_scraper import BaseScraper
from datetime import datetime
import time
import random

class ShopAScraper(BaseScraper):
    """์‡ผํ•‘๋ชฐ A ์Šคํฌ๋ž˜ํผ"""
    
    def __init__(self):
        super().__init__("ShopA")
        self.base_url = "https://example-shop-a.com"
        self.search_url = f"{self.base_url}/search"
    
    def search_product(self, keyword):
        """์ œํ’ˆ ๊ฒ€์ƒ‰"""
        self.logger.info(f"๊ฒ€์ƒ‰ ์‹œ์ž‘: {keyword}")
        
        params = {
            'q': keyword,
            'sort': 'price_asc'  # ๊ฐ€๊ฒฉ ๋‚ฎ์€์ˆœ ์ •๋ ฌ
        }
        
        html = self.get_page(f"{self.search_url}?{'&'.join([f'{k}={v}' for k, v in params.items()])}")
        soup = self.parse_html(html)
        
        if not soup:
            self.logger.error("๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ํŒŒ์‹ฑ ์‹คํŒจ")
            return []
        
        results = []
        product_elements = soup.select('.product-item')
        
        for element in product_elements[:5]:  # ์ƒ์œ„ 5๊ฐœ ์ œํ’ˆ๋งŒ ์ฒ˜๋ฆฌ
            try:
                title_element = element.select_one('.product-title')
                price_element = element.select_one('.product-price')
                link_element = element.select_one('a.product-link')
                image_element = element.select_one('img.product-image')
                
                if title_element and price_element and link_element:
                    title = title_element.text.strip()
                    price_text = price_element.text.strip()
                    price = self.clean_price(price_text)
                    url = link_element.get('href')
                    if not url.startswith('http'):
                        url = f"{self.base_url}{url}"
                    
                    image_url = image_element.get('src') if image_element else ''
                    if image_url and not image_url.startswith('http'):
                        image_url = f"{self.base_url}{image_url}"
                    
                    results.append({
                        'title': title,
                        'price': price,
                        'url': url,
                        'image_url': image_url,
                        'availability': True,
                        'timestamp': datetime.now()
                    })
                    
                    # ์„œ๋ฒ„ ๋ถ€ํ•˜ ๋ฐฉ์ง€๋ฅผ ์œ„ํ•œ ์ง€์—ฐ
                    time.sleep(random.uniform(0.5, 1.5))
            
            except Exception as e:
                self.logger.error(f"์ œํ’ˆ ์ •๋ณด ์ถ”์ถœ ์ค‘ ์˜ค๋ฅ˜: {e}")
        
        self.logger.info(f"{len(results)}๊ฐœ์˜ ์ œํ’ˆ ์ •๋ณด ์ถ”์ถœ ์™„๋ฃŒ")
        return [self.format_result(item) for item in results]
    
    def get_product_details(self, product_url):
        """์ œํ’ˆ ์ƒ์„ธ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ"""
        self.logger.info(f"์ œํ’ˆ ์ƒ์„ธ ์ •๋ณด ์ˆ˜์ง‘: {product_url}")
        
        html = self.get_page(product_url)
        soup = self.parse_html(html)
        
        if not soup:
            self.logger.error("์ œํ’ˆ ์ƒ์„ธ ํŽ˜์ด์ง€ ํŒŒ์‹ฑ ์‹คํŒจ")
            return None
        
        try:
            title = soup.select_one('h1.product-title').text.strip()
            price_text = soup.select_one('span.current-price').text.strip()
            price = self.clean_price(price_text)
            image_url = soup.select_one('img.product-main-image').get('src')
            availability = 'In Stock' in soup.select_one('div.availability').text
            
            # ์ถ”๊ฐ€ ์ •๋ณด ์ˆ˜์ง‘
            specs = {}
            spec_elements = soup.select('table.specifications tr')
            for spec in spec_elements:
                key = spec.select_one('th').text.strip()
                value = spec.select_one('td').text.strip()
                specs[key] = value
            
            result = {
                'title': title,
                'price': price,
                'url': product_url,
                'image_url': image_url,
                'availability': availability,
                'specifications': specs,
                'timestamp': datetime.now()
            }
            
            return self.format_result(result)
            
        except Exception as e:
            self.logger.error(f"์ œํ’ˆ ์ƒ์„ธ ์ •๋ณด ์ถ”์ถœ ์ค‘ ์˜ค๋ฅ˜: {e}")
            return None

์ด ์ฝ”๋“œ๋Š” ๊ฐ€์ƒ์˜ ์‡ผํ•‘๋ชฐ A๋ฅผ ์œ„ํ•œ ์Šคํฌ๋ž˜ํผ์•ผ. ์‹ค์ œ ์‚ฌ์ดํŠธ์— ๋งž๊ฒŒ ์„ ํƒ์ž(selector)์™€ URL์„ ์ˆ˜์ •ํ•ด์•ผ ํ•ด. ๋‹ค๋ฅธ ์‡ผํ•‘๋ชฐ์— ๋Œ€ํ•ด์„œ๋„ ๋น„์Šทํ•œ ๋ฐฉ์‹์œผ๋กœ ์Šคํฌ๋ž˜ํผ๋ฅผ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์–ด. ๐Ÿฌ

4๋‹จ๊ณ„: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ด€๋ฆฌ ๊ตฌํ˜„ ๐Ÿ’พ

์ด์ œ ์ˆ˜์ง‘ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๊ณ  ๊ด€๋ฆฌํ•  ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ชจ๋“ˆ์„ ๊ตฌํ˜„ํ•ด๋ณด์ž:

# db_manager.py
import sqlite3
import json
from datetime import datetime
import os

class DatabaseManager:
    """๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ด€๋ฆฌ ํด๋ž˜์Šค"""
    
    def __init__(self, db_path='database/price_tracker.db'):
        # ๋””๋ ‰ํ† ๋ฆฌ๊ฐ€ ์—†์œผ๋ฉด ์ƒ์„ฑ
        os.makedirs(os.path.dirname(db_path), exist_ok=True)
        
        self.db_path = db_path
        self.conn = None
        self.cursor = None
        self.initialize_db()
    
    def connect(self):
        """๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ"""
        self.conn = sqlite3.connect(self.db_path)
        self.conn.row_factory = sqlite3.Row  # ๊ฒฐ๊ณผ๋ฅผ ๋”•์…”๋„ˆ๋ฆฌ ํ˜•ํƒœ๋กœ ๋ฐ˜ํ™˜
        self.cursor = self.conn.cursor()
        return self.cursor
    
    def close(self):
        """๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ์ข…๋ฃŒ"""
        if self.conn:
            self.conn.close()
            self.conn = None
            self.cursor = None
    
    def initialize_db(self):
        """๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ดˆ๊ธฐํ™” ๋ฐ ํ…Œ์ด๋ธ” ์ƒ์„ฑ"""
        cursor = self.connect()
        
        # ์ œํ’ˆ ํ…Œ์ด๋ธ”
        cursor.execute('''
        CREATE TABLE IF NOT EXISTS products (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            title TEXT NOT NULL,
            keyword TEXT NOT NULL,
            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        )
        ''')
        
        # ๊ฐ€๊ฒฉ ์ •๋ณด ํ…Œ์ด๋ธ”
        cursor.execute('''
        CREATE TABLE IF NOT EXISTS prices (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            product_id INTEGER,
            shop TEXT NOT NULL,
            price INTEGER NOT NULL,
            url TEXT NOT NULL,
            image_url TEXT,
            availability BOOLEAN,
            timestamp TIMESTAMP,
            FOREIGN KEY (product_id) REFERENCES products (id)
        )
        ''')
        
        # ์•Œ๋ฆผ ์„ค์ • ํ…Œ์ด๋ธ”
        cursor.execute('''
        CREATE TABLE IF NOT EXISTS alerts (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            product_id INTEGER,
            target_price INTEGER,
            email TEXT,
            is_active BOOLEAN DEFAULT 1,
            FOREIGN KEY (product_id) REFERENCES products (id)
        )
        ''')
        
        self.conn.commit()
        self.close()
    
    def add_product(self, title, keyword):
        """์ƒˆ ์ œํ’ˆ ์ถ”๊ฐ€"""
        cursor = self.connect()
        
        # ์ด๋ฏธ ์กด์žฌํ•˜๋Š”์ง€ ํ™•์ธ
        cursor.execute(
            "SELECT id FROM products WHERE keyword = ?",
            (keyword,)
        )
        existing = cursor.fetchone()
        
        if existing:
            product_id = existing['id']
        else:
            cursor.execute(
                "INSERT INTO products (title, keyword) VALUES (?, ?)",
                (title, keyword)
            )
            product_id = cursor.lastrowid
        
        self.conn.commit()
        self.close()
        return product_id
    
    def add_price_data(self, product_id, price_data):
        """๊ฐ€๊ฒฉ ์ •๋ณด ์ถ”๊ฐ€"""
        cursor = self.connect()
        
        cursor.execute(
            """
            INSERT INTO prices 
            (product_id, shop, price, url, image_url, availability, timestamp)
            VALUES (?, ?, ?, ?, ?, ?, ?)
            """,
            (
                product_id,
                price_data['shop'],
                price_data['price'],
                price_data['url'],
                price_data.get('image_url', ''),
                price_data.get('availability', True),
                datetime.now().isoformat()
            )
        )
        
        self.conn.commit()
        self.close()
    
    def get_product_prices(self, product_id, limit=10):
        """์ œํ’ˆ์˜ ๊ฐ€๊ฒฉ ์ •๋ณด ์กฐํšŒ"""
        cursor = self.connect()
        
        cursor.execute(
            """
            SELECT * FROM prices
            WHERE product_id = ?
            ORDER BY timestamp DESC
            LIMIT ?
            """,
            (product_id, limit)
        )
        
        results = [dict(row) for row in cursor.fetchall()]
        self.close()
        return results
    
    def get_price_history(self, product_id, shop=None):
        """์ œํ’ˆ์˜ ๊ฐ€๊ฒฉ ๋ณ€๋™ ์ด๋ ฅ ์กฐํšŒ"""
        cursor = self.connect()
        
        if shop:
            cursor.execute(
                """
                SELECT timestamp, price FROM prices
                WHERE product_id = ? AND shop = ?
                ORDER BY timestamp ASC
                """,
                (product_id, shop)
            )
        else:
            cursor.execute(
                """
                SELECT timestamp, price, shop FROM prices
                WHERE product_id = ?
                ORDER BY timestamp ASC
                """,
                (product_id,)
            )
        
        results = [dict(row) for row in cursor.fetchall()]
        self.close()
        return results
    
    def get_lowest_price(self, product_id):
        """์ œํ’ˆ์˜ ํ˜„์žฌ ์ตœ์ €๊ฐ€ ์กฐํšŒ"""
        cursor = self.connect()
        
        # ๊ฐ ์‡ผํ•‘๋ชฐ๋ณ„ ์ตœ์‹  ๊ฐ€๊ฒฉ ์ค‘ ์ตœ์ €๊ฐ€ ์ฐพ๊ธฐ
        cursor.execute(
            """
            SELECT p1.* FROM prices p1
            INNER JOIN (
                SELECT shop, MAX(timestamp) as max_time
                FROM prices
                WHERE product_id = ?
                GROUP BY shop
            ) p2 ON p1.shop = p2.shop AND p1.timestamp = p2.max_time
            WHERE p1.product_id = ?
            ORDER BY p1.price ASC
            LIMIT 1
            """,
            (product_id, product_id)
        )
        
        result = cursor.fetchone()
        self.close()
        
        if result:
            return dict(result)
        return None

์ด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ด€๋ฆฌ ํด๋ž˜์Šค๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ด:

  1. ์ œํ’ˆ, ๊ฐ€๊ฒฉ, ์•Œ๋ฆผ ์ •๋ณด๋ฅผ ์ €์žฅํ•˜๋Š” ํ…Œ์ด๋ธ” ์ƒ์„ฑ
  2. ์ƒˆ ์ œํ’ˆ ๋ฐ ๊ฐ€๊ฒฉ ์ •๋ณด ์ถ”๊ฐ€
  3. ์ œํ’ˆ์˜ ๊ฐ€๊ฒฉ ์ •๋ณด ๋ฐ ๊ฐ€๊ฒฉ ๋ณ€๋™ ์ด๋ ฅ ์กฐํšŒ
  4. ์ œํ’ˆ์˜ ํ˜„์žฌ ์ตœ์ €๊ฐ€ ์กฐํšŒ

SQLite๋ฅผ ์‚ฌ์šฉํ–ˆ์ง€๋งŒ, ํ•„์š”์— ๋”ฐ๋ผ MySQL์ด๋‚˜ PostgreSQL ๊ฐ™์€ ๋‹ค๋ฅธ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋กœ ์‰ฝ๊ฒŒ ์ „ํ™˜ํ•  ์ˆ˜ ์žˆ์–ด. ๐Ÿ’พ

5๋‹จ๊ณ„: ์•Œ๋ฆผ ๊ธฐ๋Šฅ ๊ตฌํ˜„ ๐Ÿ””

๊ฐ€๊ฒฉ์ด ํŠน์ • ๊ธˆ์•ก ์ดํ•˜๋กœ ๋–จ์–ด์กŒ์„ ๋•Œ ์•Œ๋ฆผ์„ ๋ณด๋‚ด๋Š” ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•ด๋ณด์ž:

# notifier.py
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
import logging
from utils.config import get_config

logger = logging.getLogger(__name__)

class PriceNotifier:
    """๊ฐ€๊ฒฉ ์•Œ๋ฆผ ํด๋ž˜์Šค"""
    
    def __init__(self):
        self.config = get_config()
        self.email_config = self.config.get('email', {})
        self.sender_email = self.email_config.get('sender')
        self.smtp_server = self.email_config.get('smtp_server')
        self.smtp_port = self.email_config.get('smtp_port')
        self.smtp_user = self.email_config.get('username')
        self.smtp_password = self.email_config.get('password')
    
    def send_price_alert(self, email, product_title, current_price, product_url, shop_name):
        """๊ฐ€๊ฒฉ ์•Œ๋ฆผ ์ด๋ฉ”์ผ ์ „์†ก"""
        if not all([self.sender_email, self.smtp_server, self.smtp_port, self.smtp_user, self.smtp_password]):
            logger.error("์ด๋ฉ”์ผ ์„ค์ •์ด ์™„๋ฃŒ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.")
            return False
        
        try:
            # ์ด๋ฉ”์ผ ๋ฉ”์‹œ์ง€ ์ƒ์„ฑ
            msg = MIMEMultipart()
            msg['From'] = self.sender_email
            msg['To'] = email
            msg['Subject'] = f"๊ฐ€๊ฒฉ ์•Œ๋ฆผ: {product_title}"
            
            body = f"""
            <html>
            <body>
                <h2>๊ฐ€๊ฒฉ ์•Œ๋ฆผ</h2>
                <p>์•ˆ๋…•ํ•˜์„ธ์š”! ๊ด€์‹ฌ ์ƒํ’ˆ์˜ ๊ฐ€๊ฒฉ์ด ๋ชฉํ‘œ ๊ฐ€๊ฒฉ ์ดํ•˜๋กœ ๋–จ์–ด์กŒ์Šต๋‹ˆ๋‹ค.</p>
                <p><strong>์ƒํ’ˆ:</strong> {product_title}</p>
                <p><strong>ํ˜„์žฌ ๊ฐ€๊ฒฉ:</strong> {current_price:,}์›</p>
                <p><strong>์‡ผํ•‘๋ชฐ:</strong> {shop_name}</p>
                <p><a href="{product_url}">์ƒํ’ˆ ํŽ˜์ด์ง€ ๋ฐ”๋กœ๊ฐ€๊ธฐ</a></p>
                <p>๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค!</p>
            </body>
            </html>
            """
            
            msg.attach(MIMEText(body, 'html'))
            
            # SMTP ์„œ๋ฒ„ ์—ฐ๊ฒฐ ๋ฐ ์ด๋ฉ”์ผ ์ „์†ก
            with smtplib.SMTP(self.smtp_server, self.smtp_port) as server:
                server.starttls()
                server.login(self.smtp_user, self.smtp_password)
                server.send_message(msg)
            
            logger.info(f"๊ฐ€๊ฒฉ ์•Œ๋ฆผ ์ด๋ฉ”์ผ ์ „์†ก ์™„๋ฃŒ: {email}")
            return True
            
        except Exception as e:
            logger.error(f"์ด๋ฉ”์ผ ์ „์†ก ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}")
            return False
    
    def check_price_alerts(self, db_manager):
        """๊ฐ€๊ฒฉ ์•Œ๋ฆผ ์กฐ๊ฑด ํ™•์ธ ๋ฐ ์•Œ๋ฆผ ์ „์†ก"""
        cursor = db_manager.connect()
        
        # ํ™œ์„ฑํ™”๋œ ์•Œ๋ฆผ ์กฐํšŒ
        cursor.execute(
            """
            SELECT a.id, a.product_id, a.target_price, a.email, p.title
            FROM alerts a
            JOIN products p ON a.product_id = p.id
            WHERE a.is_active = 1
            """
        )
        
        alerts = [dict(row) for row in cursor.fetchall()]
        db_manager.close()
        
        for alert in alerts:
            # ์ตœ์ €๊ฐ€ ์กฐํšŒ
            lowest_price_data = db_manager.get_lowest_price(alert['product_id'])
            
            if lowest_price_data and lowest_price_data['price'] <= alert['target_price']:
                # ์•Œ๋ฆผ ์กฐ๊ฑด ์ถฉ์กฑ
                self.send_price_alert(
                    alert['email'],
                    alert['title'],
                    lowest_price_data['price'],
                    lowest_price_data['url'],
                    lowest_price_data['shop']
                )
                
                # ์•Œ๋ฆผ ๋น„ํ™œ์„ฑํ™” (์ผํšŒ์„ฑ ์•Œ๋ฆผ์ธ ๊ฒฝ์šฐ)
                cursor = db_manager.connect()
                cursor.execute(
                    "UPDATE alerts SET is_active = 0 WHERE id = ?",
                    (alert['id'],)
                )
                db_manager.conn.commit()
                db_manager.close()
                
                logger.info(f"๊ฐ€๊ฒฉ ์•Œ๋ฆผ ์ฒ˜๋ฆฌ ์™„๋ฃŒ: {alert['title']}, ๋ชฉํ‘œ๊ฐ€: {alert['target_price']}, ํ˜„์žฌ๊ฐ€: {lowest_price_data['price']}")

์ด ์•Œ๋ฆผ ํด๋ž˜์Šค๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ด:

  1. ์ด๋ฉ”์ผ์„ ํ†ตํ•œ ๊ฐ€๊ฒฉ ์•Œ๋ฆผ ์ „์†ก
  2. ์•Œ๋ฆผ ์กฐ๊ฑด ํ™•์ธ (๋ชฉํ‘œ ๊ฐ€๊ฒฉ ์ดํ•˜๋กœ ๋–จ์–ด์กŒ๋Š”์ง€)
  3. ์•Œ๋ฆผ ์ „์†ก ํ›„ ์•Œ๋ฆผ ์ƒํƒœ ์—…๋ฐ์ดํŠธ

์ด๋ฉ”์ผ ์™ธ์—๋„ SMS, ํ‘ธ์‹œ ์•Œ๋ฆผ ๋“ฑ ๋‹ค์–‘ํ•œ ์•Œ๋ฆผ ๋ฐฉ์‹์„ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์–ด. ๐Ÿ””

6๋‹จ๊ณ„: ๋ฉ”์ธ ์‹คํ–‰ ํŒŒ์ผ ๊ตฌํ˜„ ๐ŸŽฎ

์ด์ œ ๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ๋ฅผ ํ†ตํ•ฉํ•˜๋Š” ๋ฉ”์ธ ์‹คํ–‰ ํŒŒ์ผ์„ ๊ตฌํ˜„ํ•ด๋ณด์ž:

# main.py
import argparse
import logging
import csv
from datetime import datetime
import os

from scrapers.shop_a_scraper import ShopAScraper
from scrapers.shop_b_scraper import ShopBScraper
from scrapers.shop_c_scraper import ShopCScraper
from database.db_manager import DatabaseManager
from utils.notifier import PriceNotifier
from utils.logger import setup_logger

# ๋กœ๊ฑฐ ์„ค์ •
logger = setup_logger('main')

def parse_arguments():
    """๋ช…๋ นํ–‰ ์ธ์ž ํŒŒ์‹ฑ"""
    parser = argparse.ArgumentParser(description='๊ฐ€๊ฒฉ ๋น„๊ต ํฌ๋กค๋Ÿฌ')
    parser.add_argument('--keyword', '-k', required=True, help='๊ฒ€์ƒ‰ ํ‚ค์›Œ๋“œ')
    parser.add_argument('--output', '-o', default='output', help='๊ฒฐ๊ณผ ์ €์žฅ ๋””๋ ‰ํ† ๋ฆฌ')
    parser.add_argument('--notify', '-n', action='store_true', help='๊ฐ€๊ฒฉ ์•Œ๋ฆผ ํ™œ์„ฑํ™”')
    parser.add_argument('--email', '-e', help='์•Œ๋ฆผ ๋ฐ›์„ ์ด๋ฉ”์ผ')
    parser.add_argument('--target-price', '-t', type=int, help='๋ชฉํ‘œ ๊ฐ€๊ฒฉ')
    return parser.parse_args()

def save_to_csv(results, output_dir, keyword):
    """๊ฒฐ๊ณผ๋ฅผ CSV ํŒŒ์ผ๋กœ ์ €์žฅ"""
    os.makedirs(output_dir, exist_ok=True)
    timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
    filename = f"{output_dir}/{keyword.replace(' ', '_')}_{timestamp}.csv"
    
    with open(filename, 'w', newline='', encoding='utf-8-sig') as f:
        fieldnames = ['shop', 'title', 'price', 'url', 'image_url', 'availability', 'timestamp']
        writer = csv.DictWriter(f, fieldnames=fieldnames)
        writer.writeheader()
        writer.writerows(results)
    
    logger.info(f"๊ฒฐ๊ณผ๊ฐ€ {filename}์— ์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.")
    return filename

def main():
    """๋ฉ”์ธ ํ•จ์ˆ˜"""
    args = parse_arguments()
    keyword = args.keyword
    output_dir = args.output
    
    logger.info(f"๊ฒ€์ƒ‰ ์‹œ์ž‘: {keyword}")
    
    # ์Šคํฌ๋ž˜ํผ ์ดˆ๊ธฐํ™”
    scrapers = [
        ShopAScraper(),
        ShopBScraper(),
        ShopCScraper()
    ]
    
    # ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ด€๋ฆฌ์ž ์ดˆ๊ธฐํ™”
    db_manager = DatabaseManager()
    
    # ์ œํ’ˆ ๋“ฑ๋ก
    product_id = db_manager.add_product(f"๊ฒ€์ƒ‰: {keyword}", keyword)
    
    # ๋ชจ๋“  ์Šคํฌ๋ž˜ํผ์—์„œ ๊ฒฐ๊ณผ ์ˆ˜์ง‘
    all_results = []
    
    for scraper in scrapers:
        try:
            logger.info(f"{scraper.shop_name} ๊ฒ€์ƒ‰ ์ค‘...")
            results = scraper.search_product(keyword)
            
            if results:
                logger.info(f"{scraper.shop_name}์—์„œ {len(results)}๊ฐœ ๊ฒฐ๊ณผ ์ฐพ์Œ")
                all_results.extend(results)
                
                # ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ๊ฒฐ๊ณผ ์ €์žฅ
                for result in results:
                    db_manager.add_price_data(product_id, result)
            else:
                logger.warning(f"{scraper.shop_name}์—์„œ ๊ฒฐ๊ณผ๋ฅผ ์ฐพ์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.")
        
        except Exception as e:
            logger.error(f"{scraper.shop_name} ๊ฒ€์ƒ‰ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}")
    
    # ๊ฒฐ๊ณผ ์ •๋ ฌ (๊ฐ€๊ฒฉ ์˜ค๋ฆ„์ฐจ์ˆœ)
    all_results.sort(key=lambda x: x['price'] if x['price'] else float('inf'))
    
    # CSV ํŒŒ์ผ๋กœ ์ €์žฅ
    if all_results:
        csv_file = save_to_csv(all_results, output_dir, keyword)
        
        # ์ตœ์ €๊ฐ€ ์ •๋ณด ์ถœ๋ ฅ
        lowest = all_results[0]
        logger.info(f"์ตœ์ €๊ฐ€ ์ •๋ณด:")
        logger.info(f"  ์‡ผํ•‘๋ชฐ: {lowest['shop']}")
        logger.info(f"  ์ œํ’ˆ๋ช…: {lowest['title']}")
        logger.info(f"  ๊ฐ€๊ฒฉ: {lowest['price']:,}์›")
        logger.info(f"  URL: {lowest['url']}")
        
        # ๊ฐ€๊ฒฉ ์•Œ๋ฆผ ์„ค์ •
        if args.notify and args.email and args.target_price:
            cursor = db_manager.connect()
            cursor.execute(
                "INSERT INTO alerts (product_id, target_price, email) VALUES (?, ?, ?)",
                (product_id, args.target_price, args.email)
            )
            db_manager.conn.commit()
            db_manager.close()
            
            logger.info(f"๊ฐ€๊ฒฉ ์•Œ๋ฆผ์ด ์„ค์ •๋˜์—ˆ์Šต๋‹ˆ๋‹ค: {args.email}, ๋ชฉํ‘œ๊ฐ€: {args.target_price:,}์›")
            
            # ์•Œ๋ฆผ ์กฐ๊ฑด ํ™•์ธ
            notifier = PriceNotifier()
            notifier.check_price_alerts(db_manager)
    else:
        logger.warning("๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
    
    logger.info("๊ฒ€์ƒ‰ ์™„๋ฃŒ")

if __name__ == "__main__":
    main()

์ด ๋ฉ”์ธ ํŒŒ์ผ์€ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ด:

  1. ๋ช…๋ นํ–‰ ์ธ์ž ํŒŒ์‹ฑ (๊ฒ€์ƒ‰ ํ‚ค์›Œ๋“œ, ์ถœ๋ ฅ ๋””๋ ‰ํ† ๋ฆฌ, ์•Œ๋ฆผ ์„ค์ • ๋“ฑ)
  2. ์—ฌ๋Ÿฌ ์‡ผํ•‘๋ชฐ ์Šคํฌ๋ž˜ํผ ์‹คํ–‰ ๋ฐ ๊ฒฐ๊ณผ ์ˆ˜์ง‘
  3. ๊ฒฐ๊ณผ๋ฅผ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์™€ CSV ํŒŒ์ผ์— ์ €์žฅ
  4. ์ตœ์ €๊ฐ€ ์ •๋ณด ์ถœ๋ ฅ
  5. ๊ฐ€๊ฒฉ ์•Œ๋ฆผ ์„ค์ • ๋ฐ ํ™•์ธ

์ด ํ”„๋กœ๊ทธ๋žจ์€ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์–ด:

# ๊ธฐ๋ณธ ๊ฒ€์ƒ‰
python main.py --keyword "์•„์ดํฐ 13"

# ๊ฒฐ๊ณผ ์ €์žฅ ๋””๋ ‰ํ† ๋ฆฌ ์ง€์ •
python main.py --keyword "์•„์ดํฐ 13" --output "results"

# ๊ฐ€๊ฒฉ ์•Œ๋ฆผ ์„ค์ •
python main.py --keyword "์•„์ดํฐ 13" --notify --email "user@example.com" --target-price 800000

7๋‹จ๊ณ„: ์›น ๋Œ€์‹œ๋ณด๋“œ ๊ตฌํ˜„ (์„ ํƒ) ๐Ÿ–ฅ๏ธ

๋งˆ์ง€๋ง‰์œผ๋กœ, ์ˆ˜์ง‘ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ์‹œ๊ฐ์ ์œผ๋กœ ๋ณด์—ฌ์ฃผ๋Š” ๊ฐ„๋‹จํ•œ ์›น ๋Œ€์‹œ๋ณด๋“œ๋ฅผ ๊ตฌํ˜„ํ•ด๋ณด์ž:

# app.py
from flask import Flask, render_template, request, jsonify
from database.db_manager import DatabaseManager
import json
from datetime import datetime

app = Flask(__name__)
db_manager = DatabaseManager()

@app.route('/')
def index():
    """๋ฉ”์ธ ํŽ˜์ด์ง€"""
    cursor = db_manager.connect()
    cursor.execute("SELECT * FROM products ORDER BY created_at DESC")
    products = [dict(row) for row in cursor.fetchall()]
    db_manager.close()
    return render_template('index.html', products=products)

@app.route('/product/<int:product_id>')
def product_detail(product_id):
    """์ œํ’ˆ ์ƒ์„ธ ํŽ˜์ด์ง€"""
    cursor = db_manager.connect()
    
    # ์ œํ’ˆ ์ •๋ณด ์กฐํšŒ
    cursor.execute("SELECT * FROM products WHERE id = ?", (product_id,))
    product = dict(cursor.fetchone())
    
    # ์ตœ์‹  ๊ฐ€๊ฒฉ ์ •๋ณด ์กฐํšŒ
    prices = db_manager.get_product_prices(product_id)
    
    # ๊ฐ€๊ฒฉ ๋ณ€๋™ ์ด๋ ฅ ์กฐํšŒ
    price_history = db_manager.get_price_history(product_id)
    
    # ์ฐจํŠธ ๋ฐ์ดํ„ฐ ์ค€๋น„
    chart_data = {}
    for item in price_history:
        shop = item['shop']
        if shop not in chart_data:
            chart_data[shop] = []
        
        # ๋‚ ์งœ ํ˜•์‹ ๋ณ€ํ™˜
        date = datetime.fromisoformat(item['timestamp']).strftime('%Y-%m-%d')
        chart_data[shop].append({
            'date': date,
            'price': item['price']
        })
    
    db_manager.close()
    
    return render_template(
        'product_detail.html',
        product=product,
        prices=prices,
        chart_data=json.dumps(chart_data)
    )

@app.route('/api/search', methods=['POST'])
def search_api():
    """๊ฒ€์ƒ‰ API"""
    data = request.json
    keyword = data.get('keyword')
    
    if not keyword:
        return jsonify({'error': '๊ฒ€์ƒ‰์–ด๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.'}), 400
    
    # ์—ฌ๊ธฐ์„œ ํฌ๋กค๋ง ์ž‘์—…์„ ์‹คํ–‰ํ•˜๊ฑฐ๋‚˜ ์ด๋ฏธ ์ˆ˜์ง‘๋œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ˜ํ™˜
    # ์‹ค์ œ ๊ตฌํ˜„์—์„œ๋Š” ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ž‘์—…์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Œ
    
    return jsonify({'message': f'"{keyword}" ๊ฒ€์ƒ‰์ด ์‹œ์ž‘๋˜์—ˆ์Šต๋‹ˆ๋‹ค.'}), 202

@app.route('/api/alert', methods=['POST'])
def set_alert():
    """์•Œ๋ฆผ ์„ค์ • API"""
    data = request.json
    product_id = data.get('product_id')
    email = data.get('email')
    target_price = data.get('target_price')
    
    if not all([product_id, email, target_price]):
        return jsonify({'error': '๋ชจ๋“  ํ•„๋“œ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.'}), 400
    
    cursor = db_manager.connect()
    cursor.execute(
        "INSERT INTO alerts (product_id, target_price, email) VALUES (?, ?, ?)",
        (product_id, target_price, email)
    )
    db_manager.conn.commit()
    db_manager.close()
    
    return jsonify({'message': '์•Œ๋ฆผ์ด ์„ค์ •๋˜์—ˆ์Šต๋‹ˆ๋‹ค.'}), 201

if __name__ == '__main__':
    app.run(debug=True)

์ด ์›น ๋Œ€์‹œ๋ณด๋“œ๋Š” Flask๋ฅผ ์‚ฌ์šฉํ•ด ๊ตฌํ˜„ํ–ˆ์–ด. ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ด:

  1. ์ˆ˜์ง‘๋œ ์ œํ’ˆ ๋ชฉ๋ก ํ‘œ์‹œ
  2. ์ œํ’ˆ๋ณ„ ์ƒ์„ธ ์ •๋ณด ๋ฐ ๊ฐ€๊ฒฉ ๋น„๊ต
  3. ๊ฐ€๊ฒฉ ๋ณ€๋™ ์ถ”์ด ์ฐจํŠธ
  4. ์ƒˆ ๊ฒ€์ƒ‰ ์š”์ฒญ API
  5. ๊ฐ€๊ฒฉ ์•Œ๋ฆผ ์„ค์ • API

์ด ์›น ๋Œ€์‹œ๋ณด๋“œ๋ฅผ ํ†ตํ•ด ์ˆ˜์ง‘ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ์‹œ๊ฐ์ ์œผ๋กœ ํ™•์ธํ•˜๊ณ  ๋ถ„์„ํ•  ์ˆ˜ ์žˆ์–ด. ๐Ÿ–ฅ๏ธ

ํ”„๋กœ์ ํŠธ ํ™•์žฅ ์•„์ด๋””์–ด ๐Ÿ’ก

์ด ํ”„๋กœ์ ํŠธ๋ฅผ ๋” ๋ฐœ์ „์‹œํ‚ฌ ์ˆ˜ ์žˆ๋Š” ๋ช‡ ๊ฐ€์ง€ ์•„์ด๋””์–ด๋ฅผ ์†Œ๊ฐœํ• ๊ฒŒ:

  1. ๋” ๋งŽ์€ ์‡ผํ•‘๋ชฐ ์ถ”๊ฐ€: ๋‹ค์–‘ํ•œ ์‡ผํ•‘๋ชฐ์„ ์ถ”๊ฐ€ํ•˜์—ฌ ๋” ๋„“์€ ๋ฒ”์œ„์—์„œ ๊ฐ€๊ฒฉ์„ ๋น„๊ต
  2. ์ œํ’ˆ ๋ฆฌ๋ทฐ ์ˆ˜์ง‘: ๊ฐ€๊ฒฉ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ๋ฆฌ๋ทฐ๋„ ์ˆ˜์ง‘ํ•˜์—ฌ ์ œํ’ˆ ํ‰๊ฐ€ ์ •๋ณด ์ œ๊ณต
  3. ์ž๋™ ์‹คํ–‰ ์Šค์ผ€์ค„๋ง: cron์ด๋‚˜ Celery๋ฅผ ์‚ฌ์šฉํ•ด ์ •๊ธฐ์ ์œผ๋กœ ํฌ๋กค๋ง ์‹คํ–‰
  4. ๋จธ์‹ ๋Ÿฌ๋‹ ํ†ตํ•ฉ: ๊ฐ€๊ฒฉ ์˜ˆ์ธก ๋ชจ๋ธ์„ ๊ฐœ๋ฐœํ•˜์—ฌ ํ–ฅํ›„ ๊ฐ€๊ฒฉ ๋ณ€๋™ ์˜ˆ์ธก
  5. ๋ชจ๋ฐ”์ผ ์•ฑ ๊ฐœ๋ฐœ: ๋ชจ๋ฐ”์ผ์—์„œ๋„ ์‰ฝ๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์•ฑ ๊ฐœ๋ฐœ
  6. ์†Œ์…œ ๊ธฐ๋Šฅ ์ถ”๊ฐ€: ์‚ฌ์šฉ์ž๋“ค์ด ์ข‹์€ ๊ฐ€๊ฒฉ ์ •๋ณด๋ฅผ ๊ณต์œ ํ•  ์ˆ˜ ์žˆ๋Š” ์ปค๋ฎค๋‹ˆํ‹ฐ ๊ธฐ๋Šฅ

์ด ํ”„๋กœ์ ํŠธ๋Š” ์›น ํฌ๋กค๋ง์˜ ์‹ค์ œ ํ™œ์šฉ ์‚ฌ๋ก€๋ฅผ ๋ณด์—ฌ์ฃผ๋Š” ์ข‹์€ ์˜ˆ์‹œ์•ผ. ๊ธฐ๋ณธ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•œ ํ›„, ํ•„์š”์— ๋”ฐ๋ผ ๊ธฐ๋Šฅ์„ ํ™•์žฅํ•ด ๋‚˜๊ฐˆ ์ˆ˜ ์žˆ์–ด! ๐Ÿš€

๐Ÿ’ก ์‹ค์ „ ํŒ: ์‹ค์ œ ํ”„๋กœ์ ํŠธ๋ฅผ ์ง„ํ–‰ํ•  ๋•Œ๋Š” ์ž‘์€ ๋‹จ์œ„๋กœ ๋‚˜๋ˆ„์–ด ์ ์ง„์ ์œผ๋กœ ๊ฐœ๋ฐœํ•˜๋Š” ๊ฒƒ์ด ์ข‹์•„. ๋จผ์ € ํ•œ ์‡ผํ•‘๋ชฐ์— ๋Œ€ํ•œ ๊ธฐ๋ณธ ํฌ๋กค๋ง ๊ธฐ๋Šฅ์„ ์™„์„ฑํ•˜๊ณ , ๊ทธ ๋‹ค์Œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ €์žฅ, ๋‹ค๋ฅธ ์‡ผํ•‘๋ชฐ ์ถ”๊ฐ€, ์›น ์ธํ„ฐํŽ˜์ด์Šค ๋“ฑ์˜ ์ˆœ์„œ๋กœ ๊ฐœ๋ฐœํ•˜๋ฉด ๋” ํšจ์œจ์ ์ด์•ผ. ๋˜ํ•œ ๊ฐ ๋‹จ๊ณ„๋งˆ๋‹ค ์ถฉ๋ถ„ํ•œ ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ด!

๋งˆ์น˜๋ฉฐ: ์›น ํฌ๋กค๋ง์˜ ๋ฌดํ•œํ•œ ๊ฐ€๋Šฅ์„ฑ ๐ŸŒŸ

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

์šฐ๋ฆฌ๊ฐ€ ๋ฐฐ์šด ๋‚ด์šฉ์„ ์ •๋ฆฌํ•ด๋ณด๋ฉด:

  1. ์›น ํฌ๋กค๋ง๊ณผ ์Šคํฌ๋ž˜ํ•‘์˜ ๊ธฐ๋ณธ ๊ฐœ๋…๊ณผ ์›๋ฆฌ
  2. ํŒŒ์ด์ฌ์„ ํ™œ์šฉํ•œ ํฌ๋กค๋ง ๋ฐฉ๋ฒ• (requests, BeautifulSoup)
  3. ๋™์  ์›นํŽ˜์ด์ง€ ์ฒ˜๋ฆฌ ๋ฐฉ๋ฒ• (Selenium)
  4. ๋Œ€๊ทœ๋ชจ ํฌ๋กค๋ง์„ ์œ„ํ•œ ํ”„๋ ˆ์ž„์›Œํฌ (Scrapy)
  5. API๋ฅผ ํ™œ์šฉํ•œ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ๋ฐฉ๋ฒ•
  6. ํฌ๋กค๋ง ์‹œ ์ฃผ์˜ํ•ด์•ผ ํ•  ๋ฒ•์ /์œค๋ฆฌ์  ๋ฌธ์ œ
  7. ์ˆ˜์ง‘ํ•œ ๋ฐ์ดํ„ฐ์˜ ์ €์žฅ ๋ฐ ๊ด€๋ฆฌ ๋ฐฉ๋ฒ•
  8. ์‹ค์ „ ํ”„๋กœ์ ํŠธ๋ฅผ ํ†ตํ•œ ์ข…ํ•ฉ์ ์ธ ์‘์šฉ

์ด ์ง€์‹์„ ๋ฐ”ํƒ•์œผ๋กœ ๋‹ค์–‘ํ•œ ๋ถ„์•ผ์—์„œ ์›น ํฌ๋กค๋ง์„ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ์–ด:

  • ๐Ÿ” ์‹œ์žฅ ์กฐ์‚ฌ ๋ฐ ๊ฒฝ์Ÿ ๋ถ„์„: ๊ฒฝ์Ÿ์‚ฌ ์ œํ’ˆ ๊ฐ€๊ฒฉ, ๋ฆฌ๋ทฐ, ํŠน์ง• ๋“ฑ์„ ์ˆ˜์ง‘ํ•˜์—ฌ ์‹œ์žฅ ๋™ํ–ฅ ํŒŒ์•…
  • ๐Ÿ“Š ๋ฐ์ดํ„ฐ ๊ธฐ๋ฐ˜ ์˜์‚ฌ ๊ฒฐ์ •: ๋Œ€๋Ÿ‰์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์ˆ˜์ง‘ํ•˜์—ฌ ๋น„์ฆˆ๋‹ˆ์Šค ์ธ์‚ฌ์ดํŠธ ๋„์ถœ
  • ๐Ÿค– ์ž๋™ํ™” ๋ฐ ํšจ์œจํ™”: ๋ฐ˜๋ณต์ ์ธ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ์ž‘์—…์„ ์ž๋™ํ™”ํ•˜์—ฌ ์‹œ๊ฐ„๊ณผ ๋น„์šฉ ์ ˆ์•ฝ
  • ๐Ÿ“ˆ ํŠธ๋ Œ๋“œ ๋ถ„์„: ์†Œ์…œ ๋ฏธ๋””์–ด, ๋‰ด์Šค ๋“ฑ์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์ˆ˜์ง‘ํ•˜์—ฌ ํŠธ๋ Œ๋“œ ๋ถ„์„
  • ๐Ÿง  ๋จธ์‹ ๋Ÿฌ๋‹ ํ•™์Šต ๋ฐ์ดํ„ฐ: AI ๋ชจ๋ธ ํ•™์Šต์„ ์œ„ํ•œ ๋Œ€๋Ÿ‰์˜ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘

์žฌ๋Šฅ๋„ท์—์„œ๋„ ์›น ํฌ๋กค๋ง ๊ด€๋ จ ์„œ๋น„์Šค๋ฅผ ์ œ๊ณตํ•˜๋Š” ์ „๋ฌธ๊ฐ€๋“ค์„ ๋งŒ๋‚  ์ˆ˜ ์žˆ์–ด. ์ง์ ‘ ํฌ๋กค๋Ÿฌ๋ฅผ ๊ฐœ๋ฐœํ•˜๊ธฐ ์–ด๋ ต๋‹ค๋ฉด, ์žฌ๋Šฅ๋„ท์„ ํ†ตํ•ด ์ „๋ฌธ๊ฐ€์˜ ๋„์›€์„ ๋ฐ›๋Š” ๊ฒƒ๋„ ์ข‹์€ ๋ฐฉ๋ฒ•์ด์•ผ! ๐Ÿค

์›น ํฌ๋กค๋ง์€ ๊ณ„์†ํ•ด์„œ ๋ฐœ์ „ํ•˜๊ณ  ์žˆ๋Š” ๋ถ„์•ผ์•ผ. ์›น ๊ธฐ์ˆ ์ด ๋ณ€ํ™”ํ•จ์— ๋”ฐ๋ผ ํฌ๋กค๋ง ๊ธฐ์ˆ ๋„ ํ•จ๊ป˜ ์ง„ํ™”ํ•˜๊ณ  ์žˆ์ง€. ํ•ญ์ƒ ์ตœ์‹  ํŠธ๋ Œ๋“œ์™€ ๊ธฐ์ˆ ์„ ํ•™์Šตํ•˜๊ณ , ๋ฒ•์ /์œค๋ฆฌ์  ๊ฐ€์ด๋“œ๋ผ์ธ์„ ์ค€์ˆ˜ํ•˜๋ฉด์„œ ํฌ๋กค๋ง์„ ํ™œ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ด. ๐ŸŒฑ

์ด ๊ธ€์ด ์›น ํฌ๋กค๋ง๊ณผ ์Šคํฌ๋ž˜ํ•‘์˜ ์„ธ๊ณ„๋กœ ์ฒซ ๋ฐœ์„ ๋‚ด๋”›๋Š” ๋ฐ ๋„์›€์ด ๋˜์—ˆ๊ธธ ๋ฐ”๋ผ! ๋” ๋งŽ์€ ์‹ค์Šต๊ณผ ๊ฒฝํ—˜์„ ํ†ตํ•ด ํฌ๋กค๋ง ๊ธฐ์ˆ ์„ ๋งˆ์Šคํ„ฐํ•˜๊ณ , ๋ฐ์ดํ„ฐ์˜ ๋ฐ”๋‹ค์—์„œ ๊ฐ’์ง„ ๋ณด๋ฌผ์„ ์ฐพ์•„๋‚ด๊ธธ ์‘์›ํ•ด! ๐ŸŽฏ

์งˆ๋ฌธ์ด๋‚˜ ํ”ผ๋“œ๋ฐฑ์ด ์žˆ๋‹ค๋ฉด ์–ธ์ œ๋“ ์ง€ ๋Œ“๊ธ€๋กœ ๋‚จ๊ฒจ์ค˜. ํ•จ๊ป˜ ์„ฑ์žฅํ•˜๋Š” ์ฆ๊ฑฐ์›€์„ ๋‚˜๋ˆ ๋ณด์ž! ๐Ÿ‘‹

๋Œ“๊ธ€ ์ž‘์„ฑ

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

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

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