์ด์ ๊ธ์์ ์ฑ์ฉ ์ ๋ณด์ ๋ชจ๋ธ(ORM
)์ ์ค๊ณํ๊ณ ,
๊ฐ๋จํ๊ฒ ๋ฑ๋ก & ์กฐํํ๋ API
๊น์ง ๋ง๋ค์ด ๋ณด์๋ค.
์ด์ ๊ธ : ํ์ด์ฌ FastAPI๋ฅผ ์ด์ฉํ API ํ๋ก์ ํธ ๋ง๋ค๊ธฐ (0) - ์๊ฐ ๋ฐ ์์
๋ค์์ผ๋ก, ์ฑ์ฉ ์ ๋ณด๋ฅผ ์๋์ผ๋ก ์คํฌ๋ฉํ๊ณ DB
์ ์ ์ฅํ๋ ์์ ๋ฅผ ์์ ๋ณด๋๋ก ํ์.
์ฑ์ฉ ์ ๋ณด์ HTML ๊ตฌ์กฐ - ์ํฐ๋
์ํฐ๋์์ ์ํ๋ ๊ฒ์์ด(๊ธฐ์ ์คํ?)๋ก ์กฐํํ ๊ฒฐ๊ณผ ํ์ด์ง์์ ์๋ ํฌ์ง์
ํญ์ ์ฑ์ฉ ์ ๋ณด๋ฅผ ์คํฌ๋ํ ํ ๊ฒ์ด๋ค.URL
ํจํด์ https://www.wanted.co.kr/search?query={keyword}&tab=position ์ด๋ ๊ฒ ๊ตฌ์ฑ์ด ๋์ด์๋๋ฐ
์ํ๋ ํค์๋๋ง URL
์ ์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ๋ก ์ ๋ฌํ๋ฉด ๋ ๊ฒ ๊ฐ๋ค.
๋ค์ ์ด๋ฏธ์ง๋ โflutterโ ํค์๋๋ก ๊ฒ์ํ ํ๋ฉด์ด๋ค. - https://www.wanted.co.kr/search?query=flutter&tab=position
์ด 47๊ฐ์ ์ฑ์ฉ ์ ๋ณด๊ฐ ์กด์ฌํ๋๋ฐ ์์ ํ๋ฉด์์๋ ํ์ด์ง์ด ๋ฐ๋ก ์กด์ฌํ์ง ์์ผ๋ฉฐ, ์คํฌ๋กค์ ๋ด๋ฆด ๋ ์ถ๊ฐ์ ์ธ ์ฑ์ฉ ์ ๋ณด๊ฐ ๋ก๋๋๋ค.
์ฑ์ฉ ์ ๋ณด๊ฐ ์ ์ฒด ๋ก๋๋๋ฉด ์คํฌ๋กค์ ๋ด๋ ค๋ ์๋ฌด๋ฐ ์ด๋ฒคํธ๊ฐ ์์ด์ง๋ค.
๊ทธ๋ฌ๋ฉด, ๊ฐ ๊ฐ์ ์ฑ์ฉ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ค๊ธฐ ์ํ HTML element
๊ตฌ์กฐ๋ฅผ ์์๋ณด์.
๋ธ๋ผ์ฐ์ ์ ๊ฐ๋ฐ์ ๋๊ตฌ๋ฅผ ์ฌ์ฉํ๋ฉด ๋ณด๋ค ์ฝ๊ฒ ๊ตฌ์กฐ๋ฅผ ์ฝ๊ฒ ํ์
ํ ์ ์๋ค.
๊ฐ๊ฐ์ ์ฑ์ฉ์ ๋ณด๋ class
๋ช
์ด JobCard_container__FqChn
์ธ div
ํ๊ทธ๋ก ์ด๋ฃจ์ด์ ธ์๋ค.
ํด๋น div
์์
ํฌ์ง์
์ class
๋ช
์ด JobCard_title__ddkwM
์ธ strong
ํ๊ทธ์ด๊ณ ,
ํ์ฌ์ด๋ฆ์ class
๋ช
์ด JobCard_companyName__vZMqJ
์ธ span
ํ๊ทธ์ด๋ค. (์๊ฐ๋ณด๋ค ์ฌํํ๋ค.. )
์คํฌ๋ํ ์ฝ๋ ์์ฑ
HTML
๊ตฌ์กฐ๋ ํ์
ํ์ผ๋ ์ค์ ๋ก ์ํ๋ ๋ฐ์ดํฐ๋ฅผ ์ถ์ถํ๋ ์คํฌ๋ํ ์ฝ๋๋ฅผ ํ์ด์ฌ์ผ๋ก ์์ฑํด๋ณด์
1. ํจํค์ง ์ค์น
# HTML source๋ฅผ ํ์ฑํ๋ ํจํค์ง ์ค์น
pip install beautifulsoup4
# ๋ธ๋ผ์ฐ์ ์๋ํ ํจํค์ง
pip install playwright
์ด์ ์ ์์ฑํ๋ ๊ธ ์ค ํ์ด์ฌ ์น ์คํฌ๋ํ์ ์ฌ์ฉํ ๋ธ๋ผ์ฐ์ ํจํค์ง๋ Selenium
์ด์์ง๋ง ์ด๋ฒ ์์ ์์๋ Playwright
ํจํค์ง๋ฅผ ์ฌ์ฉํ์๋ค.
์ฐพ์๋ณด๋, Playwright
๊ฐ Selenium
๋ณด๋ค ๋ง์ ๋ธ๋ผ์ฐ์ ๋ฅผ ์ง์ํ์ฌ ํฌ๋ก์ค ๋ธ๋ผ์ฐ์ ํ
์คํธ๋ฅผ ์ฝ๊ฒ ์ํํ ์ ์๋ค๋ ์ ๊ณผ ์ฑ๋ฅ๊ณผ ์์ ์ฑ๋ ๋ฐ์ด๋์ ์ด๋ฒ์ ์ฌ์ฉํ๊ฒ ๋์๋ค.
2. ์คํฌ๋ํ์ผ๋ก ์ถ์ถํ ๋ฐ์ดํฐ ํด๋์ค ์ ์
# employ_schema.py ์์
... (์๋ต) ...
# (์ถ๊ฐ) ์คํฌ๋ํ์ผ๋ก ์ถ์ถํ ๋ฐ์ดํฐ ํด๋์ค ์ ์
class EmployScrap:
def __init__(self, keyword, company_name, position):
self.keyword = keyword
self.company_name = company_name
self.position = position
3. ์ฑ์ฉ์ ๋ณด ์คํฌ๋ํ ์ ๊ท ์์ฑ
# employ_scrap.py ์์ฑ
from playwright.sync_api import sync_playwright
from bs4 import BeautifulSoup
from domain.employ.employ_schema import EmployScrap
import time
# ํน์ ํค์๋๋ก ์ฑ์ฉ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ EmployScrap ๊ฐ๋ค์ ๋ฆฌํดํ๋ ํจ์ ์ ์
def get_employ_by_wanted(keyword):
# Playwright ์์
p = sync_playwright().start()
# Chromium ๋ธ๋ผ์ฐ์ ์คํ
browser = p.chromium.launch(headless=False)
# ์๋ก์ด ํ์ด์ง ์ด๊ธฐ
page = browser.new_page()
# ํค์๋๋ฅผ ์ด์ฉํ์ฌ ์ฑ์ฉ ์ ๋ณด ๊ฒ์ ๊ฒฐ๊ณผ ํ์ด์ง - ํฌ์ง์
ํญ์ผ๋ก ์ด๋
page.goto(f"https://www.wanted.co.kr/search?query={keyword}&tab=position")
# ํ์ด์ง๊ฐ ๋ก๋๋ ๋๊น์ง ์คํฌ๋กค ๋ค์ด ๋ฐ๋ณต
for x in range(5):
time.sleep(3) # 3์ด ๋๊ธฐ
page.keyboard.down("End") # End ํค๋ฅผ ๋๋ฌ ํ์ด์ง์ ๋๊น์ง ์คํฌ๋กค ๋ค์ด
# ํ์ด์ง์ HTML ๋ด์ฉ ๊ฐ์ ธ์ค๊ธฐ
content = page.content()
# ๋ธ๋ผ์ฐ์ ๋ซ๊ธฐ
browser.close()
# Playwright ์ข
๋ฃ
p.stop()
# BeautifulSoup์ ์ฌ์ฉํ์ฌ HTML ๋ด์ฉ ํ์ฑ
soup = BeautifulSoup(content, "html.parser")
# ์ฑ์ฉ ์ ๋ณด๊ฐ ๋ด๊ธด ์์๋ค์ ์ฐพ์์ jobs ๋ฆฌ์คํธ์ ์ ์ฅ
jobs = soup.find_all("div", class_="JobCard_container__FqChn")
# ์ฑ์ฉ ์ ๋ณด๋ฅผ ์ ์ฅํ ๋ฆฌ์คํธ ์ด๊ธฐํ
jobs_db = []
for job in jobs:
# ์ฑ์ฉ ์ ๋ณด์์ ํฌ์ง์
๊ณผ ํ์ฌ ์ด๋ฆ ์ถ์ถ
position = job.find("strong", class_="JobCard_title__ddkwM").text
company_name = job.find("span", class_="JobCard_companyName__vZMqJ").text
# ์ถ์ถ๋ ์ ๋ณด๋ฅผ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์ฅํ ํ์์ผ๋ก ๋ณํํ์ฌ jobs_db ๋ฆฌ์คํธ์ ์ถ๊ฐ
jobs_db.append(EmployScrap(keyword, company_name, position))
# ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์ฅํ ์ฑ์ฉ ์ ๋ณด๊ฐ ๋ด๊ธด ๋ฆฌ์คํธ ๋ฐํ
return jobs_db
4. ์คํฌ๋ํ ๋ฐ์ดํฐ ์ฟผ๋ฆฌ ํจ์ ์ ์ ์ถ๊ฐ
# employ_crud.py ์์
from sqlalchemy import and_ # ์ถ๊ฐ
from domain.employ.employ_schema import EmployCreate, EmployScrap # ์ถ๊ฐ
... (์๋ต) ...
# ์ฑ์ฉ ์ ๋ณด ์ญ์ By ํค์๋, ํ๋ซํผ
def employ_delete(db: Session, keyword: str, platform: str):
db.query(Employ) \
.filter(and_(Employ.keyword == keyword, Employ.platform == platform)) \
.delete(synchronize_session=False)
db.commit()
# ์คํฌ๋ํ ์ฑ์ฉ ์ ๋ณด ๋ฑ๋ก
def employ_scrap_renew(db: Session, create_request: EmployScrap, platform: str):
db.add(Employ(platform=platform,
keyword=create_request.keyword,
company_name=create_request.company_name,
position=create_request.position,
create_date=datetime.now()))
db.commit()
5. ์ฑ์ฉ ์ ๋ณด ๊ฐฑ์ ๋ผ์ฐํฐ ์ถ๊ฐ
# employ_router.py ์์
from domain.employ import employ_schema, employ_crud, employ_scrap ์ถ๊ฐ
... (์๋ต) ...
# ์ฑ์ฉ ์ ๋ณด ์ํฐ๋ ๊ฐฑ์ API
@router.put("/scrap/wanted/{keyword}")
def employ_scrap_wanted_create(keyword: str, db: Session = Depends(get_db)):
platform = "WANTED"
# ์คํฌ๋ฉ ํ ์ํฐ๋์ ์ฑ์ฉ ์ ๋ณด
employs = employ_scrap.get_employ_by_wanted(keyword)
if employs:
# ๊ฐฑ์ ์ , ํค์๋์ ํ๋ซํผ์ผ๋ก ๊ธฐ์กด ์ฑ์ฉ ์ ๋ณด ์ญ์
employ_crud.employ_delete(db, keyword, platform)
for employ in employs:
# ์คํฌ๋ฉํ ์ฑ์ฉ ์ ๋ณด ์ ์ฅ
employ_crud.employ_scrap_renew(db, employ, platform)
์ฝ๋ ์์ฑ์ ์๋ฃ ํ์๊ณ , ์๋ ๋ช ๋ น์ด๋ฅผ ํฐ๋ฏธ๋์ ์ ๋ ฅํ์ฌ ์๋ฒ๋ฅผ ์คํํด๋ณด์.
uvicorn main:app --reload
http://127.0.0.1:8000/docs ์ ์ ์ํ๋ฉด ์ฑ์ฉ ์ ๋ณด ๊ฐฑ์ API
๊ฐ ์ถ๊ฐ ๋ ๊ฒ์ ํ์ธํ ์ ์๋ค.
๋ค์ ์ด๋ฏธ์ง ์ฒ๋ผ keyword
์ โnode
โ๋ผ๊ณ ์
๋ ฅ ํ Execute
๋ฒํผ์ ๋๋ฅด๋ฉด API
๊ฐ ํธ์ถ๋๋ฉฐ,
์คํฌ๋ํ ๊ณผ์ ์ ์๊ฐ์ ์ผ๋ก ํ์ธ ํ ์ ์๋ค. (๋ธ๋ผ์ฐ์ ์ฐฝ์ด ๋จ๋ฉด์ ํ์ด์ง ๋ก๋ ์ดํ, ์คํฌ๋กค ๋ค์ด ์ด๋ฒคํธ๊ฐ ๋ฐ์๋๋ค.)
์คํฌ๋ํ์ด ์๋ฃ๋๋ฉด ์ด์ ์ ๋ง๋ ์กฐํ API
๋ฅผ ํตํด keyword
๊ฐ node
์ธ ๋ฐ์ดํฐ๋ฅผ ํ์ธ ํ ์ ์๋ค.
๋ค์์, ์ค์ SQLite
์ ์ ์ฅํ DB
์ฟผ๋ฆฌ ์กฐํ ๊ฒฐ๊ณผ์ด๋ค. API
์ ์กฐํ๋ ๋ฐ์ดํฐ๋ ๊ฐ์ด ํ์ธ ๊ฐ๋ฅํ๋ค.
๋ง๋ฌด๋ฆฌ
์ด๋ ๊ฒ ๊ฒ์๊ธ 2๊ฐ๋ก ์งง๊ฒ๋๋ง!? FastAPI
์ ๋ํด์ ๊ณต๋ถํด๋ณด๊ณ ์ค์ ํ ์ด ํ๋ก์ ํธ๋ ์์ฑํด๋ณด์๋ค.
ํ๋ก์ ํธ๋ฅผ ๊ตฌํํ๋ฉด์ ์ ์ ํ๊ฒ ํจํค์ง๋ฅผ ์ ํ์ฉํ๋ฉด ์์ค๊ฐ ๋ ๊ฐ๊ฒฐํด์ง๊ณ ๊ฐ๋
์ฑ๋ ์ข์์ง๋ค.
์ฌ์ด๋ ํ๋ก์ ํธ๋ ํ ์ด ํ๋ก์ ํธ์์ ๊ฐ๋จํ API
๋ฐฑ์๋๋ฅผ ๊ตฌ์ฑํด์ผ๋๋ค๋ฉด FastAPI
๋ฅผ ์ค์ ๋ก ๋์
ํด๋ณด๊ณ ์ฌ์ฉํด๋ ๋์์ง ์์ ๊ฒ ๊ฐ๋ค๋ ์๊ฐ์ด ๋ค์๋ค.
๋ง์ง๋ง์ ์ญ์๋ ํ์ด์ฌ์ ์ฌ๋ฐ๋ ์งค๊ณผ ๊นํ๋ธ ์ฃผ์์ ํจ๊ป ๊ธ์ ๋ง๋ฌด๋ฆฌ ํด๋ณผ๊น ํ๋ค.
์ถ์ฒ : https://www.inflearn.com/pages/inflearnsnack-6-20220802
https://github.com/discphy/fastapi-scrap
[์ฐธ๊ณ ]
'๐ฑBackend' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
ํ์ด์ฌ FastAPI๋ฅผ ์ด์ฉํ API ํ๋ก์ ํธ ๋ง๋ค๊ธฐ (0) - ์๊ฐ ๋ฐ ์์ (0) | 2024.11.26 |
---|---|
ํ์ด์ฌ ์น ์คํฌ๋ํ - ๋ฉ๋ก ์์ ์ถ์ถ (2) | 2024.03.29 |
SNS ๋ก๊ทธ์ธ - Spring OAuth2 Client (0) | 2023.07.24 |
๋๋ค๋ฅผ ์ฌ์ฉํด์ผ ๋๋ ์ด์ (0) | 2023.06.05 |
์คํ๋ง API ๋น๋๊ธฐ ๋ ผ๋ธ๋กํน ๋ฐฉ์ ํธ์ถ (0) | 2023.03.27 |