seo_audit.py 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. import sys
  2. import hashlib
  3. import requests
  4. import xml.etree.ElementTree as ET
  5. from bs4 import BeautifulSoup
  6. from collections import defaultdict
  7. from urllib.parse import urlparse
  8. # Force UTF-8 for console output if possible
  9. if sys.stdout.encoding != 'utf-8':
  10. try:
  11. import codecs
  12. sys.stdout = codecs.getwriter('utf-8')(sys.stdout.buffer, 'strict')
  13. sys.stderr = codecs.getwriter('utf-8')(sys.stderr.buffer, 'strict')
  14. except:
  15. pass
  16. # Production Settings
  17. PRODUCTION_DOMAIN = "https://radionica3d.me"
  18. PRODUCTION_SITEMAP = f"{PRODUCTION_DOMAIN}/sitemap.xml"
  19. def get_content_hash(soup):
  20. # Remove elements that change frequently or are boilerplate
  21. clean_soup = BeautifulSoup(str(soup), "lxml")
  22. for tag in clean_soup(["script", "style", "header", "footer", "nav", "aside", "noscript"]):
  23. tag.decompose()
  24. text = clean_soup.get_text(separator=' ', strip=True)
  25. return hashlib.md5(text.encode('utf-8')).hexdigest()
  26. def load_sitemap(url):
  27. print(f"Loading production sitemap from {url}...")
  28. urls = set()
  29. try:
  30. response = requests.get(url, timeout=15)
  31. response.raise_for_status()
  32. root = ET.fromstring(response.content)
  33. ns = {'ns': 'http://www.sitemaps.org/schemas/sitemap/0.9'}
  34. for loc in root.findall('.//ns:loc', ns):
  35. urls.add(loc.text.strip())
  36. except Exception as e:
  37. print(f"Error loading sitemap: {e}")
  38. return urls
  39. def audit_production():
  40. sitemap_urls = load_sitemap(PRODUCTION_SITEMAP)
  41. if not sitemap_urls:
  42. print("No URLs found. Is the site online?")
  43. return
  44. print(f"\n--- Starting Production SEO Audit ({len(sitemap_urls)} URLs) ---")
  45. print(f"Target: {PRODUCTION_DOMAIN}\n")
  46. results = []
  47. content_map = defaultdict(list)
  48. errors = []
  49. warnings = []
  50. info = []
  51. session = requests.Session()
  52. # Mask as a real browser to see what Google/Users see
  53. session.headers.update({
  54. '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'
  55. })
  56. for i, url in enumerate(sorted(sitemap_urls), 1):
  57. print(f"[{i}/{len(sitemap_urls)}] Checking: {url} ...", end="\r")
  58. try:
  59. response = session.get(url, timeout=15, allow_redirects=True)
  60. if response.history:
  61. # Page with redirect
  62. info.append(f"Page with redirect: {url} (Google sees redirect to {response.url})")
  63. if response.status_code != 200:
  64. errors.append(f"HTTP {response.status_code} for {url}")
  65. continue
  66. soup = BeautifulSoup(response.text, "lxml")
  67. title = soup.title.string if soup.title else None
  68. description = soup.find("meta", attrs={"name": "description"})
  69. description = description["content"] if description else None
  70. canonical = soup.find("link", rel="canonical")
  71. canonical = canonical["href"] if canonical else None
  72. h1 = [h.get_text(strip=True) for h in soup.find_all("h1")]
  73. content_hash = get_content_hash(soup)
  74. page_data = {
  75. "url": url,
  76. "title": title,
  77. "description": description,
  78. "canonical": canonical,
  79. "h1": h1,
  80. "hash": content_hash,
  81. "status": response.status_code
  82. }
  83. results.append(page_data)
  84. content_map[content_hash].append(page_data)
  85. except Exception as e:
  86. errors.append(f"Failed to fetch {url}: {e}")
  87. print("\n\nAnalyzing results...")
  88. # 1. Duplicates Analysis (GSC Style)
  89. for content_hash, pages in content_map.items():
  90. if len(pages) > 1:
  91. canonicals = set(p['canonical'] for p in pages if p['canonical'])
  92. urls_str = "\n ".join(p['url'] for p in pages)
  93. if not canonicals:
  94. errors.append(f"Page is a duplicate. Canonical version not selected by user:\n {urls_str}")
  95. elif len(canonicals) > 1:
  96. warnings.append(f"Duplicate, submitted URL not selected as canonical (Multiple canonicals found):\n {urls_str}")
  97. else:
  98. info.append(f"Duplicate content correctly handled by canonical ({list(canonicals)[0]})")
  99. # 2. Canonical & SEO Tag Analysis
  100. for p in results:
  101. # Canonical Tag
  102. if not p['canonical']:
  103. warnings.append(f"Indexing issue: Missing canonical tag (Google will choose one): {p['url']}")
  104. elif p['canonical'] != p['url']:
  105. info.append(f"Variant of page with canonical tag: {p['url']} -> {p['canonical']}")
  106. if not p['canonical'].startswith(PRODUCTION_DOMAIN):
  107. warnings.append(f"Canonical points to EXTERNAL domain: {p['url']} -> {p['canonical']}")
  108. # Meta Tags
  109. if not p['title']: errors.append(f"SEO Error: Missing <title> tag: {p['url']}")
  110. if not p['description']: warnings.append(f"SEO Warning: Missing <meta description>: {p['url']}")
  111. if not p['h1']: warnings.append(f"SEO Warning: Missing <h1> tag: {p['url']}")
  112. elif len(p['h1']) > 1: warnings.append(f"SEO Warning: Multiple <h1> tags: {p['url']} ({len(p['h1'])})")
  113. # Report
  114. print(f"\nAudit Summary for {PRODUCTION_DOMAIN}:")
  115. print(f"- Total pages checked: {len(results)}")
  116. print(f"- Errors: {len(errors)}")
  117. print(f"- Warnings: {len(warnings)}")
  118. print("\n--- ERRORS (Fix immediately) ---")
  119. if not errors: print("None")
  120. for e in errors:
  121. try: print(f"❌ {e}")
  122. except: print(f"ERROR: {e.encode('ascii', 'ignore').decode()}")
  123. print("\n--- WARNINGS (Recommended fixes) ---")
  124. if not warnings: print("None")
  125. for w in warnings:
  126. try: print(f"⚠️ {w}")
  127. except: print(f"WARNING: {w.encode('ascii', 'ignore').decode()}")
  128. print("\n--- INFO (Status OK / Handled) ---")
  129. if not info: print("None")
  130. for i in info:
  131. try: print(f"✅ {i}")
  132. except: print(f"INFO: {i.encode('ascii', 'ignore').decode()}")
  133. print("\n--- Audit Complete ---")
  134. if __name__ == "__main__":
  135. audit_production()