From b1d9f2c518ce3ef49507af328af4d13bf71a3223 Mon Sep 17 00:00:00 2001 From: empty Date: Wed, 3 Dec 2025 16:44:03 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20LLM=20Content=20Ex?= =?UTF-8?q?tractor=20=E6=B5=8F=E8=A7=88=E5=99=A8=E6=89=A9=E5=B1=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 支持框选区域提取网页内容 - 支持整页内容提取 - 输出格式:Markdown/JSON/XML - 自动复制到剪贴板 --- browser-extension/README.md | 94 +++ browser-extension/content.css | 76 +++ browser-extension/content.js | 478 ++++++++++++++ browser-extension/icons/icon128.png | Bin 0 -> 257 bytes browser-extension/icons/icon16.png | Bin 0 -> 79 bytes browser-extension/icons/icon48.png | Bin 0 -> 115 bytes browser-extension/manifest.json | 32 + browser-extension/popup.html | 139 ++++ browser-extension/popup.js | 72 +++ generate_icons.py | 47 ++ mcp.py | 91 +++ result.md | 957 ++++++++++++++++++++++++++++ wechat_dev_seo_structured.json | 81 +++ 13 files changed, 2067 insertions(+) create mode 100644 browser-extension/README.md create mode 100644 browser-extension/content.css create mode 100644 browser-extension/content.js create mode 100644 browser-extension/icons/icon128.png create mode 100644 browser-extension/icons/icon16.png create mode 100644 browser-extension/icons/icon48.png create mode 100644 browser-extension/manifest.json create mode 100644 browser-extension/popup.html create mode 100644 browser-extension/popup.js create mode 100644 generate_icons.py create mode 100644 mcp.py create mode 100644 result.md create mode 100644 wechat_dev_seo_structured.json diff --git a/browser-extension/README.md b/browser-extension/README.md new file mode 100644 index 0000000..82d8d32 --- /dev/null +++ b/browser-extension/README.md @@ -0,0 +1,94 @@ +# LLM Content Extractor + +一个 Chrome 浏览器扩展,用于截取网页内容并转换为大模型友好的格式。 + +## 功能特性 + +- 🎯 **区域框选提取** - 拖拽鼠标框选想要提取的区域 +- 📄 **整页提取** - 一键提取整个页面内容 +- 📝 **多种输出格式** - 支持 Markdown、JSON、XML +- 📋 **自动复制** - 提取后自动复制到剪贴板 +- 💾 **历史记录** - 可随时复制上次提取的内容 + +## 支持提取的内容类型 + +- 标题 (h1-h6) +- 段落 +- 代码块(保留语言标识) +- 有序/无序列表 +- 表格 +- 图片(保留 src 和 alt) +- 链接(保留文本和 href) + +## 安装方法 + +1. 打开 Chrome 浏览器,访问 `chrome://extensions/` +2. 开启右上角的 **开发者模式** +3. 点击 **加载已解压的扩展程序** +4. 选择 `browser-extension` 文件夹 + +## 使用方法 + +1. 点击浏览器工具栏中的扩展图标 +2. 选择输出格式(Markdown/JSON/XML) +3. 点击 **框选区域提取** 或 **提取整页内容** +4. 如果是框选模式,拖拽鼠标选择区域 +5. 提取完成后内容自动复制到剪贴板 + +## 快捷操作 + +- **ESC** - 取消框选模式 + +## 输出示例 + +### Markdown 格式 +```markdown +# 标题 + +这是一段文字内容。 + +- 列表项 1 +- 列表项 2 + +| 表头1 | 表头2 | +| --- | --- | +| 数据1 | 数据2 | +``` + +### JSON 格式 +```json +[ + { + "type": "heading", + "level": 1, + "content": "标题" + }, + { + "type": "paragraph", + "content": "这是一段文字内容。" + } +] +``` + +## 注意事项 + +- 首次使用需要刷新页面才能生效 +- 某些页面可能因安全策略限制而无法使用 +- 图标文件需要自行添加(16x16, 48x48, 128x128 PNG) + +## 开发 + +```bash +# 项目结构 +browser-extension/ +├── manifest.json # 扩展配置 +├── popup.html # 弹出窗口 +├── popup.js # 弹出窗口逻辑 +├── content.js # 内容脚本 +├── content.css # 内容脚本样式 +└── icons/ # 图标文件夹 +``` + +## License + +MIT diff --git a/browser-extension/content.css b/browser-extension/content.css new file mode 100644 index 0000000..a68a4b0 --- /dev/null +++ b/browser-extension/content.css @@ -0,0 +1,76 @@ +.llm-extractor-overlay { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: rgba(0, 0, 0, 0.3); + cursor: crosshair; + z-index: 999998; +} + +.llm-extractor-selection { + position: fixed; + border: 2px dashed #667eea; + background: rgba(102, 126, 234, 0.1); + z-index: 999999; + pointer-events: none; +} + +.llm-extractor-hint { + position: fixed; + top: 20px; + left: 50%; + transform: translateX(-50%); + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + padding: 12px 24px; + border-radius: 8px; + font-size: 14px; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2); + z-index: 1000000; + animation: slideDown 0.3s ease-out; +} + +.llm-extractor-notification { + position: fixed; + bottom: 20px; + right: 20px; + background: #333; + color: white; + padding: 12px 20px; + border-radius: 8px; + font-size: 14px; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2); + z-index: 1000000; + animation: slideUp 0.3s ease-out; +} + +.llm-extractor-notification.fade-out { + opacity: 0; + transition: opacity 0.3s ease-out; +} + +@keyframes slideDown { + from { + transform: translateX(-50%) translateY(-20px); + opacity: 0; + } + to { + transform: translateX(-50%) translateY(0); + opacity: 1; + } +} + +@keyframes slideUp { + from { + transform: translateY(20px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} diff --git a/browser-extension/content.js b/browser-extension/content.js new file mode 100644 index 0000000..3668886 --- /dev/null +++ b/browser-extension/content.js @@ -0,0 +1,478 @@ +// 全局变量 +let isSelecting = false; +let selectionBox = null; +let startX, startY; +let overlay = null; +let currentFormat = 'markdown'; + +// 监听来自 popup 的消息 +chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { + if (request.action === 'startSelection') { + currentFormat = request.format || 'markdown'; + startSelectionMode(); + sendResponse({ success: true }); + } else if (request.action === 'extractFullPage') { + currentFormat = request.format || 'markdown'; + const content = extractContent(document.body); + const formatted = formatContent(content, currentFormat); + copyToClipboard(formatted); + saveToStorage(formatted); + sendResponse({ success: true }); + } + return true; +}); + +// 开始框选模式 +function startSelectionMode() { + isSelecting = true; + + // 创建遮罩层 + overlay = document.createElement('div'); + overlay.className = 'llm-extractor-overlay'; + document.body.appendChild(overlay); + + // 创建提示 + const hint = document.createElement('div'); + hint.className = 'llm-extractor-hint'; + hint.textContent = '拖拽鼠标框选要提取的区域,ESC 取消'; + document.body.appendChild(hint); + + // 绑定事件 + document.addEventListener('mousedown', onMouseDown); + document.addEventListener('keydown', onKeyDown); +} + +function onMouseDown(e) { + if (!isSelecting) return; + + startX = e.clientX; + startY = e.clientY; + + // 创建选择框 + selectionBox = document.createElement('div'); + selectionBox.className = 'llm-extractor-selection'; + selectionBox.style.left = startX + 'px'; + selectionBox.style.top = startY + 'px'; + document.body.appendChild(selectionBox); + + document.addEventListener('mousemove', onMouseMove); + document.addEventListener('mouseup', onMouseUp); +} + +function onMouseMove(e) { + if (!selectionBox) return; + + const currentX = e.clientX; + const currentY = e.clientY; + + const left = Math.min(startX, currentX); + const top = Math.min(startY, currentY); + const width = Math.abs(currentX - startX); + const height = Math.abs(currentY - startY); + + selectionBox.style.left = left + 'px'; + selectionBox.style.top = top + 'px'; + selectionBox.style.width = width + 'px'; + selectionBox.style.height = height + 'px'; +} + +function onMouseUp(e) { + if (!selectionBox) return; + + const rect = selectionBox.getBoundingClientRect(); + + // 清理选择框 + document.removeEventListener('mousemove', onMouseMove); + document.removeEventListener('mouseup', onMouseUp); + + // 查找选区内的元素 + const elements = getElementsInRect(rect); + + if (elements.length > 0) { + // 提取内容 + const content = extractFromElements(elements); + const formatted = formatContent(content, currentFormat); + + copyToClipboard(formatted); + saveToStorage(formatted); + showNotification('✅ 内容已提取并复制到剪贴板'); + } else { + showNotification('❌ 未选中任何内容'); + } + + cleanup(); +} + +function onKeyDown(e) { + if (e.key === 'Escape') { + cleanup(); + } +} + +function cleanup() { + isSelecting = false; + + if (selectionBox) { + selectionBox.remove(); + selectionBox = null; + } + + if (overlay) { + overlay.remove(); + overlay = null; + } + + const hint = document.querySelector('.llm-extractor-hint'); + if (hint) hint.remove(); + + document.removeEventListener('mousedown', onMouseDown); + document.removeEventListener('mousemove', onMouseMove); + document.removeEventListener('mouseup', onMouseUp); + document.removeEventListener('keydown', onKeyDown); +} + +// 获取选区内的元素 +function getElementsInRect(rect) { + const elements = []; + const allElements = document.body.querySelectorAll('*'); + + allElements.forEach(el => { + const elRect = el.getBoundingClientRect(); + if (isRectOverlap(rect, elRect) && isVisibleElement(el)) { + elements.push(el); + } + }); + + // 找到最小公共祖先 + if (elements.length > 0) { + return [findCommonAncestor(elements)]; + } + + return elements; +} + +function isRectOverlap(rect1, rect2) { + return !(rect1.right < rect2.left || + rect1.left > rect2.right || + rect1.bottom < rect2.top || + rect1.top > rect2.bottom); +} + +function isVisibleElement(el) { + const style = window.getComputedStyle(el); + return style.display !== 'none' && + style.visibility !== 'hidden' && + style.opacity !== '0'; +} + +function findCommonAncestor(elements) { + if (elements.length === 1) return elements[0]; + + let ancestor = elements[0]; + for (let i = 1; i < elements.length; i++) { + ancestor = findAncestor(ancestor, elements[i]); + } + return ancestor; +} + +function findAncestor(el1, el2) { + const ancestors = []; + let node = el1; + while (node) { + ancestors.push(node); + node = node.parentElement; + } + + node = el2; + while (node) { + if (ancestors.includes(node)) return node; + node = node.parentElement; + } + + return document.body; +} + +// 从元素中提取内容 +function extractFromElements(elements) { + const content = []; + elements.forEach(el => { + content.push(...extractContent(el)); + }); + return content; +} + +// 提取内容 +function extractContent(root) { + const content = []; + const processed = new Set(); + + function processElement(el) { + if (processed.has(el)) return; + + const tagName = el.tagName?.toLowerCase(); + + // 跳过不需要的元素 + if (['script', 'style', 'noscript', 'iframe', 'svg'].includes(tagName)) { + return; + } + + // 标题 + if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) { + processed.add(el); + content.push({ + type: 'heading', + level: parseInt(tagName[1]), + content: el.textContent.trim() + }); + return; + } + + // 段落 + if (tagName === 'p') { + const text = el.textContent.trim(); + if (text) { + processed.add(el); + content.push({ + type: 'paragraph', + content: text + }); + } + return; + } + + // 代码块 + if (tagName === 'pre' || tagName === 'code') { + if (tagName === 'pre' || !el.closest('pre')) { + processed.add(el); + const lang = el.className.match(/language-(\w+)/)?.[1] || + el.getAttribute('data-lang') || ''; + content.push({ + type: 'code', + language: lang, + content: el.textContent + }); + } + return; + } + + // 列表 + if (tagName === 'ul' || tagName === 'ol') { + processed.add(el); + const items = Array.from(el.querySelectorAll(':scope > li')) + .map(li => li.textContent.trim()) + .filter(Boolean); + if (items.length) { + content.push({ + type: 'list', + ordered: tagName === 'ol', + items: items + }); + } + return; + } + + // 表格 + if (tagName === 'table') { + processed.add(el); + const rows = Array.from(el.querySelectorAll('tr')).map(tr => { + return Array.from(tr.querySelectorAll('th, td')) + .map(cell => cell.textContent.trim()); + }); + if (rows.length) { + content.push({ + type: 'table', + rows: rows + }); + } + return; + } + + // 图片 + if (tagName === 'img') { + processed.add(el); + content.push({ + type: 'image', + src: el.src, + alt: el.alt || '' + }); + return; + } + + // 链接 + if (tagName === 'a') { + processed.add(el); + const text = el.textContent.trim(); + if (text) { + content.push({ + type: 'link', + text: text, + href: el.href + }); + } + return; + } + + // 递归处理子元素 + Array.from(el.children).forEach(child => processElement(child)); + } + + processElement(root); + + // 如果没有提取到结构化内容,回退到纯文本 + if (content.length === 0) { + const text = root.textContent.trim(); + if (text) { + content.push({ + type: 'paragraph', + content: text + }); + } + } + + return content; +} + +// 格式化内容 +function formatContent(content, format) { + switch (format) { + case 'markdown': + return toMarkdown(content); + case 'json': + return JSON.stringify(content, null, 2); + case 'xml': + return toXML(content); + default: + return toMarkdown(content); + } +} + +// 转换为 Markdown +function toMarkdown(content) { + return content.map(item => { + switch (item.type) { + case 'heading': + return '#'.repeat(item.level) + ' ' + item.content + '\n'; + + case 'paragraph': + return item.content + '\n'; + + case 'code': + const lang = item.language || ''; + return '```' + lang + '\n' + item.content + '\n```\n'; + + case 'list': + return item.items.map((text, i) => { + const prefix = item.ordered ? `${i + 1}. ` : '- '; + return prefix + text; + }).join('\n') + '\n'; + + case 'table': + if (item.rows.length === 0) return ''; + const header = '| ' + item.rows[0].join(' | ') + ' |'; + const separator = '| ' + item.rows[0].map(() => '---').join(' | ') + ' |'; + const body = item.rows.slice(1) + .map(row => '| ' + row.join(' | ') + ' |') + .join('\n'); + return [header, separator, body].filter(Boolean).join('\n') + '\n'; + + case 'image': + return `![${item.alt}](${item.src})\n`; + + case 'link': + return `[${item.text}](${item.href})\n`; + + default: + return ''; + } + }).join('\n'); +} + +// 转换为 XML +function toXML(content) { + let xml = '\n\n'; + + content.forEach(item => { + switch (item.type) { + case 'heading': + xml += ` ${escapeXML(item.content)}\n`; + break; + case 'paragraph': + xml += ` ${escapeXML(item.content)}\n`; + break; + case 'code': + xml += ` ${escapeXML(item.content)}\n`; + break; + case 'list': + xml += ` \n`; + item.items.forEach(text => { + xml += ` ${escapeXML(text)}\n`; + }); + xml += ' \n'; + break; + case 'table': + xml += ' \n'; + item.rows.forEach((row, i) => { + xml += ` \n`; + row.forEach((cell, j) => { + xml += ` ${escapeXML(cell)}\n`; + }); + xml += ' \n'; + }); + xml += '
\n'; + break; + case 'image': + xml += ` ${escapeXML(item.alt)}\n`; + break; + case 'link': + xml += ` ${escapeXML(item.text)}\n`; + break; + } + }); + + xml += '
'; + return xml; +} + +function escapeXML(str) { + return str + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +// 复制到剪贴板 +async function copyToClipboard(text) { + try { + await navigator.clipboard.writeText(text); + } catch (err) { + // 降级方案 + const textarea = document.createElement('textarea'); + textarea.value = text; + textarea.style.position = 'fixed'; + textarea.style.opacity = '0'; + document.body.appendChild(textarea); + textarea.select(); + document.execCommand('copy'); + document.body.removeChild(textarea); + } +} + +// 保存到 storage +function saveToStorage(text) { + chrome.storage.local.set({ lastExtraction: text }); +} + +// 显示通知 +function showNotification(message) { + const notification = document.createElement('div'); + notification.className = 'llm-extractor-notification'; + notification.textContent = message; + document.body.appendChild(notification); + + setTimeout(() => { + notification.classList.add('fade-out'); + setTimeout(() => notification.remove(), 300); + }, 2000); +} diff --git a/browser-extension/icons/icon128.png b/browser-extension/icons/icon128.png new file mode 100644 index 0000000000000000000000000000000000000000..1186e48e9e84375b120d6dbe0ac3c8c19697054f GIT binary patch literal 257 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1SEZ8zRdwrCp=voLn>~)z39lxz`${2LvS|_ z%VH+U4;eFfu4mCdhDrl`;b`>_5;h=K8d4U)tOi QKNx_()78&qol`;+0O?62rvLx| literal 0 HcmV?d00001 diff --git a/browser-extension/icons/icon16.png b/browser-extension/icons/icon16.png new file mode 100644 index 0000000000000000000000000000000000000000..85647a3caa6d4b854ff4ab00fa3a21bfa575ba79 GIT binary patch literal 79 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61SBU+%rFB|VxBIJAr-fhJyut{3MENgHCUDS cBa4ASqV}A?{L=@Y097z}y85}Sb4q9e0Fveu*#H0l literal 0 HcmV?d00001 diff --git a/browser-extension/icons/icon48.png b/browser-extension/icons/icon48.png new file mode 100644 index 0000000000000000000000000000000000000000..30371dd8636fd193e5300e4b084a741db03a97ea GIT binary patch literal 115 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1SD@H"], + "js": ["content.js"], + "css": ["content.css"] + } + ], + "icons": { + "16": "icons/icon16.png", + "48": "icons/icon48.png", + "128": "icons/icon128.png" + } +} diff --git a/browser-extension/popup.html b/browser-extension/popup.html new file mode 100644 index 0000000..602d2e6 --- /dev/null +++ b/browser-extension/popup.html @@ -0,0 +1,139 @@ + + + + + + LLM Content Extractor + + + +
+

