prince1604 commited on
Commit
b458f3d
·
0 Parent(s):

Initial commit for deployment

Browse files
.idea/.gitignore ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # Default ignored files
2
+ /shelf/
3
+ /workspace.xml
.idea/inspectionProfiles/profiles_settings.xml ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ <component name="InspectionProjectProfileManager">
2
+ <settings>
3
+ <option name="USE_PROJECT_PROFILE" value="false" />
4
+ <version value="1.0" />
5
+ </settings>
6
+ </component>
.idea/misc.xml ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="Black">
4
+ <option name="sdkName" value="Python 3.10 (webscreper)" />
5
+ </component>
6
+ <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10 (webscreper)" project-jdk-type="Python SDK" />
7
+ </project>
.idea/modules.xml ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="ProjectModuleManager">
4
+ <modules>
5
+ <module fileurl="file://$PROJECT_DIR$/.idea/webscreper.iml" filepath="$PROJECT_DIR$/.idea/webscreper.iml" />
6
+ </modules>
7
+ </component>
8
+ </project>
.idea/vcs.xml ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="VcsDirectoryMappings">
4
+ <mapping directory="" vcs="Git" />
5
+ </component>
6
+ </project>
.idea/webscreper.iml ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <module type="PYTHON_MODULE" version="4">
3
+ <component name="NewModuleRootManager">
4
+ <content url="file://$MODULE_DIR$">
5
+ <excludeFolder url="file://$MODULE_DIR$/.venv" />
6
+ </content>
7
+ <orderEntry type="jdk" jdkName="Python 3.10 (webscreper)" jdkType="Python SDK" />
8
+ <orderEntry type="sourceFolder" forTests="false" />
9
+ </component>
10
+ <component name="PyDocumentationSettings">
11
+ <option name="format" value="PLAIN" />
12
+ <option name="myDocStringFormat" value="Plain" />
13
+ </component>
14
+ </module>
DEPLOYMENT_GUIDE.md ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🚀 Deployment Guide (Free & Lifetime)
2
+
3
+ This guide will show you how to deploy your Web Scraper API to **Render.com** for free. Render is an excellent cloud platform that supports Python and Playwright out of the box.
4
+
5
+ ## ✅ Prerequisites
6
+
7
+ 1. **GitHub Account**: You need a GitHub account to host your code.
8
+ 2. **Git Installed**: You must have Git installed on your computer.
9
+
10
+ ---
11
+
12
+ ## Step 1: Push Code to GitHub
13
+
14
+ First, we need to get your code onto GitHub.
15
+
16
+ 1. **Log in to GitHub** and create a **New Repository**.
17
+ * Name it something like `seo-scraper-api`.
18
+ * Make it **Public** or **Private** (Private is fine).
19
+ * **Do not** initialize with a README (we have code locally).
20
+
21
+ 2. **Open your terminal** in the project folder (`d:\webscreper`) and run these commands one by one:
22
+
23
+ ```bash
24
+ # Initialize git
25
+ git init
26
+
27
+ # Add all files
28
+ git add .
29
+
30
+ # Commit changes
31
+ git commit -m "Initial commit for deployment"
32
+
33
+ # Link to your new GitHub repo (Replace YOUR_USERNAME and REPO_NAME)
34
+ git remote add origin https://github.com/YOUR_USERNAME/seo-scraper-api.git
35
+
36
+ # Push code
37
+ git push -u origin master
38
+ ```
39
+
40
+ ---
41
+ tney aavdi gyu imean bolta
42
+ ## Step 2: Create a Web Service on Render
43
+
44
+ 1. Go to [dashboard.render.com](https://dashboard.render.com) and sign up/log in.
45
+ 2. Click the **"New +"** button and select **"Web Service"**.
46
+ 3. Connect your **GitHub** account.
47
+ 4. Find your `seo-scraper-api` repository and click **"Connect"**.
48
+
49
+ ---
50
+
51
+ ## Step 3: Configure the Service
52
+
53
+ Fill in the details exactly as follows to ensure it works with Flask and Playwright:
54
+
55
+ * **Name**: `seo-scraper` (or whatever you like)
56
+ * **Region**: Closest to you (e.g., Frankfurt or Oregon)
57
+ * **Branch**: `master`
58
+ * **Root Directory**: (Leave blank)
59
+ * **Runtime**: **Python 3**
60
+ * **Build Command**:
61
+ ```bash
62
+ ./build.sh
63
+ ```
64
+ * **Start Command**:
65
+ ```bash
66
+ gunicorn api:app
67
+ ```
68
+ * **Instance Type**: Select **Free**
69
+
70
+ ### ⚠️ crucial: Environment Variables
71
+
72
+ Scroll down to "Advanced" or "Environment Variables" and make sure these settings are correct. We already set up the `build.sh` script to handle dependencies, so this part should be automatic.
73
+
74
+ ---
75
+
76
+ ## Step 4: Deploy
77
+
78
+ 1. Click **"Create Web Service"**.
79
+ 2. Render will start building your app. You can watch the logs.
80
+ * It will install Python dependencies.
81
+ * It will install Chromium (for Playwright).
82
+ 3. Once finished, you will see a green checkmark **"Live"**.
83
+ 4. Copy your URL (e.g., `https://seo-scraper.onrender.com`).
84
+
85
+ ---
86
+
87
+ ## Step 5: Test Your API
88
+
89
+ Now you can use this URL in Postman instead of `127.0.0.1:5050`.
90
+
91
+ **New Request:**
92
+ * **Method**: `GET`
93
+ * **URL**: `https://YOUR-APP-NAME.onrender.com/api/seo-report?domain=https://example.com`
94
+
95
+ ---
96
+
97
+ ## ℹ️ Important Notes about "Free" Tier
98
+
99
+ 1. **Spin Down**: The free instance "sleeps" after 15 minutes of inactivity. The first request after sleeping might take 30-50 seconds to wake it up. This is normal.
100
+ 2. **Keep Alive (Optional)**: If you want it to stay awake, you can use a free "Uptime Monitor" (like UptimeRobot) to ping your API every 10 minutes.
101
+
102
+ You now have a fully functional, free, lifetime deployment of your scraper! 🚀
Procfile ADDED
@@ -0,0 +1 @@
 
 
1
+ web: gunicorn api:app
api.py ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, jsonify, request
2
+ from flask_cors import CORS
3
+ import json
4
+ import os
5
+ import random
6
+ from src.crawler import Crawler
7
+ from src.analyzer import ImageAnalyzer
8
+
9
+ app = Flask(__name__)
10
+ CORS(app)
11
+
12
+ REPORT_FILE = 'seo_report.json'
13
+
14
+ @app.route('/api/seo-report', methods=['GET', 'POST'])
15
+ def get_seo_report():
16
+
17
+ def get_param(name, default):
18
+ val = request.args.get(name) or request.form.get(name)
19
+ if val is None and request.is_json:
20
+ val = request.json.get(name)
21
+ return val if val is not None else default
22
+
23
+ domain = get_param('domain', None)
24
+ limit = int(get_param('limit', 25))
25
+
26
+ if domain:
27
+ try:
28
+ print(f"Starting live scan for: {domain}")
29
+
30
+ # Initialize Crawler
31
+ crawler = Crawler()
32
+ # Crawl the domain with the requested limit
33
+ site_data, total_discovered, _ = crawler.crawl_domain(domain, max_pages=limit)
34
+
35
+ if not site_data:
36
+ return jsonify({
37
+ "summary": {
38
+ "total_pages_scanned": 0,
39
+ "total_images_found": 0,
40
+ "total_images_missing_alt": 0,
41
+ "total_pages_discovered": 0
42
+ },
43
+ "details": []
44
+ })
45
+
46
+ # Analyze Results
47
+ analyzer = ImageAnalyzer()
48
+ results = analyzer.analyze_site(site_data)
49
+
50
+ # Add discovery stats
51
+ results['summary']['total_pages_discovered'] = total_discovered
52
+ results['details_count'] = len(results['details'])
53
+
54
+ return jsonify(results)
55
+
56
+ except Exception as e:
57
+ return jsonify({"error": f"Scraping failed: {str(e)}"}), 500
58
+
59
+ if not os.path.exists(REPORT_FILE):
60
+ return jsonify({"error": "Report file not found. Please run result logic first."}), 404
61
+
62
+ try:
63
+ with open(REPORT_FILE, 'r', encoding='utf-8') as f:
64
+ data = json.load(f)
65
+
66
+ # Get parameters for filtering existing report
67
+ # limit is already extracted above
68
+ random_param = str(get_param('random', 'true')).lower()
69
+ is_random = random_param == 'true'
70
+
71
+ summary = data.get('summary', {})
72
+ details = data.get('details', [])
73
+
74
+ # Filter details if limit is provided
75
+ if limit is not None and limit > 0:
76
+ if is_random and limit < len(details):
77
+ details = random.sample(details, limit)
78
+ else:
79
+ details = details[:limit]
80
+
81
+ response = {
82
+ "summary": summary,
83
+ "details_count": len(details), # Useful logic for client
84
+ "details": details
85
+ }
86
+
87
+ return jsonify(response)
88
+
89
+ except Exception as e:
90
+ return jsonify({"error": f"Failed to read report: {str(e)}"}), 500
91
+
92
+ if __name__ == '__main__':
93
+ # Run on 0.0.0.0 to be accessible if needed, default port 5000
94
+ app.run(debug=True, host='0.0.0.0', port=5050)
build.sh ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ # Exit on error
3
+ set -o errexit
4
+
5
+ pip install -r requirements.txt
6
+ playwright install chromium
current_reqs.txt ADDED
Binary file (1.2 kB). View file
 
debug_http.html ADDED
@@ -0,0 +1 @@
 
 
1
+ <!DOCTYPE html><html lang="en-US"><head><title>Just a moment...</title><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=Edge"><meta name="robots" content="noindex,nofollow"><meta name="viewport" content="width=device-width,initial-scale=1"><style>*{box-sizing:border-box;margin:0;padding:0}html{line-height:1.15;-webkit-text-size-adjust:100%;color:#313131;font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"}body{display:flex;flex-direction:column;height:100vh;min-height:100vh}.main-content{margin:8rem auto;padding-left:1.5rem;max-width:60rem}@media (width <= 720px){.main-content{margin-top:4rem}}.h2{line-height:2.25rem;font-size:1.5rem;font-weight:500}@media (width <= 720px){.h2{line-height:1.5rem;font-size:1.25rem}}#challenge-error-text{background-image:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgZmlsbD0ibm9uZSI+PHBhdGggZmlsbD0iI0IyMEYwMyIgZD0iTTE2IDNhMTMgMTMgMCAxIDAgMTMgMTNBMTMuMDE1IDEzLjAxNSAwIDAgMCAxNiAzbTAgMjRhMTEgMTEgMCAxIDEgMTEtMTEgMTEuMDEgMTEuMDEgMCAwIDEtMTEgMTEiLz48cGF0aCBmaWxsPSIjQjIwRjAzIiBkPSJNMTcuMDM4IDE4LjYxNUgxNC44N0wxNC41NjMgOS41aDIuNzgzem0tMS4wODQgMS40MjdxLjY2IDAgMS4wNTcuMzg4LjQwNy4zODkuNDA3Ljk5NCAwIC41OTYtLjQwNy45ODQtLjM5Ny4zOS0xLjA1Ny4zODktLjY1IDAtMS4wNTYtLjM4OS0uMzk4LS4zODktLjM5OC0uOTg0IDAtLjU5Ny4zOTgtLjk4NS40MDYtLjM5NyAxLjA1Ni0uMzk3Ii8+PC9zdmc+");background-repeat:no-repeat;background-size:contain;padding-left:34px}@media (prefers-color-scheme: dark){body{background-color:#222;color:#d9d9d9}}</style><meta http-equiv="refresh" content="360"></head><body><div class="main-wrapper" role="main"><div class="main-content"><noscript><div class="h2"><span id="challenge-error-text">Enable JavaScript and cookies to continue</span></div></noscript></div></div><script>(function(){window._cf_chl_opt = {cvId: '3',cZone: 'www.fbi.gov',cType: 'managed',cRay: '9c05546b1e633f68',cH: 'J0yrkRNPaDCziury9CSlfYwesCNpXp4kfPy4irDm.Ds-1768815197-1.2.1.1-q9YiSrWgTxtwx.XCt_qPc8YJxzgkXCfALBg_O0uaN1ZCE5Ze.MxpjZFgaFLPhMLY',cUPMDTk:"\/how-we-can-help-you\/victim-services?__cf_chl_tk=n6KGOAmWagfs6QF1_abE2BWYdTj2eUqXw1UwGom7jEY-1768815197-1.0.1.1-idHEIIdjaYWVO1lt5QRtWNsei_9ZLgurGHbuL02THdM",cFPWv: 'b',cITimeS: '1768815197',cTplC:0,cTplV:5,cTplB: '0',fa:"\/how-we-can-help-you\/victim-services?__cf_chl_f_tk=n6KGOAmWagfs6QF1_abE2BWYdTj2eUqXw1UwGom7jEY-1768815197-1.0.1.1-idHEIIdjaYWVO1lt5QRtWNsei_9ZLgurGHbuL02THdM",md: 'wj0rckWOoYO2WQkHlTxw1ExmOTg9NBIt0fRbvMzMw8I-1768815197-1.2.1.1-FuUpPe.mvUciLGY4rakp03B5Vsxa3rAN3.rhwvvK7KZQmscXEgw1VD_Acm1vdRIxhNyr9l4o02E2Z1KDevFXb0qukXJ.DJlLyB5fKS6MYTEkaP9dpT7eJM220EnEuUzD8gBPSXk1UW6__Iw7xhho0sAZpq.eCK6So5PN1NRTqxbz6Be3GUR4OJIoMULQlPLhvSEI42tmB_g8cibCXsR1xZtBjDXsB10DR_zIAD.NOn02SLPrWsUOE0fCtTwcc8MdEDZw1wSdWi6fkG13goTFOutuK5aPD.eb_b4mdamOrAEm3qoG42OxkXwrQB1gqb63NEaxyZdejqtO2772x4V0dbvDJ5sLtaQvleK7vpgQENG1Q03f.sLfFiI0fnhpOeAvROCtc7ow_sAAYNawKiVESJln_96MfClrzD8PUyhdCA.8nWc1ZWP.8qoQoC.8qgljsjaClSSyKvFjz9j0Mo41HADu5em5PDpeo5Y.VeFaaX6_HijDL5MkdFW.TEdy0U6_..9ZTMMComrsRSO9nUTzUZBEcdg1J9Q5JvZ2dF_TmIGJ5dvUDiuS2_qKyncU9QMstD0oiHW39vDP4OlAsYmsMvqx7OMZUcu3NWc_8RkiTfuMCzovqp66gOn9Tmh6jqpf7bx6bbkKSw_vl3hi4bKRFHqN4jT2DtMKROy5gz9YsTmtQWoMZmgv9EAVUGudQhg4xbIXyHULwVNh9oRLcsuJXmIB8AvdSZ6kjd97C3HAOWOkaIZn1YIb7UFyhHsrnBqq7lFU06OkdUnvVc3bjqv9VX.AeIR3h5ke0xO.N1GoqI3WWjpFKYeHf6JZ_nQP7m0JOaLfJ_tB7Ni2qSWEaZ1FE2QQVZN4yh_BgngRZFSNyiHvc6ytL8T.GLiFhu.IaeMYpgG7uYmqp6wmmPj.duEpLT_ttzSKJlsfjCjXri55DLlzfIfHfTdSSigKQKQEsqoBaHu85KHR13a7ZkoJmEhSBAnIGnowYa8HoxSHB0L9ldHOVfBnuq3tfMeTCmHHJ5oX9ZP3lLHIhgFeMHfyMS.9uojxpEun7vNv1kh.rYFnu2G169Sgty8uf_Ndw56GTVVKsPKoUf2TAeQUnm8vhD.drf9EAejQKp.paC.KTal1T28',mdrd: 'rSzKj773kw_pKp6Uryow95IH30LYZtHZH5zqRhYuMEs-1768815197-1.2.1.1-.2nU5JUt1K_Oy.DWKwUKZO4Kl8rDVO942Ll916fyIFt1l_aQOnJ8UDgzLDbkLe1uds_8NtD6SVT3TbXaHLN1f2Si0u2YL40nA4BWnPaf.bww65u1n8zlqBPhFazJJviMYv1na7Vifcs9DAmRAGICv36dNXMnxa0BbZVCcmxeXnVBaDej1_9pV8VZMiWrkzLTGDJi3vWZOpXt2LV6lK3ys_FGG0d7Rq2cPo2T4kdQyZ05pHAciJlIt1KcTn47n3N1yCpQoDPysyiohzlYjfX2jC6Ui6ZU0illV.dhrFu1C4HGo7_EP6iuSPH6y4e1sCi349WWcs7xpkU1FbQnOPzETRUki1mntwp0FuZbBtc0AvX82K5V_fpmYp9xSb2_6GP5YA52gHwnsvnPq.D6tCIwIrzeqsDRsw6PU55QSC8eFWd1j0hug5hIGYwkKzQRX20N7pWmGtc59t_gWpS_db.wUiE2HXlLSWbZvW8fDi283LLobk_Cpa5OosYa1guj6yjCgau8tKdbFmperQOCmhJZ4K4rTsktw4qqHSJ4f7fHa0m38VaWtnp20okJlaZXH5APc9MJv2QlD.djqZORc9lHklmfqKPdXB5wMSjslzac8zoINu8dURuUkhY8o3JXUgo83AKCf5WUSkAUBEG8ba6fFA1W6GGvyQxN.oeDxuKoMc3V2dMMDsRgGzaEaij.NWo.wxLaFFEC0EEVtMM.bKne9q1fLZ7u9.M0ZyjdzfFmb9dCNfRXzjNh8FwKssjmImGlxY50CMPgWaDK3CMSrS_aZSjs8mha_kAjq1MpINh08feCwCeiH5sXHYKxYJvxDz6VkSP5VyG8PW6P00JWY6xAiqY8csXmp2Ah.jS9x3KhUMowqhfzwWHsYriKeSGRkw5XBcqShThaskeuIfPrqmDPIWrqJtl0I3ByVZmNVnXY7.bp4i3o6xD61zb8vcTAr4XOHY.lt8ncpeNj0AfV8FTwMphc9FjggWPKK0DWyYmQ9Ah8eGPUSqfcaiJ1JdCM_BWQwJHwr8ctWjJAI1HL3PCJPz3MR55qPOVRYNqeQKhc4jA32fN9_h6ZAw5JhyRwNbws83eGQ1R2mdocBgG29NW_lOvKR0rYH9nbR3UHrqO5xo5ytha28imyfELwEyFWRU4ojms8DrLKeGgA7W3QKnlM90Z1jXjTrX0t4ypqzbbUHIUOzDlI0ibALHs92dAIb7Vi1BQj3Of1JL18u8Vn7ZqTT4MHrq8M7NFAvb12vW7cd2tSnfUPp2MM4Y1jvw75jibtZv0l6dFdcXd9xHVcJ3FhIlzfwBqT.K1FHHqJvEm59xMvL4HbHu0I0pdrPuUXkqsUo2Q2q4xEaMcZscdoLp7JKWYILfdQCP9dQIaR0pXSBMpM3snUpNH.GIyq08qev699t26KiN2L9TXfmNnizIoHj9viS1CToZ2FvJHL4HNNfHDdVbNzrBUS6LgCxSvtBql8M4ATuOFV.4QgbKT2O3Xew7g3V9BtCwfmLx7A3F4X9rgBjtirRTeuL.nFqd3WOnSh_phGEhm.8n8Fku5WcKiL9vQoOFvPNgAoq4qBgQxB254puHzDMEVGs_eGljrO87KDBaYGhFjj7gEwUy62GzZdkuW_ryolkHcIyrXMlzSIgQ5KLSZ_XH7yYA..QXMiJP8p4ju0Iy_6ycAW8EHzbH2LbjD46i9NobQHImqDtFHFE2oEZWu0UeA5ILk4dn8.5Zm4G3TBDCx1ETi4nKRFqW0nk6gOmvQKSVTmQP4KldusuJfvHWoImT_zzBQ.uLkaKc.dnBp3VWt.GJF_qWtEbGG7L75dypkzkhrS.IkajYZFOQMeEkdKPwYgcEU8RnstkMDoAN9tXn.DMpDx22Yun50j3R1j9CRRq9UMLswUX2LZygoM5vIWVDw9S_.EL5tMCJp_NSYP8NY3k_JWaZtAnnJMqBRVu_Qr0nhwYlzGK_tgkNmsMHGtU499uE58ZsmgA3e.iQ3ADmYtX9v_5t7TTtMf6HO8gOANHxBmMpI4KkMMr6FvR6DswYSnAPuNSEFz7UKkxfnAxTJa9G.pNwP.Qsr3iWlz1z_5xrVej1Rwq2sRn2xhGRq6Ud8ctFa.2vb4AaA6o4gD_.3YWb41oalPDTkUZBEjXMm2gTnehKwv3Q3JMYUU1nIODJwu_IOryVgOaHKACjOiNaaJ3DgHJu9WA839m3_fZYShU6SxIX2TWTZBwDmhyYsuVGnqswNTmbIwf1P5g6LzUn.izvrUfLNtqeZ5qqj3k0UbdOqDnZ5h0c5dP5r0UdTMBMmid26Ji47SuK0eu4E1R2T4FqgPOHc.Phy7.SUJE0d5FSKpoBklI_d5AwZg1Mzuw.FhsfrigHo0KDlk6bbDpk348x3Yy5iMmEzqfpRw_0ufJgaRpT88yUM9j3lOAQeEhL4jTEsdO_GpI8rj7ZF5sYPnAeHHxLXQH6m9vs7x40RS8k5cYxliKwlhWdZARRr11Cjk0gsOnS.DSty8Y3gYnvv02yEWY2yxG.RfyVdewVRDHnf7XWQ7mnpGOg5VG6n5RaSvsjZLHQn66kQC0l0vC8FZX0zBRMmUIOPnk7uX.0zzjhxTweuxni8CoMYmVV6HRyKHiGFTscB5zWmJIE8NNuL8Yf64ikt_VRGiMrHqg8H3z.usm3or5fS9UBlvpqv36H0fzEMfJMqtjbo.i2IUK2kVsvnWPoo3nVQcPqnJqKp35q1MItpVhAzRE4I',};var a = document.createElement('script');a.src = '/cdn-cgi/challenge-platform/h/b/orchestrate/chl_page/v1?ray=9c05546b1e633f68';window._cf_chl_opt.cOgUHash = location.hash === '' && location.href.indexOf('#') !== -1 ? '#' : location.hash;window._cf_chl_opt.cOgUQuery = location.search === '' && location.href.slice(0, location.href.length - window._cf_chl_opt.cOgUHash.length).indexOf('?') !== -1 ? '?' : location.search;if (window.history && window.history.replaceState) {var ogU = location.pathname + window._cf_chl_opt.cOgUQuery + window._cf_chl_opt.cOgUHash;history.replaceState(null, null,"\/how-we-can-help-you\/victim-services?__cf_chl_rt_tk=n6KGOAmWagfs6QF1_abE2BWYdTj2eUqXw1UwGom7jEY-1768815197-1.0.1.1-idHEIIdjaYWVO1lt5QRtWNsei_9ZLgurGHbuL02THdM"+ window._cf_chl_opt.cOgUHash);a.onload = function() {history.replaceState(null, null, ogU);}}document.getElementsByTagName('head')[0].appendChild(a);}());</script></body></html>
main.py ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+ import sys
3
+ from src.crawler import Crawler
4
+ from src.analyzer import ImageAnalyzer
5
+ from src.reporter import generate_json_report, generate_html_report
6
+
7
+ def main():
8
+ parser = argparse.ArgumentParser(description="Web Image Alt Text Checker (Whole Domain)")
9
+ parser.add_argument("url", help="The Start URL of the domain to crawl (e.g. https://example.com)")
10
+ parser.add_argument("--output", choices=['json', 'html', 'both'], default='html', help="Report output format")
11
+ parser.add_argument("--limit", type=int, default=100, help="Max pages to crawl (default 100)")
12
+ args = parser.parse_args()
13
+
14
+ # Crawl
15
+ # Crawl
16
+ print(f"Starting domain crawl from: {args.url}")
17
+ crawler = Crawler()
18
+ site_data, total_discovered, blocked_reason = crawler.crawl_domain(args.url, max_pages=args.limit)
19
+
20
+ if not site_data and blocked_reason:
21
+ print(f"\n[!] CRAWL BLOCKED: {blocked_reason}")
22
+ # Proceed to generate report even if empty to show the block status
23
+ elif not site_data:
24
+ print("No data crawled. Please check the URL and try again.")
25
+ sys.exit(1)
26
+
27
+ print(f"Crawl complete. Scanned {len(site_data)} pages.")
28
+
29
+ # Analyze
30
+ print("Analyzing images for missing alt text...")
31
+ analyzer = ImageAnalyzer()
32
+ results = analyzer.analyze_site(site_data)
33
+
34
+ # Add discovery stats to summary
35
+ results['summary']['total_pages_discovered'] = total_discovered
36
+ results['summary']['blocked_reason'] = blocked_reason
37
+ results['summary']['crawl_blocked'] = bool(blocked_reason)
38
+
39
+ # Report
40
+ print("Generating reports...")
41
+ if args.output in ['json', 'both']:
42
+ generate_json_report(results, "seo_report.json")
43
+ if args.output in ['html', 'both']:
44
+ generate_html_report(results, "seo_report.html")
45
+
46
+ summary = results['summary']
47
+ print("\n--- Summary ---")
48
+ if summary.get('crawl_blocked'):
49
+ print(f"STATUS: BLOCKED ({summary['blocked_reason']})")
50
+ print("Results are likely incomplete.")
51
+
52
+ print(f"Total Pages Discovered: {summary['total_pages_discovered']}")
53
+ print(f"Pages Scanned: {summary['total_pages_scanned']}")
54
+ print(f"Total Images: {summary['total_images_found']}")
55
+ print(f"Images Missing Alt: {summary['total_images_missing_alt']}")
56
+
57
+ if summary['total_images_missing_alt'] > 0:
58
+ print("Please check the generated report for details.")
59
+ elif not summary.get('crawl_blocked'):
60
+ print("All images have alt text! Great job.")
61
+
62
+ if __name__ == "__main__":
63
+ main()
qodana.yaml ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #-------------------------------------------------------------------------------#
2
+ # Qodana analysis is configured by qodana.yaml file #
3
+ # https://www.jetbrains.com/help/qodana/qodana-yaml.html #
4
+ #-------------------------------------------------------------------------------#
5
+ version: "1.0"
6
+ #Specify inspection profile for code analysis
7
+ profile:
8
+ name: qodana.starter
9
+ #Enable inspections
10
+ #include:
11
+ # - name: <SomeEnabledInspectionId>
12
+ #Disable inspections
13
+ #exclude:
14
+ # - name: <SomeDisabledInspectionId>
15
+ # paths:
16
+ # - <path/where/not/run/inspection>
17
+ #Execute shell command before Qodana execution (Applied in CI/CD pipeline)
18
+ #bootstrap: sh ./prepare-qodana.sh
19
+ #Install IDE plugins before Qodana execution (Applied in CI/CD pipeline)
20
+ #plugins:
21
+ # - id: <plugin.id> #(plugin id can be found at https://plugins.jetbrains.com)
22
+ #Specify Qodana linter for analysis (Applied in CI/CD pipeline)
23
+ linter: jetbrains/qodana-<linter>:2024.3
requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ requests
2
+ beautifulsoup4
3
+ Pillow
4
+ fake-useragent
5
+ cloudscraper
6
+ curl_cffi
7
+ flask
8
+ flask-cors
9
+ playwright
10
+ gunicorn
seo_report.json ADDED
@@ -0,0 +1,227 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "summary": {
3
+ "total_pages_scanned": 25,
4
+ "total_images_found": 233,
5
+ "total_images_missing_alt": 68,
6
+ "total_pages_discovered": 301,
7
+ "blocked_reason": null,
8
+ "crawl_blocked": false
9
+ },
10
+ "details": [
11
+ {
12
+ "page_url": "https://www.fbi.gov/",
13
+ "missing_alt_count": 2,
14
+ "images_without_alt": [
15
+ "https://www.fbi.gov/++theme++fbigov.theme/uswds-2.9.0/img/icon-dot-gov.svg",
16
+ "https://www.fbi.gov/++theme++fbigov.theme/uswds-2.9.0/img/icon-https.svg"
17
+ ]
18
+ },
19
+ {
20
+ "page_url": "https://www.fbi.gov/history",
21
+ "missing_alt_count": 3,
22
+ "images_without_alt": [
23
+ "https://www.fbi.gov/++theme++fbigov.theme/images/fbi_seal_new.png",
24
+ "https://www.fbi.gov/++theme++fbigov.theme/uswds-2.9.0/img/icon-dot-gov.svg",
25
+ "https://www.fbi.gov/++theme++fbigov.theme/uswds-2.9.0/img/icon-https.svg"
26
+ ]
27
+ },
28
+ {
29
+ "page_url": "https://www.fbi.gov/how-we-can-help-you/active-shooter-safety-resources",
30
+ "missing_alt_count": 0,
31
+ "images_without_alt": []
32
+ },
33
+ {
34
+ "page_url": "https://www.fbi.gov/about/faqs",
35
+ "missing_alt_count": 3,
36
+ "images_without_alt": [
37
+ "https://www.fbi.gov/++theme++fbigov.theme/images/fbi_seal_new.png",
38
+ "https://www.fbi.gov/++theme++fbigov.theme/uswds-2.9.0/img/icon-dot-gov.svg",
39
+ "https://www.fbi.gov/++theme++fbigov.theme/uswds-2.9.0/img/icon-https.svg"
40
+ ]
41
+ },
42
+ {
43
+ "page_url": "https://www.fbi.gov/about/faqs/can-i-obtain-detailed-information-about-a-current-fbi-investigation-that-i-see-in-the-news",
44
+ "missing_alt_count": 3,
45
+ "images_without_alt": [
46
+ "https://www.fbi.gov/++theme++fbigov.theme/images/fbi_seal_new.png",
47
+ "https://www.fbi.gov/++theme++fbigov.theme/uswds-2.9.0/img/icon-dot-gov.svg",
48
+ "https://www.fbi.gov/++theme++fbigov.theme/uswds-2.9.0/img/icon-https.svg"
49
+ ]
50
+ },
51
+ {
52
+ "page_url": "https://www.fbi.gov/investigate/violent-crime",
53
+ "missing_alt_count": 3,
54
+ "images_without_alt": [
55
+ "https://www.fbi.gov/++theme++fbigov.theme/images/fbi_seal_new.png",
56
+ "https://www.fbi.gov/++theme++fbigov.theme/uswds-2.9.0/img/icon-dot-gov.svg",
57
+ "https://www.fbi.gov/++theme++fbigov.theme/uswds-2.9.0/img/icon-https.svg"
58
+ ]
59
+ },
60
+ {
61
+ "page_url": "https://www.fbi.gov/about/faqs/what-career-paths-does-the-fbi-offer-besides-the-special-agent-role",
62
+ "missing_alt_count": 3,
63
+ "images_without_alt": [
64
+ "https://www.fbi.gov/++theme++fbigov.theme/images/fbi_seal_new.png",
65
+ "https://www.fbi.gov/++theme++fbigov.theme/uswds-2.9.0/img/icon-dot-gov.svg",
66
+ "https://www.fbi.gov/++theme++fbigov.theme/uswds-2.9.0/img/icon-https.svg"
67
+ ]
68
+ },
69
+ {
70
+ "page_url": "https://www.fbi.gov/how-we-can-help-you/victim-services",
71
+ "missing_alt_count": 0,
72
+ "images_without_alt": []
73
+ },
74
+ {
75
+ "page_url": "https://www.fbi.gov/about/faqs/what-is-the-fbis-foreign-counterintelligence-responsibility",
76
+ "missing_alt_count": 3,
77
+ "images_without_alt": [
78
+ "https://www.fbi.gov/++theme++fbigov.theme/images/fbi_seal_new.png",
79
+ "https://www.fbi.gov/++theme++fbigov.theme/uswds-2.9.0/img/icon-dot-gov.svg",
80
+ "https://www.fbi.gov/++theme++fbigov.theme/uswds-2.9.0/img/icon-https.svg"
81
+ ]
82
+ },
83
+ {
84
+ "page_url": "https://www.fbi.gov/about/faqs/has-the-ending-of-the-cold-war-reduced-the-amount-of-espionage",
85
+ "missing_alt_count": 3,
86
+ "images_without_alt": [
87
+ "https://www.fbi.gov/++theme++fbigov.theme/images/fbi_seal_new.png",
88
+ "https://www.fbi.gov/++theme++fbigov.theme/uswds-2.9.0/img/icon-dot-gov.svg",
89
+ "https://www.fbi.gov/++theme++fbigov.theme/uswds-2.9.0/img/icon-https.svg"
90
+ ]
91
+ },
92
+ {
93
+ "page_url": "https://www.fbi.gov/about/faqs/what-is-the-fbis-responsibility-in-bombing-cases",
94
+ "missing_alt_count": 3,
95
+ "images_without_alt": [
96
+ "https://www.fbi.gov/++theme++fbigov.theme/images/fbi_seal_new.png",
97
+ "https://www.fbi.gov/++theme++fbigov.theme/uswds-2.9.0/img/icon-dot-gov.svg",
98
+ "https://www.fbi.gov/++theme++fbigov.theme/uswds-2.9.0/img/icon-https.svg"
99
+ ]
100
+ },
101
+ {
102
+ "page_url": "https://artcrimes.fbi.gov",
103
+ "missing_alt_count": 7,
104
+ "images_without_alt": [
105
+ "https://artcrimes.fbi.gov/image-repository/nsaf_slide2.jpg/@@images/image/large",
106
+ "https://artcrimes.fbi.gov/image-repository/nsaf_slide1.jpg/@@images/image/large",
107
+ "https://artcrimes.fbi.gov/++theme++fbinsaf.theme/img/assets/logo/banner_1295x200.png",
108
+ "https://artcrimes.fbi.gov/++theme++fbinsaf.theme/uswds-2.9.0/img/icon-dot-gov.svg",
109
+ "https://artcrimes.fbi.gov/++theme++fbinsaf.theme/img/assets/logo/fbi-seal.png",
110
+ "https://artcrimes.fbi.gov/++theme++fbinsaf.theme/uswds-2.9.0/img/icon-https.svg",
111
+ "https://artcrimes.fbi.gov/image-repository/nsaf_slide3.jpg/@@images/image/large"
112
+ ]
113
+ },
114
+ {
115
+ "page_url": "https://www.fbi.gov/investigate/organized-crime",
116
+ "missing_alt_count": 3,
117
+ "images_without_alt": [
118
+ "https://www.fbi.gov/++theme++fbigov.theme/images/fbi_seal_new.png",
119
+ "https://www.fbi.gov/++theme++fbigov.theme/uswds-2.9.0/img/icon-dot-gov.svg",
120
+ "https://www.fbi.gov/++theme++fbigov.theme/uswds-2.9.0/img/icon-https.svg"
121
+ ]
122
+ },
123
+ {
124
+ "page_url": "https://www.fbi.gov/about/faqs/do-fbi-agents-work-with-state-local-or-other-law-enforcement-officers-on-task-forces",
125
+ "missing_alt_count": 3,
126
+ "images_without_alt": [
127
+ "https://www.fbi.gov/++theme++fbigov.theme/images/fbi_seal_new.png",
128
+ "https://www.fbi.gov/++theme++fbigov.theme/uswds-2.9.0/img/icon-dot-gov.svg",
129
+ "https://www.fbi.gov/++theme++fbigov.theme/uswds-2.9.0/img/icon-https.svg"
130
+ ]
131
+ },
132
+ {
133
+ "page_url": "https://www.fbi.gov/investigate",
134
+ "missing_alt_count": 3,
135
+ "images_without_alt": [
136
+ "https://www.fbi.gov/++theme++fbigov.theme/images/fbi_seal_new.png",
137
+ "https://www.fbi.gov/++theme++fbigov.theme/uswds-2.9.0/img/icon-dot-gov.svg",
138
+ "https://www.fbi.gov/++theme++fbigov.theme/uswds-2.9.0/img/icon-https.svg"
139
+ ]
140
+ },
141
+ {
142
+ "page_url": "https://www.fbi.gov/about/faqs/what-is-the-critical-incident-response-group",
143
+ "missing_alt_count": 3,
144
+ "images_without_alt": [
145
+ "https://www.fbi.gov/++theme++fbigov.theme/images/fbi_seal_new.png",
146
+ "https://www.fbi.gov/++theme++fbigov.theme/uswds-2.9.0/img/icon-dot-gov.svg",
147
+ "https://www.fbi.gov/++theme++fbigov.theme/uswds-2.9.0/img/icon-https.svg"
148
+ ]
149
+ },
150
+ {
151
+ "page_url": "https://www.fbi.gov/history/history-stories--multimedia",
152
+ "missing_alt_count": 3,
153
+ "images_without_alt": [
154
+ "https://www.fbi.gov/++theme++fbigov.theme/images/fbi_seal_new.png",
155
+ "https://www.fbi.gov/++theme++fbigov.theme/uswds-2.9.0/img/icon-dot-gov.svg",
156
+ "https://www.fbi.gov/++theme++fbigov.theme/uswds-2.9.0/img/icon-https.svg"
157
+ ]
158
+ },
159
+ {
160
+ "page_url": "https://www.fbi.gov/history/wall-of-honor/jimmie-john-daniels",
161
+ "missing_alt_count": 3,
162
+ "images_without_alt": [
163
+ "https://www.fbi.gov/++theme++fbigov.theme/images/fbi_seal_new.png",
164
+ "https://www.fbi.gov/++theme++fbigov.theme/uswds-2.9.0/img/icon-dot-gov.svg",
165
+ "https://www.fbi.gov/++theme++fbigov.theme/uswds-2.9.0/img/icon-https.svg"
166
+ ]
167
+ },
168
+ {
169
+ "page_url": "https://www.fbi.gov/investigate/violent-crime/gangs",
170
+ "missing_alt_count": 3,
171
+ "images_without_alt": [
172
+ "https://www.fbi.gov/++theme++fbigov.theme/images/fbi_seal_new.png",
173
+ "https://www.fbi.gov/++theme++fbigov.theme/uswds-2.9.0/img/icon-dot-gov.svg",
174
+ "https://www.fbi.gov/++theme++fbigov.theme/uswds-2.9.0/img/icon-https.svg"
175
+ ]
176
+ },
177
+ {
178
+ "page_url": "https://www.fbi.gov/about/faqs/what-does-the-fbi-do-with-information-and-evidence-gathered-during-an-investigation",
179
+ "missing_alt_count": 3,
180
+ "images_without_alt": [
181
+ "https://www.fbi.gov/++theme++fbigov.theme/images/fbi_seal_new.png",
182
+ "https://www.fbi.gov/++theme++fbigov.theme/uswds-2.9.0/img/icon-dot-gov.svg",
183
+ "https://www.fbi.gov/++theme++fbigov.theme/uswds-2.9.0/img/icon-https.svg"
184
+ ]
185
+ },
186
+ {
187
+ "page_url": "https://www.fbi.gov/contact-us/field-offices/baltimore/news/ms-13-clique-leader-indicted-in-connection-with-racketeering-conspiracy-involving-four-murders-and-two-attempted-murders-in-baltimore",
188
+ "missing_alt_count": 0,
189
+ "images_without_alt": []
190
+ },
191
+ {
192
+ "page_url": "https://www.fbi.gov/news/espanol",
193
+ "missing_alt_count": 2,
194
+ "images_without_alt": [
195
+ "https://www.fbi.gov/++theme++fbigov.theme/uswds-2.9.0/img/icon-dot-gov.svg",
196
+ "https://www.fbi.gov/++theme++fbigov.theme/uswds-2.9.0/img/icon-https.svg"
197
+ ]
198
+ },
199
+ {
200
+ "page_url": "https://www.fbi.gov/about/faqs/when-was-the-fbi-founded",
201
+ "missing_alt_count": 3,
202
+ "images_without_alt": [
203
+ "https://www.fbi.gov/++theme++fbigov.theme/images/fbi_seal_new.png",
204
+ "https://www.fbi.gov/++theme++fbigov.theme/uswds-2.9.0/img/icon-dot-gov.svg",
205
+ "https://www.fbi.gov/++theme++fbigov.theme/uswds-2.9.0/img/icon-https.svg"
206
+ ]
207
+ },
208
+ {
209
+ "page_url": "https://www.fbi.gov/image-repository/fbi-nasa-flag-certificate-070522.jpg/view",
210
+ "missing_alt_count": 3,
211
+ "images_without_alt": [
212
+ "https://www.fbi.gov/++theme++fbigov.theme/images/fbi_seal_new.png",
213
+ "https://www.fbi.gov/++theme++fbigov.theme/uswds-2.9.0/img/icon-dot-gov.svg",
214
+ "https://www.fbi.gov/++theme++fbigov.theme/uswds-2.9.0/img/icon-https.svg"
215
+ ]
216
+ },
217
+ {
218
+ "page_url": "https://www.fbi.gov/news/stories/events-mark-the-50th-anniversary-of-female-special-agents-in-the-fbi",
219
+ "missing_alt_count": 3,
220
+ "images_without_alt": [
221
+ "https://www.fbi.gov/++theme++fbigov.theme/images/fbi_seal_new.png",
222
+ "https://www.fbi.gov/++theme++fbigov.theme/uswds-2.9.0/img/icon-dot-gov.svg",
223
+ "https://www.fbi.gov/++theme++fbigov.theme/uswds-2.9.0/img/icon-https.svg"
224
+ ]
225
+ }
226
+ ]
227
+ }
src/__init__.py ADDED
File without changes
src/__pycache__/__init__.cpython-313.pyc ADDED
Binary file (147 Bytes). View file
 
