본문 바로가기

명사 美 비격식 (무리 중에서) 아주 뛰어난[눈에 띄는] 사람[것]

SK 네트웍스 AI 캠프

SK 네트웍스 AI 캠프 - 1_프로그래밍 데이터 기초 - Day12_Web Crawling_웹크롤러 만들기

 

샘플프로젝트 auto_crawler

 

 

 

설치파일 확인

 

 

 

설치

cd auto_crawler_project
python -m venv venv
venv\Scripts\activate

pip install -r requirements.txt
playwright install chromium

 

 

 

streamlit run app.py 실행

https://standout.tistory.com/1718

 

설치하지않았거나, 설치한 module을 다시 확인해라: ModuleNotFoundError: No module named 'apscheduler'

설치파일 확인 설치cd auto_crawler_projectpython -m venv venvvenv\Scripts\activatepip install -r requirements.txtplaywright install chromium 정상적으로 설치한것같은데... 에러가 터졌다.설치한 모듈이 없다한다. tudy\sk_playd

standout.tistory.com

https://standout.tistory.com/1719

 

~라는 데이터베이스가 존재하지 않음: sqlalchemy.exc.OperationalError: (pymysql.err.OperationalError) (1049, "Unkno

앱 실행중 에러가 터졌다. 기존 studentdb를 사용하고있었는데 강사님이 진행중 문제가 생겨 mydb로 생성했고, 샘플 프로젝트를 주시고 실행하니 하필 studentdb가 잘 생성됬었던 나는 오히려 db connect

standout.tistory.com

 

 

 

 

steamlit 웹화면 확인

 

 

 

크롤링 실행 테스트

 

 

 

 

 

 

use db 선언

 

 

 

 

 

 

수령받은 sql 실행

crawler_db는 사용하지않고 studentdb로 그대로 진행하기로 수업에서 약속했으니 해당 코드는 주석처리했다. 

use studentdb;

-- drop DATABASE crawler_db;
-- CREATE DATABASE crawler_db
-- DEFAULT CHARACTER SET utf8mb4
-- COLLATE utf8mb4_unicode_ci;

-- USE crawler_db;

CREATE TABLE crawl_news (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    title VARCHAR(500) NOT NULL,
    link TEXT,
    source_url TEXT,
    crawled_at DATETIME NOT NULL
);

 

 

 

 

 

table 확인

 

 

 

 

 

입력된 데이터 확인

 

 

 

 

 

 

 

 

 

시각화 확인. 오늘 하루의데이터가 표현되어있다.

 

 

스케줄 기능 확인 및 스케줄 등록 및 삭제 테스트

 

 

 

 

 

 

 

cron 작업 등록 삭제 테스트

 

 

 

 

 

 

 

코드를 분석해보자.

가벼운 소스파일들로 구성되어있다.

 

 

 

 

동봉된 sql

 

 

 

 

 

 

 

 

먼저 모듈 install시 확인했던 readme와 requirements

 

 

 

 

 

 

readme에 명시되어있는

streamlit run app.py.

 

app.py부터 분석해보자.

streamli, Plotly를 import해 화면과 그래프를 그릴 수 있게했다. 

외 init_table와 scheduler_service에서 CrawlService, SchedulerManager를 import했다. 

set_page_config로 페이지의 title과 layout을 wide로 정하고

db의 init_table을 실행했다.

 

db.py를 확인해보자. os와 dotenv, sqlalchemy가 import되어있다. 

dotenv에 load_dotenv를 호출해 설정파일 .env를 읽도록했다. db.py는 get_engine와 init_table로 구성된다.

get_engine에서는 host, post, user, password, db_name을 load_dotenv하여 얻은 정보를 os로 get하여 각각 변수에 할당하고 db_url에 변수값을 대입하여 세팅해 create_engine를 호출했다. 이 create_engine은 db연결엔진을 만드는 함수인 sqlalchemy의 import한 create_engine를 사용해 db_url를 넘기며 엔진을 생성하고 있다. 이때 pool_pre_ping=True ping이 살아있는지 먼저 확인해 오류를 최소화했다.

init_table은 앞서 연결한 엔진을 get_engine가져와 create sql을 engine.begin() as conn해 문자열로 sql을 실행가능한 sql객체로 변환하는  sqlalchemy의 text에 담아 execute 실행시켰다.  필요한 table을 미리 세팅하고있다는 것. 이때 engine.begin() as conn:는 db트랜젝션으로 with 작업이 끝나면 연결을 자동으로 close하고 자동 commit을 실행하며 오류발생시 자동 rollback하는 트랜젝션을 지원한다.

즉 db.py는 엔진을 만들어 db커넥트하고, 필요한 table을 미리 set햇다고 보면 되겠다. 

 

다시 app.py로 돌아와보자. 이 init_table이 완료되었다.

if문을 활용해 st.session_state로 게속 매번 객체를 다시 실행하는 steamlit을 위해 처음 한번만 저장하는 객체 CrawlService를 지정했다. 이 CrawlService는 scheduler_service에 존재한다. 

 

scheduler_service를 살펴보자. scheduler_service은 pandas, apscheduler, sqlalchemy를 import하고있다.

앞서 살펴본 db.py의 get_engine 또한, crawler import했다.

pandas는 조회결과를 DataFrame으로 처리해주고, apscheduler에 BackgroundScheduler는 프로그램이 실행되는 동안 백그라운드에서 작업을 예약시킬 수 있다. sqlalchemy의 text는 이미 살펴봤으니 넘어가자.

crawler에 NaverNewsCrawler도 import했다. 

 

crawler를 먼저 살펴보자. crawler은 os, datetime, urllib, bs4, from dotenv import load_dotenv, playwright, models를 import했다. 

os로는 운영체제의 환경변수값을 읽을 수 있고 urllib의 urljoin은 상대url을 절대 url로 변환할 수 있다. dotenv의 load_dotenv는 앞써 살펴봤던 것처럼 .env 파일에 저장된 내용을 읽을 수 있다. playwright는 실제 브라우저를 실행해서 동적 웹페이지도 크롤링 할 수 있게 해주며 이를 동기방식으로 사용하기 위해 sync_playwright를 가져왔다.

datetime은 날짜와 시간 , BeautifulSoup은 html를 분석한다.

그리고 models에서 CrawlItem를 가져왔다. 

 

잠시 models로 이동해보자.

models에서는 dataclasses와 datetime를 가져왔다. datetime는 앞서서 살펴봤고 dataclasses은 클래스를 데이터 저장용으로 쉽게  만들게 해주는 기능으로 자동으로 클래스를 생성해주는 편의 기능이다.

models는 CrawlItem class안에 title, link, source_url, crawled_at와 이 자료형을 명시하여 dto와 같은 역할을 수행하고 있다. 

 

다시 crawler로 돌아오자. 

crawler는 먼저 load_dotenv해 .env파일을 읽었다. 

crawlersms NaverNewsCrawler class 하나로 구성되어있으며 생성자인 __init__함수와, crawl, _get_html, _parse로 구성되어있다. 

__init__에서는 os.getenv해 env파일에서 CRAWL_URL를 가져와 self.url에 할당했다. 

crawl은 이 url에서 self._get_html() html를 가져와 각 필요한 데이터를 parse 추출해 return한다. self._parse(html, limit)

_get_html은 앞서 살펴봤던 동기방식으로 사용하는, 브라우저를 실행해 동적 웹페이지도 크롤링 할 수 있게 해준다는 playwright.sync_api의 동기방식 sync_playwright를 실행해 crome  브라우저를 실행하고. p.chromium.launch browser 변수에 할당했다. 

새 browser.new_page 브라우저를 띄워 user_agent를 활용해 해당 브라우저에게 실제 사용자가 새창을 띄운듯한 설정을 주며 url를 열었다. 이때 네트워크 요청이 어느정도 끝날때까지 기다리는 대신 wait_until="networkidle" 최대로 timeout=60000 60초를 기다리라고 알리고 혹시모르니 렌더링 시간을 1.5초를 더 줬다. page.wait_for_timeout(1500)

url로 연 페이지에 전체 html를 가져오고 html에 할당한 뒤 page.content() 브라우저를 닫고 html을 리턴했다. 즉 url을 열어 html를 리턴한것.

함수 _parse은 이 리턴한 html를 활용한다. BeautifulSoup을 활용해 빠른 html 파서인 lxml을 활용해 변수 soup에 할당한다. 

for문을 돌려 a태그를 찾아 공백없는 text를 추출해 title에 a.get_text(strip=True), a.get("href")도 부여하고 이 둘중 하나가 없으면 건너뛰되 title길이도 너무 짧으면 뉴스제목이 아닐 가능성이 높음으로 건너뛰고, 중복을 허용하지않는 set()형인 seen에 있는 내용이라도 제외하며 조건에 부합한 내용을 seen에 추가한다. 

최종결과를 위해 만들어놓은 리스트형 result에 CrawlItem형으로 title,link,  source_url, crawled_at를 묶어  append 했다. 이떄 CrawlItem은 앞서 살펴봤던 dto와 같은 성질의 models.py이며 url은 앞서 상대 url을 절대 url로 변환해준다는 urllib.parse의 urljoin을 거쳐 url을 절대링크로 변환해 브라우저에 보이는 실제 주소창 url으로 저장한다. 또 crawled_at는 데이터를 따로 입력하지않고 현재의 시간을 구해 datetime.now() append할 수 있도록 한다. 

단 이 수집 갯수가  len(result) >= limit limit을 넘어간다면 중단하도록 했다.

이러한 과정을 거친 result를 _parse는 return 한다. 즉 수령받은 html에서 각 영역을 parse하여 CrawlItem형으로 맞춰  list에 담아 반환하는것.

 

다시 scheduler_service로 돌아와보자. scheduler_service는 class CrawlService와 SchedulerManager로 구성되어있다.

CrawlService은 __init__시 마찬가지로 get_engine mysql 연결 엔진을 생성해 crawler.py에 있는 NaverNewsCrawler객체를 만들어 self.crawler에 할당했다. NaverNewsCrawler는 앞서 살펴본 __init__, crawl, _get_html, _parse를 수행하는 py module이다. 

run_crawling시 매개변수 limit에 주의하며 self.crawler.crawl(limit=limit) limit수에 맞춰진 크롤링 결과를 가져와 items변수에 할당한다. 문자열 INSERT sql로 engine.begin() 트랜젝션을 시작하고 앞서 받았던 items를 for문으로 순회하며 text()를 사용해 문자열 sql insert을 실행하며 이때 각 변수에 실제 item안에 값들을 명시해 바인딩해 전달하도록 한다.

완료한 뒤 저장한 데이터 갯수 len(items)를 반환한다.

find_all는 비교적 간단하다. SELECT문 sql을 실행하여 pd.read_sql(sql, self.engine) 결과를 padas를 활용해 DataFrame으로 변환해 return한다. 저장된 값들이 table과 같은 값으로 return 되는것

find_count_by_time는 select sql을 마찬가지로 pd.read_sql(sql, self.engine) 실행하되, 문자열 sql에 '%%Y-%%m-%%d %%H:%%i와 같이 대입하여 실행시켰다. 이때 포맷문자로 해석되지않도록 %르 하나씩 더 붙였다. 즉 지정된 시간에 맞춰 가져오는것.

즉 CrawlService는 crawler.py 의 __init__, _get_html, _parse기능을 지원하는 crawlclass NaverNewsCrawler 크롤링을 실행하고, limit에 맞춰 값을 insert하고 모두 select하거나 지정된 datetime에 맞게 select하는 서비스인것. 크롤링하고 크롤링한것 가져오는 크롤러서비스.

다음 또다른 class SchedulerManager를 살펴보자. SchedulerManager는 __init__, start,  add_interval_job, add_cron_job, remove_job, get_jobs로 구성되어있다. 

__init__을 살펴보니 프로그램이 실행되는 동안 백그라운드에서 작업을 예약 실행한다는백그라운드 BackgroundScheduler를 생성하고 self.scheduler에 할당하고 앞선CrawlService 객체를 생성해 self.service에 할당했다.

start에서는 만약 self.scheduler.running가 not일경우, 즉 스케줄러가 실행중이 아닐경우에 scheduler.start() 스케줄러를 start하도록 했다. 즉 start는 background 서비스가 있는지 없는지 확인을 우선하고 start하는 함수라고 봐야겠다.

add_interval_job는 limit과 minutes 매개변수를 받아 기존에 등록된 작업이 있으면 먼저 삭제하고 시작한다.self.remove_job()

self.scheduler에 add_job 등록하는데, CrawlService에 INSERT문을 실행하고 len을 반환했던 run_crawling func를 할당하고, 

trigger 는 interbal로, minutes는 그대로 minutes, limit인자는 args, id는 auto_crawling_job라고 명명하고 

만일 같은 작업이있을경우 replace_existing true값을 줬다.  딱히 하는일이 없어보인다 background 객체에 각각 조건을 부여해서 .scheduler.add_job했을 뿐이다.

add_cron_job를 살펴보자. 마찬가지로 기존작업이 있다면 삭제하고 시작한다. remove_job()

scheduler.add_job 마찬가지고 job을 등록하되 trigger="cron"로 해 앞선 add_interval_job과 구별할 수 있도록했고, hour, minute을 받아 set, limit을 동일하게 args, id는 auto_crawling_job라는 이름으로 명명하고 마찬가지로 같은 작업이 있으면 replace_existing=True값을 주었다. 이또한 hour를 받고있다는 점,  trigger="cron",값이 다르다는 점 외 add_interval_job와 비슷하다.
remove_job은 이 self.scheduler.get_job("auto_crawling_job") auto_crawling_job라는 작업을 찾는다. 앞서서 살펴본 add_interval_job와 add_cron_job의  id 값이 모두 auto_crawling_job이였다. id="auto_crawling_job", 이 찾은 작업을 모두 if존재한다면 삭제한다.
get_jobs은 비교적 간단하다. 이 self객체에 있는 스케줄러 작업을 get_jobs해 반환한다.

 

다시 app.py로 돌아와보자. app.py에서 set_page_config로 화면의 title을 설정하고 엔진을 사용해 init_table해 db연결을 한뒤에 table이 없으면 테이블을 생성까지 시켜주며 st.session_state에 CrawlService를 할당해 앞서 살펴본 __init__, crawl, _get_html, _parse한 내용을 유지시켰다. 

다음 if문을 활용해 scheduler_manager또한 설정해 CrawlService와 SchedulerManager로 구성되어있는 SchedulerManager를 추가시켰다. 앞서 CrawlService객체를 한번 만들어 세션에 저장했던것처럼 새로고침할때마다 스케줄러가 여러개 실행되지않도록 scheduler_manager또한 start() 실행해 저장해놓는다. 앞서 이 스케줄 start함수는 background 서비스가 있는지 없는지 확인을 우선하고 start하는 함수라고 봤었다.

이 세션에 저장된 객체를 각각 st.session_state.crawl_service는 service에, st.session_state.scheduler_manager는 scheduler 변수에 담았다.

st.title를 예쁘게 출력하고, sidebar 영역을 만들었다. 이때 앞서 게속 확인했었던 변수 limit을 여기에 slider형태로 설정했다. limit = st.slider("수집 개수", 5, 100, 30)

크롤링을 실행할 수 있는 버튼을 만들고 button, 로딩UI를 위해 spinner를 추가했고 이 limit에 맞춰 돌아간 크롤링의 갯수를 count 변수에 저장해 성공메세지를 출력했다. st.success(f"{count}건 수집 완료")

subheader 서브헤더를 Interval 스케줄링로 예쁘게 출력하고

interval_minutes에도 마찬가지로 number_input inputbox를 활용해 값을 부여할 수 있게 세팅했다. 다만 이때 min_value는 1, max_value는 1440, 기본값은 10으로 설정해 button을 클릭하면 add_interval_job를 실행해 크롤링 작업을 등록할 수 있도록 하고 성공메세지를 출력한다.

구분선을 추가하고 서브헤더를 Cron 스케줄링로 추가하여 마찬가지로 nucron_hourmber_input를 활용해 값을 입력하게 하되 최소0 최대 23 기본값은 9로 설정했고 number_input로 최소 0, 최대 59, 기본값 0 로 cron_hour와 cron_minute를 받아 button을 누를경우 add_cron_job을 실행해 완료시 성공메세지를 출력하도록했다.

마지막으로 button("스케줄 작업 삭제")라는 버튼을 클릭하면 remove_job()을 실행하여 스케줄을삭제시키고 warning로 강조해 사용자에게 알렸다. 

sidebar가 완료됬다. 이제 body영역으로 눈을 돌려보자.

tab1, tab2, tab3 변수를 만들어 ["수집 데이터", "수집 통계", "스케줄 상태"]를 할당해 구성되했다.

tab1에서는 소제목을 MySQL 저장 데이터 출력하고 전체 데이터를 조회해 find_all df에 할당한다. 만일 df.empty:가 없을 경우 info() 사용자에게 메세지를 출력하고 else 있을 경우 dataframe을 활용해 테이블로 표현했다. 이때 width는 use_container_width=True 화면 너비에 맞게 조정했다.

tab2는 서브타이틀을 크롤링 수집량 시각화라 출력하고 find_count_by_time() 데이터를 시간별 건수를 조회해 stat_df에 할당했다. 이때 stat_df.empty 데이터가 없으면 사용자에게 info()메세지를 출력하고 else 있을 경우 px.bar 형태로 x축에는 crawl_time, y축에는 count, text에는 count로 설정하고 이설정값들을 fig 변수에 저장했다. text에 count란 막대 위에 수집 건수를 count수로 표시한다는 의미이다.

이 생성한 fig로 st.plotly_chart 그래프를 생성하되 use_container_width=True width를 맞춰 출력하도록 했다.

마지막 tab3에서는 소제목을 현재 등록된 스케줄 작업로 추가하고 등록한 jobs리스트들을 get_jobs 가져와 jobs 변수에 할당했다. 

jobs가 없으면 마찬가지로 사용자에게 info() 메세지로 알리고 있을 경우 jobs를 for문으로 순회하며 job.id와 job.next_run_time 다음 실행 시간과 구분선을 출력했다.

 

https://standout.tistory.com/778

 

데이터베이스 트랜잭션 Transaction, 롤백 rollback()

데이터베이스 Transaction 일관되고 믿을 수 있는 시스템, 모든 작업이 성공적으로 완료되거나 실패했을 때 원래의 상태로 롤백할 수 있다. https://ko.wikipedia.org/wiki/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4

standout.tistory.com

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

ping테스트

내 db,  네트워크가 잘 살아있는가

8.8.8.8은 구글이다.

ping 8.8.8.8

 

 

 

 

 

ipconfig

window에서 내컴퓨터 ip 확인하는 명령, IPv4 주소 (내 PC 주소), 게이트웨이 (공유기 주소), DNS가 나온다. 

 

 

 

샘플프로젝트 auto_crawler에서 다뤘던 스케줄모듈에 대해 이론적으로 이해해보자.

 

 

schedule 모듈 

파이썬에서 작업 예약을 하기 위한 모듈. 

명령어가 직관적이기에 간단한 스케줄을 적용하기에 적합한대신 복잡한 스케줄은 작성하기 힘듦

 

 

APScheduler advanced python scheduler의 약자

pip install apscheduler 명령어로 설치 가능

cron(job_function, month, day, hour.. 문자열방식),
date(arg로 매개변수를 전달하여 특정시간에 작업 수행),
interval(start_date, end_date) 방식의 수행방식을 지원하며 3가지의 스케줄러 종류가 있다.

 - BlockingScheduler 단일 작업 수행시 사용

 - BackgroundScheduler 다수 작업 수행시 사용

 - AsyncIOScheduler, GeventScheduler 각 프레임 워크 내 작업 수행시 사용