下面我将为你提供一个完整的、可以直接使用的解决方案,包括:

  1. 功能设计:明确这个助手应该做什么。
  2. 界面设计:一个清晰、易用的用户界面。
  3. 完整代码:HTML, CSS, 和 JavaScript 的全部代码。
  4. 使用方法:如何使用这个工具。
  5. 进阶功能:可以进一步扩展的方向。

功能设计

这个助手至少需要包含以下核心功能:

  • URL 输入:用户输入目标网页的地址。
  • 一键抓取:点击按钮后,加载目标网页并分析其资源。
  • CSS 展示
    • 列出页面中所有引入的 CSS 文件链接(<link> 标签)。
    • 列出页面中所有内联的 CSS 样式(<style> 标签)。
    • 可以将所有 CSS 内容合并成一个文件并下载。
  • JS 展示
    • 列出页面中所有引入的外部 JS 文件链接(<script src="..."> 标签)。
    • 列出页面中所有内联的 JS 代码(<script> 标签)。
    • 可以将所有 JS 内容合并成一个文件并下载。
  • DOM 树分析
    • 展示页面的基本 DOM 结构(HTML 标签树)。
    • 点击某个标签,可以查看其内联的 style 属性。
  • 错误处理:处理无效链接、跨域错误等情况,并给出友好提示。

界面设计

界面分为三个主要部分:

  1. 控制区:顶部,包含 URL 输入框、抓取按钮和一些状态提示。
  2. 结果展示区:主体部分,使用选项卡(Tabs)切换显示 CSS、JS 和 DOM 的分析结果。
  3. 预览区:右侧或下方,一个 iframe 用于实时预览抓取的网页。

完整代码