src/__pycache__/__init__.cpython-314.pyc ADDED
Binary file (128 Bytes). View file
 
src/__pycache__/analyzer.cpython-313.pyc ADDED
Binary file (3.25 kB). View file
 
src/__pycache__/analyzer.cpython-314.pyc ADDED
Binary file (1.62 kB). View file
 
src/__pycache__/crawler.cpython-313.pyc ADDED
Binary file (2.5 kB). View file
 
src/__pycache__/crawler.cpython-314.pyc ADDED
Binary file (13 kB). View file
 
src/__pycache__/reporter.cpython-313.pyc ADDED
Binary file (3.25 kB). View file
 
src/__pycache__/reporter.cpython-314.pyc ADDED
Binary file (5.8 kB). View file
 
src/analyzer.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ class ImageAnalyzer:
3
+ def analyze_site(self, site_data):
4
+ """
5
+ Analyzes the crawled site data for missing ALT text.
6
+ :param site_data: Dict { page_url: [image_dicts] }
7
+ :return: Dict containing summary and detailed report.
8
+ """
9
+ total_images = 0
10
+ total_missing_alt = 0
11
+ pages_report = []
12
+
13
+ for page_url, images in site_data.items():
14
+ missing_images = []
15
+ for img in images:
16
+ total_images += 1
17
+ alt_text = img.get('alt')
18
+
19
+ # Check for strictly missing alt text (None or empty string)
20
+ if not alt_text or alt_text.strip() == "":
21
+ # Double check if 'alt' attribute existed but was empty vs didn't exist?
22
+ # The crawler uses .get('alt', '') so it's '' if missing.
23
+ # User asked for "missing alt text".
24
+ # We will count empty string as missing alt text for SEO purposes.
25
+ missing_images.append(img['src'])
26
+ total_missing_alt += 1
27
+
28
+ pages_report.append({
29
+ "page_url": page_url,
30
+ "missing_alt_count": len(missing_images),
31
+ "images_without_alt": list(set(missing_images))
32
+ })
33
+
34
+ return {
35
+ "summary": {
36
+ "total_pages_scanned": len(site_data),
37
+ "total_images_found": total_images,
38
+ "total_images_missing_alt": total_missing_alt
39
+ },
40
+ "details": pages_report
41
+ }
src/crawler.py ADDED
@@ -0,0 +1,276 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from curl_cffi import requests
2
+ import time
3
+ import random
4
+ from bs4 import BeautifulSoup
5
+ from urllib.parse import urljoin, urlparse
6
+ import sys
7
+ import logging
8
+ import os
9
+
10
+ # Configure logging
11
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
12
+ logger = logging.getLogger(__name__)
13
+
14
+ class Crawler:
15
+ def __init__(self, use_playwright=True):
16
+ # 1. Improved HTTP Client Setup
17
+ self.session = requests.Session(impersonate="chrome120")
18
+ self.session.headers.update({
19
+ "Referer": "https://www.google.com/",
20
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
21
+ "Accept-Language": "en-US,en;q=0.9",
22
+ "Upgrade-Insecure-Requests": "1",
23
+ "Sec-Fetch-Site": "none",
24
+ "Sec-Fetch-Mode": "navigate",
25
+ "Sec-Fetch-User": "?1",
26
+ "Sec-Fetch-Dest": "document",
27
+ })
28
+ self.visited_urls = set()
29
+ self.use_playwright_fallback = use_playwright
30
+ self.blocked_reason = None
31
+
32
+ def fetch(self, url):
33
+ """
34
+ Orchestrates fetching: tries HTTP first, detects WAF/Challenge, falls back to Playwright if needed.
35
+ """
36
+ content = self._fetch_http(url)
37
+
38
+ # WAF/Challenge Detection
39
+ # Key terms often found in block pages
40
+ block_keywords = [
41
+ "api-services-support@amazon.com",
42
+ "robot check",
43
+ "type the characters you see",
44
+ "challenge.js",
45
+ "verify you are human",
46
+ "captcha",
47
+ "incident id"
48
+ ]
49
+
50
+ is_blocked = False
51
+ if content:
52
+ lower_content = content.lower()
53
+ if any(k in lower_content for k in block_keywords):
54
+ is_blocked = True
55
+ elif content is None:
56
+ # If HTTP failed completely (e.g. 403 or connection error), we treat it as potentially blocked if it's high-profile
57
+ is_blocked = True
58
+
59
+ if is_blocked:
60
+ logger.warning(f"WAF/CAPTCHA or block detected for {url} using HTTP client.")
61
+ self.blocked_reason = "WAF/JS challenge/consent"
62
+
63
+ if self.use_playwright_fallback:
64
+ logger.info("Attempting fallback to Playwright (Browser Automation)...")
65
+ return self._fetch_playwright(url)
66
+ else:
67
+ logger.error("Playwright fallback disabled. Skipping page.")
68
+ return None
69
+
70
+ return content
71
+
72
+ def _fetch_http(self, url):
73
+ """
74
+ Standard robust HTTP fetch with retry and backoff.
75
+ """
76
+ max_retries = 3
77
+ for attempt in range(max_retries):
78
+ try:
79
+ response = self.session.get(url, timeout=30, allow_redirects=True)
80
+
81
+ # Debug logging for response
82
+ logger.info(f"HTTP GET {url} - Status: {response.status_code} - URL: {response.url}")
83
+
84
+ # Check for 200 OK (or close to it)
85
+ if 200 <= response.status_code < 300:
86
+ return response.text
87
+
88
+ # If 403, it might be WAF. Return text so we can check for challenge keywords.
89
+ if response.status_code == 403:
90
+ # Save HTML for debugging
91
+ with open("debug_http.html", "w", encoding="utf-8") as f:
92
+ f.write(response.text)
93
+ logger.info("Saved 403 response body to debug_http.html")
94
+ return response.text
95
+
96
+ if response.status_code in [429, 500, 502, 503]:
97
+ logger.warning(f"Status {response.status_code} for {url}. Retrying ({attempt + 1}/{max_retries})...")
98
+ time.sleep(2 * (attempt + 1))
99
+ continue
100
+
101
+ logger.error(f"Error fetching {url}: Status {response.status_code}")
102
+ return None
103
+
104
+ except Exception as e:
105
+ logger.error(f"Exception fetching {url}: {e}. Retrying...")
106
+ time.sleep(2 * (attempt + 1))
107
+
108
+ return None
109
+
110
+ def _fetch_playwright(self, url):
111
+ """
112
+ Fallback Strategy A: Use Playwright to render full page.
113
+ """
114
+ try:
115
+ from playwright.sync_api import sync_playwright
116
+ except ImportError:
117
+ logger.error("Playwright not installed. Run `pip install playwright` and `playwright install`.")
118
+ return None
119
+
120
+ try:
121
+ with sync_playwright() as p:
122
+ # Launch options
123
+ # headless=False can be useful for visual debugging if needed, but we keep True for automation.
124
+ browser = p.chromium.launch(headless=True)
125
+
126
+ # Context with realistic viewport and locale
127
+ context = browser.new_context(
128
+ viewport={'width': 1920, 'height': 1080},
129
+ 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",
130
+ locale="en-US",
131
+ timezone_id="America/New_York",
132
+ java_script_enabled=True
133
+ )
134
+
135
+ page = context.new_page()
136
+ try:
137
+ logger.info(f"Playwright navigating to {url}...")
138
+ page.goto(url, timeout=60000, wait_until="domcontentloaded")
139
+
140
+ # Wait for a bit of hydration
141
+ page.wait_for_timeout(3000)
142
+
143
+ # Log page title
144
+ title = page.title()
145
+ logger.info(f"Page Title: {title}")
146
+
147
+ # CAPTCHA Detection via Selectors
148
+ # Amazon often uses #captchacharacters
149
+ if page.locator("input#captchacharacters").count() > 0 or "Robot Check" in title:
150
+ logger.error("Playwright hit visible CAPTCHA. Manual intervention required.")
151
+ self.blocked_reason = "Hard Catcha/Robot Check"
152
+
153
+ # Save screenshot for proof
154
+ page.screenshot(path="amazon.png")
155
+ logger.info("Saved screenshot to amazon.png")
156
+
157
+ # Save HTML
158
+ with open("debug_playwright.html", "w", encoding="utf-8") as f:
159
+ f.write(page.content())
160
+
161
+ return None # Return None to signify blocked/useless content
162
+
163
+ content = page.content()
164
+ return content
165
+
166
+ except Exception as nav:
167
+ logger.error(f"Playwright navigation error: {nav}")
168
+ return None
169
+ finally:
170
+ browser.close()
171
+
172
+ except Exception as e:
173
+ logger.error(f"Playwright critical error: {e}")
174
+ return None
175
+
176
+ def crawl_domain(self, start_url, max_pages=100):
177
+ """
178
+ Crawls domain with robust link discovery and normalization.
179
+ """
180
+ # Reset blocked state
181
+ self.blocked_reason = None
182
+
183
+ parsed_start = urlparse(start_url)
184
+ start_domain = parsed_start.netloc
185
+ base_domain = start_domain.replace('www.', '') # simplistic base domain
186
+
187
+ queue = [start_url]
188
+ self.visited_urls.add(start_url)
189
+
190
+ site_data = {}
191
+ pages_crawled = 0
192
+
193
+ logger.info(f"Starting crawl for domain: {base_domain}")
194
+
195
+ while queue and pages_crawled < max_pages:
196
+ pop_index = random.randint(0, len(queue) - 1)
197
+ current_url = queue.pop(pop_index)
198
+
199
+ logger.info(f"Crawling [{pages_crawled + 1}]: {current_url}")
200
+
201
+ html_content = self.fetch(current_url)
202
+
203
+ # If fetch failed or blocked
204
+ if not html_content:
205
+ if self.blocked_reason and pages_crawled == 0:
206
+ logger.error("Crawl blocked on first page. Aborting.")
207
+ break
208
+ continue
209
+
210
+ # Extract images
211
+ images = self.extract_images(html_content, current_url)
212
+ site_data[current_url] = images
213
+ pages_crawled += 1
214
+
215
+ # --- Robust Link Discovery ---
216
+ soup = BeautifulSoup(html_content, 'html.parser')
217
+ links_found = soup.find_all('a', href=True)
218
+
219
+ links_stats = {"total": len(links_found), "kept": 0, "skipped": 0}
220
+
221
+ for link in links_found:
222
+ href = link['href']
223
+
224
+ # Resolve relative URL
225
+ full_url = urljoin(current_url, href)
226
+ parsed_url = urlparse(full_url)
227
+
228
+ # Normalize: remove fragment
229
+ full_url = parsed_url._replace(fragment="").geturl()
230
+
231
+ link_domain = parsed_url.netloc
232
+
233
+ # Domain Check (Allow www. and non-www.)
234
+ is_internal = link_domain == start_domain or link_domain.endswith('.' + base_domain) or link_domain == base_domain
235
+
236
+ if is_internal:
237
+ # Filter non-html resources
238
+ path = parsed_url.path.lower()
239
+ excluded_exts = ['.jpg', '.jpeg', '.png', '.gif', '.css', '.js', '.ico', '.svg', '.pdf', '.zip', '.xml']
240
+
241
+ if any(path.endswith(ext) for ext in excluded_exts):
242
+ links_stats["skipped"] += 1
243
+ continue
244
+
245
+ if full_url not in self.visited_urls:
246
+ self.visited_urls.add(full_url)
247
+ queue.append(full_url)
248
+ links_stats["kept"] += 1
249
+ else:
250
+ links_stats["skipped"] += 1 # Already visited
251
+ else:
252
+ links_stats["skipped"] += 1 # External
253
+
254
+ logger.info(f"Link Discovery: Found {links_stats['total']}, Added {links_stats['kept']} new unique internal links.")
255
+
256
+ # Final block check: if we scraped 0 pages or only 1 page with 0 images and blocked_reason is set
257
+ if pages_crawled == 0 and self.blocked_reason:
258
+ pass # blocked_reason is already set
259
+ elif pages_crawled > 0 and not self.blocked_reason:
260
+ # Check if we successfully scanned but found suspiciously nothing?
261
+ pass
262
+
263
+ return site_data, len(self.visited_urls), self.blocked_reason
264
+
265
+ def extract_images(self, html_content, base_url):
266
+ if not html_content:
267
+ return []
268
+ soup = BeautifulSoup(html_content, 'html.parser')
269
+ images = []
270
+ for img in soup.find_all('img'):
271
+ raw_src = img.get('src')
272
+ if not raw_src:
273
+ continue
274
+ full_url = urljoin(base_url, raw_src)
275
+ images.append({'src': full_url, 'alt': img.get('alt', '')})
276
+ return images
src/reporter.py ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+
4
+ def generate_json_report(data, output_file="report.json"):
5
+ """
6
+ Generates a JSON report of the site analysis.
7
+ """
8
+ try:
9
+ with open(output_file, 'w') as f:
10
+ json.dump(data, f, indent=4)
11
+ print(f"JSON report generated at {os.path.abspath(output_file)}")
12
+ except Exception as e:
13
+ print(f"Error generating JSON report: {e}")
14
+
15
+ def generate_html_report(data, output_file="report.html"):
16
+ """
17
+ Generates an HTML report for the domain scan.
18
+ """
19
+ summary = data['summary']
20
+ details = data['details']
21
+
22
+ html = f"""
23
+ <!DOCTYPE html>
24
+ <html>
25
+ <head>
26
+ <title>SEO Image Alt Text Report</title>
27
+ <style>
28
+ body {{ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; margin: 0; padding: 20px; background-color: #f4f6f9; }}
29
+ h1, h2 {{ color: #333; }}
30
+ .container {{ max-width: 1200px; margin: 0 auto; background: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); }}
31
+ .summary-box {{ display: flex; gap: 20px; margin-bottom: 30px; }}
32
+ .card {{ flex: 1; padding: 20px; border-radius: 8px; color: white; text-align: center; }}
33
+ .bg-blue {{ background-color: #007bff; }}
34
+ .bg-green {{ background-color: #28a745; }}
35
+ .bg-red {{ background-color: #dc3545; }}
36
+ .card h3 {{ margin: 0; font-size: 3em; }}
37
+ .card p {{ margin: 0; opacity: 0.9; }}
38
+
39
+ table {{ border-collapse: collapse; width: 100%; margin-top: 10px; }}
40
+ th, td {{ border: 1px solid #dee2e6; padding: 12px; text-align: left; }}
41
+ th {{ background-color: #f8f9fa; color: #495057; }}
42
+ tr:nth-child(even) {{ background-color: #f9f9f9; }}
43
+ .page-section {{ margin-bottom: 40px; border-top: 2px solid #eee; padding-top: 20px; }}
44
+ .page-url {{ font-size: 1.2em; font-weight: bold; color: #0056b3; margin-bottom: 10px; display: block; }}
45
+ .no-issues {{ color: #28a745; font-style: italic; }}
46
+ .img-link {{ word-break: break-all; color: #555; }}
47
+ </style>
48
+ </head>
49
+ <body>
50
+ <div class="container">
51
+ <h1>SEO Image Alt Text Report</h1>
52
+
53
+ <div class="summary-box">
54
+ <div class="card bg-blue">
55
+ <h3>{summary['total_pages_scanned']}</h3>
56
+ <p>Pages Scanned</p>
57
+ </div>
58
+ <div class="card bg-green">
59
+ <h3>{summary['total_images_found']}</h3>
60
+ <p>Total Images Found</p>
61
+ </div>
62
+ <div class="card bg-red">
63
+ <h3>{summary['total_images_missing_alt']}</h3>
64
+ <p>Images Missing Alt Text</p>
65
+ </div>
66
+ </div>
67
+ """
68
+
69
+ if summary.get('crawl_blocked'):
70
+ html += f"""
71
+ <div style="background-color: #fff3cd; color: #856404; padding: 15px; border: 1px solid #ffeeba; border-radius: 5px; margin-bottom: 20px;">
72
+ <strong>⚠️ CRAWL BLOCKED:</strong> The crawler was blocked by the site ({summary.get('blocked_reason')}). Results are likely incomplete.
73
+ <p>Try using a residential proxy or a headless browser with stealth plugins to bypass this restriction.</p>
74
+ </div>
75
+ """
76
+
77
+ html += """
78
+ <h2>Detailed Breakdown</h2>
79
+ """
80
+
81
+ if not details:
82
+ html += "<p class='no-issues'>No pages found with missing alt text images! Great job.</p>"
83
+
84
+ for page in details:
85
+ html += f"""
86
+ <div class="page-section">
87
+ <a href="{page['page_url']}" class="page-url" target="_blank">Page: {page['page_url']}</a>
88
+ <p><strong>{page['missing_alt_count']}</strong> images missing alt text</p>
89
+ <table>
90
+ <tr><th>Image Source URL</th></tr>
91
+ """
92
+ for img_src in page['images_without_alt']:
93
+ html += f"""
94
+ <tr>
95
+ <td><a href="{img_src}" target="_blank" class="img-link">{img_src}</a></td>
96
+ </tr>
97
+ """
98
+ html += "</table></div>"
99
+
100
+ html += """
101
+ </div>
102
+ </body>
103
+ </html>
104
+ """
105
+
106
+ try:
107
+ with open(output_file, 'w') as f:
108
+ f.write(html)
109
+ print(f"HTML report generated at {os.path.abspath(output_file)}")
110
+ except Exception as e:
111
+ print(f"Error generating HTML report: {e}")
112
+
113
+
114
+
115
+
tests/__init__.py ADDED
File without changes