luciagomez commited on
Commit
1019e6a
Β·
verified Β·
1 Parent(s): 518406c

remove mcp section, improve readability

Browse files
Files changed (1) hide show
  1. app.py +125 -273
app.py CHANGED
@@ -5,64 +5,24 @@ from markdownify import markdownify as md
5
  import tempfile
6
  import zipfile
7
  import re
8
- from typing import Tuple, Set, List
9
  import os
10
  import gradio as gr
11
  from collections import deque
12
- import time
13
-
14
 
15
  # ===========================================================
16
- # TEXT CLEANING UTILITIES
17
  # ===========================================================
18
- def clean_text_content(text: str) -> str:
19
- """Clean text: remove extra whitespace, normalize line breaks."""
20
- # Remove multiple consecutive newlines
21
- text = re.sub(r'\n\s*\n\s*\n+', '\n\n', text)
22
- # Remove leading/trailing whitespace from lines
23
- lines = [line.strip() for line in text.split('\n')]
24
- # Remove empty lines
25
- lines = [line for line in lines if line.strip()]
26
- # Join and clean up
27
- text = '\n'.join(lines)
28
- # Fix common HTML entity artifacts
29
- text = text.replace('&nbsp;', ' ').replace('&amp;', '&').replace('&lt;', '<').replace('&gt;', '>')
30
- return text.strip()
31
-
32
-
33
- def extract_main_content(soup: BeautifulSoup) -> str:
34
- """Extract main content from HTML, removing boilerplate."""
35
- # Remove unwanted elements
36
- for tag in soup(["script", "style", "nav", "footer", "header", "aside", "noscript", "meta", "link"]):
37
- tag.decompose()
38
-
39
- # Find main content area
40
- main_content = (
41
- soup.find("main") or
42
- soup.find("article") or
43
- soup.find("div", class_=re.compile(r"content|main|post|article|body-text", re.I)) or
44
- soup.find("body")
45
- )
46
-
47
- if not main_content:
48
- return ""
49
-
50
- # Convert to markdown
51
- markdown_text = md(str(main_content), heading_style="ATX")
52
-
53
- # Clean up markdown artifacts
54
- markdown_text = re.sub(r'\[([^\]]+)\]\(javascript:[^)]*\)', r'\1', markdown_text) # Remove JS links
55
- markdown_text = re.sub(r'\n{3,}', '\n\n', markdown_text) # Remove excessive newlines
56
- markdown_text = re.sub(r'#{7,}', '###', markdown_text) # Cap heading levels
57
-
58
- return clean_text_content(markdown_text)
59
 
 
 
 
 
 
 
 
 
60
 
61
- # ===========================================================
62
- # RECURSIVE CRAWLER
63
- # ===========================================================
64
- def crawl_site_for_links(start_url: str, max_pages: int = 50, max_depth: int = 2, progress=None):
65
- """Crawl the given site (up to max_depth) and return sets of HTML and PDF URLs."""
66
  visited = set()
67
  html_links = set()
68
  pdf_links = set()
