下面我将为你提供一个完整的、可以直接使用的解决方案,包括:
- 功能设计:明确这个助手应该做什么。
- 界面设计:一个清晰、易用的用户界面。
- 完整代码:HTML, CSS, 和 JavaScript 的全部代码。
- 使用方法:如何使用这个工具。
- 进阶功能:可以进一步扩展的方向。
功能设计
这个助手至少需要包含以下核心功能:
- URL 输入:用户输入目标网页的地址。
- 一键抓取:点击按钮后,加载目标网页并分析其资源。
- CSS 展示:
- 列出页面中所有引入的 CSS 文件链接(
<link>标签)。 - 列出页面中所有内联的 CSS 样式(
<style>标签)。 - 可以将所有 CSS 内容合并成一个文件并下载。
- 列出页面中所有引入的 CSS 文件链接(
- JS 展示:
- 列出页面中所有引入的外部 JS 文件链接(
<script src="...">标签)。 - 列出页面中所有内联的 JS 代码(
<script>标签)。 - 可以将所有 JS 内容合并成一个文件并下载。
- 列出页面中所有引入的外部 JS 文件链接(
- DOM 树分析:
- 展示页面的基本 DOM 结构(HTML 标签树)。
- 点击某个标签,可以查看其内联的
style属性。
- 错误处理:处理无效链接、跨域错误等情况,并给出友好提示。
界面设计
界面分为三个主要部分:
- 控制区:顶部,包含 URL 输入框、抓取按钮和一些状态提示。
- 结果展示区:主体部分,使用选项卡(Tabs)切换显示 CSS、JS 和 DOM 的分析结果。
- 预览区:右侧或下方,一个
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>
使用方法
- 保存代码:将上面的完整代码复制并粘贴到一个新的文本文件中,将其命名为
web_scraper.html。 - 打开文件:用 Chrome、Firefox 或 Edge 等现代浏览器打开这个
web_scraper.html文件。 - 输入 URL:在顶部的输入框中输入你想要分析的网页地址。
https://www.github.com或https://www.bilibili.com。 - 开始抓取:点击“开始抓取”按钮。
- 查看结果:
- 右侧预览区会显示目标网页的实时渲染效果。
- 左侧结果区会自动切换到“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 文件(例如通过
fetch或XHR请求),这些动态加载的资源可能不会被这个工具捕获,因为它只在页面初始加载时进行分析。 - 复杂页面:对于非常复杂的单页应用,DOM 树可能会非常庞大,渲染和交互可能会有性能问题。
进阶功能方向
如果你希望进一步增强这个工具,可以考虑:
- 解决跨域问题:
- 浏览器扩展:将其开发成一个浏览器扩展,扩展拥有更高的权限,可以绕过部分同源策略限制。
- 代理服务器:搭建一个简单的代理服务器,所有请求都通过代理转发,由后端服务器去抓取目标网页内容,再返回给前端。
- 更多分析功能:
- CSS 选择器分析:列出页面中使用的所有 CSS 类名和 ID。
- 图片资源分析:提取所有图片链接,并分析其格式、大小等。
- 性能分析:估算资源加载时间、文件大小等。
- 更好的交互:
- 代码高亮:使用
Prism.js或highlight.js对抓取到的 CSS 和 JS 代码进行语法高亮。 - 可折叠的 DOM 树:对于大型 DOM 树,提供折叠/展开功能。
- 样式计算:点击 DOM 节点时,不仅显示内联样式,还显示最终计算出的所有样式。
- 代码高亮:使用
这个助手已经是一个非常强大的起点,希望能满足你的需求!
