Spaces:
Paused
Paused
| import streamlit as st | |
| import requests | |
| import threading | |
| import queue | |
| import time | |
| from bs4 import BeautifulSoup | |
| from urllib.parse import urljoin, urlparse | |
| from concurrent.futures import ThreadPoolExecutor | |
| # Streamlit UI | |
| st.title("Web Crawler - Link Checker") | |
| BASE_URL = st.text_input("Enter the Base URL:", "https://www.example.com/") | |
| MAX_CONCURRENT_THREAD = st.slider("Select Max Concurrent Threads", 1, 20, 10) | |
| LOGGING = st.checkbox("Enable Logging", True) | |
| start_crawling = st.button("Start Crawling") | |
| # Global Variables | |
| relativeLinks = queue.Queue() | |
| visitedLinks = set() | |
| # File Output Data | |
| visited_links = [] | |
| ok_links = [] | |
| error_links = [] | |
| exception_links = [] | |
| session = requests.Session() # Reuse session for connection pooling | |
| session.headers.update({"User-Agent": "Mozilla/5.0"}) # Set a common User-Agent | |
| def link_checker(source, address): | |
| """Check the validity of a link.""" | |
| try: | |
| resp = session.get(address, timeout=5) | |
| if resp.status_code in [400, 404, 403, 408, 409, 501, 502, 503]: | |
| error_links.append(f"[{source}] {resp.status_code} {resp.reason} --> {address}") | |
| else: | |
| ok_links.append(f"[{source}] OK --> {address}") | |
| except requests.RequestException as e: | |
| exception_links.append(f"[{source}] Exception --> {e} {address}") | |
| def normalize_url(a): | |
| """Normalize URLs to absolute format.""" | |
| if a.startswith("#") or a.startswith("javascript:") or a.startswith("mailto:") or a.startswith("tel:"): | |
| return None | |
| return urljoin(BASE_URL, a).rstrip('/') | |
| def link_extractor(address): | |
| """Extract links from a webpage and check their validity.""" | |
| visited_links.append(address) | |
| try: | |
| res = session.get(address, timeout=5) | |
| soup = BeautifulSoup(res.text, 'html.parser') | |
| extracted_links = [] | |
| for tag, attr in [('img', 'src'), ('link', 'href'), ('script', 'src'), ('a', 'href')]: | |
| extracted_links.extend( | |
| normalize_url(link[attr]) for link in soup.find_all(tag) if link.has_attr(attr) | |
| ) | |
| extracted_links = set(filter(None, extracted_links)) # Remove duplicates and None values | |
| with ThreadPoolExecutor(max_workers=MAX_CONCURRENT_THREAD) as executor: | |
| executor.map(lambda p: link_checker(source=address, address=p), extracted_links) | |
| for p in extracted_links: | |
| if p.startswith(BASE_URL) and p not in visitedLinks: | |
| visitedLinks.add(p) | |
| relativeLinks.put(p) | |
| except requests.RequestException as e: | |
| exception_links.append(f"[RELATIVE LINK] Exception --> {e} {address}") | |
| def threader(): | |
| """Worker function for handling queued links.""" | |
| while not relativeLinks.empty(): | |
| value = relativeLinks.get() | |
| if value: | |
| print(f"Checking: {value} | Remaining: {relativeLinks.qsize()}") | |
| link_extractor(value) | |
| relativeLinks.task_done() | |
| if start_crawling: | |
| start_time = time.time() | |
| visitedLinks.add(BASE_URL.strip()) | |
| relativeLinks.put(BASE_URL.strip()) | |
| with ThreadPoolExecutor(max_workers=MAX_CONCURRENT_THREAD) as executor: | |
| executor.map(lambda _: threader(), range(MAX_CONCURRENT_THREAD)) | |
| end_time = time.time() | |
| elapsed_time = end_time - start_time | |
| # Display Results | |
| st.success("Crawling Completed!") | |
| st.write(f"Total Time Taken: {elapsed_time:.2f} seconds") | |
| st.write(f"Total Links Visited: {len(visited_links)}") | |
| st.write(f"Total OK Links: {len(ok_links)}") | |
| st.write(f"Total Error Links: {len(error_links)}") | |
| st.write(f"Total Exception Links: {len(exception_links)}") | |
| # Display Logs | |
| with st.expander("Visited Links"): | |
| st.write(visited_links) | |
| with st.expander("OK Links"): | |
| st.write(ok_links) | |
| with st.expander("Error Links"): | |
| st.write(error_links) | |
| with st.expander("Exception Links"): | |
| st.write(exception_links) | |