@@ -70,33 +30,33 @@ def crawl_site_for_links(start_url: str, max_pages: int = 50, max_depth: int = 2
70
  parsed_base = urlparse(start_url)
71
  domain = parsed_base.netloc
72
 
73
- queue = deque([(start_url, 0)]) # (url, depth)
74
  session = requests.Session()
75
  session.headers.update({
76
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
77
  })
78
 
79
  while queue and len(visited) < max_pages:
80
  current_url, depth = queue.popleft()
 
81
  if current_url in visited or depth > max_depth:
82
  continue
83
 
84
  visited.add(current_url)
85
- if progress:
86
- progress(f"πŸ” Crawling: {len(visited)}/{max_pages} pages discovered...")
87
-
88
  try:
89
  response = session.get(current_url, timeout=10)
 
90
  if "text/html" not in response.headers.get("Content-Type", ""):
91
  continue
92
 
93
  soup = BeautifulSoup(response.content, "html.parser")
 
94
  for a in soup.find_all("a", href=True):
95
  href = a["href"]
96
  full_url = urljoin(current_url, href)
97
  parsed = urlparse(full_url)
98
 
99
- # Stay in same domain
100
  if parsed.netloc != domain:
101
  continue
102
 
@@ -104,274 +64,166 @@ def crawl_site_for_links(start_url: str, max_pages: int = 50, max_depth: int = 2
104
  pdf_links.add(full_url)
105
  elif not href.startswith(("#", "javascript:", "mailto:", "tel:")):
106
  html_links.add(full_url)
107
- if full_url not in visited and depth + 1 <= max_depth:
108
  queue.append((full_url, depth + 1))
109
- except Exception as e:
 
110
  continue
111
 
112
  return html_links, pdf_links
113
 
114
 
115
  # ===========================================================
116
- # MAIN EXTRACTION FUNCTION
117
  # ===========================================================
118
- def extract_all_content_as_zip(url: str, max_links: int = None, max_depth: int = 2, progress=None) -> Tuple[str, str]:
 
119
  """
120
- Extract clean text content and PDFs from a website recursively.
121
- Content is processed to remove HTML noise and keep only meaningful text.
 
 
 
122
  """
 
123
  try:
124
- if not url.strip():
125
- return "❌ Please enter a valid URL", None
126
-
127
  if not url.startswith(("http://", "https://")):
128
  url = "https://" + url
129
 
130
- if progress:
131
- progress("🌐 Starting crawl...")
132
-
133
- html_links, pdf_links = crawl_site_for_links(
134
- url,
135
- max_pages=(max_links or 50),
136
- max_depth=max_depth,
137
- progress=progress
138
- )
139
 
140
  if not html_links and not pdf_links:
141
- return "❌ No internal links or PDFs found to extract.", None
142
 
143
- # Create ZIP
144
  with tempfile.NamedTemporaryFile(delete=False, suffix=".zip") as temp_zip:
145
  zip_path = temp_zip.name
146
 
147
- successful_html = 0
148
- failed_html = 0
149
- successful_pdfs = 0
150
- failed_pdfs = 0
151
-
152
  session = requests.Session()
153
- session.headers.update({
154
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"
155
- })
 
156
 
157
  with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zip_file:
158
- # Create index file
159
- index_content = f"# Website Extraction Report\n\n"
160
- index_content += f"**Source:** {url}\n"
161
- index_content += f"**Extracted:** {time.strftime('%Y-%m-%d %H:%M:%S')}\n"
162
- index_content += f"**Total Pages:** {len(html_links)}\n"
163
- index_content += f"**Total PDFs:** {len(pdf_links)}\n\n"
164
- index_content += "## Table of Contents\n\n"
165
-
166
- # Process HTML pages
167
- html_list = list(html_links)
168
- for i, link_url in enumerate(html_list, 1):
169
- if progress:
170
- progress(f"πŸ“„ Processing HTML ({i}/{len(html_list)})...")
171
-
172
  try:
173
  resp = session.get(link_url, timeout=10)
174
- resp.raise_for_status()
175
-
176
  soup = BeautifulSoup(resp.content, "html.parser")
177
-
178
- # Extract title
179
- title_tag = soup.find("title")
180
- title = title_tag.get_text().strip() if title_tag else f"Page {i}"
181
-
182
- # Extract and clean content
183
- content = extract_main_content(soup)
184
-
185
- if not content or len(content) < 20:
186
- failed_html += 1
187
- continue
188
-
189
- # Build markdown file
190
- markdown_text = f"# {title}\n\n"
191
- markdown_text += f"*Source: [{link_url}]({link_url})*\n\n"
192
- markdown_text += content
193
-
194
- # Safe filename
195
- filename = re.sub(r"[^\w\-_.]", "_", title[:50])
196
- if not filename or filename == "_":
197
- filename = f"page_{i}"
198
- filename = f"{filename}.md"
199
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
200
  zip_file.writestr(filename, markdown_text)
201
- successful_html += 1
202
-
203
- # Add to index
204
- index_content += f"- [{title}]({filename})\n"
205
-
206
- except Exception as e:
207
- failed_html += 1
208
- continue
209
 
210
- # Process PDFs
211
- pdf_list = list(pdf_links)
212
- for j, pdf_url in enumerate(pdf_list, 1):
213
- if progress:
214
- progress(f"πŸ“„ Downloading PDFs ({j}/{len(pdf_list)})...")
215
-
216
  try:
217
  resp = session.get(pdf_url, timeout=20)
218
- resp.raise_for_status()
219
- pdf_filename = os.path.basename(urlparse(pdf_url).path)
220
- if not pdf_filename.lower().endswith(".pdf"):
221
- pdf_filename = f"document_{j}.pdf"
222
- zip_file.writestr(f"pdfs/{pdf_filename}", resp.content)
223
- successful_pdfs += 1
224
- except Exception as e:
225
- failed_pdfs += 1
226
- continue
227
 
228
- # Write index
229
- zip_file.writestr("INDEX.md", index_content)
230
 
231
- # Build status message
232
- status_message = (
233
- f"βœ… **Extraction Complete!**\n\n"
234
- f"πŸ“Š **Results:**\n"
235
- f"- πŸ“„ HTML Pages: **{successful_html}** extracted"
236
- )
237
- if failed_html > 0:
238
- status_message += f", {failed_html} failed"
239
-
240
- status_message += f"\n- πŸ“‹ PDFs: **{successful_pdfs}** downloaded"
241
- if failed_pdfs > 0:
242
- status_message += f", {failed_pdfs} failed"
243
-
244
- status_message += f"\n\nπŸ“¦ ZIP contains {successful_html} markdown files + {successful_pdfs} PDFs + index"
245
 
246
- return status_message, zip_path
 
 
247
 
248
  except Exception as e:
249
  return f"❌ Error: {str(e)}", None
250
 
251
 
252
  # ===========================================================
253
- # GRADIO UI WITH CUSTOM THEME
254
  # ===========================================================
255
- def gradio_extract(url, max_links, max_depth):
256
- message, zip_path = extract_all_content_as_zip(
257
- url,
258
- int(max_links) if max_links else None,
259
- int(max_depth),
260
- progress=gr.update
261
- )
262
- return message, zip_path
263
-
264
-
265
- # Custom CSS for better visuals
266
- custom_css = """
267
- .gradio-container {
268
- max-width: 900px;
269
- margin: 0 auto;
270
- }
271
-
272
- .header {
273
- text-align: center;
274
- padding: 20px;
275
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
276
- color: white;
277
- border-radius: 10px;
278
- margin-bottom: 20px;
279
- }
280
-
281
- .header h1 {
282
- margin: 0;
283
- font-size: 2.5em;
284
- }
285
-
286
- .header p {
287
- margin: 10px 0 0 0;
288
- font-size: 1.1em;
289
- opacity: 0.9;
290
- }
291
- """
292
 
293
- with gr.Blocks(theme=gr.themes.Soft(), css=custom_css, title="🌐 Website Content Extractor") as demo:
294
- gr.HTML("""
295
- <div class="header">
296
- <h1>🌐 Website Content Extractor</h1>
297
- <p>Extract clean, human-readable content from any website</p>
298
- </div>
299
- """)
300
-
301
  gr.Markdown("""
302
- ### How it works:
303
- 1. Enter a website URL
304
- 2. Set the maximum number of pages to crawl
305
- 3. Choose crawl depth (1-3 levels)
306
- 4. Click **Extract** to start
307
- 5. Download the ZIP with cleaned markdown files and PDFs
308
-
309
- The extractor removes HTML noise and keeps only meaningful content.
310
- """)
311
-
312
- with gr.Group():
313
- gr.Markdown("### Input Settings")
 
 
 
 
 
 
 
 
 
 
 
 
314
  url_input = gr.Textbox(
315
  label="Website URL",
316
- placeholder="https://example.com",
317
- info="Enter the starting URL"
318
  )
319
-
320
- with gr.Row():
321
- max_links = gr.Number(
322
- label="Max Pages to Extract",
323
- value=50,
324
- minimum=1,
325
- maximum=200,
326
- step=1,
327
- info="Limit the number of pages crawled"
328
- )
329
- max_depth = gr.Slider(
330
- label="Crawl Depth",
331
- minimum=1,
332
- maximum=3,
333
- value=2,
334
- step=1,
335
- info="How deep to follow links"
336
- )
337
-
338
- extract_btn = gr.Button("πŸš€ Extract Content", variant="primary", size="lg")
339
-
340
- gr.Divider()
341
-
342
- with gr.Group():
343
- gr.Markdown("### Results")
344
- status_output = gr.Textbox(
345
- label="Status",
346
- interactive=False,
347
- lines=3
348
  )
349
- zip_output = gr.File(
350
- label="πŸ“¦ Download ZIP",
351
- type="filepath"
 
 
352
  )
353
-
354
- # Connect button
355
- extract_btn.click(
356
- fn=gradio_extract,
357
- inputs=[url_input, max_links, max_depth],
358
- outputs=[status_output, zip_output]
 
 
 
 
359
  )
360
-
361
- gr.Markdown("""
362
- ---
363
- **Made with ❀️ for better web scraping**
364
- """)
365
 
366
 
367
  # ===========================================================
368
- # ENTRY POINT
369
  # ===========================================================
 
370
  if __name__ == "__main__":
371
- port = int(os.environ.get("PORT", 7860))
372
- demo.launch(
373
- server_name="0.0.0.0",
374
- server_port=port,
375
- ssr_mode=False,
376
- show_error=True
377
- )
 
5
  import tempfile
6
  import zipfile
7
  import re
8
+ from typing import Tuple
9
  import os
10
  import gradio as gr
11
  from collections import deque
 
 
12
 
13
  # ===========================================================
14
+ # 🌐 WEBSITE CRAWLER
15
  # ===========================================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
 
17
+ def crawl_site_for_links(start_url: str, max_pages: int = 50, max_depth: int = 2):
18
+ """
19
+ Recursively crawl a website and collect:
20
+ β€’ Internal HTML pages
21
+ β€’ PDF files
22
+
23
+ We stay inside the same domain for safety.
24
+ """
25
 
 
 
 
 
 
26
  visited = set()
27
  html_links = set()
28
  pdf_links = set()
 
30
  parsed_base = urlparse(start_url)
31
  domain = parsed_base.netloc
32
 
33
+ queue = deque([(start_url, 0)])
34
  session = requests.Session()
35
  session.headers.update({
36
+ "User-Agent": "Mozilla/5.0"
37
  })
38
 
39
  while queue and len(visited) < max_pages:
40
  current_url, depth = queue.popleft()
41
+
42
  if current_url in visited or depth > max_depth:
43
  continue
44
 
45
  visited.add(current_url)
46
+
 
 
47
  try:
48
  response = session.get(current_url, timeout=10)
49
+
50
  if "text/html" not in response.headers.get("Content-Type", ""):
51
  continue
52
 
53
  soup = BeautifulSoup(response.content, "html.parser")
54
+
55
  for a in soup.find_all("a", href=True):
56
  href = a["href"]
57
  full_url = urljoin(current_url, href)
58
  parsed = urlparse(full_url)
59
 
 
60
  if parsed.netloc != domain:
61
  continue
62
 
 
64
  pdf_links.add(full_url)
65
  elif not href.startswith(("#", "javascript:", "mailto:", "tel:")):
66
  html_links.add(full_url)
67
+ if full_url not in visited:
68
  queue.append((full_url, depth + 1))
69
+
70
+ except Exception:
71
  continue
72
 
73
  return html_links, pdf_links
74
 
75
 
76
  # ===========================================================
77
+ # πŸ“¦ EXTRACTION ENGINE
78
  # ===========================================================
79
+
80
+ def extract_all_content_as_zip(url: str, max_links: int, max_depth: int) -> Tuple[str, str]:
81
  """
82
+ Main function:
83
+ β€’ Crawls the site
84
+ β€’ Converts pages to Markdown
85
+ β€’ Downloads PDFs
86
+ β€’ Packs everything into a ZIP file
87
  """
88
+
89
  try:
 
 
 
90
  if not url.startswith(("http://", "https://")):
91
  url = "https://" + url
92
 
93
+ html_links, pdf_links = crawl_site_for_links(url, max_links, max_depth)
 
 
 
 
 
 
 
 
94
 
95
  if not html_links and not pdf_links:
96
+ return "❌ No internal pages or PDFs found.", None
97
 
 
98
  with tempfile.NamedTemporaryFile(delete=False, suffix=".zip") as temp_zip:
99
  zip_path = temp_zip.name
100
 
 
 
 
 
 
101
  session = requests.Session()
102
+ session.headers.update({"User-Agent": "Mozilla/5.0"})
103
+
104
+ html_ok = 0
105
+ pdf_ok = 0
106
 
107
  with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zip_file:
108
+
109
+ # ---- HTML β†’ Markdown ----
110
+ for i, link_url in enumerate(html_links, 1):
 
 
 
 
 
 
 
 
 
 
 
111
  try:
112
  resp = session.get(link_url, timeout=10)
 
 
113
  soup = BeautifulSoup(resp.content, "html.parser")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
 
115
+ for tag in soup(["script","style","nav","footer","header","aside"]):
116
+ tag.decompose()
117
+
118
+ main_content = (
119
+ soup.find("main")
120
+ or soup.find("article")
121
+ or soup.find("body")
122
+ )
123
+
124
+ markdown_text = md(str(main_content))
125
+ title = soup.find("title")
126
+ if title:
127
+ markdown_text = f"# {title.text.strip()}\n\n{markdown_text}"
128
+
129
+ filename = f"page_{i}.md"
130
  zip_file.writestr(filename, markdown_text)
131
+ html_ok += 1
132
+
133
+ except Exception:
134
+ pass
 
 
 
 
135
 
136
+ # ---- PDFs ----
137
+ for j, pdf_url in enumerate(pdf_links, 1):
 
 
 
 
138
  try:
139
  resp = session.get(pdf_url, timeout=20)
140
+ zip_file.writestr(f"pdfs/document_{j}.pdf", resp.content)
141
+ pdf_ok += 1
142
+ except Exception:
143
+ pass
 
 
 
 
 
144
 
145
+ message = f"""
146
+ βœ… Extraction completed!
147
 
148
+ β€’ HTML pages saved as Markdown: {html_ok}
149
+ β€’ PDFs downloaded: {pdf_ok}
 
 
 
 
 
 
 
 
 
 
 
 
150
 
151
+ You can now download the ZIP file below.
152
+ """
153
+ return message, zip_path
154
 
155
  except Exception as e:
156
  return f"❌ Error: {str(e)}", None
157
 
158
 
159
  # ===========================================================
160
+ # 🎨 BEAUTIFUL GRADIO WEB APP
161
  # ===========================================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
 
163
+ def run_extraction(url, max_links, depth):
164
+ return extract_all_content_as_zip(url, int(max_links), int(depth))
165
+
166
+
167
+ with gr.Blocks(theme=gr.themes.Soft(), title="Website Content Extractor") as app:
168
+
 
 
169
  gr.Markdown("""
170
+ # 🌍 Website Content & PDF Extractor
171
+
172
+ This tool downloads the **text and PDFs from a website** and packages everything into a ZIP file.
173
+
174
+ Perfect for:
175
+ β€’ Research
176
+ β€’ Archiving documentation
177
+ β€’ Creating AI knowledge bases
178
+ β€’ Offline reading
179
+ """)
180
+
181
+ with gr.Box():
182
+ gr.Markdown("## 🧭 How to use")
183
+
184
+ gr.Markdown("""
185
+ 1️⃣ Enter the homepage of a website
186
+ 2️⃣ Choose how deep the crawler should explore
187
+ 3️⃣ Click **Start Extraction**
188
+ 4️⃣ Download your ZIP file
189
+
190
+ ⚠️ Large websites may take several minutes.
191
+ """)
192
+
193
+ with gr.Row():
194
  url_input = gr.Textbox(
195
  label="Website URL",
196
+ placeholder="https://example.com"
 
197
  )
198
+
199
+ with gr.Row():
200
+ max_links_input = gr.Slider(
201
+ 10, 200, value=50, step=10,
202
+ label="Maximum pages to scan",
203
+ info="Higher = more content but slower"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
204
  )
205
+
206
+ depth_input = gr.Slider(
207
+ 1, 3, value=2, step=1,
208
+ label="Crawl depth",
209
+ info="How many clicks away from homepage"
210
  )
211
+
212
+ run_btn = gr.Button("πŸš€ Start Extraction", variant="primary")
213
+
214
+ status_output = gr.Textbox(label="Status")
215
+ file_output = gr.File(label="Download ZIP")
216
+
217
+ run_btn.click(
218
+ fn=run_extraction,
219
+ inputs=[url_input, max_links_input, depth_input],
220
+ outputs=[status_output, file_output]
221
  )
 
 
 
 
 
222
 
223
 
224
  # ===========================================================
225
+ # πŸš€ ENTRY POINT β€” WEB APP ONLY
226
  # ===========================================================
227
+
228
  if __name__ == "__main__":
229
+ app.launch(server_name="0.0.0.0", server_port=7860)