你可以将以下所有代码保存为一个单独的 HTML 文件(web_scraper.html),然后用浏览器打开即可使用。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">网页 CSS/JS 抓取助手</title>
    <style>
        /* --- 全局样式 --- */
        body {
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
            margin: 0;
            padding: 20px;
            background-color: #f4f7f6;
            color: #333;
        }
        .container {
            max-width: 1400px;
            margin: 0 auto;
            background-color: #fff;
            border-radius: 8px;
            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
            overflow: hidden;
        }
        h1 {
            text-align: center;
            color: #2c3e50;
            padding: 20px;
            margin: 0;
            background-color: #ecf0f1;
        }
        /* --- 控制区样式 --- */
        .control-panel {
            padding: 20px;
            border-bottom: 1px solid #ddd;
            display: flex;
            gap: 10px;
            align-items: center;
            flex-wrap: wrap;
        }
        #urlInput {
            flex-grow: 1;
            padding: 10px 15px;
            border: 1px solid #ccc;
            border-radius: 4px;
            font-size: 16px;
            min-width: 300px;
        }
        #scrapeBtn {
            padding: 10px 20px;
            background-color: #3498db;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 16px;
            transition: background-color 0.3s;
        }
        #scrapeBtn:hover {
            background-color: #2980b9;
        }
        #status {
            padding: 10px;
            border-radius: 4px;
            font-weight: bold;
            min-width: 200px;
            text-align: center;
        }
        .status-idle { background-color: #f8f9fa; color: #6c757d; }
        .status-loading { background-color: #d1ecf1; color: #0c5460; }
        .status-success { background-color: #d4edda; color: #155724; }
        .status-error { background-color: #f8d7da; color: #721c24; }
        /* --- 主内容区样式 --- */
        .main-content {
            display: flex;
            height: 70vh;
        }
        .results-panel {
            flex: 1;
            overflow: hidden;
            display: flex;
            flex-direction: column;
        }
        .preview-panel {
            flex: 1;
            border-left: 1px solid #ddd;
            display: flex;
            flex-direction: column;
        }
        .preview-panel h3 {
            margin: 0;
            padding: 10px 20px;
            background-color: #ecf0f1;
            border-bottom: 1px solid #ddd;
        }
        #previewFrame {
            flex-grow: 1;
            border: none;
            width: 100%;
        }
        /* --- 选项卡样式 --- */
        .tabs {
            display: flex;
            background-color: #f8f9fa;
            border-bottom: 1px solid #ddd;
        }
        .tab {
            padding: 12px 20px;
            cursor: pointer;
            border: none;
            background-color: inherit;
            font-size: 16px;
            transition: background-color 0.3s;
        }
        .tab.active {
            background-color: #fff;
            border-bottom: 2px solid #3498db;
            font-weight: bold;
        }
        .tab-content {
            flex-grow: 1;
            padding: 20px;
            overflow-y: auto;
            display: none;
        }
        .tab-content.active {
            display: block;
        }
        .resource-list {
            list-style-type: none;
            padding: 0;
        }
        .resource-item {
            padding: 10px;
            border: 1px solid #e0e0e0;
            border-radius: 4px;
            margin-bottom: 10px;
            background-color: #fafafa;
        }
        .resource-item h4 {
            margin: 0 0 5px 0;
            color: #2c3e50;
        }
        .resource-item p {
            margin: 5px 0 0 0;
            color: #555;
            word-break: break-all;
        }
        .download-btn {
            margin-top: 10px;
            padding: 5px 10px;
            background-color: #28a745;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 14px;
        }
        .download-btn:hover {
            background-color: #218838;
        }
        #domTree {
            font-family: 'Courier New', Courier, monospace;
            white-space: pre-wrap;
            word-wrap: break-word;
            background-color: #f8f9fa;
            padding: 10px;
            border-radius: 4px;
        }
        .dom-node {
            cursor: pointer;
            padding: 2px 5px;
            border-radius: 3px;
        }
        .dom-node:hover {
            background-color: #e9ecef;
        }
        .dom-node.selected {
            background-color: #cce5ff;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>网页 CSS/JS 抓取助手</h1>
        <div class="control-panel">
            <input type="text" id="urlInput" placeholder="请输入要分析的网页 URL ( https://www.example.com)" value="https://www.example.com">
            <button id="scrapeBtn">开始抓取</button>
            <div id="status" class="status-idle">准备就绪</div>
        </div>
        <div class="main-content">
            <div class="results-panel">
                <div class="tabs">
                    <button class="tab active" data-tab="css">CSS</button>
                    <button class="tab" data-tab="js">JavaScript</button>
                    <button class="tab" data-tab="dom">DOM 结构</button>
                </div>
                <div id="css-tab" class="tab-content active">
                    <h3>CSS 资源</h3>
                    <ul id="cssList" class="resource-list"></ul>
                    <button id="downloadCssBtn" class="download-btn" style="display: none;">下载合并的 CSS 文件</button>
                </div>
                <div id="js-tab" class="tab-content">
                    <h3>JavaScript 资源</h3>
                    <ul id="jsList" class="resource-list"></ul>
                    <button id="downloadJsBtn" class="download-btn" style="display: none;">下载合并的 JS 文件</button>
                </div>
                <div id="dom-tab" class="tab-content">
                    <h3>DOM 树结构</h3>
                    <pre id="domTree"></pre>
                    <div id="nodeStyleInfo" style="margin-top: 15px; padding: 10px; background-color: #f8f9fa; border-radius: 4px; display: none;">
                        <strong>选中节点的内联样式:</strong>
                        <pre id="inlineStyle"></pre>
                    </div>
                </div>
            </div>
            <div class="preview-panel">
                <h3>网页预览</h3>
                <iframe id="previewFrame" title="网页预览"></iframe>
            </div>
        </div>
    </div>
    <script>
        document.addEventListener('DOMContentLoaded', () => {
            const urlInput = document.getElementById('urlInput');
            const scrapeBtn = document.getElementById('scrapeBtn');
            const status = document.getElementById('status');
            const previewFrame = document.getElementById('previewFrame');
            const cssList = document.getElementById('cssList');
            const jsList = document.getElementById('jsList');
            const domTree = document.getElementById('domTree');
            const downloadCssBtn = document.getElementById('downloadCssBtn');
            const downloadJsBtn = document.getElementById('downloadJsBtn');
            const nodeStyleInfo = document.getElementById('nodeStyleInfo');
            const inlineStyle = document.getElementById('inlineStyle');
            let allCssContent = '';
            let allJsContent = '';
            // 选项卡切换逻辑
            const tabs = document.querySelectorAll('.tab');
            const tabContents = document.querySelectorAll('.tab-content');
            tabs.forEach(tab => {
                tab.addEventListener('click', () => {
                    const targetTab = tab.getAttribute('data-tab');
                    tabs.forEach(t => t.classList.remove('active'));
                    tabContents.forEach(tc => tc.classList.remove('active'));
                    tab.classList.add('active');
                    document.getElementById(`${targetTab}-tab`).classList.add('active');
                });
            });
            // 抓取按钮点击事件
            scrapeBtn.addEventListener('click', async () => {
                const url = urlInput.value.trim();
                if (!url) {
                    updateStatus('请输入有效的 URL', 'error');
                    return;
                }
                // 确保 URL 是完整的
                const fullUrl = url.startsWith('http') ? url : `https://${url}`;
                updateStatus('正在抓取...', 'loading');
                clearResults();
                allCssContent = '';
                allJsContent = '';
                try {
                    // 1. 在 iframe 中加载页面
                    previewFrame.src = fullUrl;
                    // 等待 iframe 加载完成
                    await new Promise((resolve, reject) => {
                        previewFrame.onload = resolve;
                        previewFrame.onerror = () => reject(new Error('无法加载页面,可能是跨域问题或 URL 无效'));
                    });
                    // 2. 从 iframe 的 document 中提取信息
                    const iframeDoc = previewFrame.contentDocument || previewFrame.contentWindow.document;
                    // 提取 CSS
                    extractCss(iframeDoc);
                    // 提取 JS
                    extractJs(iframeDoc);
                    // 提取 DOM
                    extractDom(iframeDoc.documentElement);
                    // 显示下载按钮
                    if (allCssContent) downloadCssBtn.style.display = 'inline-block';
                    if (allJsContent) downloadJsBtn.style.display = 'inline-block';
                    updateStatus('抓取完成!', 'success');
                } catch (error) {
                    console.error(error);
                    updateStatus(`抓取失败: ${error.message}`, 'error');
                }
            });
            function updateStatus(message, type) {
                status.textContent = message;
                status.className = `status-${type}`;
            }
            function clearResults() {
                cssList.innerHTML = '';
                jsList.innerHTML = '';
                domTree.textContent = '';
                downloadCssBtn.style.display = 'none';
                downloadJsBtn.style.display = 'none';
                nodeStyleInfo.style.display = 'none';
            }
            function extractCss(doc) {
                const cssLinks = Array.from(doc.querySelectorAll('link[rel="stylesheet"]'));
                const styleTags = Array.from(doc.querySelectorAll('style'));
                cssLinks.forEach(link => {
                    const href = link.href;
                    if (href) {
                        const li = document.createElement('li');
                        li.className = 'resource-item';
                        li.innerHTML = `<h4>外部 CSS 文件</h4><p>链接: ${href}</p>`;
                        cssList.appendChild(li);
                        // 尝试获取 CSS 内容(同源策略限制)
                        fetchCssContent(href, li);
                    }
                });
                styleTags.forEach(style => {
                    const li = document.createElement('li');
                    li.className = 'resource-item';
                    li.innerHTML = `<h4>内联 CSS 样式</h4><p>内容长度: ${style.textContent.length} 字符</p>`;
                    cssList.appendChild(li);
                    allCssContent += `\n/* --- 来自内联样式 --- */\n${style.textContent}\n`;
                });
            }
            async function fetchCssContent(url, listItem) {
                try {
                    // 对于跨域 CSS,fetch 可能会失败,但我们可以尝试
                    const response = await fetch(url);
                    if (response.ok) {
                        const cssText = await response.text();
                        allCssContent += `\n/* --- 来自 ${url} --- */\n${cssText}\n`;
                        const p = document.createElement('p');
                        p.textContent = `✅ 内容已获取并合并`;
                        listItem.appendChild(p);
                    } else {
                        throw new Error('HTTP ' + response.status);
                    }
                } catch (error) {
                    const p = document.createElement('p');
                    p.textContent = `❌ 无法获取内容 (跨域或网络错误): ${error.message}`;
                    p.style.color = 'red';
                    listItem.appendChild(p);
                }
            }
            function extractJs(doc) {
                const externalScripts = Array.from(doc.querySelectorAll('script[src]'));
                const inlineScripts = Array.from(doc.querySelectorAll('script:not([src])'));
                externalScripts.forEach(script => {
                    const src = script.src;
                    if (src) {
                        const li = document.createElement('li');
                        li.className = 'resource-item';
                        li.innerHTML = `<h4>外部 JS 文件</h4><p>链接: ${src}</p>`;
                        jsList.appendChild(li);
                        fetchJsContent(src, li);
                    }
                });
                inlineScripts.forEach(script => {
                    const li = document.createElement('li');
                    li.className = 'resource-item';
                    li.innerHTML = `<h4>内联 JS 脚本</h4><p>内容长度: ${script.textContent.length} 字符</p>`;
                    jsList.appendChild(li);
                    allJsContent += `\n// --- 来自内联脚本 ---\n${script.textContent}\n`;
                });
            }
            async function fetchJsContent(url, listItem) {
                try {
                    const response = await fetch(url);
                    if (response.ok) {
                        const jsText = await response.text();
                        allJsContent += `\n// --- 来自 ${url} ---\n${jsText}\n`;
                        const p = document.createElement('p');
                        p.textContent = `✅ 内容已获取并合并`;
                        listItem.appendChild(p);
                    } else {
                        throw new Error('HTTP ' + response.status);
                    }
                } catch (error) {
                    const p = document.createElement('p');
                    p.textContent = `❌ 无法获取内容 (跨域或网络错误): ${error.message}`;
                    p.style.color = 'red';
                    listItem.appendChild(p);
                }
            }
            function extractDom(element, indent = 0) {
                const tagName = element.tagName.toLowerCase();
                let attributes = '';
                for (let attr of element.attributes) {
                    attributes += ` ${attr.name}="${attr.value}"`;
                }
                const nodeText = ' '.repeat(indent * 4) + `<${tagName}${attributes}>`;
                domTree.textContent += nodeText + '\n';
                // 添加点击事件查看内联样式
                const domNodeSpan = document.createElement('span');
                domNodeSpan.className = 'dom-node';
                domNodeSpan.textContent = nodeText;
                domNodeSpan.addEventListener('click', () => {
                    document.querySelectorAll('.dom-node').forEach(n => n.classList.remove('selected'));
                    domNodeSpan.classList.add('selected');
                    if (element.style.cssText) {
                        inlineStyle.textContent = element.style.cssText;
                        nodeStyleInfo.style.display = 'block';
                    } else {
                        nodeStyleInfo.style.display = 'none';
                    }
                });
                // 为了可点击,我们需要用 DOM 替换 pre 的文本内容
                // 这是一个简化的实现,实际项目中可能需要更复杂的渲染逻辑
                // 这里我们直接输出文本,并附加一个隐藏的点击区域
                domTree.innerHTML += `<span class="dom-node" style="display:inline-block; width:100%; padding:2px 5px; border-radius:3px;">${nodeText}</span>`;
                for (let child of element.children) {
                    extractDom(child, indent + 1);
                }
            }
            // 下载功能
            downloadCssBtn.addEventListener('click', () => {
                const blob = new Blob([allCssContent], { type: 'text/css' });
                const url = URL.createObjectURL(blob);
                const a = document.createElement('a');
                a.href = url;
                a.download = 'extracted_styles.css';
                a.click();
                URL.revokeObjectURL(url);
            });
            downloadJsBtn.addEventListener('click', () => {
                const blob = new Blob([allJsContent], { type: 'application/javascript' });
                const url = URL.createObjectURL(blob);
                const a = document.createElement('a');
                a.href = url;
                a.download = 'extracted_scripts.js';
                a.click();
                URL.revokeObjectURL(url);
            });
        });
    </script>
</body>
</html>

使用方法

  1. 保存代码:将上面的完整代码复制并粘贴到一个新的文本文件中,将其命名为 web_scraper.html
  2. 打开文件:用 Chrome、Firefox 或 Edge 等现代浏览器打开这个 web_scraper.html 文件。
  3. 输入 URL:在顶部的输入框中输入你想要分析的网页地址。https://www.github.comhttps://www.bilibili.com
  4. 开始抓取:点击“开始抓取”按钮。
  5. 查看结果
    • 右侧预览区会显示目标网页的实时渲染效果。
    • 左侧结果区会自动切换到“CSS”选项卡,列出所有找到的 CSS 资源。
    • 你可以点击“JavaScript”和“DOM 结构”选项卡查看对应的分析结果。
    • 如果成功抓取到了内容,底部的“下载合并的 CSS 文件”和“下载合并的 JS 文件”按钮会显示出来,点击即可下载。

重要限制与注意事项

  • 同源策略:这是最大的限制,由于浏览器的安全策略,这个工具只能成功抓取与自身(web_scraper.html)同源的网页资源
    • 什么是同源? web_scraper.html 是通过 file:// 协议打开的,那么它只能抓取 file:// 协议的页面(几乎没用),如果它运行在 http://localhost:8000 上,那么它可以抓取 http://localhost:8000 下的任何页面。
    • 如何解决? 你可以将这个 HTML 文件和一个简单的网页(test.html)放在同一个 Web 服务器(如 VS Code 的 Live Server 插件、Python 的 http.server 等)上运行,然后分析 test.html
  • 动态加载内容:如果目标网页使用 JavaScript 动态加载 CSS 或 JS 文件(例如通过 fetchXHR 请求),这些动态加载的资源可能不会被这个工具捕获,因为它只在页面初始加载时进行分析。
  • 复杂页面:对于非常复杂的单页应用,DOM 树可能会非常庞大,渲染和交互可能会有性能问题。

进阶功能方向

如果你希望进一步增强这个工具,可以考虑:

  1. 解决跨域问题
    • 浏览器扩展:将其开发成一个浏览器扩展,扩展拥有更高的权限,可以绕过部分同源策略限制。
    • 代理服务器:搭建一个简单的代理服务器,所有请求都通过代理转发,由后端服务器去抓取目标网页内容,再返回给前端。
  2. 更多分析功能
    • CSS 选择器分析:列出页面中使用的所有 CSS 类名和 ID。
    • 图片资源分析:提取所有图片链接,并分析其格式、大小等。
    • 性能分析:估算资源加载时间、文件大小等。
  3. 更好的交互
    • 代码高亮:使用 Prism.jshighlight.js 对抓取到的 CSS 和 JS 代码进行语法高亮。
    • 可折叠的 DOM 树:对于大型 DOM 树,提供折叠/展开功能。
    • 样式计算:点击 DOM 节点时,不仅显示内联样式,还显示最终计算出的所有样式。

这个助手已经是一个非常强大的起点,希望能满足你的需求!