ogx786 commited on
Commit
cdc7861
·
verified ·
1 Parent(s): 3a3782d

Create crawler.py

Browse files
Files changed (1) hide show
  1. crawler.py +215 -0
crawler.py ADDED
@@ -0,0 +1,215 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Habib Bank Limited (HBL) - Enterprise Knowledge RAG Pipeline
3
+ -----------------------------------------------------------
4
+ This production script runs completely offline on an HBL enterprise laptop.
5
+ It deeply crawls the internal domain (including sublinks), downloads PDFs,
6
+ processes text/tables, and applies Semantic Topic Chunking to create a
7
+ local FAISS vector store.
8
+
9
+ Requirements:
10
+ pip install requests beautifulsoup4 pdfplumber langchain langchain-community langchain-experimental sentence-transformers faiss-cpu torch
11
+ """
12
+
13
+ import os
14
+ import re
15
+ import time
16
+ from collections import deque
17
+ from urllib.parse import urljoin, urlparse
18
+
19
+ # Enforce strict offline execution to bypass HBL corporate proxy/firewall checks
20
+ os.environ["TRANSFORMERS_OFFLINE"] = "1"
21
+ os.environ["HF_DATASETS_OFFLINE"] = "1"
22
+ os.environ["HF_HUB_DISABLE_SYMLINKS_WARNING"] = "1"
23
+
24
+ import requests
25
+ import torch
26
+ import pdfplumber
27
+ from bs4 import BeautifulSoup
28
+ from langchain.schema import Document
29
+ from langchain_experimental.text_splitter import SemanticChunker
30
+ from langchain_community.embeddings import HuggingFaceEmbeddings
31
+ from langchain_community.vectorstores import FAISS
32
+
33
+ # =================================---------
34
+ # CONFIGURATION PORTAL (EDIT AS NEEDED)
35
+ # =================================---------
36
+ # 1. Target links and network boundaries
37
+ TARGET_URL = "http://iamhbl.hbl.com/"
38
+ ALLOWED_DOMAIN = "iamhbl.hbl.com"
39
+
40
+ # 2. Crawler limits to prevent infinite loops
41
+ MAX_PAGES = 500 # Maximum HTML pages to scan
42
+ MAX_DEPTH = 3 # Sub-folder depth limit (e.g., Home -> HR -> Circulars)
43
+
44
+ # 3. Local Model Path - Absolute path to your downloaded bge-m3 weights directory
45
+ # e.g., "C:/Users/Ezan/Models/bge-m3" or "/home/ezan/models/bge-m3"
46
+ LOCAL_MODEL_PATH = "C:/path/to/your/local/folder/bge-m3"
47
+
48
+ # 4. Output directories
49
+ DOWNLOAD_DIR = "./iamhbl_downloaded_pdfs"
50
+ INDEX_DIR = "./iamhbl_faiss_index"
51
+ USER_AGENT = "HBL-Internal-Semantic-RAG-Pipeline/1.0"
52
+ # =================================---------
53
+
54
+
55
+ def get_pdf_links(seed_url, max_pages=MAX_PAGES, max_depth=MAX_DEPTH):
56
+ """Recursively crawls the domain and sublinks to discover all PDFs."""
57
+ print(f"[+] Starting deep-crawl on {seed_url} (Max Depth: {max_depth})")
58
+
59
+ queue = deque([(seed_url, 0)])
60
+ visited_pages = set()
61
+ pdf_links = set()
62
+ headers = {"User-Agent": USER_AGENT}
63
+
64
+ while queue and len(visited_pages) < max_pages:
65
+ url, depth = queue.popleft()
66
+
67
+ if url in visited_pages or depth > max_depth:
68
+ continue
69
+
70
+ visited_pages.add(url)
71
+
72
+ try:
73
+ response = requests.get(url, headers=headers, timeout=10)
74
+ if response.status_code != 200:
75
+ continue
76
+
77
+ content_type = response.headers.get("Content-Type", "")
78
+ if "application/pdf" in content_type or url.lower().endswith(".pdf"):
79
+ pdf_links.add(url)
80
+ continue
81
+
82
+ if "text/html" not in content_type:
83
+ continue
84
+
85
+ print(f"[*] Crawling [Depth {depth}]: {url}")
86
+
87
+ soup = BeautifulSoup(response.text, "html.parser")
88
+ for link in soup.find_all("a", href=True):
89
+ next_url = urljoin(url, link["href"]).split("#")[0]
90
+
91
+ if next_url.lower().endswith(".pdf"):
92
+ pdf_links.add(next_url)
93
+ else:
94
+ parsed_url = urlparse(next_url)
95
+ if ALLOWED_DOMAIN in parsed_url.netloc and next_url not in visited_pages:
96
+ queue.append((next_url, depth + 1))
97
+
98
+ except Exception as e:
99
+ print(f"[!] Warning: Failed to crawl {url}: {e}")
100
+
101
+ print(f"\n[+] Deep-Crawl Complete. Scanned {len(visited_pages)} pages.")
102
+ print(f"[+] Found {len(pdf_links)} unique PDF targets.")
103
+
104
+ return list(pdf_links)
105
+
106
+
107
+ def download_pdfs(pdf_urls, download_dir):
108
+ """Downloads discovered assets locally without cloud storage dependencies."""
109
+ os.makedirs(download_dir, exist_ok=True)
110
+ downloaded_paths = []
111
+ headers = {"User-Agent": USER_AGENT}
112
+
113
+ for i, url in enumerate(pdf_urls):
114
+ filename = os.path.join(download_dir, f"hbl_asset_{i+1}.pdf")
115
+ try:
116
+ print(f"[->] Fetching [{i+1}/{len(pdf_urls)}]: {url}")
117
+ response = requests.get(url, headers=headers, stream=True, timeout=15)
118
+ response.raise_for_status()
119
+ with open(filename, 'wb') as f:
120
+ for chunk in response.iter_content(chunk_size=8192):
121
+ f.write(chunk)
122
+ downloaded_paths.append(filename)
123
+ except Exception as e:
124
+ print(f"[!] Warning: Failed to download asset from {url}: {e}")
125
+
126
+ return downloaded_paths
127
+
128
+
129
+ def extract_content_from_pdfs(pdf_paths):
130
+ """Parses text content and structured tabular data side-by-side using pdfplumber."""
131
+ documents = []
132
+ for path in pdf_paths:
133
+ print(f"[+] Parsing document extraction layout: {path}")
134
+ text_content = []
135
+ try:
136
+ with pdfplumber.open(path) as pdf:
137
+ for i, page in enumerate(pdf.pages):
138
+ page_text = page.extract_text()
139
+ if page_text:
140
+ text_content.append(page_text)
141
+
142
+ tables = page.extract_tables()
143
+ for table in tables:
144
+ table_str = "\n".join([
145
+ " | ".join(map(lambda x: str(x).replace('\n', ' ') if x else "", row))
146
+ for row in table
147
+ ])
148
+ text_content.append(
149
+ f"\n--- Structured Matrix Table (Page {i+1}) ---\n"
150
+ f"{table_str}\n"
151
+ f"--------------------------------------------"
152
+ )
153
+
154
+ full_text = "\n\n".join(text_content).strip()
155
+ if full_text and len(full_text.split()) >= 30:
156
+ doc = Document(page_content=full_text, metadata={"source": os.path.basename(path)})
157
+ documents.append(doc)
158
+ except Exception as e:
159
+ print(f"[!] Layout Error: Could not read structural data from {path}: {e}")
160
+
161
+ return documents
162
+
163
+
164
+ def build_semantic_faiss_index(documents, output_dir, model_path):
165
+ """Vectorizes data using semantic breakpoints matching subject topics."""
166
+ if not documents:
167
+ print("[-] Vectorization Pipeline Aborted: No valid textual documents loaded.")
168
+ return
169
+
170
+ if not os.path.isdir(model_path):
171
+ raise FileNotFoundError(f"[-] Local directory for bge-m3 weights not found at: {model_path}")
172
+
173
+ print("[+] Initializing local Embedding Engine...")
174
+ device = "cuda" if torch.cuda.is_available() else "cpu"
175
+ print(f"[*] Compute Target Acceleration: {device.upper()}")
176
+
177
+ embeddings = HuggingFaceEmbeddings(
178
+ model_name=model_path,
179
+ model_kwargs={"device": device},
180
+ encode_kwargs={"normalize_embeddings": True}
181
+ )
182
+
183
+ print("[+] Analyzing text layouts for semantic split processing...")
184
+ # Semantic Chunking evaluates the distance between sentences to group them by topic.
185
+ text_splitter = SemanticChunker(
186
+ embeddings,
187
+ breakpoint_threshold_type="percentile"
188
+ )
189
+
190
+ chunks = text_splitter.split_documents(documents)
191
+ print(f"[*] Generated {len(chunks)} contextual topic-based document chunks.")
192
+
193
+ print("[+] Computing vector embeddings & building FAISS Index...")
194
+ t0 = time.time()
195
+ vectorstore = FAISS.from_documents(chunks, embeddings)
196
+ vectorstore.save_local(output_dir)
197
+ print(f"[+] Success! FAISS indexing layer compiled in {time.time() - t0:.2f}s.")
198
+ print(f" Assets stored securely at: {os.path.abspath(output_dir)}")
199
+ print(" Files ready for distribution: 'index.faiss' and 'index.pkl'")
200
+
201
+
202
+ if __name__ == "__main__":
203
+ print("=================================================================")
204
+ print(" HBL INTERNAL ASSISTANT - INGESTION ENGINE ")
205
+ print("=================================================================")
206
+
207
+ pdf_urls = get_pdf_links(TARGET_URL, max_pages=MAX_PAGES, max_depth=MAX_DEPTH)
208
+
209
+ if pdf_urls:
210
+ downloaded_files = download_pdfs(pdf_urls, DOWNLOAD_DIR)
211
+ docs = extract_content_from_pdfs(downloaded_files)
212
+ build_semantic_faiss_index(docs, output_dir=INDEX_DIR, model_path=LOCAL_MODEL_PATH)
213
+ print("\n[+] System Execution Sequence Completed Successfully.")
214
+ else:
215
+ print("\n[-] Core Pipeline Stopped: Ensure network boundaries or target paths are correct.")