mktgtech commited on
Commit
21d5d62
·
verified ·
1 Parent(s): f31f1f4

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +91 -38
app.py CHANGED
@@ -1,7 +1,7 @@
1
  """
2
- B2B Content Strategy Planner (Master Industry Edition)
3
  Deployed for Hugging Face Spaces.
4
- Features: 25+ Industry Packs, Multi-Model AI, Visual Competitive Intelligence.
5
  """
6
 
7
  import gradio as gr
@@ -21,6 +21,15 @@ from typing import List, Dict, Set, Tuple, Optional
21
  # 1. CONFIGURATION & MASSIVE TOPIC PACKS
22
  # ============================================================================
23
 
 
 
 
 
 
 
 
 
 
24
  TOPIC_PACKS = {
25
  # --- Tech & Infra ---
26
  "cloud": ["cloud", "cloud-computing", "saas", "paas", "iaas", "cloud-migration", "cloud-native", "multicloud", "hybrid-cloud", "serverless"],
@@ -73,7 +82,7 @@ AI_MODELS = [
73
  ]
74
 
75
  # ============================================================================
76
- # 2. CRAWLER & UTILS
77
  # ============================================================================
78
 
79
  def normalize_str(s: str) -> str:
@@ -94,49 +103,96 @@ def classify_stage(url: str) -> str:
94
  return 'TOFU'
95
 
96
  def crawl_website_unlimited(start_url: str):
97
- """Robust Crawler"""
98
  if not start_url.startswith(('http://', 'https://')): start_url = f'https://{start_url}'
99
- domain = urlparse(start_url).netloc.replace('www.', '')
 
 
 
 
 
 
100
 
101
  queue = [start_url]
102
  visited = set()
103
  found_urls = []
104
- headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'}
105
 
106
- while queue and len(found_urls) < 300: # Limit for demo speed
 
 
 
107
  curr = queue.pop(0)
108
  if curr in visited: continue
109
  visited.add(curr)
110
 
111
  try:
112
- time.sleep(0.05)
113
- r = requests.get(curr, headers=headers, timeout=5)
114
- if 'text/html' not in r.headers.get('Content-Type', '').lower(): continue
115
 
116
- soup = BeautifulSoup(r.text, 'lxml')
117
- if r.status_code == 200: found_urls.append(curr)
 
 
 
 
 
 
118
 
119
  for a in soup.find_all('a', href=True):
120
- full = urljoin(curr, a['href']).split('#')[0].split('?')[0].rstrip('/')
121
- if domain in full and full not in visited:
122
- queue.append(full)
123
- except: continue
 
 
 
 
 
 
 
 
 
 
124
 
125
  return list(set(found_urls))
126
 
127
  def fetch_sitemap_links(url: str):
128
- # Try XML first
 
129
  if not url.startswith('http'): url = f'https://{url}'
130
- try:
131
- r = requests.get(f"{url.rstrip('/')}/sitemap.xml", timeout=5)
132
- if r.status_code == 200:
133
- urls = re.findall(r'<loc>(.*?)</loc>', r.text)
134
- if len(urls) > 5: return list(set(urls)), f"✅ Found {len(urls)} URLs (XML)"
135
- except: pass
136
 
137
- # Fallback crawl
138
- urls = crawl_website_unlimited(url)
139
- return urls, f"✅ Crawled {len(urls)} pages"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
 
141
  def categorize_links(urls):
142
  cats = defaultdict(list)
@@ -150,7 +206,13 @@ def categorize_links(urls):
150
  break
151
  if not assigned:
152
  parts = list(filter(None, urlparse(url).path.split('/')))
153
- if parts: cats[parts[0].title()].append(parts[-1])
 
 
 
 
 
 
154
  return dict(cats)
155
 
156
  # ============================================================================
@@ -379,7 +441,7 @@ def ui_fetch(url):
379
  cats = categorize_links(urls)
380
  state['urls'] = urls
381
  cat_list = ["All"] + sorted(list(cats.keys()))
382
- return urls, cats, msg, gr.update(choices=cat_list, value="All"), gr.update(choices=[])
383
 
384
  def ui_update_topics(category, categories_data):
385
  if not categories_data: return gr.update(choices=[])
@@ -392,7 +454,7 @@ def ui_analyze_planner(urls, category, topic):
392
  if not urls: return "Fetch data first.", None, None
393
  if not topic: return "Select a topic.", None, None
394
 
395
- # Match logic reused from previous (simplified for brevity)
396
  matched = [u for u in urls if normalize_str(topic) in path_norm(u)]
397
 
398
  stages = [classify_stage(u) for u in matched]
@@ -417,14 +479,6 @@ def ui_add_comp(url, current_comps):
417
  txt = "\n".join([f"✅ {c['domain']}: {c['stats']['Count']} pages" for c in current_comps])
418
  return current_comps, txt, ""
419
 
420
- def ui_comp_search(user_urls, competitors, gap_topic, str_topic):
421
- if not user_urls: return "Fetch user data first.", ""
422
-
423
- g_html, _ = analyze_gaps_strengths(user_urls, competitors, gap_topic)
424
- _, s_html = analyze_gaps_strengths(user_urls, competitors, str_topic)
425
-
426
- return g_html, s_html
427
-
428
  def ui_comp_master(user_urls, competitors):
429
  if not user_urls: return None, "", "", ""
430
 
@@ -444,7 +498,6 @@ def ui_comp_master(user_urls, competitors):
444
  g_html, s_html = analyze_gaps_strengths(user_urls, competitors, None)
445
 
446
  # 3. Strategy
447
- # Extract raw lists for summary
448
  raw_gaps = g_html.count("<span")
449
  raw_str = s_html.count("<span")
450
  strat = generate_strategic_summary(range(raw_gaps), range(raw_str))
 
1
  """
2
+ B2B Content Strategy Planner (Final Robust Edition)
3
  Deployed for Hugging Face Spaces.
4
+ Features: Browser-Like Fetching, Multi-Model AI, Visual Intelligence, 25+ Industry Packs.
5
  """
6
 
7
  import gradio as gr
 
21
  # 1. CONFIGURATION & MASSIVE TOPIC PACKS
22
  # ============================================================================
23
 
24
+ # Global Headers to mimic a real browser (Fixes "Not able to fetch" issues)
25
+ HEADERS = {
26
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
27
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
28
+ 'Accept-Language': 'en-US,en;q=0.5',
29
+ 'Connection': 'keep-alive',
30
+ 'Upgrade-Insecure-Requests': '1'
31
+ }
32
+
33
  TOPIC_PACKS = {
34
  # --- Tech & Infra ---
35
  "cloud": ["cloud", "cloud-computing", "saas", "paas", "iaas", "cloud-migration", "cloud-native", "multicloud", "hybrid-cloud", "serverless"],
 
82
  ]
83
 
84
  # ============================================================================
85
+ # 2. ROBUST CRAWLER & FETCHER (FIXED)
86
  # ============================================================================
87
 
88
  def normalize_str(s: str) -> str:
 
103
  return 'TOFU'
104
 
105
  def crawl_website_unlimited(start_url: str):
106
+ """Robust Crawler with Real Browser Headers"""
107
  if not start_url.startswith(('http://', 'https://')): start_url = f'https://{start_url}'
108
+
109
+ # Parse domain safely
110
+ try:
111
+ parsed_url = urlparse(start_url)
112
+ domain = parsed_url.netloc.replace('www.', '')
113
+ if not domain: return []
114
+ except: return []
115
 
116
  queue = [start_url]
117
  visited = set()
118
  found_urls = []
 
119
 
120
+ # Safety limit
121
+ max_pages = 250
122
+
123
+ while queue and len(found_urls) < max_pages:
124
  curr = queue.pop(0)
125
  if curr in visited: continue
126
  visited.add(curr)
127
 
128
  try:
129
+ time.sleep(0.1) # Small delay to avoid blocking
130
+ r = requests.get(curr, headers=HEADERS, timeout=8)
 
131
 
132
+ # Skip non-HTML
133
+ content_type = r.headers.get('Content-Type', '').lower()
134
+ if 'text/html' not in content_type: continue
135
+
136
+ if r.status_code == 200:
137
+ found_urls.append(curr)
138
+
139
+ soup = BeautifulSoup(r.text, 'lxml') # Uses lxml for speed
140
 
141
  for a in soup.find_all('a', href=True):
142
+ href = a['href'].strip()
143
+ full = urljoin(curr, href)
144
+
145
+ # Basic cleaning
146
+ clean_url = full.split('#')[0].split('?')[0].rstrip('/')
147
+
148
+ # Check domain match
149
+ if domain in urlparse(clean_url).netloc:
150
+ # Filter junk
151
+ if not any(ext in clean_url.lower() for ext in ['.pdf', '.jpg', '.png', '.css', '.js', 'login', 'signup']):
152
+ if clean_url not in visited and clean_url not in queue:
153
+ queue.append(clean_url)
154
+ except Exception:
155
+ continue
156
 
157
  return list(set(found_urls))
158
 
159
  def fetch_sitemap_links(url: str):
160
+ """Tries multiple sitemap locations, falls back to crawl"""
161
+ url = url.strip()
162
  if not url.startswith('http'): url = f'https://{url}'
 
 
 
 
 
 
163
 
164
+ base_url = f"{urlparse(url).scheme}://{urlparse(url).netloc}"
165
+
166
+ # Common sitemap locations
167
+ candidates = [
168
+ url,
169
+ f"{base_url}/sitemap.xml",
170
+ f"{base_url}/sitemap_index.xml",
171
+ f"{base_url}/wp-sitemap.xml",
172
+ f"{base_url}/page-sitemap.xml"
173
+ ]
174
+
175
+ found_urls = []
176
+
177
+ # 1. Try Sitemaps
178
+ for candidate in candidates:
179
+ if not candidate.endswith('.xml') and candidate != url: continue
180
+ try:
181
+ r = requests.get(candidate, headers=HEADERS, timeout=10)
182
+ if r.status_code == 200:
183
+ # Use regex for speed & robustness against bad XML
184
+ urls = re.findall(r'<loc>(.*?)</loc>', r.text)
185
+ if len(urls) > 5:
186
+ found_urls = list(set(urls))
187
+ return found_urls, f"✅ Found {len(found_urls)} URLs via XML Sitemap ({candidate})"
188
+ except: continue
189
+
190
+ # 2. Fallback to Crawler if sitemap failed
191
+ crawl_urls = crawl_website_unlimited(base_url)
192
+ if len(crawl_urls) > 0:
193
+ return crawl_urls, f"✅ Crawled {len(crawl_urls)} pages (Sitemap not found)"
194
+
195
+ return [], "❌ Could not fetch content. Site might be blocking bots."
196
 
197
  def categorize_links(urls):
198
  cats = defaultdict(list)
 
206
  break
207
  if not assigned:
208
  parts = list(filter(None, urlparse(url).path.split('/')))
209
+ if parts:
210
+ # Use first folder as category if sensible
211
+ cat_candidate = parts[0].title()
212
+ if len(cat_candidate) < 20 and not any(x in cat_candidate.lower() for x in ['202', '0', '1']):
213
+ cats[cat_candidate].append(parts[-1])
214
+ else:
215
+ cats["General"].append(parts[-1])
216
  return dict(cats)
217
 
218
  # ============================================================================
 
441
  cats = categorize_links(urls)
442
  state['urls'] = urls
443
  cat_list = ["All"] + sorted(list(cats.keys()))
444
+ return urls, cats, msg, gr.update(choices=cat_list, value="All"), gr.update(choices=[], value=None)
445
 
446
  def ui_update_topics(category, categories_data):
447
  if not categories_data: return gr.update(choices=[])
 
454
  if not urls: return "Fetch data first.", None, None
455
  if not topic: return "Select a topic.", None, None
456
 
457
+ # Match logic
458
  matched = [u for u in urls if normalize_str(topic) in path_norm(u)]
459
 
460
  stages = [classify_stage(u) for u in matched]
 
479
  txt = "\n".join([f"✅ {c['domain']}: {c['stats']['Count']} pages" for c in current_comps])
480
  return current_comps, txt, ""
481
 
 
 
 
 
 
 
 
 
482
  def ui_comp_master(user_urls, competitors):
483
  if not user_urls: return None, "", "", ""
484
 
 
498
  g_html, s_html = analyze_gaps_strengths(user_urls, competitors, None)
499
 
500
  # 3. Strategy
 
501
  raw_gaps = g_html.count("<span")
502
  raw_str = s_html.count("<span")
503
  strat = generate_strategic_summary(range(raw_gaps), range(raw_str))