LLM Content Extractor

+ + + + + + + +
+ + + +
+ +

提取后内容自动复制到剪贴板

+
+ + + + diff --git a/browser-extension/popup.js b/browser-extension/popup.js new file mode 100644 index 0000000..82d9599 --- /dev/null +++ b/browser-extension/popup.js @@ -0,0 +1,72 @@ +document.addEventListener('DOMContentLoaded', () => { + const selectBtn = document.getElementById('selectBtn'); + const fullPageBtn = document.getElementById('fullPageBtn'); + const copyLastBtn = document.getElementById('copyLastBtn'); + const formatSelect = document.getElementById('formatSelect'); + const status = document.getElementById('status'); + + function showStatus(message, type) { + status.textContent = message; + status.className = `status ${type}`; + setTimeout(() => { + status.className = 'status'; + }, 3000); + } + + // 框选区域提取 + selectBtn.addEventListener('click', async () => { + const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); + const format = formatSelect.value; + + await chrome.tabs.sendMessage(tab.id, { + action: 'startSelection', + format: format + }); + + window.close(); + }); + + // 提取整页内容 + fullPageBtn.addEventListener('click', async () => { + const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); + const format = formatSelect.value; + + try { + const response = await chrome.tabs.sendMessage(tab.id, { + action: 'extractFullPage', + format: format + }); + + if (response && response.success) { + showStatus('✅ 内容已复制到剪贴板', 'success'); + } else { + showStatus('❌ 提取失败', 'error'); + } + } catch (err) { + showStatus('❌ 请刷新页面后重试', 'error'); + } + }); + + // 复制上次结果 + copyLastBtn.addEventListener('click', async () => { + const result = await chrome.storage.local.get('lastExtraction'); + if (result.lastExtraction) { + await navigator.clipboard.writeText(result.lastExtraction); + showStatus('✅ 已复制上次结果', 'success'); + } else { + showStatus('❌ 暂无历史记录', 'error'); + } + }); + + // 恢复上次选择的格式 + chrome.storage.local.get('format', (result) => { + if (result.format) { + formatSelect.value = result.format; + } + }); + + // 保存格式选择 + formatSelect.addEventListener('change', () => { + chrome.storage.local.set({ format: formatSelect.value }); + }); +}); diff --git a/generate_icons.py b/generate_icons.py new file mode 100644 index 0000000..a5bda30 --- /dev/null +++ b/generate_icons.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +"""生成简单的扩展图标""" +import struct +import zlib + +def create_png(size, color=(102, 126, 234)): + """创建简单的纯色 PNG 图标""" + + def chunk(chunk_type, data): + return struct.pack('>I', len(data)) + chunk_type + data + struct.pack('>I', zlib.crc32(chunk_type + data) & 0xffffffff) + + # PNG signature + signature = b'\x89PNG\r\n\x1a\n' + + # IHDR chunk + ihdr_data = struct.pack('>IIBBBBB', size, size, 8, 2, 0, 0, 0) + ihdr = chunk(b'IHDR', ihdr_data) + + # IDAT chunk (raw image data) + raw_data = b'' + for y in range(size): + raw_data += b'\x00' # filter byte + for x in range(size): + # 创建圆角矩形效果 + cx, cy = size / 2, size / 2 + radius = size * 0.4 + corner_radius = size * 0.15 + + # 简化:纯色填充 + raw_data += bytes(color) + + compressed = zlib.compress(raw_data, 9) + idat = chunk(b'IDAT', compressed) + + # IEND chunk + iend = chunk(b'IEND', b'') + + return signature + ihdr + idat + iend + +# 生成不同尺寸的图标 +for size in [16, 48, 128]: + png_data = create_png(size) + with open(f'icons/icon{size}.png', 'wb') as f: + f.write(png_data) + print(f'Generated icons/icon{size}.png') + +print('Done!') diff --git a/mcp.py b/mcp.py new file mode 100644 index 0000000..1895975 --- /dev/null +++ b/mcp.py @@ -0,0 +1,91 @@ +import requests +from bs4 import BeautifulSoup +import json + +url = "https://developers.weixin.qq.com/miniprogram/dev/framework/search/seo.html" + +headers = { + "User-Agent": ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/121.0.0.0 Safari/537.36" + ) +} + + +def fetch_html(url): + resp = requests.get(url, headers=headers, timeout=10) + resp.raise_for_status() + resp.encoding = resp.apparent_encoding + return resp.text + + +def extract_structured_content(html): + soup = BeautifulSoup(html, "html.parser") + + # 可能的正文容器 + main = soup.select_one("#docContent, .content, #page-content, .page-content") + if not main: + main = soup.body # 最后兜底 + + data = [] + + for el in main.descendants: + if el.name in ["h1", "h2", "h3", "h4"]: + data.append({ + "type": "heading", + "level": int(el.name[-1]), + "content": el.get_text(strip=True) + }) + + elif el.name == "p": + txt = el.get_text(" ", strip=True) + if txt: + data.append({ + "type": "paragraph", + "content": txt + }) + + elif el.name == "pre": + code = el.get_text("\n", strip=False) + if code: + data.append({ + "type": "code", + "lang": el.get("lang") or el.get("data-lang") or "text", + "content": code + }) + + elif el.name == "table": + rows = [] + for tr in el.select("tr"): + cols = [td.get_text(" ", strip=True) for td in tr.select("th,td")] + rows.append(cols) + + data.append({ + "type": "table", + "rows": rows + }) + + elif el.name in ["ul", "ol"]: + items = [ + li.get_text(" ", strip=True) + for li in el.select("li") + ] + if items: + data.append({ + "type": "list", + "ordered": el.name == "ol", + "items": items + }) + + return data + + +if __name__ == "__main__": + html = fetch_html(url) + structured = extract_structured_content(html) + + with open("wechat_dev_seo_structured.json", "w", encoding="utf-8") as f: + json.dump(structured, f, ensure_ascii=False, indent=2) + + print("结构化内容已写入 wechat_dev_seo_structured.json") diff --git a/result.md b/result.md new file mode 100644 index 0000000..1c38e86 --- /dev/null +++ b/result.md @@ -0,0 +1,957 @@ +[小程序]() + +- 小程序 +- 小游戏 +- 公众号 +- 服务号 +- 开放平台 +- 企业微信 +- 微信支付 +- 视频号 +- 微信小店 +- 智能对话 +- 腾讯小微 + +[开发](https://developers.weixin.qq.com/miniprogram/dev/framework/) + +[介绍](https://developers.weixin.qq.com/miniprogram/introduction/) + +[设计](https://developers.weixin.qq.com/miniprogram/design/) + +[运营](https://developers.weixin.qq.com/miniprogram/product/) + +[数据](https://developers.weixin.qq.com/miniprogram/analysis/wedata/intro/) + +[安全](https://developers.weixin.qq.com/miniprogram/security/basic/) + +[社区](https://developers.weixin.qq.com/community/develop/mixflow) + +[学堂](https://developers.weixin.qq.com/community/business) + +[取消](javascript:;) + +[查看更多](https://developers.weixin.qq.com/doc/search?source=more&query=&doc_type=miniprogram&jumpbackUrl=%2Fdoc%2F) + +[在小程序下暂无结果,查看其它业务相关内容 >](https://developers.weixin.qq.com/doc/search?source=more&query=&doc_type=miniprogram&jumpbackUrl=%2Fdoc%2F) + +- 指南 +- 框架 +- 组件 +- API +- 服务端 +- 平台能力 + 行业能力 + + 商业能力 + + 多端能力 + + 服务市场 + + 城市服务 + + 付费能力 + + 拓展能力 +- 工具 +- 云服务 + 云开发 + + 云托管 +- 更新日志 + +[开发](javascript:;) + +[开发](https://developers.weixin.qq.com/miniprogram/dev/framework/) + +[介绍](https://developers.weixin.qq.com/miniprogram/introduction/) + +[设计](https://developers.weixin.qq.com/miniprogram/design/) + +[运营](https://developers.weixin.qq.com/miniprogram/product/) + +[数据](https://developers.weixin.qq.com/miniprogram/analysis/wedata/intro/) + +[安全](https://developers.weixin.qq.com/miniprogram/security/basic/) + +[指南](javascript:;) + +- 起步 + + 小程序简介 + + 小程序技术发展史 + + 小程序与普通网页开发的区别 + + 体验小程序 + + 开始 + + 申请账号 + + 安装开发者工具 + + 你的第一个小程序 + + 编译预览 + + 小程序代码构成 + + JSON 配置 + + WXML 模板 + + WXSS 样式 + + JS 逻辑交互 + + 小程序宿主环境 + + 渲染层和逻辑层 + + 程序与页面 + + 组件 + + API + + 小程序协同工作和发布 + + 协同工作 + + 小程序的版本 + + 发布上线 + + 运营数据 + 小程序开发指南 +- 目录结构 +- 配置小程序 + + 全局配置 + + 页面配置 +- 小程序框架 + + 介绍 + + 场景值 + + 逻辑层 + + 介绍 + + 注册小程序 + + 注册页面 + + 页面生命周期 + + 页面路由 + + 介绍 + + 页面路由监听 + + 路由事件重写 + + 模块化 + + API + + 视图层 + + 介绍 + + WXML + + WXSS + + WXS + + 事件系统 + + 介绍 + + WXS 响应事件 + + Tap 事件 + + Pointer 事件 + + 简易双向绑定 + + 基础组件 + + 获取界面上的节点信息 + + 响应显示区域变化 + + 分栏模式 + + 动画 + + 初始渲染缓存 +- 小程序运行时 + + 运行环境 + + JavaScript 支持情况 + + 运行机制 + + 更新机制 +- Skyline 渲染引擎 + + 概览 + + 介绍 + + 特性 + + 性能对比 + + 示例体验 + + 支持与差异 + + 基础组件 + + WXSS 样式 + + 增强特性 + + Worklet 动画 + + 手势系统 + + 自定义路由 + + 预设路由效果 + + 容器转场动画 + + 页面返回手势 + + 共享元素动画 + + 全局工具栏 + + 滚动容器及其应用场景 + + 从 WebView 迁移 + + 起步 + + 新版组件框架适配指引 + + 最佳实践 + + 常见兼容问题 + + 发布上线 + + 迁移工具 + + 性能调试 + + 动态 + + 更新日志 + + 特性状态 +- glass-easel 组件框架 + + 介绍 + + 适配指引 + + 新增特性 + + 介绍 + + 在模板中调用 data 里的函数 + + Chaining API + + Chaining API 的 init 函数 + + 动态 slot +- 自定义组件 + + 介绍 + + 组件系统 + + 组件模板和样式 + + Component 构造器 + + 组件间通信与事件 + + 组件生命周期 + + behaviors + + 组件间关系 + + 数据监听器 + + 纯数据字段 + + 抽象节点 + + 自定义组件扩展 + + 开发第三方自定义组件 + + 单元测试 + + 获取更新性能统计信息 + + 占位组件 + + 查看自定义组件数据 +- 插件 + + 介绍 + + 开发插件 + + 使用插件 + + 插件调用 API 的限制 + + 插件使用组件的限制 + + 插件功能页 + + 介绍 + + 用户信息功能页 + + 支付功能页 + + 收货地址功能页 + + 发票功能页 + + 发票抬头功能页 +- 基础能力 + + 网络 + + 使用说明 + + 业务域名 + + 局域网通信 + + 移动解析HttpDNS + + 存储 + + 文件系统 + + 画布 + + 介绍 + + 旧版迁移指南 + + 分包加载 + + 使用分包 + + 独立分包 + + 分包预下载 + + 分包异步化 + + 按需注入和用时注入 + + 多线程 Worker + + 服务端能力 + + 服务端 API + + 消息推送 + + 自定义 tabBar + + 周期性更新 + + 数据预拉取 + + DarkMode 适配指南 + + 大屏适配指南 + + HarmonyOS 适配指南 + + AI/AR + + AI推理能力 + Beta + 介绍 + + 算子支持列表 + + 模型量化推理 + + VisionKit 视觉能力 + + VisionKit 基础 + + 6Dof-水平面 AR + + 6Dof-水平面 AR 扩展能力 + + Marker AR + + 单样本检测(OSD) + + 人脸检测 + + 人体检测 + + 人手检测 + + 鞋部检测 + + OCR检测 + + 身份证检测 + + 深度估计 +- XR-FRAME + + 开发指南 + + 开始 + + 新建一个XR组件 + + 在页面中使用这个组件 + + 添加一个物体 + + 来点颜色和灯光 + + 有点寡淡,加上图像 + + 让场景更丰富,环境数据 + + 动起来,加入动画 + + 还是不够,放个模型 + + 再来点交互 + + 组件通信,加上HUD + + 虚拟 x 现实,追加 AR 能力 + + 识别人脸,给自己戴个面具 + + 手势,给喜欢的作品点赞 + + OSDMarker,给现实物体做标记 + + 2DMarker+视频,让照片动起来 + + 加上魔法,来点粒子 + + 后处理,让画面更加好玩 + + 分享给你的好友吧! + + 之后的,就交给你的创意 +- 连接硬件能力 + + 蓝牙 + + 介绍 + + 蓝牙低功耗 (BLE) + + 蓝牙低功耗网状网络 (BLE Mesh) + + 蓝牙信标 (Beacon) + + 近场通信 (NFC) + + 无线局域网 (Wi-Fi) + + 硬件设备接入 + + 设备消息 + + 设备认证 + + 指引 + + 使用 WMPF(安卓)认证设备 + + 设备认证 SDK(安卓) + + 设备认证 TEE 规范 + + 穿戴设备小程序框架 + + 音视频通话+摄像头(for 硬件) + + 介绍 + + VoIP 通话插件 + + 接入指引 + + 接口文档 + + 发起通话 + + 介绍 + + initByCaller + + callWMPF + + callDevice + + forceHangUpVoip + + getPluginEnterOptions + + getPluginOnloadOptions + + onVoipEvent + + setCustomBtnText + + setUIConfig + + setVoipEndPagePath + + getIotBindContactList + + 错误码 + + 支付刷脸模式 + + 更新日志 + + 小程序摄像头插件 + + 接入指引 + + 开发安卓设备端应用 + + 小程序音视频通话 SDK + + Linux 设备 + + RTOS 设备 + + 云对云设备端 + + 云对云服务端 + + VoIP 视频流指南 + + 异步接口使用指南 + + 硬件抽象层 + + 用户授权设备 + + 设备呼叫手机微信 + + 手机微信呼叫设备(安卓) + + 手机微信呼叫设备(Linux) + + 性能与体验优化 + + 问题排查 + + 常见问题 FAQ + + 通话异常排查指南 + + 通话提醒异常排查指南 + 下载 安卓小程序硬件框架 + 设备组 + + 需要帮助 +- 开放能力 + + 用户信息 + + 小程序登录 + + UnionID 机制说明 + + 授权 + + 开放数据校验与解密 + + 手机号快速验证组件 + + 手机号实时验证组件 + + 获取头像昵称 + + 生物认证 + + 账户卡片 + + 分享到朋友圈 + + 转发 + + 转发 + + 动态消息 + + 小程序私密消息 + + 收藏 + + 聊天素材打开 + + 聊天工具 + + 用工关系 + + 安全能力 + + 小程序加密网络通道 + + 安全键盘 + + 分享数据到微信运动 + + 音视频通话 + + 多人音视频对话 + + 双人音视频对话 + + 打开 App + + 打开半屏小程序 + + 消息 + + 订阅消息 + + 新版一次性订阅消息开发指南 + + 小程序订阅消息(用户通过弹窗订阅)开发指南 + + 订阅消息语音提醒 + + 订阅消息添加提醒 + + 统一服务消息 + + 客服消息 + + 概述 + + 接收消息和事件 + + 发送消息 + + 转发消息 + + 下发客服输入状态 + + 临时素材 + + 位置消息 + + 获取小程序码 + + 获取小程序链接 + + 获取 URL Scheme + + 获取 URL Link + + 获取 Short Link + + 应用:短信打开小程序 + + 应用:NFC 标签打开小程序 + + 小程序账号迁移 + + 视频号 + + 视频号主页 + + 视频号视频 + + 视频号直播 + + 视频号活动 + + 数据分析 + + 附近的小程序 + + 广告 + + Banner 广告 + + 激励视频广告 + + 插屏广告 + + 视频广告 + + 视频前贴广告 + + 格子广告 + + 原生模板广告 + + 广告预加载接口 + + 广告数据接口 + + 广告汇总数据 + + 广告细分数据 + + 广告位清单 + + 结算收入数据 +- 调试 + + 概述 + + vConsole + + Source Map + + 实时日志 + + Errno错误码 +- 性能与体验 + + 概述 + + 启动性能 + + 概述 + + 小程序启动流程 + + 代码包体积优化 + + 代码注入优化 + + 首屏渲染优化 + + 其他优化建议 + + 运行时性能 + + 概述 + + 合理使用 setData + + 渲染性能优化 + + 页面切换优化 + + 资源加载优化 + + 内存优化 + + 性能数据 + + 性能诊断工具 + + 工具介绍 + + 工具评测标准 + + 体验分析 + + 调试工具 + + 概述 + + 真机调试 2.0 + + 「模拟器」和「调试器」 + + 代码质量分析面板 + + FPS 面板 + + 性能面板 + + 体验评分 + + 体验评分简介 + + 评分方法与规则 + + 评分方法 + + 性能 + + 体验 + + 最佳实践 + + WXWebAssembly + + 接口调用频率规范 + + 网络调优 + + 弱网体验优化 +- 安全指引 + + 开发原则与注意事项 + + 通用 + + 接口鉴权 + + 代码管理与泄漏 + + 信息泄露 + + 授权用户信息变更 + + 小程序违规处罚信息通知 + + 后台 + + 注入漏洞 + + 弱口令 + + 文件上传漏洞 + + 文件下载 + + 目录遍历 + + 条件竞争 +- 健康运营指引 + + 用户隐私保护 + + 用户隐私保护指引填写说明 + + 小程序用户隐私保护指引内容介绍 + + 插件用户隐私保护说明内容介绍 + + 小程序隐私协议开发指南 + + 用户安全解决方案 + + 内容安全解决方案 + + 业务安全解决方案 +- 企业微信兼容 +- 基础库 + + 介绍 + + 版本分布 + + 低版本兼容 +- 小程序搜索 + + 小程序搜索优化指南 + + 商品数据接入(内测) +- PC 小程序 + + PC 小程序接入指南 + +# # 小程序搜索优化指南 + +爬虫访问小程序内页面时,会携带特定的 user-agent "mpcrawler" 及场景值:1129 + +判断请求是否来源于官方搜索爬虫的方法: + +签名算法与小程序消息推送接口的签名算法一致。详情 + +参数在请求的header里设置,分别是: +X-WXApp-Crawler-Timestamp +X-WXApp-Crawler-Nonce +X-WXApp-Crawler-Signature + +签名流程如下: +1.将token、X-WXApp-Crawler-Timestamp、X-WXApp-Crawler-Nonce三个参数进行字典序排序 +2.将三个参数字符串拼接成一个字符串进行sha1加密 +3.开发者获得加密后的字符串可与X-WXApp-Crawler-Signature对比,标识该请求来源于微信 + +## # 1. 小程序里跳转的页面 (url) 可被直接打开。 + +小程序页面内的跳转url是我们爬虫发现页面的重要来源,且搜索引擎召回的结果页面 (url) 是必须能直接打开,不依赖上下文状态的。 +特别的:建议页面所需的参数都包含在url + +## # 2. 页面跳转优先采用navigator组件。 + +小程序提供了两种页面路由方式: +a. navigator 组件 +b. 路由 API,包括 navigateTo / redirectTo / switchTab / navigateBack / reLaunch +建议使用 navigator 组件,若不得不使用API,可在爬虫访问时屏蔽针对点击设置的时间锁或变量锁。 + +## # 3. 清晰简洁的页面参数。 + +结构清晰、简洁、参数有含义的 querystring 对抓取以及后续的分析都有很大帮助,但是将 JSON 数据作为参数的方式是比较糟糕的实现。 + +## # 4. 必要的时候才请求用户进行授权、登录、绑定手机号等。 + +建议在必须的时候才要求用户授权(比如阅读文章可以匿名,而发表评论需要留名)。 + +## # 5. 我们不收录 web-view 中的任何内容。 + +我们暂时做不到这一点,长期来看,我们可能也做不到。 + +## # 6. 设置一个清晰的标题和页面缩略图。 + +页面标题和缩略图对于我们理解页面和提高曝光转化有重要的作用。 +通过 wx.setNavigationBarTitle 或 自定义转发内容 onShareAppMessage 对页面的标题和缩略图设置,另外也为 video、audio 组件补齐 poster / poster-for-crawler 属性。 + +[Tap to report.](javascript:;) + +- 关于腾讯 +- 文档中心 +- 辟谣中心 +- 客服中心 + +Copyright © 2012-2025 Tencent. All Rights Reserved. + +- 1. 小程序里跳转的页面 (url) 可被直接打开。 + +- 2. 页面跳转优先采用navigator组件。 + +- 3. 清晰简洁的页面参数。 + +- 4. 必要的时候才请求用户进行授权、登录、绑定手机号等。 + +- 5. 我们不收录 web-view 中的任何内容。 + +- 6. 设置一个清晰的标题和页面缩略图。 + +- 复制 +- 问题反馈 + +[反馈](javascript:;) diff --git a/wechat_dev_seo_structured.json b/wechat_dev_seo_structured.json new file mode 100644 index 0000000..7070e4c --- /dev/null +++ b/wechat_dev_seo_structured.json @@ -0,0 +1,81 @@ +[ + { + "type": "heading", + "level": 1, + "content": "#小程序搜索优化指南" + }, + { + "type": "paragraph", + "content": "爬虫访问小程序内页面时,会携带特定的 user-agent \"mpcrawler\" 及场景值:1129" + }, + { + "type": "paragraph", + "content": "判断请求是否来源于官方搜索爬虫的方法:" + }, + { + "type": "paragraph", + "content": "签名算法与小程序消息推送接口的签名算法一致。 详情" + }, + { + "type": "paragraph", + "content": "参数在请求的header里设置,分别是:\nX-WXApp-Crawler-Timestamp\nX-WXApp-Crawler-Nonce\nX-WXApp-Crawler-Signature" + }, + { + "type": "paragraph", + "content": "签名流程如下:\n1.将token、X-WXApp-Crawler-Timestamp、X-WXApp-Crawler-Nonce三个参数进行字典序排序\n2.将三个参数字符串拼接成一个字符串进行sha1加密\n3.开发者获得加密后的字符串可与X-WXApp-Crawler-Signature对比,标识该请求来源于微信" + }, + { + "type": "heading", + "level": 2, + "content": "#1. 小程序里跳转的页面 (url) 可被直接打开。" + }, + { + "type": "paragraph", + "content": "小程序页面内的跳转url是我们爬虫发现页面的重要来源,且搜索引擎召回的结果页面 (url) 是必须能直接打开,不依赖上下文状态的。\n特别的:建议页面所需的参数都包含在url" + }, + { + "type": "heading", + "level": 2, + "content": "#2. 页面跳转优先采用navigator组件。" + }, + { + "type": "paragraph", + "content": "小程序提供了两种页面路由方式: a. navigator 组件 b. 路由 API,包括 navigateTo / redirectTo / switchTab / navigateBack / reLaunch\n建议使用 navigator 组件,若不得不使用API,可在爬虫访问时屏蔽针对点击设置的时间锁或变量锁。" + }, + { + "type": "heading", + "level": 2, + "content": "#3. 清晰简洁的页面参数。" + }, + { + "type": "paragraph", + "content": "结构清晰、简洁、参数有含义的 querystring 对抓取以及后续的分析都有很大帮助,但是将 JSON 数据作为参数的方式是比较糟糕的实现。" + }, + { + "type": "heading", + "level": 2, + "content": "#4. 必要的时候才请求用户进行授权、登录、绑定手机号等。" + }, + { + "type": "paragraph", + "content": "建议在必须的时候才要求用户授权(比如阅读文章可以匿名,而发表评论需要留名)。" + }, + { + "type": "heading", + "level": 2, + "content": "#5. 我们不收录 web-view 中的任何内容。" + }, + { + "type": "paragraph", + "content": "我们暂时做不到这一点,长期来看,我们可能也做不到。" + }, + { + "type": "heading", + "level": 2, + "content": "#6. 设置一个清晰的标题和页面缩略图。" + }, + { + "type": "paragraph", + "content": "页面标题和缩略图对于我们理解页面和提高曝光转化有重要的作用。\n通过 wx.setNavigationBarTitle 或 自定义转发内容 onShareAppMessage 对页面的标题和缩略图设置,另外也为 video、audio 组件补齐 poster / poster-for-crawler 属性。" + } +] \ No newline at end of file