Ver2.0 ๐ ์น ํฌ๋กค๋ง๊ณผ ์คํฌ๋ํ ์์ ์ ๋ณต: ๋ฐ์ดํฐ ์์ง ์๋ํ์ ๋ชจ๋ ๊ฒ ๐

๐ ์น ํฌ๋กค๋ง๊ณผ ์คํฌ๋ํ ์์ ์ ๋ณต: ๋ฐ์ดํฐ ์์ง ์๋ํ์ ๋ชจ๋ ๊ฒ ๐
์๋ ? ์ค๋์ ์น์์ ๋ฐ์ดํฐ๋ฅผ ์๋์ผ๋ก ์์งํ๋ ์น ํฌ๋กค๋ง๊ณผ ์คํฌ๋ํ์ ๋ํด ํจ๊ป ์์๋ณผ ๊ฑฐ์ผ. ์ธํฐ๋ท์ ์ ๋ณด์ ๋ฐ๋ค๋ผ๊ณ ํ์์? ์ด ๋ฐ๋ค์์ ์ํ๋ ์ ๋ณด๋ง ์์ ๊ฑด์ ธ์ฌ๋ฆฌ๋ ๋ฐฉ๋ฒ์ ๋ฐฐ์ฐ๋ฉด ์ ๋ง ๋ง์ ์ผ์ ์๋ํํ ์ ์์ด! ๐
์์ฆ ๊ฐ์ ๋ฐ์ดํฐ ์๋์ ์ด๋ฐ ๊ธฐ์ ์ ๊ฐ์ธ ํ๋ก์ ํธ๋ถํฐ ๋น์ฆ๋์ค๊น์ง ๋ค์ํ ๋ถ์ผ์์ ํ์ฉ๋๊ณ ์์ด. ์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์๋ ์น ํฌ๋กค๋ง ์๋น์ค๋ฅผ ์ ๊ณตํ๋ ์ ๋ฌธ๊ฐ๋ค์ด ๋ง์ด ํ๋ํ๊ณ ์์ง. ์ด ๊ธ์ ํตํด ๋ ์น ํฌ๋กค๋ง์ ๊ธฐ์ด๋ถํฐ ๊ณ ๊ธ ๊ธฐ๋ฒ๊น์ง ์ฝ๊ณ ์ฌ๋ฏธ์๊ฒ ๋ฐฐ์ธ ์ ์์ ๊ฑฐ์ผ! ๐
๐ ๋ชฉ์ฐจ
- ์น ํฌ๋กค๋ง๊ณผ ์คํฌ๋ํ ์ดํดํ๊ธฐ
- ํฌ๋กค๋ง์ ๊ธฐ๋ณธ ์๋ฆฌ์ ์๋ ๋ฐฉ์
- ํ์ด์ฌ์ผ๋ก ์์ํ๋ ์น ํฌ๋กค๋ง
- BeautifulSoup์ผ๋ก HTML ํ์ฑํ๊ธฐ
- Selenium์ผ๋ก ๋์ ์นํ์ด์ง ๋ค๋ฃจ๊ธฐ
- Scrapy ํ๋ ์์ํฌ ํ์ฉํ๊ธฐ
- API๋ฅผ ํ์ฉํ ๋ฐ์ดํฐ ์์ง
- ํฌ๋กค๋ง ์ ์ฃผ์ํด์ผ ํ ๋ฒ์ /์ค๋ฆฌ์ ๋ฌธ์
- ๋ฐ์ดํฐ ์ ์ฅ ๋ฐ ๊ด๋ฆฌ ๋ฐฉ๋ฒ
- ์ค์ ํ๋ก์ ํธ: ๋๋ง์ ํฌ๋กค๋ฌ ๋ง๋ค๊ธฐ
1. ์น ํฌ๋กค๋ง๊ณผ ์คํฌ๋ํ ์ดํดํ๊ธฐ ๐ธ๏ธ
์น ํฌ๋กค๋ง(Web Crawling)๊ณผ ์คํฌ๋ํ(Scraping)์ด๋ผ๋ ๋ง, ๋ค์ด๋ณธ ์ ์์ง? ์ด ๋ ์ฉ์ด๋ ์ข ์ข ํผ์ฉ๋์ง๋ง ์ฌ์ค ์ฝ๊ฐ์ ์ฐจ์ด๊ฐ ์์ด.
์น ํฌ๋กค๋ง(Web Crawling): ์น ํ์ด์ง๋ฅผ ์๋์ผ๋ก ํ์ํ๋ฉด์ ๋งํฌ๋ฅผ ๋ฐ๋ผ ์ด๋ํ๋ฉฐ ์น์ '๊ธฐ์ด ๋ค๋๋' ๊ณผ์ ์ด์ผ. ๊ตฌ๊ธ ๊ฐ์ ๊ฒ์ ์์ง์ด ์ ๋ณด๋ฅผ ์์งํ ๋ ์ฌ์ฉํ๋ ๋ฐฉ์์ด์ง.
์น ์คํฌ๋ํ(Web Scraping): ํน์ ์น ํ์ด์ง์์ ์ํ๋ ๋ฐ์ดํฐ๋ฅผ ์ถ์ถํ๋ ๊ณผ์ ์ด์ผ. ํฌ๋กค๋ง์ด '์ด๋'์ ์ด์ ์ ๋๋ค๋ฉด, ์คํฌ๋ํ์ '์ถ์ถ'์ ์ด์ ์ ๋๋ ๊ฑฐ์ง.
์ฝ๊ฒ ๋งํ๋ฉด, ํฌ๋กค๋ง์ ์น ํ์ด์ง๋ค์ ๋์๋ค๋๋ ๊ฒ์ด๊ณ , ์คํฌ๋ํ์ ๊ทธ ํ์ด์ง์์ ํ์ํ ์ ๋ณด๋ฅผ ๊ธ์ด์ค๋ ๊ฒ์ด๋ผ๊ณ ์๊ฐํ๋ฉด ๋ผ. ๋ณดํต์ ์ด ๋ ๊ฐ์ง๋ฅผ ํจ๊ป ์ฌ์ฉํด์ ์ํ๋ ๋ฐ์ดํฐ๋ฅผ ์์งํ๊ฒ ๋์ง! ๐ง
์น ํฌ๋กค๋ง๊ณผ ์คํฌ๋ํ์ด ํ์ํ ์ด์ ๐ค
์ ์ด๋ฐ ๊ธฐ์ ์ด ํ์ํ ๊น? ๋ช ๊ฐ์ง ์๋ฅผ ๋ค์ด๋ณผ๊ฒ:
- ๋ฐ์ดํฐ ๋ถ์: ์์ฅ ๋ํฅ, ๊ฐ๊ฒฉ ๋น๊ต, ์๋น์ ๋ฆฌ๋ทฐ ๋ฑ์ ๋ถ์ํ๊ธฐ ์ํ ๋ฐ์ดํฐ ์์ง
- ์ฝํ ์ธ ๋ชจ๋ํฐ๋ง: ๊ฒฝ์์ฌ ์น์ฌ์ดํธ๋ ๋ด์ค ์ฌ์ดํธ์ ์ ๋ฐ์ดํธ ์ถ์
- ์ฐ๊ตฌ ๋ชฉ์ : ํ์ ์ฐ๊ตฌ๋ ํธ๋ ๋ ๋ถ์์ ์ํ ๋๋์ ๋ฐ์ดํฐ ์์ง
- ์๋ํ: ๋ฐ๋ณต์ ์ธ ๋ฐ์ดํฐ ์์ง ์์ ์ ์๋ํ
- ๊ฐ์ธ ํ๋ก์ ํธ: ์ทจ๋ฏธ๋ ํ์ต ๋ชฉ์ ์ ๋ฐ์ดํฐ ์์ง
์๋ฅผ ๋ค์ด, ๋ถ๋์ฐ ๊ฐ๊ฒฉ ๋ํฅ์ ๋ถ์ํ๊ณ ์ถ๋ค๋ฉด ์ฌ๋ฌ ๋ถ๋์ฐ ์ฌ์ดํธ์์ ๋งค๋ฌผ ์ ๋ณด๋ฅผ ์๋์ผ๋ก ์์งํ ์ ์์ด. ๋๋ ์ผํ๋ชฐ์์ ํน์ ์ ํ์ ๊ฐ๊ฒฉ ๋ณ๋์ ์ถ์ ํ๊ฑฐ๋, ๋ด์ค ์ฌ์ดํธ์์ ํน์ ํค์๋๊ฐ ํฌํจ๋ ๊ธฐ์ฌ๋ง ๋ชจ์๋ณผ ์๋ ์์ง. ๊ฐ๋ฅ์ฑ์ ๋ฌด๊ถ๋ฌด์งํด! ๐
์น ํฌ๋กค๋ง์ ๊ธฐ๋ณธ ์๋ฆฌ ๐งฉ
์น ํฌ๋กค๋ง์ ๊ธฐ๋ณธ์ ์ผ๋ก ๋ค์๊ณผ ๊ฐ์ ๋จ๊ณ๋ก ์ด๋ฃจ์ด์ ธ:
- URL ์์ฒญ: ์น ํ์ด์ง์ HTTP ์์ฒญ์ ๋ณด๋ด HTML ๋ฌธ์๋ฅผ ๊ฐ์ ธ์ด
- HTML ํ์ฑ: ๊ฐ์ ธ์จ HTML ๋ฌธ์๋ฅผ ๋ถ์ํ์ฌ ๊ตฌ์กฐํ๋ ๋ฐ์ดํฐ๋ก ๋ณํ
- ๋ฐ์ดํฐ ์ถ์ถ: ์ํ๋ ์ ๋ณด๋ฅผ ์ ํ์ ์ผ๋ก ์ถ์ถ
- ๋ค์ ํ์ด์ง ํ์: ๋งํฌ๋ฅผ ๋ฐ๋ผ ๋ค๋ฅธ ํ์ด์ง๋ก ์ด๋ํ๊ฑฐ๋ ํ์ด์ง๋ค์ด์ ์ฒ๋ฆฌ
- ๋ฐ์ดํฐ ์ ์ฅ: ์ถ์ถํ ๋ฐ์ดํฐ๋ฅผ ํ์ผ์ด๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์ฅ
์ด ๊ณผ์ ์ด ์๋ํ๋๋ฉด ์์ฒ, ์๋ง ๊ฐ์ ์นํ์ด์ง์์ ์ ๋ณด๋ฅผ ์์งํ ์ ์๊ฒ ๋๋ ๊ฑฐ์ง. ์์ผ๋ก ํ๋ฉด ๋ช ๋ฌ์ด ๊ฑธ๋ฆด ์ผ์ ๋ช ๋ถ ๋๋ ๋ช ์๊ฐ ๋ง์ ๋๋ผ ์ ์์ด! ๐
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 ์์ฒญ์ ๋ณด๋ด๋ ๊ฑฐ์ผ. ์ด ๊ณผ์ ์ ์ข ๋ ์์ธํ ์ดํด๋ณผ๊ฒ:
- HTTP ์์ฒญ(Request): ํฌ๋กค๋ฌ๊ฐ ์น ์๋ฒ์ ํน์ URL์ ํ์ด์ง๋ฅผ ์์ฒญํจ
- HTTP ์๋ต(Response): ์๋ฒ๊ฐ ์์ฒญ๋ ํ์ด์ง์ HTML ๋ด์ฉ์ ์๋ต์ผ๋ก ๋ณด๋
- ์ํ ์ฝ๋: ์๋ฒ๋ ์๋ต๊ณผ ํจ๊ป ์ํ ์ฝ๋(์: 200 ์ฑ๊ณต, 404 ํ์ด์ง ์์)๋ฅผ ๋ณด๋
์์๋๋ฉด ์ข์ HTTP ์ํ ์ฝ๋:
- โ 200 OK: ์์ฒญ ์ฑ๊ณต
- ๐ 301/302: ํ์ด์ง ๋ฆฌ๋ค์ด๋ ํธ
- โ 403 Forbidden: ์ ๊ทผ ๊ถํ ์์
- ๐ 404 Not Found: ํ์ด์ง๋ฅผ ์ฐพ์ ์ ์์
- โ ๏ธ 429 Too Many Requests: ๋๋ฌด ๋ง์ ์์ฒญ (ํฌ๋กค๋ง ์ ํ)
- ๐ 500 Internal Server Error: ์๋ฒ ์ค๋ฅ
ํฌ๋กค๋งํ ๋ 429 ์๋ฌ๋ฅผ ์์ฃผ ๋ง๋๊ฒ ๋๋ค๋ฉด, ๋๋ฌด ๋น ๋ฅธ ์๋๋ก ์์ฒญ์ ๋ณด๋ด๊ณ ์๋ค๋ ์ ํธ์ผ. ์ด๋ด ๋๋ ์์ฒญ ์ฌ์ด์ ์๊ฐ ๊ฐ๊ฒฉ์ ๋๋ ๊ฒ์ด ์ข์! ๐ข
์น ํฌ๋กค๋ฌ์ ํต์ฌ ์ปดํฌ๋ํธ ๐งฉ
์น ํฌ๋กค๋ฌ๋ ๋ณดํต ๋ค์๊ณผ ๊ฐ์ ํต์ฌ ์ปดํฌ๋ํธ๋ก ๊ตฌ์ฑ๋ผ:
- URL ํ๋ก ํฐ์ด(Frontier): ๋ฐฉ๋ฌธํ URL๋ค์ ๋๊ธฐ์ด
- HTML ๋ค์ด๋ก๋: ์น ํ์ด์ง๋ฅผ ๋ค์ด๋ก๋ํ๋ ์ปดํฌ๋ํธ
- ์ฝํ ์ธ ํ์: HTML์ ๋ถ์ํ๊ณ ๊ตฌ์กฐํํ๋ ์ปดํฌ๋ํธ
- ๋ฐ์ดํฐ ์ถ์ถ๊ธฐ: ์ํ๋ ์ ๋ณด๋ฅผ ์ถ์ถํ๋ ์ปดํฌ๋ํธ
- ๋ฐ์ดํฐ ์ ์ฅ์: ์ถ์ถํ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ๋ ์ปดํฌ๋ํธ
์ด ์ปดํฌ๋ํธ๋ค์ด ์ ๊ธฐ์ ์ผ๋ก ์๋ํ๋ฉด์ ์น์ ๋์๋ค๋๋ฉฐ ๋ฐ์ดํฐ๋ฅผ ์์งํ๊ฒ ๋๋ ๊ฑฐ์ผ. ๋ง์น ์ธํฐ๋ท์ ํํํ๋ ๋ก๋ด์ด๋ผ๊ณ ์๊ฐํ๋ฉด ์ดํดํ๊ธฐ ์ฌ์ธ ๊ฑฐ์ผ! ๐ค
ํฌ๋กค๋ง ์ ๋ต ๐
ํฌ๋กค๋ง์ ํ ๋๋ ์ด๋ค ์์๋ก ํ์ด์ง๋ฅผ ๋ฐฉ๋ฌธํ ์ง ์ ๋ต์ด ํ์ํด:
- ๋๋น ์ฐ์ ํ์(BFS): ํ์ฌ ํ์ด์ง์ ๋ชจ๋ ๋งํฌ๋ฅผ ๋จผ์ ๋ฐฉ๋ฌธํ ํ ๋ค์ ๋จ๊ณ๋ก ์ด๋
- ๊น์ด ์ฐ์ ํ์(DFS): ํ ๊ฒฝ๋ก๋ฅผ ๋๊น์ง ํ์ํ ํ ๋ค๋ฅธ ๊ฒฝ๋ก๋ก ์ด๋
- ์ฐ์ ์์ ๊ธฐ๋ฐ ํ์: ์ค์๋๋ ๊ด๋ จ์ฑ์ ๋ฐ๋ผ ํ์ด์ง ๋ฐฉ๋ฌธ ์์ ๊ฒฐ์
์ด๋ค ์ ๋ต์ ์ ํํ ์ง๋ ํฌ๋กค๋ง ๋ชฉ์ ์ ๋ฐ๋ผ ๋ฌ๋ผ์ ธ. ์๋ฅผ ๋ค์ด, ํน์ ์นดํ ๊ณ ๋ฆฌ์ ๋ชจ๋ ์ ํ ์ ๋ณด๋ฅผ ์์งํ๊ณ ์ถ๋ค๋ฉด BFS๊ฐ ์ ํฉํ ์ ์๊ณ , ํน์ ์ฃผ์ ์ ๊ดํ ๊น์ ์ ๋ณด๋ฅผ ์ฐพ๊ณ ์ถ๋ค๋ฉด DFS๊ฐ ์ข์ ์ ์์ด. ๐งญ
๐ ํฌ๋กค๋ง ํ: ์น์ฌ์ดํธ๋ฅผ ํฌ๋กค๋งํ ๋๋ robots.txt ํ์ผ์ ํญ์ ํ์ธํ์! ์ด ํ์ผ์ ์น์ฌ์ดํธ ์ด์์๊ฐ ํฌ๋กค๋ฌ์๊ฒ ์ด๋ค ํ์ด์ง๋ฅผ ๋ฐฉ๋ฌธํด๋ ๋๊ณ , ์ด๋ค ํ์ด์ง๋ ๋ฐฉ๋ฌธํ๋ฉด ์ ๋๋์ง ์๋ ค์ฃผ๋ ๊ฐ์ด๋๋ผ์ธ์ด์ผ.
์: https://example.com/robots.txt
3. ํ์ด์ฌ์ผ๋ก ์์ํ๋ ์น ํฌ๋กค๋ง ๐
์ด์ ์ค์ ๋ก ํฌ๋กค๋ง์ ์์ํด๋ณผ๊น? ํ์ด์ฌ์ ์น ํฌ๋กค๋ง์ ๊ฐ์ฅ ๋ง์ด ์ฌ์ฉ๋๋ ์ธ์ด ์ค ํ๋์ผ. ๋ฌธ๋ฒ์ด ๊ฐ๊ฒฐํ๊ณ , ๋ค์ํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ง์ํ๊ธฐ ๋๋ฌธ์ด์ง. ํ์ด์ฌ์ผ๋ก ํฌ๋กค๋ง์ ์์ํ๋ ๋ฐฉ๋ฒ์ ์์๋ณด์! ๐
ํ์ํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ค์นํ๊ธฐ ๐
ํ์ด์ฌ์ผ๋ก ํฌ๋กค๋ง์ ํ๋ ค๋ฉด ๋ช ๊ฐ์ง ํต์ฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ํ์ํด:
# pip๋ฅผ ์ฌ์ฉํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ค์น
pip install requests
pip install beautifulsoup4
pip install selenium
pip install scrapy
๊ฐ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ์ญํ ์ ๊ฐ๋จํ ์ค๋ช ํ ๊ฒ:
- requests: HTTP ์์ฒญ์ ๋ณด๋ด๊ณ ์๋ต์ ๋ฐ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
- BeautifulSoup: HTML๊ณผ XML ํ์ผ์ ํ์ฑํ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
- Selenium: ์น ๋ธ๋ผ์ฐ์ ๋ฅผ ์๋ํํ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ (JavaScript๊ฐ ํ์ํ ๋์ ํ์ด์ง ์ฒ๋ฆฌ)
- 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}')
์ด ์ฝ๋๋ ๋ค์๊ณผ ๊ฐ์ ๋จ๊ณ๋ก ์๋ํด:
- requests๋ฅผ ์ฌ์ฉํด ์น ํ์ด์ง์ HTML์ ๊ฐ์ ธ์ด
- BeautifulSoup์ ์ฌ์ฉํด HTML์ ํ์ฑ
- soup.title.text๋ก ํ์ด์ง ์ ๋ชฉ์ ์ถ์ถ
- soup.find_all('a')๋ก ๋ชจ๋ ๋งํฌ(a ํ๊ทธ)๋ฅผ ์ฐพ์
- ๊ฐ ๋งํฌ์ 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)}๊ฐ์ ์์ดํ
์ ์์งํ์ต๋๋ค.')
์ด ์ฝ๋๋ ์ฌ๋ฌ ํ์ด์ง๋ฅผ ์ํํ๋ฉด์ ๊ฐ ํ์ด์ง์์ ์์ดํ (์: ์ํ)์ ์ถ์ถํด. ๋ช ๊ฐ์ง ์ค์ํ ํฌ์ธํธ๊ฐ ์์ด:
- for ๋ฃจํ๋ก ์ฌ๋ฌ ํ์ด์ง๋ฅผ ์ํ
- CSS ์ ํ์(.item, .title, .price)๋ก ์ํ๋ ์์๋ฅผ ์ ํํ ์ ํ
- 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()
์ด ์ฝ๋๋ ๋ค์๊ณผ ๊ฐ์ ์์ ์ ์ํํด:
- Chrome ๋ธ๋ผ์ฐ์ ์ด๊ธฐ
- ์ง์ ๋ URL๋ก ์ด๋
- ํ์ด์ง ์ ๋ชฉ ์ถ๋ ฅ
- h1 ํ๊ทธ ์์ ์ฐพ์์ ํ ์คํธ ์ถ๋ ฅ
- ๋ชจ๋ ๋งํฌ(a ํ๊ทธ) ์ฐพ๊ธฐ
- ์ฒซ ๋ฒ์งธ ๋งํฌ ํด๋ฆญ ๋ฐ ์ ํ์ด์ง ์ ๋ชฉ ์ถ๋ ฅ
- ๋ค๋ก ๊ฐ๊ธฐ
- ํ์ด์ง ์์ค ๊ฐ์ ธ์ค๊ธฐ
- ๋ธ๋ผ์ฐ์ ๋ซ๊ธฐ
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()
์ด ์ฝ๋์์ ์ฃผ๋ชฉํ ์ ์:
- ๋ช ์์ ๋๊ธฐ(Explicit Wait): WebDriverWait์ expected_conditions๋ฅผ ์ฌ์ฉํด ํน์ ์กฐ๊ฑด์ด ์ถฉ์กฑ๋ ๋๊น์ง ๋๊ธฐ
- ์์ ์ํธ์์ฉ: send_keys()๋ก ํ ์คํธ ์ ๋ ฅ, Keys.RETURN์ผ๋ก Enter ํค ๋๋ฅด๊ธฐ
- 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()
์ด ์ฝ๋๋ ๋ฌดํ ์คํฌ๋กค ํ์ด์ง์์ ๋ค์๊ณผ ๊ฐ์ ๋ฐฉ์์ผ๋ก ๋ฐ์ดํฐ๋ฅผ ์์งํด:
- ํ์ฌ ํ์ด์ง์ ์๋ ์์ดํ ์์ง
- ํ์ด์ง ๋๊น์ง ์คํฌ๋กค
- ์๋ก์ด ์์ดํ ์ด ๋ก๋๋ ๋๊น์ง ๋๊ธฐ
- ์๋ก ๋ก๋๋ ์์ดํ ๋ง ์ฒ๋ฆฌ
- ์ง์ ๋ ํ์๋งํผ ๋ฐ๋ณตํ๊ฑฐ๋ ๋ ์ด์ ์ ์์ดํ ์ด ๋ก๋๋์ง ์์ ๋๊น์ง ๋ฐ๋ณต
๋ฌดํ ์คํฌ๋กค, ๋์ ๋ก๋ฉ, ํ์ ์ฒ๋ฆฌ ๋ฑ ๋ณต์กํ ์นํ์ด์ง๋ Selenium์ ์ฌ์ฉํ๋ฉด ํจ๊ณผ์ ์ผ๋ก ํฌ๋กค๋งํ ์ ์์ด! ์ด๊ฒ์ด Selenium์ ๊ฐ์ฅ ํฐ ์ฅ์ ์ด์ง. ๐ฆธโโ๏ธ
โ ๏ธ ์ฃผ์์ฌํญ: Selenium์ ๊ฐ๋ ฅํ์ง๋ง, ์ค์ ๋ธ๋ผ์ฐ์ ๋ฅผ ์ฌ์ฉํ๊ธฐ ๋๋ฌธ์ requests๋ณด๋ค ๋๋ฆฌ๊ณ ๋ฆฌ์์ค๋ฅผ ๋ง์ด ์ฌ์ฉํด. ๊ฐ๋ฅํ๋ฉด API๋ requests๋ฅผ ๋จผ์ ์๋ํ๊ณ , ๊ผญ ํ์ํ ๊ฒฝ์ฐ์๋ง Selenium์ ์ฌ์ฉํ๋ ๊ฒ์ด ์ข์!
6. Scrapy ํ๋ ์์ํฌ ํ์ฉํ๊ธฐ ๐ท๏ธ
์ง๊ธ๊น์ง requests, BeautifulSoup, Selenium ๊ฐ์ ๋๊ตฌ๋ค์ ์ดํด๋ดค์ด. ์ด ๋๊ตฌ๋ค์ ๊ฐ๊ฐ ์ ์ฉํ์ง๋ง, ๋๊ท๋ชจ ํฌ๋กค๋ง ํ๋ก์ ํธ๋ฅผ ์งํํ ๋๋ ๋ ์ฒด๊ณ์ ์ธ ์ ๊ทผ์ด ํ์ํด. ์ฌ๊ธฐ์ ๋ฑ์ฅํ๋ ๊ฒ์ด ๋ฐ๋ก Scrapy๋ผ๋ ๊ฐ๋ ฅํ ์น ํฌ๋กค๋ง ํ๋ ์์ํฌ์ผ! ๐
Scrapy ์๊ฐ ๋ฐ ์ค์น ๐ฆ
Scrapy๋ ํ์ด์ฌ์ผ๋ก ์์ฑ๋ ์คํ ์์ค ์น ํฌ๋กค๋ง ํ๋ ์์ํฌ์ผ. ๋๊ท๋ชจ ํฌ๋กค๋ง ํ๋ก์ ํธ๋ฅผ ์ํ ์์ ํ ์ธํ๋ผ๋ฅผ ์ ๊ณตํ๋ฉฐ, ๋ค์๊ณผ ๊ฐ์ ์ฅ์ ์ด ์์ด:
- ๋น๋๊ธฐ ๋คํธ์ํน์ผ๋ก ๋น ๋ฅธ ์ฑ๋ฅ
- ๋ด์ฅ๋ ์ ํ๊ธฐ(selector)๋ก ์ฌ์ด ๋ฐ์ดํฐ ์ถ์ถ
- ๋ฏธ๋ค์จ์ด์ ํ์ดํ๋ผ์ธ์ ํตํ ์ ์ฐํ ์ฒ๋ฆฌ
- ์๋ ์ฌ์๋, ์ฟ ํค ๋ฐ ์ธ์ ์ฒ๋ฆฌ ๋ฑ ๋ด์ฅ ๊ธฐ๋ฅ
- ๋ค์ํ ์ถ๋ ฅ ํ์(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)
์ด ์คํ์ด๋๋ ๋ค์๊ณผ ๊ฐ์ ์์ ์ ์ํํด:
- start_urls์ ์ง์ ๋ URL์์ ์์
- ๊ฐ ์ฑ ํญ๋ชฉ(div.book-item)์ ์ฐพ์์ ์ ๋ณด ์ถ์ถ
- ๋ค์ ํ์ด์ง ๋งํฌ๋ฅผ ์ฐพ์์ ์ฌ๊ท์ ์ผ๋ก ํฌ๋กค๋ง
์คํ์ด๋๋ฅผ ์คํํ๋ ค๋ฉด ๋ค์ ๋ช ๋ น์ด๋ฅผ ์ฌ์ฉํด:
# ์คํ์ด๋ ์คํ ๋ฐ ๊ฒฐ๊ณผ๋ฅผ 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๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ ์ฅ์ ์ ๋ค์๊ณผ ๊ฐ์:
- ๊ตฌ์กฐํ๋ ๋ฐ์ดํฐ๋ฅผ ์ง์ ๋ฐ์ ์ ์์
- HTML ๊ตฌ์กฐ ๋ณ๊ฒฝ์ ์ํฅ์ ๋ฐ์ง ์์
- ์๋ฒ ๋ถํ๊ฐ ์ ๊ณ ํจ์จ์
- ๋๋ถ๋ถ ๊ณต์์ ์ผ๋ก ์ง์๋๋ฏ๋ก ๋ฒ์ /์ค๋ฆฌ์ ๋ฌธ์ ๊ฐ ์ ์
REST API ๊ธฐ๋ณธ ์ดํดํ๊ธฐ ๐
๊ฐ์ฅ ํํ ์น API ์ ํ์ REST(Representational State Transfer) API์ผ. REST API๋ ๋ค์๊ณผ ๊ฐ์ ํน์ง์ด ์์ด:
- ์์(Resource)์ URI๋ก ํํ
- HTTP ๋ฉ์๋(GET, POST, PUT, DELETE ๋ฑ)๋ก ์์์ ๋ํ ํ๋ ์ ์
- ํด๋ผ์ด์ธํธ์ ์๋ฒ์ ์ํ๋ฅผ ๋ถ๋ฆฌ(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 ๋ฌธ์๋ฅผ ๊ผผ๊ผผํ ํ์ธํ๊ณ ์ ํ์ ์ค์ํ๋ ๊ฒ์ด ์ค์ํด!
8. ํฌ๋กค๋ง ์ ์ฃผ์ํด์ผ ํ ๋ฒ์ /์ค๋ฆฌ์ ๋ฌธ์ โ๏ธ
์น ํฌ๋กค๋ง์ ๊ฐ๋ ฅํ ๋๊ตฌ์ง๋ง, ๋ฒ์ , ์ค๋ฆฌ์ ๋ฌธ์ ๋ฅผ ๊ณ ๋ คํ์ง ์์ผ๋ฉด ํฐ ๋ฌธ์ ๊ฐ ์๊ธธ ์ ์์ด. ์ด ์น์ ์์๋ ํฌ๋กค๋ง์ ํ ๋ ์์์ผ ํ ์ค์ํ ๋ฒ์ , ์ค๋ฆฌ์ ๊ณ ๋ ค์ฌํญ์ ๋ํด ์์๋ณผ๊ฒ. ๐จ
์น์ฌ์ดํธ ์ด์ฉ์ฝ๊ด ๋ฐ robots.txt ์ค์ ๐
๋ชจ๋ ์น์ฌ์ดํธ๋ ์ด์ฉ์ฝ๊ด(Terms of Service)์ ๊ฐ์ง๊ณ ์์ด. ๋ง์ ์น์ฌ์ดํธ๊ฐ ์๋ํ๋ ํฌ๋กค๋ง์ ๋ช ์์ ์ผ๋ก ๊ธ์งํ๊ณ ์์ง. ๋ํ ๋๋ถ๋ถ์ ์น์ฌ์ดํธ๋ robots.txt ํ์ผ์ ํตํด ํฌ๋กค๋ฌ์๊ฒ ์ ๊ทผ ๊ฐ๋ฅํ ์์ญ์ ์๋ ค์ค.
# robots.txt ํ์ผ ์์
User-agent: * # ๋ชจ๋ ํฌ๋กค๋ฌ์ ์ ์ฉ
Disallow: /private/ # /private/ ๋๋ ํ ๋ฆฌ ํฌ๋กค๋ง ๊ธ์ง
Disallow: /admin/ # /admin/ ๋๋ ํ ๋ฆฌ ํฌ๋กค๋ง ๊ธ์ง
Allow: /public/ # /public/ ๋๋ ํ ๋ฆฌ ํฌ๋กค๋ง ํ์ฉ
Crawl-delay: 10 # ์์ฒญ ๊ฐ 10์ด ์ง์ฐ ์์ฒญ
ํ์ด์ฌ์์ robots.txt๋ฅผ ํ์ธํ๋ ๋ฐฉ๋ฒ:
import requests
from urllib.robotparser import RobotFileParser
url = 'https://example.com'
robots_url = f'{url}/robots.txt'
# robots.txt ํ์ผ ๊ฐ์ ธ์ค๊ธฐ
rp = RobotFileParser()
rp.set_url(robots_url)
rp.read()
# ํน์ URL์ ํฌ๋กค๋ฌ๊ฐ ์ ๊ทผ ๊ฐ๋ฅํ์ง ํ์ธ
test_url = f'{url}/some/page'
can_fetch = rp.can_fetch('MyBot', test_url)
print(f'ํฌ๋กค๋ง ๊ฐ๋ฅ ์ฌ๋ถ: {can_fetch}')
print(f'ํฌ๋กค๋ง ์ง์ฐ ์๊ฐ: {rp.crawl_delay("MyBot")} ์ด')
robots.txt๋ ๊ธฐ์ ์ ์ ํ์ด ์๋๋ผ ๊ถ๊ณ ์ฌํญ์ด์ง๋ง, ์ด๋ฅผ ๋ฌด์ํ๋ ๊ฒ์ ๋ฒ์ ๋ฌธ์ ๋ฅผ ์ด๋ํ ์ ์์ด. ํญ์ robots.txt๋ฅผ ํ์ธํ๊ณ ์ค์ํ๋ ๊ฒ์ด ์ข์! ๐ฆ
์ ์๊ถ ๋ฐ ๋ฐ์ดํฐ ์์ ๊ถ ๋ฌธ์ ยฉ๏ธ
์น์ฌ์ดํธ์ ์ฝํ ์ธ ๋ ์ ์๊ถ์ผ๋ก ๋ณดํธ๋ฐ์ ์ ์์ด. ํฌ๋กค๋งํ ๋ฐ์ดํฐ๋ฅผ ์ด๋ป๊ฒ ์ฌ์ฉํ๋๋์ ๋ฐ๋ผ ์ ์๊ถ ์นจํด๊ฐ ๋ ์ ์์ง.
์ผ๋ฐ์ ์ผ๋ก ๋ค์๊ณผ ๊ฐ์ ๊ฒฝ์ฐ์ ์ฃผ์ํด์ผ ํด:
- ํฌ๋กค๋งํ ์ฝํ ์ธ ๋ฅผ ๊ทธ๋๋ก ์ฌ๋ฐํํ๋ ๊ฒฝ์ฐ
- ํฌ๋กค๋งํ ๋ฐ์ดํฐ๋ก ์ ์ฌ์ดํธ์ ๊ฒฝ์ํ๋ ์๋น์ค๋ฅผ ๋ง๋๋ ๊ฒฝ์ฐ
- ํฌ๋กค๋งํ ๋ฐ์ดํฐ๋ฅผ ์์ ์ ์ผ๋ก ์ด์ฉํ๋ ๊ฒฝ์ฐ
๋ฐ๋ฉด, ๋ค์๊ณผ ๊ฐ์ ๊ฒฝ์ฐ๋ ๋น๊ต์ ์์ ํ ์ ์์ด:
- ๊ฐ์ธ์ ์ธ ์ฐ๊ตฌ๋ ํ์ต ๋ชฉ์ ์ผ๋ก ์ฌ์ฉํ๋ ๊ฒฝ์ฐ
- ๊ณต๊ฐ๋ ์ฌ์ค ์ ๋ณด(๊ฐ๊ฒฉ, ๋ ์ง ๋ฑ)๋ง ์์งํ๋ ๊ฒฝ์ฐ
- ๋ฐ์ดํฐ๋ฅผ ์ง๊ณํ์ฌ ํต๊ณ์ ์ผ๋ก๋ง ์ฌ์ฉํ๋ ๊ฒฝ์ฐ
ํ์ง๋ง ์ด๋ฐ ์ผ๋ฐ์ ์ธ ๊ฐ์ด๋๋ผ์ธ์ด ๋ชจ๋ ์ํฉ์ ์ ์ฉ๋๋ ๊ฒ์ ์๋๋, ์ค์ํ ํ๋ก์ ํธ๋ผ๋ฉด ๋ฒ๋ฅ ์ ๋ฌธ๊ฐ์ ์กฐ์ธ์ ๊ตฌํ๋ ๊ฒ์ด ์ข์. โ๏ธ
๊ฐ์ธ์ ๋ณด ๋ณดํธ ๋ฐ GDPR ์ค์ ๐
์น ํฌ๋กค๋ง์ ํตํด ๊ฐ์ธ์ ๋ณด๋ฅผ ์์งํ๋ ๊ฒฝ์ฐ, ๊ฐ์ธ์ ๋ณด ๋ณดํธ๋ฒ๊ณผ ๊ฐ์ ๊ด๋ จ ๋ฒ๊ท๋ฅผ ์ค์ํด์ผ ํด. ํนํ ์ ๋ฝ์์๋ GDPR(General Data Protection Regulation)์ด ์๊ฒฉํ๊ฒ ์ ์ฉ๋ผ.
๊ฐ์ธ์ ๋ณด ์์ง ์ ์ฃผ์ํด์ผ ํ ์ :
- ์ด๋ฆ, ์ด๋ฉ์ผ, ์ ํ๋ฒํธ, ์ฃผ์ ๋ฑ ๊ฐ์ธ ์๋ณ ์ ๋ณด ์์ง ์์
- ์์งํ ๊ฐ์ธ์ ๋ณด์ ์์ ํ ์ ์ฅ ๋ฐ ๊ด๋ฆฌ
- ํ์ ์ด์์ ๊ฐ์ธ์ ๋ณด ์์ง ๊ธ์ง
- ๊ฐ์ธ์ ๋ณด ์์ง ๋ชฉ์ ๊ณผ ๋ฐฉ๋ฒ์ ๋ํ ํฌ๋ช ํ ๊ณต๊ฐ
๊ฐ์ธ์ ๋ณด๊ฐ ํฌํจ๋ ๋ฐ์ดํฐ๋ฅผ ํฌ๋กค๋งํ ๋๋ ์ต๋ช ํํ๊ฑฐ๋ ๋น์๋ณํํ๋ ๊ฒ์ด ์ค์ํด. ๋ํ ์์งํ ๋ฐ์ดํฐ์ ๋ณด์์๋ ์ ๊ฒฝ ์จ์ผ ํด! ๐
์๋ฒ ๋ถํ ๋ฐ DoS ๊ณต๊ฒฉ ๋ฐฉ์ง ๐ฆ
๋๋ฌด ๋น ๋ฅธ ์๋๋ก ํฌ๋กค๋ง์ ํ๋ฉด ์น์ฌ์ดํธ ์๋ฒ์ ๊ณผ๋ํ ๋ถํ๋ฅผ ์ค ์ ์์ด. ์ด๋ ์๋์น ์๊ฒ ์๋น์ค ๊ฑฐ๋ถ(DoS) ๊ณต๊ฒฉ๊ณผ ์ ์ฌํ ํจ๊ณผ๋ฅผ ๋ผ ์ ์์ง.
์๋ฒ ๋ถํ๋ฅผ ์ค์ด๊ธฐ ์ํ ๋ฐฉ๋ฒ:
- ์์ฒญ ์ฌ์ด์ ์ ์ ํ ์ง์ฐ(delay) ์ถ๊ฐ
- ๋์ ์์ฒญ ์ ์ ํ
- ํผํฌ ์๊ฐ๋ ํฌ๋กค๋ง ์์
- ํ์ํ ํ์ด์ง๋ง ํฌ๋กค๋ง
- ์บ์ฑ์ ํ์ฉํ์ฌ ์ค๋ณต ์์ฒญ ๋ฐฉ์ง
import requests
import time
from random import uniform
urls = [
'https://example.com/page1',
'https://example.com/page2',
'https://example.com/page3',
# ...
]
for url in urls:
try:
response = requests.get(url, headers={'User-Agent': 'MyBot/1.0'})
print(f"์ํ ์ฝ๋: {response.status_code}, URL: {url}")
# ๋๋ค ์ง์ฐ (2-5์ด)
delay = uniform(2, 5)
print(f"{delay:.2f}์ด ๋๊ธฐ ์ค...")
time.sleep(delay)
except Exception as e:
print(f"์ค๋ฅ ๋ฐ์: {e}")
์์ ๋ฐ๋ฅธ ํฌ๋กค๋ง์ ์น์ฌ์ดํธ ์ด์์์์ ๊ด๊ณ๋ฅผ ์ข๊ฒ ์ ์งํ๊ณ , ๋ฒ์ ๋ฌธ์ ๋ฅผ ์๋ฐฉํ๋ ๋ฐ ๋์์ด ๋ผ. ํญ์ "์ข์ ์ด์"์ด ๋๋ ๊ฒ์ ๋ชฉํ๋ก ํ์! ๐ก
์์ ์ ์ด์ฉ๊ณผ ๊ฒฝ์ ๊ด๊ณ ๐ผ
์น์ฌ์ดํธ์ ๋ฐ์ดํฐ๋ฅผ ํฌ๋กค๋งํด์ ์์ ์ ์ผ๋ก ์ด์ฉํ๊ฑฐ๋, ์ ์ฌ์ดํธ์ ๊ฒฝ์ ๊ด๊ณ์ ์๋ ์๋น์ค๋ฅผ ๋ง๋๋ ๊ฒ์ ๋ฒ์ ๋ฌธ์ ๋ฅผ ์ผ์ผํฌ ๊ฐ๋ฅ์ฑ์ด ๋์.
ํนํ ๋ค์๊ณผ ๊ฐ์ ๊ฒฝ์ฐ์ ์ฃผ์ํด์ผ ํด:
- ๊ฒฝ์ ์ฌ์ดํธ์ ๊ฐ๊ฒฉ ์ ๋ณด๋ฅผ ์ค์๊ฐ์ผ๋ก ํฌ๋กค๋งํ์ฌ ์๋ ๊ฐ๊ฒฉ ์กฐ์
- ๋ค๋ฅธ ์ฌ์ดํธ์ ์ฝํ ์ธ ๋ฅผ ์์งํ์ฌ ์์ฒด ์ฌ์ดํธ์ ๊ฒ์
- ํฌ๋กค๋งํ ๋ฐ์ดํฐ๋ฅผ ํ๋งคํ๊ฑฐ๋ ์์ ์ ์ผ๋ก ํ์ฉ
์์ ์ ๋ชฉ์ ์ผ๋ก ํฌ๋กค๋ง์ ํ๋ ค๋ฉด, ํด๋น ์น์ฌ์ดํธ์ API๋ฅผ ์ฌ์ฉํ๊ฑฐ๋ ๋ฐ์ดํฐ ์ฌ์ฉ ๋ผ์ด์ผ์ค๋ฅผ ๊ตฌ๋งคํ๋ ๊ฒ์ด ์์ ํด. ๋๋ ์น์ฌ์ดํธ ์ด์์์๊ฒ ์ง์ ํ๊ฐ๋ฅผ ๊ตฌํ๋ ๊ฒ๋ ์ข์ ๋ฐฉ๋ฒ์ด์ผ. ๐
๋ฒ์ ์ฌ๋ก ๋ฐ ํ๋ก โ๏ธ
์น ํฌ๋กค๋ง๊ณผ ๊ด๋ จ๋ ๋ช ๊ฐ์ง ์ค์ํ ๋ฒ์ ์ฌ๋ก๋ฅผ ์์๋ณด์:
hiQ Labs v. LinkedIn (2019)
hiQ Labs๋ LinkedIn์ ๊ณต๊ฐ ํ๋กํ ๋ฐ์ดํฐ๋ฅผ ํฌ๋กค๋งํ์ฌ ์ธ์ฌ ๋ถ์ ์๋น์ค๋ฅผ ์ ๊ณตํ์ด. LinkedIn์ ์ด๋ฅผ ์ค๋จํ๋ผ๊ณ ์๊ตฌํ์ง๋ง, ๋ฒ์์ ๊ณต๊ฐ์ ์ผ๋ก ์ ๊ทผ ๊ฐ๋ฅํ ๋ฐ์ดํฐ๋ ํฌ๋กค๋งํ ์ ์๋ค๊ณ ํ๊ฒฐํ์ด.
Facebook v. Power Ventures (2016)
Power Ventures๋ ์ฌ์ฉ์์ ๋์๋ฅผ ๋ฐ์ Facebook ๋ฐ์ดํฐ๋ฅผ ํฌ๋กค๋งํ์ง๋ง, Facebook์ ์ด์ฉ์ฝ๊ด์ ์๋ฐํ๋ค๋ ์ด์ ๋ก ํจ์ํ์ด. ์ด ์ฌ๋ก๋ ์ด์ฉ์ฝ๊ด์ ์ค์์ฑ์ ๋ณด์ฌ์ค.
QVC v. Resultly (2015)
Resultly์ ํฌ๋กค๋ฌ๊ฐ QVC ์น์ฌ์ดํธ์ ๊ณผ๋ํ ๋ถํ๋ฅผ ์ฃผ์ด ์๋น์ค ์ค๋จ์ ์ผ์ผ์ผฐ์ด. ๋ฒ์์ ์ ์์ ์๋๊ฐ ์์๋ค๋ฉด ๋ฒ์ ์ฑ ์์ด ์ ํ๋ ์ ์๋ค๊ณ ํ๋จํ์ด.
์ด๋ฌํ ์ฌ๋ก๋ค์ ์น ํฌ๋กค๋ง์ ๋ฒ์ ๊ฒฝ๊ณ๊ฐ ๋ณต์กํ๊ณ ๊ณ์ ๋ณํํ๊ณ ์์์ ๋ณด์ฌ์ค. ํญ์ ์ต์ ๋ฒ์ ๋ํฅ์ ํ์ ํ๊ณ , ํ์ํ๋ค๋ฉด ๋ฒ๋ฅ ์ ๋ฌธ๊ฐ์ ์กฐ์ธ์ ๊ตฌํ๋ ๊ฒ์ด ์ค์ํด! ๐
ํฌ๋กค๋ง ์ค๋ฆฌ ๊ฐ์ด๋๋ผ์ธ ๐งญ
๋ฒ์ ๋ฌธ์ ๋ฅผ ์๋ฐฉํ๊ณ ์ค๋ฆฌ์ ์ธ ํฌ๋กค๋ง์ ์ํ ๊ฐ์ด๋๋ผ์ธ์ ์ ๋ฆฌํด๋ณผ๊ฒ:
- robots.txt ์ค์: ํญ์ robots.txt ํ์ผ์ ํ์ธํ๊ณ ๊ทธ ์ง์นจ์ ๋ฐ๋ฅด์
- ์ ์ ๊ณต๊ฐ: User-Agent ํค๋์ ํฌ๋กค๋ฌ ์ ๋ณด์ ์ฐ๋ฝ์ฒ๋ฅผ ํฌํจํ์
- ์๋ ์ ํ: ์์ฒญ ์ฌ์ด์ ์ ์ ํ ์ง์ฐ์ ๋์ด ์๋ฒ ๋ถํ๋ฅผ ์ต์ํํ์
- ํ์ํ ๋ฐ์ดํฐ๋ง ์์ง: ๋ชฉ์ ์ ํ์ํ ์ต์ํ์ ๋ฐ์ดํฐ๋ง ์์งํ์
- ์บ์ฑ ํ์ฉ: ๊ฐ์ ํ์ด์ง๋ฅผ ๋ฐ๋ณตํด์ ์์ฒญํ์ง ๋ง๊ณ ์บ์ฑ์ ํ์ฉํ์
- ์ค๋ฅ ์ฒ๋ฆฌ: 429(Too Many Requests) ๊ฐ์ ์ค๋ฅ๋ฅผ ๊ฐ์งํ๊ณ ์ ์ ํ ๋์ํ์
- ๊ฐ์ธ์ ๋ณด ๋ณดํธ: ๊ฐ์ธ ์๋ณ ์ ๋ณด๋ ์์งํ์ง ๋ง๊ฑฐ๋ ์ต๋ช ํํ์
- ์ถ์ฒ ํ์: ๋ฐ์ดํฐ๋ฅผ ์ฌ์ฉํ ๋ ์ถ์ฒ๋ฅผ ๋ช ํํ ํ์ํ์
- API ์ฐ์ ์ฌ์ฉ: ๊ฐ๋ฅํ๋ฉด ํฌ๋กค๋ง๋ณด๋ค ๊ณต์ 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 ๊ฐ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ๋ฉด ํฌ๋กค๋งํ ๋ฐ์ดํฐ๋ฅผ ์๊ฐ์ ์ผ๋ก ํํํ ์ ์์ด. ์ด๋ฅผ ํตํด ๋ฐ์ดํฐ์ ํจํด, ์ถ์ธ, ์ด์์น ๋ฑ์ ์ฝ๊ฒ ํ์ ํ ์ ์์ง. ๋ฐ์ดํฐ ์๊ฐํ๋ ๋ณต์กํ ์ ๋ณด๋ฅผ ์ง๊ด์ ์ผ๋ก ์ดํดํ๋ ๋ฐ ํฐ ๋์์ด ๋ผ! ๐
๋ฐ์ดํฐ ์ ์ฅ ์ ๋ต ๋ฐ ํ ๐ก
ํฌ๋กค๋ง ๋ฐ์ดํฐ๋ฅผ ํจ๊ณผ์ ์ผ๋ก ์ ์ฅํ๊ณ ๊ด๋ฆฌํ๊ธฐ ์ํ ๋ช ๊ฐ์ง ํ์ ์๊ฐํ ๊ฒ:
- ์ฆ๋ถ ํฌ๋กค๋ง ๋ฐ ์ ์ฅ: ์ด๋ฏธ ์์งํ ๋ฐ์ดํฐ๋ฅผ ๋ค์ ์์งํ์ง ์๋๋ก ์ฆ๋ถ ํฌ๋กค๋ง์ ๊ตฌํํ๊ณ , ์๋ก์ด ๋ฐ์ดํฐ๋ง ์ถ๊ฐํ๋ ๋ฐฉ์์ผ๋ก ์ ์ฅํด.
- ๋ฐ์ดํฐ ์ ๊ทํ: ์ค๋ณต ๋ฐ์ดํฐ๋ฅผ ์ ๊ฑฐํ๊ณ ์ผ๊ด๋ ํ์์ผ๋ก ์ ์ฅํด. ํนํ ๊ด๊ณํ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ์ฌ์ฉํ ๋๋ ์ ๊ทํ๊ฐ ์ค์ํด.
- ๋ฉํ๋ฐ์ดํฐ ํฌํจ: ๋ฐ์ดํฐ ์์ง ์๊ฐ, ์ถ์ฒ URL, ํฌ๋กค๋ฌ ๋ฒ์ ๋ฑ์ ๋ฉํ๋ฐ์ดํฐ๋ฅผ ํจ๊ป ์ ์ฅํด. ์ด๋ ๋์ค์ ๋ฐ์ดํฐ์ ์ ๋ขฐ์ฑ๊ณผ ์ถ์ ์ฑ์ ํ๋ณดํ๋ ๋ฐ ๋์์ด ๋ผ.
- ๋ฐฑ์ ์ ๋ต: ์ค์ํ ๋ฐ์ดํฐ๋ ์ ๊ธฐ์ ์ผ๋ก ๋ฐฑ์ ํ๊ณ , ๊ฐ๋ฅํ๋ฉด ๋ค๋ฅธ ์์น์ ๋ณต์ฌ๋ณธ์ ์ ์ฅํด.
- ๋ฐ์ดํฐ ์์ถ: ๋์ฉ๋ ๋ฐ์ดํฐ์ ๊ฒฝ์ฐ ์์ถ ๊ธฐ์ ์ ์ฌ์ฉํด ์ ์ฅ ๊ณต๊ฐ์ ์ ์ฝํด.
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. ์ค์ ํ๋ก์ ํธ: ๋๋ง์ ํฌ๋กค๋ฌ ๋ง๋ค๊ธฐ ๐
์ง๊ธ๊น์ง ๋ฐฐ์ด ๋ด์ฉ์ ๋ฐํ์ผ๋ก ์ค์ ํฌ๋กค๋ง ํ๋ก์ ํธ๋ฅผ ์งํํด๋ณด์! ์ด ์น์ ์์๋ ์ํ ๊ฐ๊ฒฉ ๋น๊ต ํฌ๋กค๋ฌ๋ฅผ ๋ง๋๋ ๊ณผ์ ์ ๋จ๊ณ๋ณ๋ก ์์๋ณผ ๊ฑฐ์ผ. ์ด ํ๋ก์ ํธ๋ฅผ ํตํด ํฌ๋กค๋ง์ ์ ์ฒด ๊ณผ์ ์ ๊ฒฝํํ๊ณ , ์ค์ ๋ก ์ ์ฉํ ๋๊ตฌ๋ฅผ ๋ง๋ค ์ ์์ ๊ฑฐ์ผ! ๐ ๏ธ
ํ๋ก์ ํธ ๊ฐ์ ๐
์ฐ๋ฆฌ๊ฐ ๋ง๋ค ํฌ๋กค๋ฌ๋ ๋ค์๊ณผ ๊ฐ์ ๊ธฐ๋ฅ์ ๊ฐ์ง ๊ฑฐ์ผ:
- ์ฌ๋ฌ ์ผํ๋ชฐ์์ ํน์ ์ ํ์ ๊ฐ๊ฒฉ ์ ๋ณด๋ฅผ ์์ง
- ์์งํ ๊ฐ๊ฒฉ์ ๋น๊ตํ์ฌ ์ต์ ๊ฐ ์ฐพ๊ธฐ
- ๊ฐ๊ฒฉ ๋ณ๋ ์ถ์ ๋ฐ ์๋ฆผ ๊ธฐ๋ฅ
- ๊ฒฐ๊ณผ๋ฅผ CSV ํ์ผ๊ณผ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์ฅ
- ๊ฐ๋จํ ์น ๋์๋ณด๋๋ก ๊ฒฐ๊ณผ ์๊ฐํ
์ด ํ๋ก์ ํธ๋ ์ค์ ์ฌ์ฉ ๊ฐ๋ฅํ ๋๊ตฌ์ด๋ฉด์, ์ง๊ธ๊น์ง ๋ฐฐ์ด ๋ค์ํ ๊ธฐ์ ์ ํ์ฉํ ์ ์๋ ์ข์ ์์ ์ผ. ๊ทธ๋ผ ์์ํด๋ณผ๊น? ๐
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)
}
์ด ๊ธฐ๋ณธ ํด๋์ค๋ ๋ค์๊ณผ ๊ฐ์ ๊ธฐ๋ฅ์ ์ ๊ณตํด:
- ์น ํ์ด์ง ์์ฒญ ๋ฐ HTML ํ์ฑ
- ๋ก๊น ์ค์
- ๊ฐ๊ฒฉ ์ ๋ณด ์ ์
- ๊ฒฐ๊ณผ ํฌ๋งทํ
์ถ์ ๋ฉ์๋(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
์ด ๋ฐ์ดํฐ๋ฒ ์ด์ค ๊ด๋ฆฌ ํด๋์ค๋ ๋ค์๊ณผ ๊ฐ์ ๊ธฐ๋ฅ์ ์ ๊ณตํด:
- ์ ํ, ๊ฐ๊ฒฉ, ์๋ฆผ ์ ๋ณด๋ฅผ ์ ์ฅํ๋ ํ ์ด๋ธ ์์ฑ
- ์ ์ ํ ๋ฐ ๊ฐ๊ฒฉ ์ ๋ณด ์ถ๊ฐ
- ์ ํ์ ๊ฐ๊ฒฉ ์ ๋ณด ๋ฐ ๊ฐ๊ฒฉ ๋ณ๋ ์ด๋ ฅ ์กฐํ
- ์ ํ์ ํ์ฌ ์ต์ ๊ฐ ์กฐํ
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']}")
์ด ์๋ฆผ ํด๋์ค๋ ๋ค์๊ณผ ๊ฐ์ ๊ธฐ๋ฅ์ ์ ๊ณตํด:
- ์ด๋ฉ์ผ์ ํตํ ๊ฐ๊ฒฉ ์๋ฆผ ์ ์ก
- ์๋ฆผ ์กฐ๊ฑด ํ์ธ (๋ชฉํ ๊ฐ๊ฒฉ ์ดํ๋ก ๋จ์ด์ก๋์ง)
- ์๋ฆผ ์ ์ก ํ ์๋ฆผ ์ํ ์ ๋ฐ์ดํธ
์ด๋ฉ์ผ ์ธ์๋ 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()
์ด ๋ฉ์ธ ํ์ผ์ ๋ค์๊ณผ ๊ฐ์ ๊ธฐ๋ฅ์ ์ ๊ณตํด:
- ๋ช ๋ นํ ์ธ์ ํ์ฑ (๊ฒ์ ํค์๋, ์ถ๋ ฅ ๋๋ ํ ๋ฆฌ, ์๋ฆผ ์ค์ ๋ฑ)
- ์ฌ๋ฌ ์ผํ๋ชฐ ์คํฌ๋ํผ ์คํ ๋ฐ ๊ฒฐ๊ณผ ์์ง
- ๊ฒฐ๊ณผ๋ฅผ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ CSV ํ์ผ์ ์ ์ฅ
- ์ต์ ๊ฐ ์ ๋ณด ์ถ๋ ฅ
- ๊ฐ๊ฒฉ ์๋ฆผ ์ค์ ๋ฐ ํ์ธ
์ด ํ๋ก๊ทธ๋จ์ ๋ค์๊ณผ ๊ฐ์ด ์คํํ ์ ์์ด:
# ๊ธฐ๋ณธ ๊ฒ์
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๋ฅผ ์ฌ์ฉํด ๊ตฌํํ์ด. ๋ค์๊ณผ ๊ฐ์ ๊ธฐ๋ฅ์ ์ ๊ณตํด:
- ์์ง๋ ์ ํ ๋ชฉ๋ก ํ์
- ์ ํ๋ณ ์์ธ ์ ๋ณด ๋ฐ ๊ฐ๊ฒฉ ๋น๊ต
- ๊ฐ๊ฒฉ ๋ณ๋ ์ถ์ด ์ฐจํธ
- ์ ๊ฒ์ ์์ฒญ API
- ๊ฐ๊ฒฉ ์๋ฆผ ์ค์ API
์ด ์น ๋์๋ณด๋๋ฅผ ํตํด ์์งํ ๋ฐ์ดํฐ๋ฅผ ์๊ฐ์ ์ผ๋ก ํ์ธํ๊ณ ๋ถ์ํ ์ ์์ด. ๐ฅ๏ธ
ํ๋ก์ ํธ ํ์ฅ ์์ด๋์ด ๐ก
์ด ํ๋ก์ ํธ๋ฅผ ๋ ๋ฐ์ ์ํฌ ์ ์๋ ๋ช ๊ฐ์ง ์์ด๋์ด๋ฅผ ์๊ฐํ ๊ฒ:
- ๋ ๋ง์ ์ผํ๋ชฐ ์ถ๊ฐ: ๋ค์ํ ์ผํ๋ชฐ์ ์ถ๊ฐํ์ฌ ๋ ๋์ ๋ฒ์์์ ๊ฐ๊ฒฉ์ ๋น๊ต
- ์ ํ ๋ฆฌ๋ทฐ ์์ง: ๊ฐ๊ฒฉ๋ฟ๋ง ์๋๋ผ ๋ฆฌ๋ทฐ๋ ์์งํ์ฌ ์ ํ ํ๊ฐ ์ ๋ณด ์ ๊ณต
- ์๋ ์คํ ์ค์ผ์ค๋ง: cron์ด๋ Celery๋ฅผ ์ฌ์ฉํด ์ ๊ธฐ์ ์ผ๋ก ํฌ๋กค๋ง ์คํ
- ๋จธ์ ๋ฌ๋ ํตํฉ: ๊ฐ๊ฒฉ ์์ธก ๋ชจ๋ธ์ ๊ฐ๋ฐํ์ฌ ํฅํ ๊ฐ๊ฒฉ ๋ณ๋ ์์ธก
- ๋ชจ๋ฐ์ผ ์ฑ ๊ฐ๋ฐ: ๋ชจ๋ฐ์ผ์์๋ ์ฝ๊ฒ ์ฌ์ฉํ ์ ์๋ ์ฑ ๊ฐ๋ฐ
- ์์ ๊ธฐ๋ฅ ์ถ๊ฐ: ์ฌ์ฉ์๋ค์ด ์ข์ ๊ฐ๊ฒฉ ์ ๋ณด๋ฅผ ๊ณต์ ํ ์ ์๋ ์ปค๋ฎค๋ํฐ ๊ธฐ๋ฅ
์ด ํ๋ก์ ํธ๋ ์น ํฌ๋กค๋ง์ ์ค์ ํ์ฉ ์ฌ๋ก๋ฅผ ๋ณด์ฌ์ฃผ๋ ์ข์ ์์์ผ. ๊ธฐ๋ณธ ๊ธฐ๋ฅ์ ๊ตฌํํ ํ, ํ์์ ๋ฐ๋ผ ๊ธฐ๋ฅ์ ํ์ฅํด ๋๊ฐ ์ ์์ด! ๐
๐ก ์ค์ ํ: ์ค์ ํ๋ก์ ํธ๋ฅผ ์งํํ ๋๋ ์์ ๋จ์๋ก ๋๋์ด ์ ์ง์ ์ผ๋ก ๊ฐ๋ฐํ๋ ๊ฒ์ด ์ข์. ๋จผ์ ํ ์ผํ๋ชฐ์ ๋ํ ๊ธฐ๋ณธ ํฌ๋กค๋ง ๊ธฐ๋ฅ์ ์์ฑํ๊ณ , ๊ทธ ๋ค์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ ์ฅ, ๋ค๋ฅธ ์ผํ๋ชฐ ์ถ๊ฐ, ์น ์ธํฐํ์ด์ค ๋ฑ์ ์์๋ก ๊ฐ๋ฐํ๋ฉด ๋ ํจ์จ์ ์ด์ผ. ๋ํ ๊ฐ ๋จ๊ณ๋ง๋ค ์ถฉ๋ถํ ํ ์คํธ๋ฅผ ์งํํ๋ ๊ฒ์ด ์ค์ํด!
๋ง์น๋ฉฐ: ์น ํฌ๋กค๋ง์ ๋ฌดํํ ๊ฐ๋ฅ์ฑ ๐
์ง๊ธ๊น์ง ์น ํฌ๋กค๋ง๊ณผ ์คํฌ๋ํ์ ๊ธฐ๋ณธ ๊ฐ๋ ๋ถํฐ ์ค์ ํ๋ก์ ํธ๊น์ง ๋ค์ํ ๋ด์ฉ์ ์ดํด๋ดค์ด. ์น ํฌ๋กค๋ง์ ๋จ์ํ ๋ฐ์ดํฐ ์์ง ๊ธฐ์ ์ ๋์ด, ๋์งํธ ์ธ๊ณ์ ์ ๋ณด๋ฅผ ํ์ฉํ๋ ๊ฐ๋ ฅํ ๋๊ตฌ๋ผ๋ ๊ฒ์ ์ ์ ์์์ง! ๐
์ฐ๋ฆฌ๊ฐ ๋ฐฐ์ด ๋ด์ฉ์ ์ ๋ฆฌํด๋ณด๋ฉด:
- ์น ํฌ๋กค๋ง๊ณผ ์คํฌ๋ํ์ ๊ธฐ๋ณธ ๊ฐ๋ ๊ณผ ์๋ฆฌ
- ํ์ด์ฌ์ ํ์ฉํ ํฌ๋กค๋ง ๋ฐฉ๋ฒ (requests, BeautifulSoup)
- ๋์ ์นํ์ด์ง ์ฒ๋ฆฌ ๋ฐฉ๋ฒ (Selenium)
- ๋๊ท๋ชจ ํฌ๋กค๋ง์ ์ํ ํ๋ ์์ํฌ (Scrapy)
- API๋ฅผ ํ์ฉํ ๋ฐ์ดํฐ ์์ง ๋ฐฉ๋ฒ
- ํฌ๋กค๋ง ์ ์ฃผ์ํด์ผ ํ ๋ฒ์ /์ค๋ฆฌ์ ๋ฌธ์
- ์์งํ ๋ฐ์ดํฐ์ ์ ์ฅ ๋ฐ ๊ด๋ฆฌ ๋ฐฉ๋ฒ
- ์ค์ ํ๋ก์ ํธ๋ฅผ ํตํ ์ข ํฉ์ ์ธ ์์ฉ
์ด ์ง์์ ๋ฐํ์ผ๋ก ๋ค์ํ ๋ถ์ผ์์ ์น ํฌ๋กค๋ง์ ํ์ฉํ ์ ์์ด:
- ๐ ์์ฅ ์กฐ์ฌ ๋ฐ ๊ฒฝ์ ๋ถ์: ๊ฒฝ์์ฌ ์ ํ ๊ฐ๊ฒฉ, ๋ฆฌ๋ทฐ, ํน์ง ๋ฑ์ ์์งํ์ฌ ์์ฅ ๋ํฅ ํ์
- ๐ ๋ฐ์ดํฐ ๊ธฐ๋ฐ ์์ฌ ๊ฒฐ์ : ๋๋์ ๋ฐ์ดํฐ๋ฅผ ์์งํ์ฌ ๋น์ฆ๋์ค ์ธ์ฌ์ดํธ ๋์ถ
- ๐ค ์๋ํ ๋ฐ ํจ์จํ: ๋ฐ๋ณต์ ์ธ ๋ฐ์ดํฐ ์์ง ์์ ์ ์๋ํํ์ฌ ์๊ฐ๊ณผ ๋น์ฉ ์ ์ฝ
- ๐ ํธ๋ ๋ ๋ถ์: ์์ ๋ฏธ๋์ด, ๋ด์ค ๋ฑ์์ ๋ฐ์ดํฐ๋ฅผ ์์งํ์ฌ ํธ๋ ๋ ๋ถ์
- ๐ง ๋จธ์ ๋ฌ๋ ํ์ต ๋ฐ์ดํฐ: AI ๋ชจ๋ธ ํ์ต์ ์ํ ๋๋์ ๋ฐ์ดํฐ ์์ง
์ฌ๋ฅ๋ท์์๋ ์น ํฌ๋กค๋ง ๊ด๋ จ ์๋น์ค๋ฅผ ์ ๊ณตํ๋ ์ ๋ฌธ๊ฐ๋ค์ ๋ง๋ ์ ์์ด. ์ง์ ํฌ๋กค๋ฌ๋ฅผ ๊ฐ๋ฐํ๊ธฐ ์ด๋ ต๋ค๋ฉด, ์ฌ๋ฅ๋ท์ ํตํด ์ ๋ฌธ๊ฐ์ ๋์์ ๋ฐ๋ ๊ฒ๋ ์ข์ ๋ฐฉ๋ฒ์ด์ผ! ๐ค
์น ํฌ๋กค๋ง์ ๊ณ์ํด์ ๋ฐ์ ํ๊ณ ์๋ ๋ถ์ผ์ผ. ์น ๊ธฐ์ ์ด ๋ณํํจ์ ๋ฐ๋ผ ํฌ๋กค๋ง ๊ธฐ์ ๋ ํจ๊ป ์งํํ๊ณ ์์ง. ํญ์ ์ต์ ํธ๋ ๋์ ๊ธฐ์ ์ ํ์ตํ๊ณ , ๋ฒ์ /์ค๋ฆฌ์ ๊ฐ์ด๋๋ผ์ธ์ ์ค์ํ๋ฉด์ ํฌ๋กค๋ง์ ํ์ฉํ๋ ๊ฒ์ด ์ค์ํด. ๐ฑ
์ด ๊ธ์ด ์น ํฌ๋กค๋ง๊ณผ ์คํฌ๋ํ์ ์ธ๊ณ๋ก ์ฒซ ๋ฐ์ ๋ด๋๋ ๋ฐ ๋์์ด ๋์๊ธธ ๋ฐ๋ผ! ๋ ๋ง์ ์ค์ต๊ณผ ๊ฒฝํ์ ํตํด ํฌ๋กค๋ง ๊ธฐ์ ์ ๋ง์คํฐํ๊ณ , ๋ฐ์ดํฐ์ ๋ฐ๋ค์์ ๊ฐ์ง ๋ณด๋ฌผ์ ์ฐพ์๋ด๊ธธ ์์ํด! ๐ฏ
์ง๋ฌธ์ด๋ ํผ๋๋ฐฑ์ด ์๋ค๋ฉด ์ธ์ ๋ ์ง ๋๊ธ๋ก ๋จ๊ฒจ์ค. ํจ๊ป ์ฑ์ฅํ๋ ์ฆ๊ฑฐ์์ ๋๋ ๋ณด์! ๐
๊ด๋ จ ํค์๋
๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.