feat: 셀레니움 선택적 사용 및 폴백 메커니즘 추가
This commit is contained in:
@@ -95,6 +95,12 @@ class AIAgent:
|
|||||||
|
|
||||||
model_settings = self.config.get('model_settings', {})
|
model_settings = self.config.get('model_settings', {})
|
||||||
use_quantization = bool(model_settings.get('use_quantization', False))
|
use_quantization = bool(model_settings.get('use_quantization', False))
|
||||||
|
# 양자화 비트/오프로딩 옵션
|
||||||
|
try:
|
||||||
|
quant_bits = int(model_settings.get('quantization_bits', 8))
|
||||||
|
except Exception:
|
||||||
|
quant_bits = 8
|
||||||
|
cpu_offload = bool(model_settings.get('cpu_offload', False))
|
||||||
torch_dtype_cfg = str(model_settings.get('torch_dtype', 'auto')).lower()
|
torch_dtype_cfg = str(model_settings.get('torch_dtype', 'auto')).lower()
|
||||||
|
|
||||||
# dtype 파싱
|
# dtype 파싱
|
||||||
@@ -114,20 +120,7 @@ class AIAgent:
|
|||||||
if not model_source:
|
if not model_source:
|
||||||
raise RuntimeError("모델 경로/이름이 설정되지 않았습니다.")
|
raise RuntimeError("모델 경로/이름이 설정되지 않았습니다.")
|
||||||
|
|
||||||
# quantization 설정 (가능한 경우에만)
|
# (이전) quant_args 경로 제거: load_kwargs에서 직접 처리
|
||||||
quant_args = {}
|
|
||||||
if use_quantization:
|
|
||||||
try:
|
|
||||||
from transformers import BitsAndBytesConfig
|
|
||||||
quant_args["quantization_config"] = BitsAndBytesConfig(
|
|
||||||
load_in_8bit=True,
|
|
||||||
llm_int8_enable_fp32_cpu_offload=True
|
|
||||||
)
|
|
||||||
print("8bit 양자화 적용")
|
|
||||||
except Exception as _:
|
|
||||||
# transformers/bitsandbytes 호환 문제 시 양자화 비활성화
|
|
||||||
print("bitsandbytes/transformers 호환 문제로 양자화를 비활성화합니다.")
|
|
||||||
quant_args = {}
|
|
||||||
|
|
||||||
# 메모리 제한/오프로딩 설정
|
# 메모리 제한/오프로딩 설정
|
||||||
mm_cfg = model_settings.get('max_memory', {}) if isinstance(model_settings.get('max_memory', {}), dict) else {}
|
mm_cfg = model_settings.get('max_memory', {}) if isinstance(model_settings.get('max_memory', {}), dict) else {}
|
||||||
@@ -167,9 +160,26 @@ class AIAgent:
|
|||||||
if max_memory:
|
if max_memory:
|
||||||
load_kwargs["max_memory"] = max_memory
|
load_kwargs["max_memory"] = max_memory
|
||||||
|
|
||||||
# use_quantization=True면 8bit 우선 시도 (항상 레거시 플래그 사용)
|
# use_quantization=True면 4bit 우선, 아니면 8bit 레거시 플래그 사용
|
||||||
if use_quantization:
|
if use_quantization:
|
||||||
|
if quant_bits == 4:
|
||||||
|
try:
|
||||||
|
from transformers import BitsAndBytesConfig
|
||||||
|
load_kwargs["quantization_config"] = BitsAndBytesConfig(
|
||||||
|
load_in_4bit=True,
|
||||||
|
bnb_4bit_quant_type="nf4",
|
||||||
|
bnb_4bit_use_double_quant=True,
|
||||||
|
bnb_4bit_compute_dtype=__import__('torch').bfloat16
|
||||||
|
)
|
||||||
|
print("4bit 양자화 적용 (bnb nf4)")
|
||||||
|
except Exception as _:
|
||||||
load_kwargs["load_in_8bit"] = True
|
load_kwargs["load_in_8bit"] = True
|
||||||
|
if cpu_offload:
|
||||||
|
load_kwargs["llm_int8_enable_fp32_cpu_offload"] = True
|
||||||
|
print("4bit 미지원 → 8bit(레거시)로 폴백")
|
||||||
|
else:
|
||||||
|
load_kwargs["load_in_8bit"] = True
|
||||||
|
if cpu_offload:
|
||||||
load_kwargs["llm_int8_enable_fp32_cpu_offload"] = True
|
load_kwargs["llm_int8_enable_fp32_cpu_offload"] = True
|
||||||
print("8bit 양자화 적용 (레거시 플래그)")
|
print("8bit 양자화 적용 (레거시 플래그)")
|
||||||
|
|
||||||
@@ -206,11 +216,11 @@ class AIAgent:
|
|||||||
except Exception as e_noq:
|
except Exception as e_noq:
|
||||||
print(f"비양자화 재시도 실패: {e_noq}")
|
print(f"비양자화 재시도 실패: {e_noq}")
|
||||||
|
|
||||||
# 2b. 8-bit 양자화로 재시도 (가능 시)
|
# 2b. 양자화로 재시도 (4bit 우선, 아니면 8bit)
|
||||||
|
loaded = False
|
||||||
try:
|
try:
|
||||||
print("8bit 양자화로 재시도합니다...")
|
print("양자화로 재시도합니다...")
|
||||||
self.tokenizer = AutoTokenizer.from_pretrained(model_source, trust_remote_code=True)
|
self.tokenizer = AutoTokenizer.from_pretrained(model_source, trust_remote_code=True)
|
||||||
# config 재생성 및 quantization_config 제거
|
|
||||||
cfg = AutoConfig.from_pretrained(model_source, trust_remote_code=True)
|
cfg = AutoConfig.from_pretrained(model_source, trust_remote_code=True)
|
||||||
if hasattr(cfg, 'quantization_config'):
|
if hasattr(cfg, 'quantization_config'):
|
||||||
try:
|
try:
|
||||||
@@ -224,19 +234,30 @@ class AIAgent:
|
|||||||
offload_state_dict=True,
|
offload_state_dict=True,
|
||||||
trust_remote_code=True,
|
trust_remote_code=True,
|
||||||
config=cfg,
|
config=cfg,
|
||||||
load_in_8bit=True,
|
|
||||||
llm_int8_enable_fp32_cpu_offload=True,
|
|
||||||
)
|
)
|
||||||
if dtype is not None:
|
if dtype is not None:
|
||||||
retry_kwargs["torch_dtype"] = dtype
|
retry_kwargs["torch_dtype"] = dtype
|
||||||
if max_memory:
|
if max_memory:
|
||||||
retry_kwargs["max_memory"] = max_memory
|
retry_kwargs["max_memory"] = max_memory
|
||||||
|
if quant_bits == 4:
|
||||||
|
from transformers import BitsAndBytesConfig
|
||||||
|
retry_kwargs["quantization_config"] = BitsAndBytesConfig(
|
||||||
|
load_in_4bit=True,
|
||||||
|
bnb_4bit_quant_type="nf4",
|
||||||
|
bnb_4bit_use_double_quant=True,
|
||||||
|
bnb_4bit_compute_dtype=__import__('torch').bfloat16
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
retry_kwargs["load_in_8bit"] = True
|
||||||
|
if cpu_offload:
|
||||||
|
retry_kwargs["llm_int8_enable_fp32_cpu_offload"] = True
|
||||||
|
|
||||||
self.model = AutoModelForCausalLM.from_pretrained(model_source, **retry_kwargs)
|
self.model = AutoModelForCausalLM.from_pretrained(model_source, **retry_kwargs)
|
||||||
except Exception as e_int8:
|
loaded = True
|
||||||
print(f"8bit 재시도 실패: {e_int8}")
|
except Exception as e_q:
|
||||||
|
print(f"양자화 재시도 실패: {e_q}")
|
||||||
|
|
||||||
if not tried_int8:
|
if not loaded:
|
||||||
print("CPU로 폴백합니다.")
|
print("CPU로 폴백합니다.")
|
||||||
try:
|
try:
|
||||||
import torch, gc
|
import torch, gc
|
||||||
|
|||||||
@@ -8,7 +8,8 @@
|
|||||||
"web_scraping": {
|
"web_scraping": {
|
||||||
"max_pages": 100,
|
"max_pages": 100,
|
||||||
"delay_between_requests": 2,
|
"delay_between_requests": 2,
|
||||||
"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
|
"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
||||||
|
"use_selenium": false
|
||||||
},
|
},
|
||||||
"data_storage": {
|
"data_storage": {
|
||||||
"local_storage_path": "./collected_data",
|
"local_storage_path": "./collected_data",
|
||||||
@@ -17,8 +18,9 @@
|
|||||||
},
|
},
|
||||||
"model_settings": {
|
"model_settings": {
|
||||||
"use_quantization": true,
|
"use_quantization": true,
|
||||||
"quantization_bits": 8,
|
"quantization_bits": 4,
|
||||||
"torch_dtype": "auto",
|
"torch_dtype": "auto",
|
||||||
|
"cpu_offload": false,
|
||||||
"max_memory": {
|
"max_memory": {
|
||||||
"gpu": "20GB",
|
"gpu": "20GB",
|
||||||
"cpu": "60GB"
|
"cpu": "60GB"
|
||||||
|
|||||||
@@ -68,6 +68,15 @@ drive.mount('/content/drive')
|
|||||||
|
|
||||||
또는 실행 시 `--save-path` 옵션으로 지정할 수 있습니다.
|
또는 실행 시 `--save-path` 옵션으로 지정할 수 있습니다.
|
||||||
|
|
||||||
|
웹 스크래핑은 기본으로 Requests+BeautifulSoup 모드로 동작합니다(`use_selenium=false`).
|
||||||
|
Selenium을 사용하려면 `web_scraping.use_selenium`을 `true`로 바꾸고, Colab에 Chrome/ChromeDriver를 설치해야 합니다:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt-get update && sudo apt-get install -y google-chrome-stable || true
|
||||||
|
pip install selenium webdriver-manager
|
||||||
|
```
|
||||||
|
설치가 어려우면 기본 Requests 모드를 유지하세요.
|
||||||
|
|
||||||
## 3. 시스템 실행
|
## 3. 시스템 실행
|
||||||
|
|
||||||
### 3.1 기본 실행 (AI가 스스로 주제 선정)
|
### 3.1 기본 실행 (AI가 스스로 주제 선정)
|
||||||
@@ -110,7 +119,7 @@ os.environ["HF_TOKEN"] = "hf_********************************"
|
|||||||
|
|
||||||
## 4. 실행 과정 설명
|
## 4. 실행 과정 설명
|
||||||
|
|
||||||
1. **모델 다운로드**: Hugging Face에서 `jxm/gpt-oss-20b-base` 모델을 다운로드
|
1. **모델 다운로드**: Hugging Face에서 `jxm/gpt-oss-20b-base` 모델 파일을 동기화(snapshot)
|
||||||
2. **AI 에이전트 초기화**: 모델을 로드하고 도구들을 설정
|
2. **AI 에이전트 초기화**: 모델을 로드하고 도구들을 설정
|
||||||
3. **정보 수집**: 각 주제에 대해 AI가 스스로 웹을 탐색하며 정보 수집
|
3. **정보 수집**: 각 주제에 대해 AI가 스스로 웹을 탐색하며 정보 수집
|
||||||
4. **데이터 저장**: 수집된 데이터를 마운트된 Google Drive의 지정된 폴더에 자동 저장
|
4. **데이터 저장**: 수집된 데이터를 마운트된 Google Drive의 지정된 폴더에 자동 저장
|
||||||
@@ -138,6 +147,14 @@ os.environ["HF_TOKEN"] = "hf_********************************"
|
|||||||
- 모델 접근 권한(토큰) 필요 여부 확인: 필요 시 `HF_TOKEN` 설정
|
- 모델 접근 권한(토큰) 필요 여부 확인: 필요 시 `HF_TOKEN` 설정
|
||||||
- 네트워크 일시 오류일 수 있으므로 런타임 재시작 후 재시도
|
- 네트워크 일시 오류일 수 있으므로 런타임 재시작 후 재시도
|
||||||
|
|
||||||
|
### 6.1.1 모델 로딩 시 GPU 사용이 0%로 보이는 경우
|
||||||
|
- 기본 설정은 4bit 양자화 + GPU/CPU 오프로딩을 사용합니다. 로딩 초기에는 RAM이 먼저 오르고 GPU 사용이 0%일 수 있습니다.
|
||||||
|
- 실행 중에도 GPU가 계속 0%라면 bitsandbytes가 GPU 커널을 잡지 못한 것입니다. 아래를 확인하세요:
|
||||||
|
- `pip install -U transformers accelerate bitsandbytes`
|
||||||
|
- `import torch, bitsandbytes as bnb; print(torch.cuda.is_available())`
|
||||||
|
- `from bitsandbytes.cuda_setup import main_check; print(main_check())`
|
||||||
|
- 여전히 문제가 있으면 `model_settings.max_memory.gpu`를 소폭 올리거나(예: 24GB), `cpu_offload`를 false로 유지하세요.
|
||||||
|
|
||||||
### 6.2 메모리 부족 오류 해결
|
### 6.2 메모리 부족 오류 해결
|
||||||
모델이 클 경우 GPU 메모리가 부족할 수 있습니다. 다음 방법으로 해결하세요:
|
모델이 클 경우 GPU 메모리가 부족할 수 있습니다. 다음 방법으로 해결하세요:
|
||||||
|
|
||||||
|
|||||||
@@ -3,63 +3,92 @@ from bs4 import BeautifulSoup
|
|||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
import os
|
import os
|
||||||
from selenium import webdriver
|
|
||||||
from selenium.webdriver.chrome.options import Options
|
try:
|
||||||
from selenium.webdriver.common.by import By
|
from selenium import webdriver
|
||||||
from webdriver_manager.chrome import ChromeDriverManager
|
from selenium.webdriver.chrome.options import Options
|
||||||
from selenium.webdriver.chrome.service import Service
|
from selenium.webdriver.common.by import By
|
||||||
|
from webdriver_manager.chrome import ChromeDriverManager
|
||||||
|
from selenium.webdriver.chrome.service import Service
|
||||||
|
_SELENIUM_AVAILABLE = True
|
||||||
|
except Exception:
|
||||||
|
_SELENIUM_AVAILABLE = False
|
||||||
|
|
||||||
class WebScraper:
|
class WebScraper:
|
||||||
def __init__(self, config_path='./config.json'):
|
def __init__(self, config_path='./config.json'):
|
||||||
with open(config_path, 'r') as f:
|
with open(config_path, 'r') as f:
|
||||||
self.config = json.load(f)
|
self.config = json.load(f)
|
||||||
|
|
||||||
self.max_pages = self.config['web_scraping']['max_pages']
|
ws_conf = self.config.get('web_scraping', {})
|
||||||
self.delay = self.config['web_scraping']['delay_between_requests']
|
self.max_pages = ws_conf.get('max_pages', 100)
|
||||||
self.user_agent = self.config['web_scraping']['user_agent']
|
self.delay = ws_conf.get('delay_between_requests', 2)
|
||||||
|
self.user_agent = ws_conf.get('user_agent', 'Mozilla/5.0')
|
||||||
|
self.use_selenium = bool(ws_conf.get('use_selenium', False))
|
||||||
|
|
||||||
# Selenium 설정
|
self.driver = None
|
||||||
|
if self.use_selenium and _SELENIUM_AVAILABLE:
|
||||||
|
try:
|
||||||
chrome_options = Options()
|
chrome_options = Options()
|
||||||
chrome_options.add_argument("--headless") # Colab에서는 headless 모드
|
chrome_options.add_argument("--headless=new")
|
||||||
chrome_options.add_argument("--no-sandbox")
|
chrome_options.add_argument("--no-sandbox")
|
||||||
chrome_options.add_argument("--disable-dev-shm-usage")
|
chrome_options.add_argument("--disable-dev-shm-usage")
|
||||||
chrome_options.add_argument(f"user-agent={self.user_agent}")
|
chrome_options.add_argument(f"user-agent={self.user_agent}")
|
||||||
|
|
||||||
|
# Chrome 바이너리 탐색 (Colab/리눅스 일반 경로)
|
||||||
|
chrome_bin_candidates = [
|
||||||
|
os.environ.get('GOOGLE_CHROME_BIN'),
|
||||||
|
os.environ.get('CHROME_BIN'),
|
||||||
|
'/usr/bin/google-chrome',
|
||||||
|
'/usr/bin/chromium-browser',
|
||||||
|
'/usr/bin/chromium'
|
||||||
|
]
|
||||||
|
chrome_bin = next((p for p in chrome_bin_candidates if p and os.path.exists(p)), None)
|
||||||
|
if chrome_bin:
|
||||||
|
chrome_options.binary_location = chrome_bin
|
||||||
|
|
||||||
self.driver = webdriver.Chrome(
|
self.driver = webdriver.Chrome(
|
||||||
service=Service(ChromeDriverManager().install()),
|
service=Service(ChromeDriverManager().install()),
|
||||||
options=chrome_options
|
options=chrome_options
|
||||||
)
|
)
|
||||||
|
print("Selenium 모드 활성화")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Selenium 초기화 실패, Requests 모드로 폴백: {e}")
|
||||||
|
self.driver = None
|
||||||
|
self.use_selenium = False
|
||||||
|
else:
|
||||||
|
if self.use_selenium and not _SELENIUM_AVAILABLE:
|
||||||
|
print("Selenium 패키지 미설치, Requests 모드로 폴백합니다.")
|
||||||
|
self.use_selenium = False
|
||||||
|
|
||||||
def scrape_website(self, url, keywords=None):
|
def scrape_website(self, url, keywords=None):
|
||||||
"""
|
"""
|
||||||
웹사이트에서 정보를 수집합니다.
|
웹사이트에서 정보를 수집합니다.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
|
if self.use_selenium and self.driver is not None:
|
||||||
self.driver.get(url)
|
self.driver.get(url)
|
||||||
time.sleep(self.delay)
|
time.sleep(self.delay)
|
||||||
|
|
||||||
# 페이지 내용 추출
|
|
||||||
page_source = self.driver.page_source
|
page_source = self.driver.page_source
|
||||||
|
else:
|
||||||
|
headers = {"User-Agent": self.user_agent}
|
||||||
|
resp = requests.get(url, headers=headers, timeout=20)
|
||||||
|
resp.raise_for_status()
|
||||||
|
page_source = resp.text
|
||||||
|
|
||||||
soup = BeautifulSoup(page_source, 'html.parser')
|
soup = BeautifulSoup(page_source, 'html.parser')
|
||||||
|
|
||||||
# 텍스트 내용 추출
|
|
||||||
text_content = soup.get_text(separator=' ', strip=True)
|
text_content = soup.get_text(separator=' ', strip=True)
|
||||||
|
|
||||||
# 메타데이터 추출
|
|
||||||
title = soup.title.string if soup.title else "No Title"
|
title = soup.title.string if soup.title else "No Title"
|
||||||
meta_description = soup.find('meta', attrs={'name': 'description'})
|
meta_description = soup.find('meta', attrs={'name': 'description'})
|
||||||
description = meta_description['content'] if meta_description else "No Description"
|
description = meta_description['content'] if (meta_description and meta_description.has_attr('content')) else "No Description"
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
'url': url,
|
'url': url,
|
||||||
'title': title,
|
'title': title,
|
||||||
'description': description,
|
'description': description,
|
||||||
'content': text_content[:5000], # 내용 제한
|
'content': text_content[:5000],
|
||||||
'timestamp': time.time()
|
'timestamp': time.time()
|
||||||
}
|
}
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"스크래핑 실패: {url} - {e}")
|
print(f"스크래핑 실패: {url} - {e}")
|
||||||
return None
|
return None
|
||||||
@@ -83,16 +112,25 @@ class WebScraper:
|
|||||||
|
|
||||||
# 추가 링크 찾기 (단순히 현재 페이지의 링크들)
|
# 추가 링크 찾기 (단순히 현재 페이지의 링크들)
|
||||||
try:
|
try:
|
||||||
|
if self.use_selenium and self.driver is not None:
|
||||||
links = self.driver.find_elements(By.TAG_NAME, "a")
|
links = self.driver.find_elements(By.TAG_NAME, "a")
|
||||||
for link in links[:10]: # 최대 10개 링크만
|
hrefs = [link.get_attribute("href") for link in links[:20]]
|
||||||
href = link.get_attribute("href")
|
else:
|
||||||
|
# Requests 모드일 때는 현재 페이지를 다시 받아서 링크 파싱
|
||||||
|
headers = {"User-Agent": self.user_agent}
|
||||||
|
resp = requests.get(url, headers=headers, timeout=20)
|
||||||
|
resp.raise_for_status()
|
||||||
|
soup = BeautifulSoup(resp.text, 'html.parser')
|
||||||
|
hrefs = [a.get('href') for a in soup.find_all('a', href=True)][:20]
|
||||||
|
|
||||||
|
for href in hrefs:
|
||||||
if href and href.startswith("http") and href not in visited_urls:
|
if href and href.startswith("http") and href not in visited_urls:
|
||||||
if len(collected_data) < self.max_pages:
|
if len(collected_data) < self.max_pages:
|
||||||
data = self.scrape_website(href, keywords)
|
data = self.scrape_website(href, keywords)
|
||||||
if data:
|
if data:
|
||||||
collected_data.append(data)
|
collected_data.append(data)
|
||||||
visited_urls.add(href)
|
visited_urls.add(href)
|
||||||
except:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return collected_data
|
return collected_data
|
||||||
@@ -112,6 +150,7 @@ class WebScraper:
|
|||||||
print(f"데이터 저장 완료: {filepath}")
|
print(f"데이터 저장 완료: {filepath}")
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
|
if self.driver is not None:
|
||||||
self.driver.quit()
|
self.driver.quit()
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
Reference in New Issue
Block a user