zenith / src /document_processing /web_scraper.py
hello-yaaash25's picture
Initial commit without binary files
29c4d1d
Raw History Blame Contribute Delete
4.69 kB
import requests
from bs4 import BeautifulSoup
from urllib.parse import urlparse, urljoin
import logging
from src.document_processing.chunking import chunk_text
from typing import List, Optional, Set, Dict, Callable
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_community.vectorstores import FAISS
from langchain_core.documents.base import Document
logger = logging.getLogger(__name__)
class WebScraper:
def __init__(self, headers: Optional[dict] = None):
self.headers = headers or {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"
}
self.visited_urls = set()
def scrape_url(self, url: str) -> str:
try:
response = requests.get(url, headers=self.headers)
response.raise_for_status()
soup = BeautifulSoup(response.content, "html.parser")
for script in soup(["script", "style", "nav", "footer", "header"]):
script.extract()
text = soup.get_text(separator='\n')
lines = (line.strip() for line in text.splitlines())
chunks = (phrase.strip() for line in lines for phrase in line.split(" "))
text = '\n'.join(chunk for chunk in chunks if chunk)
return text, soup
except Exception as e:
logger.error(f"Error scraping {url}: {str(e)}")
return f"Error scraping {url}: {str(e)}", None
def extract_links(self, url: str, soup: BeautifulSoup) -> List[str]:
"""Extract all hyperlinks from a webpage"""
links = []
base_url = urlparse(url)
domain = base_url.netloc
for link in soup.find_all('a', href=True):
href = link['href']
# Create absolute URL
if href.startswith('/'):
full_url = f"{base_url.scheme}://{domain}{href}"
elif href.startswith(('http://', 'https://')):
full_url = href
else:
full_url = urljoin(url, href)
# Only include links from same domain
if urlparse(full_url).netloc == domain:
links.append(full_url)
return links
def get_domain(self, url: str) -> str:
return urlparse(url).netloc
def crawl_website(self, start_url: str, max_depth: int = 2, max_pages: int = 20,
callback: Optional[Callable[[str], None]] = None) -> Dict[str, str]:
self.visited_urls = set()
results = {}
def _crawl(url: str, depth: int):
if depth > max_depth or len(results) >= max_pages or url in self.visited_urls:
return
self.visited_urls.add(url)
if callback:
callback(url)
logger.info(f"Scraping {url} (depth {depth})")
content, soup = self.scrape_url(url)
if soup is not None:
results[url] = content
links = self.extract_links(url, soup)
for link in links:
_crawl(link, depth + 1)
_crawl(start_url, 1)
return results
def process_web_content(urls: List[str], vector_store_path: str,
max_depth: int = 1, max_pages: int = 20,
ui_callback: Optional[Callable[[str], None]] = None) -> FAISS:
scraper = WebScraper()
documents = []
for start_url in urls:
if ui_callback:
ui_callback(f"Starting to crawl from: {start_url}")
page_contents = scraper.crawl_website(
start_url,
max_depth=max_depth,
max_pages=max_pages,
callback=ui_callback
)
for url, content in page_contents.items():
domain = scraper.get_domain(url)
chunks = chunk_text(content)
for i, chunk in enumerate(chunks):
metadata = {
"source": url,
"domain": domain,
"chunk": i
}
documents.append(Document(page_content=chunk, metadata=metadata))
if ui_callback:
ui_callback(f"Creating vector embeddings for {len(documents)} chunks...")
embeddings = HuggingFaceEmbeddings(
model_name="sentence-transformers/all-MiniLM-L6-v2",
model_kwargs={'device': 'cpu'}
)
vector_store = FAISS.from_documents(documents, embeddings)
vector_store.save_local(vector_store_path)
return vector_store