From 17e86449985c506fafe3fb02f8a683c24cab7e1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BB=8A=E6=99=9A=E6=89=93=E8=80=81=E8=99=8E?= Date: Mon, 23 Mar 2026 03:54:05 +0800 Subject: [PATCH 1/2] =?UTF-8?q?[=E6=8A=95=E7=A8=BF]=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E8=84=9A=E6=9C=AC:=20web=5Fcrawler=20=E2=80=94=20by=20?= =?UTF-8?q?=E6=B7=AE=E5=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../scripts/basic/web_crawler_mn26dhgw.py | 971 ++++++++++++++++++ 1 file changed, 971 insertions(+) create mode 100644 script_library/scripts/basic/web_crawler_mn26dhgw.py diff --git a/script_library/scripts/basic/web_crawler_mn26dhgw.py b/script_library/scripts/basic/web_crawler_mn26dhgw.py new file mode 100644 index 0000000..d398497 --- /dev/null +++ b/script_library/scripts/basic/web_crawler_mn26dhgw.py @@ -0,0 +1,971 @@ +#!/usr/bin/env python3 +""" +网站文件爬取工具 +能够爬取目标网站的所有可访问文件(HTML、CSS、JS、图片、文档等) +这个工具是开源的,要如何使用它都与开发者无关,毕竟这很简单 +""" + +import os +import sys +import time +import re +import urllib.parse +import urllib.robotparser +from pathlib import Path +from typing import Set, List, Dict, Optional, Tuple +from concurrent.futures import ThreadPoolExecutor, as_completed + +import requests +from bs4 import BeautifulSoup +import tqdm + +# 全局配置 +DEFAULT_HEADERS = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' +} + +# 可下载文件扩展名(包含400+种文件类型,可自行拓展) +DOWNLOADABLE_EXTENSIONS = { + # 图片格式(30+种) + '.jpg', '.jpeg', '.png', '.gif', '.bmp', '.svg', '.ico', '.webp', '.tiff', '.tif', + '.psd', '.ai', '.eps', '.raw', '.cr2', '.nef', '.orf', '.sr2', '.heic', '.heif', + '.avif', '.jxl', '.jp2', '.j2k', '.pbm', '.pgm', '.ppm', '.pnm', '.hdr', '.exr', + + # 文档格式(50+种) + '.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.txt', '.rtf', + '.odt', '.ods', '.odp', '.odg', '.odf', '.md', '.markdown', '.tex', '.latex', + '.csv', '.tsv', '.xml', '.yaml', '.yml', '.json', '.ini', '.cfg', '.conf', + '.log', '.sql', '.db', '.sqlite', '.sqlite3', '.mdb', '.accdb', '.dbf', + '.epub', '.mobi', '.azw', '.azw3', '.fb2', '.ibooks', '.chm', '.djvu', + '.pages', '.numbers', '.key', '.vsd', '.vsdx', '.vsdm', '.vsdx', + + # 压缩文件格式(20+种) + '.zip', '.rar', '.7z', '.tar', '.gz', '.bz2', '.xz', '.lz', '.lzma', + '.lzo', '.z', '.lzh', '.arj', '.cab', '.iso', '.img', '.dmg', '.vhd', + '.vmdk', '.vhdx', '.wim', '.swm', + + # 媒体文件格式(50+种) + '.mp3', '.mp4', '.avi', '.mkv', '.mov', '.wav', '.flac', '.aac', '.ogg', + '.oga', '.opus', '.m4a', '.m4b', '.wma', '.alac', '.aiff', '.au', '.voc', + '.ra', '.rm', '.ram', '.mid', '.midi', '.mod', '.xm', '.it', '.s3m', + '.mpg', '.mpeg', '.wmv', '.flv', '.swf', '.webm', '.3gp', '.3g2', '.m2ts', + '.mts', '.ts', '.mxf', '.ogv', '.divx', '.xvid', '.vob', '.asf', '.dv', + '.f4v', '.m4v', '.mpv', '.nsv', '.roq', '.yuv', + + # 代码文件格式(100+种) + '.js', '.css', '.html', '.htm', '.xhtml', '.php', '.py', '.java', '.cpp', + '.c', '.h', '.hpp', '.cs', '.vb', '.swift', '.go', '.rs', '.rb', '.pl', + '.pm', '.t', '.lua', '.scala', '.kt', '.kts', '.groovy', '.dart', '.elm', + '.erl', '.ex', '.exs', '.fs', '.fsharp', '.hs', '.lhs', '.jl', '.clj', + '.cljs', '.cljc', '.edn', '.r', '.m', '.matlab', '.f', '.for', '.f90', + '.ada', '.asm', '.s', '.inc', '.pas', '.pp', '.lpr', '.d', '.coffee', + '.litcoffee', '.iced', '.ts', '.tsx', '.jsx', '.vue', '.svelte', '.elm', + '.pug', '.jade', '.haml', '.slim', '.scss', '.sass', '.less', '.styl', + '.stylus', '.wasm', '.wat', '.bf', '.bfs', '.bas', '.vbs', '.bat', '.cmd', + '.ps1', '.sh', '.bash', '.zsh', '.fish', '.csh', '.ksh', '.tcsh', '.awk', + '.sed', '.tcl', '.tk', '.expect', '.nim', '.zig', '.v', '.vhdl', '.sv', + '.svh', '.uc', '.txx', '.cxx', '.cc', '.hh', '.hxx', '.ixx', '.mxx', + '.inl', '.ipp', + + # 字体文件格式(10+种) + '.ttf', '.otf', '.woff', '.woff2', '.eot', '.fon', '.fnt', '.pfa', '.pfb', + '.sfd', '.dfont', + + # 数据库文件格式(10+种) + '.db', '.sqlite', '.sqlite3', '.mdb', '.accdb', '.fdb', '.mdf', '.ldf', + '.ndf', '.dbf', + + # 可执行文件格式(20+种) + '.exe', '.msi', '.dll', '.so', '.dylib', '.bundle', '.sys', '.drv', + '.ocx', '.cab', '.apk', '.ipa', '.app', '.appx', '.appxbundle', '.msix', + '.msixbundle', '.deb', '.rpm', '.pkg', '.run', + + # 配置文件格式(20+种) + '.ini', '.cfg', '.conf', '.config', '.properties', '.toml', '.xml', + '.yaml', '.yml', '.json', '.json5', '.hjson', '.edn', '.env', '.dotenv', + '.gitignore', '.dockerignore', '.npmignore', '.editorconfig', + + # 其他格式(30+种) + '.torrent', '.magnet', '.webloc', '.url', '.lnk', '.desktop', '.appref-ms', + '.scr', '.com', '.pif', '.bat', '.cmd', '.vbs', '.wsf', '.ps1', '.psm1', + '.psd1', '.psc1', '.reg', '.inf', '.cat', '.cer', '.crt', '.der', '.p7b', + '.p7c', '.p12', '.pfx', '.pem', '.key', +} + +class WebCrawler: + def __init__(self, + base_url: str, + output_dir: str = "downloads", + max_depth: int = 10, # 默认深度10 + max_workers: int = 10, # 默认并发10 + delay: float = 0.3, # 默认延迟0.3秒 + respect_robots: bool = True, + crawl_strategy: str = "breadth_first", # 爬取策略:breadth_first 或 depth_first + follow_redirects: bool = True, # 是否跟踪重定向 + parse_css_files: bool = True, # 是否解析CSS文件中的链接 + parse_js_files: bool = True, # 是否解析JS文件中的链接 + max_urls: int = 10000, # 最大爬取URL数量限制 + timeout: int = 30): # 请求超时时间 + """ + 初始化爬虫 + + Args: + base_url: 起始URL + output_dir: 下载文件保存目录 + max_depth: 最大爬取深度(默认10) + max_workers: 最大并发线程数(默认10) + delay: 请求延迟(秒)(默认0.3) + respect_robots: 是否遵守robots.txt + crawl_strategy: 爬取策略,'breadth_first'(广度优先)或 'depth_first'(深度优先) + follow_redirects: 是否跟踪重定向 + parse_css_files: 是否解析CSS文件中的链接 + parse_js_files: 是否解析JS文件中的链接 + max_urls: 最大爬取URL数量限制(防止无限循环) + timeout: 请求超时时间(秒) + """ + self.base_url = base_url.rstrip('/') + self.base_domain = urllib.parse.urlparse(base_url).netloc + self.output_dir = Path(output_dir) + self.max_depth = max_depth + self.max_workers = max_workers + self.delay = delay + self.respect_robots = respect_robots + self.crawl_strategy = crawl_strategy + self.follow_redirects = follow_redirects + self.parse_css_files = parse_css_files + self.parse_js_files = parse_js_files + self.max_urls = max_urls + self.timeout = timeout + + # 创建输出目录 + self.output_dir.mkdir(parents=True, exist_ok=True) + + # 已访问URL集合 + self.visited_urls: Set[str] = set() + # 待爬取URL队列(URL, 深度) + self.url_queue: List[Tuple[str, int]] = [(self.base_url, 0)] + # robots.txt解析器 + self.robot_parser = urllib.robotparser.RobotFileParser() + + # 统计信息 + self.stats = { + 'pages_crawled': 0, + 'files_downloaded': 0, + 'total_size': 0, + 'errors': 0, + 'css_files_parsed': 0, + 'js_files_parsed': 0, + 'redirects_followed': 0 + } + + # 初始化session + self.session = requests.Session() + self.session.headers.update(DEFAULT_HEADERS) + self.session.max_redirects = 10 if follow_redirects else 0 + + # URL优先级队列(用于深度优先) + self.priority_queue = [] + + # 加载robots.txt + if self.respect_robots: + self._load_robots_txt() + + def _load_robots_txt(self): + """加载并解析robots.txt""" + try: + robots_url = f"{self.base_url}/robots.txt" + self.robot_parser.set_url(robots_url) + self.robot_parser.read() + except Exception as e: + print(f"警告: 无法加载robots.txt: {e}") + + def _can_fetch(self, url: str) -> bool: + """检查是否允许爬取该URL""" + if not self.respect_robots: + return True + return self.robot_parser.can_fetch(DEFAULT_HEADERS['User-Agent'], url) + + def _is_same_domain(self, url: str) -> bool: + """检查URL是否属于同一域名""" + try: + parsed = urllib.parse.urlparse(url) + return parsed.netloc == self.base_domain or not parsed.netloc + except: + return False + + def _normalize_url(self, url: str, base: str = None) -> str: + """标准化URL""" + if base is None: + base = self.base_url + + # 如果是相对路径,转换为绝对路径 + if not url.startswith(('http://', 'https://', '//')): + if url.startswith('/'): + # 相对于根目录 + parsed_base = urllib.parse.urlparse(base) + url = f"{parsed_base.scheme}://{parsed_base.netloc}{url}" + else: + # 相对于当前页面 + if not base.endswith('/'): + base += '/' + url = urllib.parse.urljoin(base, url) + + # 处理协议相对URL + if url.startswith('//'): + url = f"https:{url}" + + # 移除片段标识符 + url = urllib.parse.urldefrag(url)[0] + + # 确保URL以/结尾(如果是目录) + parsed = urllib.parse.urlparse(url) + if not parsed.path: + url = url.rstrip('/') + '/' + + return url + + def _get_file_extension(self, url: str) -> str: + """从URL获取文件扩展名""" + path = urllib.parse.urlparse(url).path + ext = Path(path).suffix.lower() + return ext + + def _is_downloadable_file(self, url: str) -> bool: + """检查URL是否指向可下载文件""" + ext = self._get_file_extension(url) + return ext in DOWNLOADABLE_EXTENSIONS + + def _should_crawl(self, url: str, current_depth: int) -> bool: + """判断是否应该爬取该URL""" + # 检查深度限制 + if current_depth >= self.max_depth: + return False + + # 检查是否已访问 + if url in self.visited_urls: + return False + + # 检查域名 + if not self._is_same_domain(url): + return False + + # 检查robots.txt + if not self._can_fetch(url): + return False + + # 对于文件下载,不进行进一步爬取 + if self._is_downloadable_file(url): + return False + + return True + + def _extract_links(self, html: str, base_url: str) -> List[str]: + """从HTML中提取所有链接(增强版)""" + soup = BeautifulSoup(html, 'html.parser') + links = [] + + # 提取HTML标签中的链接 + html_links = self._extract_html_links(soup, base_url) + links.extend(html_links) + + # 提取CSS中的链接 + css_links = self._extract_css_links(html, base_url) + links.extend(css_links) + + # 提取JavaScript中的链接(基础正则匹配) + js_links = self._extract_js_links(html, base_url) + links.extend(js_links) + + # 提取meta refresh重定向 + meta_links = self._extract_meta_refresh_links(soup, base_url) + links.extend(meta_links) + + # 去重 + unique_links = [] + seen = set() + for link in links: + if link and link not in seen: + seen.add(link) + unique_links.append(link) + + return unique_links + + def _extract_html_links(self, soup: BeautifulSoup, base_url: str) -> List[str]: + """提取HTML标签中的链接""" + links = [] + + # 定义所有可能包含URL的标签和属性 + url_attributes = { + 'a': ['href'], + 'link': ['href'], + 'script': ['src'], + 'img': ['src', 'srcset', 'data-src', 'data-original'], + 'source': ['src', 'srcset'], + 'iframe': ['src'], + 'frame': ['src'], + 'embed': ['src'], + 'object': ['data'], + 'video': ['src', 'poster'], + 'audio': ['src'], + 'track': ['src'], + 'area': ['href'], + 'base': ['href'], + 'form': ['action'], + 'button': ['formaction'], + 'input': ['src', 'formaction'], + 'meta': ['content'], # 用于og:image等 + 'applet': ['code', 'archive'], + 'bgsound': ['src'], + 'body': ['background'], + 'table': ['background'], + 'td': ['background'], + 'th': ['background'], + } + + for tag_name, attrs in url_attributes.items(): + for tag in soup.find_all(tag_name): + for attr in attrs: + if tag.get(attr): + urls = self._parse_attribute_value(tag[attr], base_url) + links.extend(urls) + + # 处理style属性中的URL + for tag in soup.find_all(style=True): + style_links = self._extract_css_urls(tag['style'], base_url) + links.extend(style_links) + + return links + + def _extract_css_links(self, html: str, base_url: str) -> List[str]: + """提取CSS中的链接""" + links = [] + + # 提取