一个仿站工具的开发心得

159 阅读5分钟

大家好!今天我想分享一个我最近开发的小工具——Web Mirror Pro(网页模仿器) 。这是一个能让你输入网址,一键克隆整个网页的工具,生成可离线浏览的HTML文件,包含所有图片、CSS和JS资源。无论你是前端开发者想学习网站结构,还是需要备份网页内容,这个工具都能帮上大忙!

🌟 为什么我要开发这个工具?

在日常工作中,我经常遇到这样的需求:

  • 想学习优秀网站的设计,但需要离线查看
  • 需要备份某个网页,但担心原网站关闭
  • 作为建站参考,快速获取网页结构

市面上虽然有一些网页保存工具,但要么功能单一(只保存HTML),要么操作复杂。所以我决定自己动手,用Python开发一个简单易用、功能完整的网页克隆工具。

💡 核心功能

  • ✅ 输入网址,一键克隆网页(含图片/CSS/JS)
  • ✅ 生成可完全离线浏览的HTML文件
  • ✅ 桌面GUI界面,无需浏览器
  • ✅ 可打包为独立.exe文件,无需安装Python
  • ✅ 自动替换外链为本地资源路径
  • ✅ 支持递归下载(CSS中的背景图等)

🧩 代码实现与关键函数解析

下面我将重点解析几个核心函数,解释为什么这样设计以及解决了什么问题

1. clone_page_with_deep_assets - 深度克隆函数

def clone_page_with_deep_assets(url, save_path, max_depth=1, current_depth=0, log_callback=None, downloaded_urls=None):
if downloaded_urls is None:
    downloaded_urls = set()
    if current_depth > max_depth:
    return True

# ...(中间代码省略)

# 下载图片
for img in soup.find_all("img", src=True):
    src = img['src']
    if src.lower().startswith(''):
        continue  # 跳过 base64
    abs_url = urljoin(url, src)
    if abs_url in downloaded_urls:
        continue
    downloaded_urls.add(abs_url)
    
    local_file = download_resource(session, src, url, assets_dir)
    if local_file:
        img['src'] = f"assets/{local_file}"
        log_callback(f"{'  ' * current_depth}🖼️ 图片: {src}{local_file}")

# 处理 CSS 及其背景图
for link in soup.find_all("link", rel="stylesheet", href=True):
    href = link['href']
    abs_url = urljoin(url, href)
    if abs_url in downloaded_urls:
        continue
    downloaded_urls.add(abs_url)
    
    local_file = download_resource(session, href, url, assets_dir)
    if local_file:
        link['href'] = f"assets/{local_file}"
        log_callback(f"{'  ' * current_depth}🎨 样式: {href}{local_file}")
        
        # 解析 CSS 中的 url(...)
        css_path = os.path.join(assets_dir, local_file)
        try:
            with open(css_path, 'r', encoding='utf-8', errors='ignore') as f:
                text = f.read()
                urls = re.findall(r'url\([\'"]?(.*?)[\'"]?\)', text)
                for bg_url in urls:
                    full_bg = urljoin(abs_url, bg_url.strip())
                    if full_bg in downloaded_urls:
                        continue
                    bg_file = download_resource(session, full_bg, url, assets_dir)
                    if bg_file:
                        log_callback(f"{'  ' * (current_depth+1)}🖼️ CSS 图: {bg_url}{bg_file}")
        except:
            pass

# ...(后续代码省略)

为什么需要这个函数?

大多数网页克隆工具只下载HTML和直接引用的资源,但CSS中可能包含背景图,而这些图片不会被普通爬虫捕获。这个函数通过递归方式:

  1. 深度优先遍历:设置max_depth参数控制递归深度,避免无限爬取
  2. 自动识别资源:不仅下载HTML中的图片,还解析CSS文件中的url(...)引用
  3. 避免重复下载:使用downloaded_urls集合记录已下载URL,防止循环和重复

解决了什么问题?

  • 游戏网站、设计类网站常有大量CSS背景图,普通工具无法完整保存
  • 没有递归机制,生成的网页会缺少关键视觉元素
  • 通过限制深度,平衡了完整性和效率

2. download_resource - 资源下载函数

    def download_resource(session, url, base_url, save_dir):
try:
    abs_url = urljoin(base_url, url)
    parsed = urlparse(abs_url)
    if not parsed.path or parsed.path == '/':
        return None
               # 解码 URL(处理 %20 等)
    filename = os.path.basename(unquote(parsed.path))
    if not filename or '.' not in filename:
        try:
            head = session.head(abs_url, timeout=5)
            content_type = head.headers.get('content-type', '').lower()
            if 'jpeg' in content_type or 'jpg' in content_type:
                ext = '.jpg'
            elif 'png' in content_type:
                ext = '.png'
            elif 'gif' in content_type:
                ext = '.gif'
            elif 'svg' in content_type:
                ext = '.svg'
            else:
                ext = '.bin'
            filename = f"res_{hash(abs_url) % 10000}{ext}"
        except:
            filename = f"res_{hash(abs_url) % 10000}.bin"

    filename = re.sub(r'[^\w\.\-]', '_', filename)
    filepath = os.path.join(save_dir, filename)

    if os.path.exists(filepath):
        return filename  # 已存在则跳过

    headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'}
    response = session.get(abs_url, headers=headers, timeout=10, stream=True)
    response.raise_for_status()

    # 检查文件大小(超过 10MB 跳过)
    total_size = int(response.headers.get('content-length', 0))
    if total_size > 10 * 1024 * 1024:  # 10MB
        return None

    downloaded_size = 0
    with open(filepath, 'wb') as f:
        for chunk in response.iter_content(1024):
            if chunk:
                f.write(chunk)
                downloaded_size += len(chunk)
                if downloaded_size > 10 * 1024 * 1024:  # 实时检查
                    os.remove(filepath)
                    return None

    return filename

except Exception as e:
    return None

为什么需要这个函数?

网页资源千奇百怪,直接下载可能会遇到:

  • 文件名包含特殊字符(如空格、中文)
  • 没有文件扩展名
  • 超大文件导致程序卡死
  • 重复下载同一资源

解决了什么问题?

  1. 智能文件名处理

    • 使用unquote解码URL,处理%20等编码
    • 通过Content-Type推断文件扩展名
    • 清理非法字符(re.sub(r'[^\w.-]', '_', filename)
  2. 大文件保护机制

    • 检查Content-Length头部
    • 流式下载并实时检查大小
    • 超过10MB自动跳过,避免卡顿
  3. 重复下载优化

    • 检查文件是否存在
    • 避免多次下载同一资源

3. GUI界面设计 - 为什么选择ttkbootstrap

GUI主程序部分

    class WebMirrorApp:
def __init__(self, root):
    self.root = root
    self.root.title("🚀 自研快速仿站神器 - 输入网址一键生成HTML")
    self.root.geometry("800x600")
    self.root.resizable(True, True)
    #标题区
    title_frame = ttk.Frame(root)
    title_frame.pack(pady=15, fill=X, padx=20)
    
    ttk.Label(
        title_frame,
        text="🌐 自研快速仿站神器",
        font=("微软雅黑", 16, "bold"),
        bootstyle="primary"
    ).pack()
    
    # ...(后续代码省略)
    

为什么使用ttkbootstrap?

  1. 美观现代的界面:原生tkinter界面太简陋,而ttkbootstrap提供多种主题(cosmo, darkly, flatly等),让工具看起来更专业
  2. 跨平台一致性:确保在Windows/Mac/Linux上都有良好的显示效果
  3. 简化开发:提供圆角按钮、颜色主题等现代UI元素,无需手动绘制

解决了什么问题?

  • 专业外观提升用户体验和信任度
  • 让非技术用户也能轻松操作
  • 日志区域使用ScrolledText,方便查看详细过程

🚀 如何使用这个工具?

见资源下载