├── .gitignore ├── chrome_extension ├── manifest.json ├── popup.html ├── popup.js ├── background.js └── content.js ├── README.md └── extract-content.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.har -------------------------------------------------------------------------------- /chrome_extension/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "Virtual Scroll Clicker", 4 | "version": "1.0", 5 | "description": "自动点击带滚动提示", 6 | "permissions": ["activeTab", "scripting"], 7 | "action": { 8 | "default_popup": "popup.html" 9 | }, 10 | "content_scripts": [ 11 | { 12 | "matches": [""], 13 | "js": ["content.js"] 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /chrome_extension/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 18 | 19 | 20 |
21 | 22 | 27 |
28 | 29 | 30 |
当前状态:未启动
31 |
已点击次数:0
32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /chrome_extension/popup.js: -------------------------------------------------------------------------------- 1 | document.getElementById('start').addEventListener('click', async () => { 2 | const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); 3 | const interval = document.getElementById('clickInterval').value; 4 | 5 | // 使用 chrome.tabs.sendMessage 替代 window.postMessage 6 | chrome.tabs.sendMessage(tab.id, { 7 | type: 'START_CLICKING', 8 | interval: parseInt(interval) 9 | }); 10 | document.getElementById('currentStatus').textContent = '运行中'; 11 | }); 12 | 13 | document.getElementById('stop').addEventListener('click', async () => { 14 | const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); 15 | chrome.tabs.sendMessage(tab.id, { type: 'STOP_CLICKING' }); 16 | document.getElementById('currentStatus').textContent = '已停止'; 17 | }); 18 | 19 | // 更新点击计数 20 | chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { 21 | if (request.type === 'CLICK_COUNT') { 22 | const countElement = document.getElementById('clickCount'); 23 | if (countElement) { 24 | const currentCount = parseInt(countElement.textContent || '0'); 25 | countElement.textContent = currentCount + 1; 26 | } 27 | } 28 | }); -------------------------------------------------------------------------------- /chrome_extension/background.js: -------------------------------------------------------------------------------- 1 | // 存储网络请求 2 | let networkRequests = []; 3 | let isCapturing = false; 4 | 5 | // 监听网络请求 6 | chrome.webRequest.onCompleted.addListener( 7 | function (details) { 8 | if (isCapturing && details.url.toLowerCase().includes('feed')) { 9 | networkRequests.push({ 10 | url: details.url, 11 | method: details.method, 12 | timeStamp: details.timeStamp, 13 | type: details.type, 14 | statusCode: details.statusCode, 15 | statusLine: details.statusLine 16 | }); 17 | } 18 | }, 19 | { urls: [""] } 20 | ); 21 | 22 | // 监听来自content script的消息 23 | chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { 24 | if (request.action === 'START_CAPTURE') { 25 | networkRequests = []; 26 | isCapturing = true; 27 | sendResponse({ status: 'started' }); 28 | } 29 | else if (request.action === 'STOP_CAPTURE') { 30 | isCapturing = false; 31 | // 导出HAR文件 32 | const har = { 33 | log: { 34 | version: '1.2', 35 | creator: { 36 | name: 'Feed Capture Extension', 37 | version: '1.0' 38 | }, 39 | pages: [], 40 | entries: networkRequests.map(request => ({ 41 | startedDateTime: new Date(request.timeStamp).toISOString(), 42 | request: { 43 | method: request.method, 44 | url: request.url 45 | }, 46 | response: { 47 | status: request.statusCode, 48 | statusText: request.statusLine 49 | } 50 | })) 51 | } 52 | }; 53 | 54 | const blob = new Blob([JSON.stringify(har, null, 2)], { type: 'application/json' }); 55 | const url = URL.createObjectURL(blob); 56 | 57 | chrome.downloads.download({ 58 | url: url, 59 | filename: `feed-requests-${new Date().toISOString().replace(/:/g, '-')}.har`, 60 | saveAs: false 61 | }, () => { 62 | URL.revokeObjectURL(url); 63 | }); 64 | 65 | sendResponse({ status: 'completed', count: networkRequests.length }); 66 | } 67 | return true; 68 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # XHS-Easy-Crawler 2 | 3 | 一个基于 Chrome 扩展的小红书数据采集工具,采用模拟点击方式获取数据,无需担心反爬限制。 4 | 5 | ## 特点 6 | 7 | - 🚀 基于 Chrome 扩展,使用简单 8 | - 🛡️ 模拟真实用户行为,不会触发反爬措施 9 | - 📦 自动化数据采集和处理 10 | - 🔍 支持关键词搜索采集 11 | - 💾 支持 HAR 文件导出和解析数据 12 | 13 | ## 工作原理 14 | 15 | 该工具通过模拟用户在小红书网页版的浏览行为来采集数据。使用 Chrome 扩展监控和记录网络请求,最后通过 Python 脚本处理导出的数据。 16 | 17 | ## 使用说明 18 | 19 | ### 前置要求 20 | 21 | - Google Chrome 浏览器 22 | - Python 3.6+ 23 | - 小红书账号 24 | 25 | ### 安装步骤 26 | 27 | 1. 克隆仓库 28 | 29 | ```bash 30 | git clone https://github.com/leafiy/xhs_web_crawler 31 | cd xhs-easy-crawler 32 | ``` 33 | 34 | 2. 在 Chrome 中安装扩展 35 | - 打开 Chrome 扩展管理页面 (chrome://extensions/) 36 | - 启用开发者模式 37 | - 点击"加载已解压的扩展程序" 38 | - 选择项目中的 `chrome_extension` 文件夹 39 | - ![image](https://github.com/user-attachments/assets/30d2a0f4-90c0-4aac-be1f-3d4a963ed14a) 40 | 41 | 42 | ### 使用流程 43 | 44 | 1. **准备工作** 45 | 46 | - 打开小红书网页版 (https://www.xiaohongshu.com) 47 | - 登录您的账号 48 | - 打开 Chrome 开发者工具(F12) 49 | 50 | 2. **数据采集** 51 | 52 | - 在搜索框输入目标关键词 53 | - 点击扩展图标,打开控制面板 54 | - 点击"开始采集"按钮 55 | - 等待自动采集完成 56 | - ![image](https://github.com/user-attachments/assets/ab890a31-f5d3-414a-8533-e988689fdde1) 57 | 58 | 59 | 3. **数据导出** 60 | 61 | - 在 Chrome 开发者工具的 Network 面板中 62 | - 右键点击并选择"Save all as HAR with content" 63 | - 保存生成的 .har 文件 64 | - ![image](https://github.com/user-attachments/assets/a8662677-3d9c-4e13-a001-9e417e8cbb4f) 65 | 66 | 67 | 4. **数据处理** 68 | 69 | ```bash 70 | python extract-content.py 71 | ``` 72 | 73 | 按提示输入 HAR 文件路径,程序将自动提取笔记数据并保存为 JSON 格式。 74 | 75 | ## 输出格式 76 | 77 | 提取的数据将保存为 JSON 文件,包含以下字段: 78 | 79 | ```json 80 | [ 81 | { 82 | "time": 1708778459000, 83 | "share_info": { 84 | "un_share": false 85 | }, 86 | "type": "normal", 87 | "title": "9999", 88 | "user": { 89 | "user_id": "999", 90 | "nickname": "DOME", 91 | "avatar": "999", 92 | "xsec_token": "999=" 93 | }, 94 | "tag_list": [ 95 | { 96 | "type": "topic", 97 | "id": "123", 98 | "name": "论文学习" 99 | } 100 | ], 101 | "at_user_list": [], 102 | "last_update_time": 1709233626000, 103 | "note_id": "7890", 104 | "desc": "78907890", 105 | "interact_info": { 106 | "collected": false, 107 | "collected_count": "123", 108 | "comment_count": "0", 109 | "share_count": "31", 110 | "followed": false, 111 | "relation": "none", 112 | "liked": false, 113 | "liked_count": "127" 114 | }, 115 | "image_list": [ 116 | { 117 | "width": 1179, 118 | "trace_id": "", 119 | "live_photo": false, 120 | "file_id": "", 121 | "height": 2007, 122 | "url": "", 123 | "info_list": [ 124 | { 125 | "image_scene": "WB_PRV", 126 | "url": "123" 127 | }, 128 | { 129 | "image_scene": "WB_DFT", 130 | "url": "123" 131 | } 132 | ], 133 | "url_pre": "123", 134 | "url_default": "123", 135 | "stream": {} 136 | } 137 | ] 138 | } 139 | ] 140 | ``` 141 | 142 | ## 注意事项 143 | 144 | - 请遵守小红书的使用条款和服务协议 145 | - 建议控制爬取频率,避免对服务器造成压力 146 | - 请合理使用采集到的数据,遵守相关法律法规 147 | - 建议在使用前先进行小规模测试 148 | 149 | ## 常见问题 150 | 151 | **Q: 为什么数据采集会暂停?** 152 | A: 可能是网络问题或页面加载延迟,请增加采集延迟 153 | 154 | **Q: 如何设置采集的页数?** 155 | A: 不需要设置,会自动滚动页面 156 | 157 | **Q: 数据解析出错怎么办?** 158 | A: 确保 HAR 文件完整且格式正确,如果问题持续,请提交 Issue。 159 | 160 | ## 许可证 161 | 162 | [MIT License](LICENSE) 163 | 164 | ## 免责声明 165 | 166 | 本工具仅供学习和研究使用,请勿用于商业用途。对于使用本工具所产生的任何问题,作者不承担任何责任。 167 | -------------------------------------------------------------------------------- /chrome_extension/content.js: -------------------------------------------------------------------------------- 1 | let isRunning = false; 2 | const clickedIndexes = new Set(); 3 | let clickInterval = 1000; 4 | 5 | // 创建提示元素 6 | const createPrompt = () => { 7 | const prompt = document.createElement('div'); 8 | prompt.style.cssText = ` 9 | position: fixed; 10 | top: 50%; 11 | left: 50%; 12 | transform: translate(-50%, -50%); 13 | background: rgba(0, 0, 0, 0.8); 14 | color: white; 15 | padding: 20px; 16 | border-radius: 8px; 17 | z-index: 10000; 18 | font-size: 16px; 19 | text-align: center; 20 | `; 21 | prompt.textContent = '当前内容已处理完毕,请滚动页面加载更多内容'; 22 | return prompt; 23 | }; 24 | 25 | // 延时函数 26 | const delay = ms => new Promise(resolve => setTimeout(resolve, ms)); 27 | 28 | // 等待元素出现的函数 29 | async function waitForElement(selector, timeout = 1000) { 30 | const startTime = Date.now(); 31 | while (Date.now() - startTime < timeout) { 32 | const element = document.querySelector(selector); 33 | if (element) return element; 34 | await delay(100); 35 | } 36 | return null; 37 | } 38 | 39 | // 改进的页面滚动函数 40 | async function scrollPage() { 41 | const initialHeight = document.body.scrollHeight; 42 | 43 | // 滚动到页面底部 44 | window.scrollTo(0, document.body.scrollHeight); 45 | 46 | // 等待新内容加载 47 | await delay(1000); 48 | 49 | // 检查是否成功加载了新内容 50 | const newHeight = document.body.scrollHeight; 51 | return newHeight > initialHeight; 52 | } 53 | 54 | // 检查是否所有可见元素都已点击 55 | function allVisibleElementsClicked() { 56 | const covers = document.querySelectorAll('.cover.ld.mask'); 57 | let allClicked = true; 58 | let hasVisibleElements = false; 59 | 60 | for (let cover of covers) { 61 | const noteItem = cover.closest('.note-item'); 62 | if (!noteItem) continue; 63 | 64 | const dataIndex = noteItem.getAttribute('data-index'); 65 | if (window.getComputedStyle(cover).display !== 'none') { 66 | hasVisibleElements = true; 67 | if (!clickedIndexes.has(dataIndex)) { 68 | allClicked = false; 69 | break; 70 | } 71 | } 72 | } 73 | 74 | return hasVisibleElements && allClicked; 75 | } 76 | 77 | // 查找下一个可点击的元素 78 | function findNextClickableElement() { 79 | const covers = document.querySelectorAll('.cover.ld.mask'); 80 | console.log('找到covers元素数量:', covers.length); 81 | 82 | for (let [index, cover] of covers.entries()) { 83 | const noteItem = cover.closest('.note-item'); 84 | if (!noteItem) continue; 85 | 86 | const dataIndex = noteItem.getAttribute('data-index'); 87 | if (clickedIndexes.has(dataIndex)) continue; 88 | 89 | if (window.getComputedStyle(cover).display !== 'none') { 90 | return { cover, dataIndex }; 91 | } 92 | } 93 | return null; 94 | } 95 | 96 | // 点击处理函数 97 | async function handleClick(cover, dataIndex) { 98 | clickedIndexes.add(dataIndex); 99 | console.log(`点击 data-index: ${dataIndex},当前已点击数量:${clickedIndexes.size}`); 100 | 101 | cover.click(); 102 | console.log('点击了封面'); 103 | 104 | chrome.runtime.sendMessage({ type: 'CLICK_COUNT' }); 105 | 106 | await delay(2000); 107 | 108 | console.log('等待关闭按钮出现...'); 109 | const closeButton = await waitForElement('.close-circle'); 110 | if (closeButton) { 111 | console.log('找到关闭按钮,准备关闭'); 112 | closeButton.click(); 113 | console.log('点击关闭按钮'); 114 | await delay(500); 115 | } else { 116 | console.log('未能找到关闭按钮,尝试备用方案'); 117 | const mask = document.querySelector('.note-detail-mask'); 118 | if (mask) { 119 | console.log('尝试点击蒙层关闭'); 120 | mask.click(); 121 | await delay(500); 122 | } 123 | } 124 | } 125 | 126 | // 导出HAR文件 127 | async function exportHAR(filename) { 128 | try { 129 | // 使用CDP获取HAR数据 130 | const harData = await chrome.debugger.sendCommand({ 131 | target: { tabId: chrome.devtools.inspectedWindow.tabId } 132 | }, 'Network.getHAR'); 133 | 134 | // 过滤只包含feed的请求 135 | if (harData && harData.log && Array.isArray(harData.log.entries)) { 136 | harData.log.entries = harData.log.entries.filter(entry => { 137 | return entry.request.url.toLowerCase().includes('feed'); 138 | }); 139 | } 140 | 141 | // 创建Blob对象 142 | const blob = new Blob([JSON.stringify(harData, null, 2)], { type: 'application/json' }); 143 | 144 | // 创建下载链接 145 | const url = URL.createObjectURL(blob); 146 | const a = document.createElement('a'); 147 | a.href = url; 148 | a.download = filename || 'network-log.har'; 149 | 150 | // 触发下载 151 | document.body.appendChild(a); 152 | a.click(); 153 | 154 | // 清理 155 | document.body.removeChild(a); 156 | URL.revokeObjectURL(url); 157 | 158 | console.log('HAR文件导出成功'); 159 | } catch (error) { 160 | console.error('导出HAR文件失败:', error); 161 | } 162 | } 163 | 164 | // 主循环 165 | async function startClickingLoop() { 166 | // 开始记录网络请求 167 | try { 168 | await chrome.debugger.attach({ tabId: chrome.devtools.inspectedWindow.tabId }, '1.3'); 169 | await chrome.debugger.sendCommand({ 170 | target: { tabId: chrome.devtools.inspectedWindow.tabId } 171 | }, 'Network.enable'); 172 | } catch (error) { 173 | console.error('启用网络请求记录失败:', error); 174 | } 175 | 176 | while (isRunning) { 177 | const nextElement = findNextClickableElement(); 178 | 179 | if (nextElement) { 180 | await handleClick(nextElement.cover, nextElement.dataIndex); 181 | await delay(clickInterval); 182 | } else if (allVisibleElementsClicked()) { 183 | console.log('当前视图元素已处理完毕,尝试滚动'); 184 | 185 | const scrolled = await scrollPage(); 186 | 187 | if (!scrolled) { 188 | console.log('已到达底部或无法滚动,停止处理'); 189 | isRunning = false; 190 | break; 191 | } else { 192 | console.log('滚动成功,等待新内容加载'); 193 | await delay(1000); 194 | } 195 | } else { 196 | await delay(1000); 197 | } 198 | } 199 | } 200 | 201 | // 监听来自popup的消息 202 | chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { 203 | console.log('收到消息:', request); 204 | 205 | if (request.type === 'START_CLICKING' && !isRunning) { 206 | console.log('开始点击操作'); 207 | console.log('已点击的索引:', Array.from(clickedIndexes)); 208 | isRunning = true; 209 | if (request.interval) { 210 | clickInterval = request.interval; 211 | } 212 | startClickingLoop(); 213 | } else if (request.type === 'STOP_CLICKING') { 214 | console.log('停止点击操作'); 215 | console.log('已点击的索引:', Array.from(clickedIndexes)); 216 | isRunning = false; 217 | } 218 | }); 219 | 220 | // 初始化MutationObserver 221 | const observer = new MutationObserver((mutations) => { 222 | for (let mutation of mutations) { 223 | if (mutation.type === 'childList' || 224 | (mutation.type === 'attributes' && mutation.attributeName === 'data-index')) { 225 | // 新元素加载的处理 226 | } 227 | } 228 | }); 229 | 230 | function initializeObserver() { 231 | const container = document.querySelector('.feeds-container'); 232 | if (container) { 233 | observer.observe(container, { 234 | childList: true, 235 | subtree: true, 236 | attributes: true, 237 | attributeFilter: ['data-index'] 238 | }); 239 | console.log('观察器已初始化'); 240 | } 241 | } 242 | 243 | // 初始化 244 | if (document.readyState === 'loading') { 245 | document.addEventListener('DOMContentLoaded', initializeObserver); 246 | } else { 247 | initializeObserver(); 248 | } 249 | 250 | 251 | // 错误处理 252 | window.addEventListener('error', (event) => { 253 | console.error('捕获到错误:', event.error); 254 | }); 255 | 256 | window.addEventListener('unhandledrejection', (event) => { 257 | console.error('未处理的Promise拒绝:', event.reason); 258 | }); -------------------------------------------------------------------------------- /extract-content.py: -------------------------------------------------------------------------------- 1 | import json 2 | import base64 3 | from typing import Dict, List, Union, Any, Optional 4 | import os 5 | import sys 6 | from datetime import datetime 7 | 8 | 9 | class HarExtractor: 10 | def __init__(self, har_file: str): 11 | """ 12 | 初始化 HAR 提取器 13 | Args: 14 | har_file (str): HAR 文件的路径 15 | """ 16 | self.har_file = har_file 17 | self.content_list = [] 18 | print(f"\n初始化 HAR 提取器... 目标文件: {har_file}") 19 | 20 | def decode_base64_content(self, content: str) -> str: 21 | """ 22 | 解码 base64 编码的内容 23 | Args: 24 | content (str): base64 编码的字符串 25 | Returns: 26 | str: 解码后的字符串 27 | """ 28 | try: 29 | # 添加填充 30 | padding = 4 - (len(content) % 4) 31 | if padding != 4: 32 | content += '=' * padding 33 | 34 | decoded = base64.b64decode(content) 35 | return decoded.decode('utf-8') 36 | except Exception as e: 37 | print(f"Base64 解码失败: {str(e)}") 38 | return content 39 | 40 | def extract_note_cards(self, content: Any) -> List[Dict]: 41 | """ 42 | 从内容中提取 note_card 信息 43 | Args: 44 | content: 解析后的内容 45 | Returns: 46 | List[Dict]: note_card 列表 47 | """ 48 | note_cards = [] 49 | try: 50 | if isinstance(content, dict): 51 | # 处理标准响应格式 52 | items = content.get('data', {}).get('items', []) 53 | for item in items: 54 | if item.get('model_type') == 'note' and 'note_card' in item: 55 | note_cards.append(item['note_card']) 56 | except Exception as e: 57 | print(f"提取 note_card 时发生错误: {str(e)}") 58 | return note_cards 59 | 60 | def process_content(self, content_text: str, url: str) -> Any: 61 | """ 62 | 处理内容文本,尝试解析 JSON 或解码 base64,并提取 note_card 63 | Args: 64 | content_text (str): 原始内容文本 65 | url (str): 请求URL,用于日志 66 | Returns: 67 | Any: 处理后的内容 68 | """ 69 | try: 70 | # 首先尝试直接解析 JSON 71 | parsed_content = json.loads(content_text) 72 | except json.JSONDecodeError: 73 | try: 74 | # 如果失败,尝试解码 base64 75 | decoded_content = self.decode_base64_content(content_text) 76 | parsed_content = json.loads(decoded_content) 77 | except (json.JSONDecodeError, UnicodeDecodeError): 78 | print(f"警告: URL {url} 的内容既不是有效的 JSON 也不是有效的 base64,将跳过") 79 | return None 80 | 81 | # 提取 note_cards 82 | return self.extract_note_cards(parsed_content) 83 | 84 | def extract_content(self) -> List[Dict[str, Any]]: 85 | """ 86 | 提取 HAR 文件中的内容,支持两种格式 87 | Returns: 88 | List[Dict]: 提取的内容列表 89 | """ 90 | print("\n开始提取内容...") 91 | har_data = self.read_har_file() 92 | 93 | try: 94 | entries = har_data.get('log', {}).get('entries', []) 95 | total_entries = len(entries) 96 | print(f"找到 {total_entries} 个请求记录") 97 | 98 | for entry in entries: 99 | response = entry.get('response', {}) 100 | content = response.get('content', {}) 101 | request = entry.get('request', {}) 102 | 103 | # 处理第一种格式:直接包含内容的情况 104 | if isinstance(entry, dict) and 'url' in entry and 'content' in entry: 105 | note_cards = self.extract_note_cards(entry['content']) 106 | if note_cards: 107 | self.content_list.extend(note_cards) 108 | print(f"成功提取 URL: {entry['url']} 的 {len(note_cards)} 个笔记") 109 | continue 110 | 111 | # 处理第二种格式:标准 HAR 格式 112 | if content and 'text' in content: 113 | url = request.get('url', '') 114 | method = request.get('method', '') 115 | mime_type = content.get('mimeType', '') 116 | 117 | # 处理内容 118 | content_text = content['text'] 119 | note_cards = self.process_content(content_text, url) 120 | 121 | if note_cards: 122 | self.content_list.extend(note_cards) 123 | print(f"成功提取 URL: {url} 的 {len(note_cards)} 个笔记") 124 | 125 | print(f"\n内容提取完成!共提取了 {len(self.content_list)} 个内容项") 126 | return self.content_list 127 | 128 | except Exception as e: 129 | raise Exception(f"提取内容失败: {str(e)}") 130 | 131 | def read_har_file(self) -> Dict: 132 | """ 133 | 读取 HAR 文件 134 | Returns: 135 | Dict: HAR 文件的 JSON 内容 136 | """ 137 | print("正在读取 HAR 文件...") 138 | try: 139 | with open(self.har_file, 'r', encoding='utf-8') as f: 140 | data = json.load(f) 141 | print("HAR 文件读取成功!") 142 | return data 143 | except json.JSONDecodeError as e: 144 | raise Exception(f"HAR 文件格式错误,不是有效的 JSON 格式: {str(e)}") 145 | except Exception as e: 146 | raise Exception(f"读取 HAR 文件失败: {str(e)}") 147 | 148 | def save_to_json(self, output_file: str = None) -> str: 149 | """ 150 | 将提取的内容保存为 JSON 文件 151 | Args: 152 | output_file (str, optional): 输出文件路径。默认为 None,将自动生成 153 | Returns: 154 | str: 保存的文件路径 155 | """ 156 | if not self.content_list: 157 | raise Exception("没有要保存的内容,请先调用 extract_content()") 158 | 159 | if output_file is None: 160 | file_name = os.path.basename(self.har_file) 161 | base_name = os.path.splitext(file_name)[0] 162 | output_file = os.path.join(os.getcwd(), f"{base_name}_content.json") 163 | 164 | print(f"\n正在保存内容到文件: {output_file}") 165 | try: 166 | with open(output_file, 'w', encoding='utf-8') as f: 167 | json.dump(self.content_list, f, ensure_ascii=False, indent=2) 168 | print("文件保存成功!") 169 | return output_file 170 | except Exception as e: 171 | raise Exception(f"保存 JSON 文件失败: {str(e)}") 172 | 173 | def process_har_file(har_file: str, output_file: str = None) -> Dict[str, Any]: 174 | """ 175 | 处理 HAR 文件的便捷函数 176 | Args: 177 | har_file (str): HAR 文件路径 178 | output_file (str, optional): 输出文件路径 179 | Returns: 180 | Dict: 处理结果 181 | """ 182 | start_time = datetime.now() 183 | try: 184 | extractor = HarExtractor(har_file) 185 | content_list = extractor.extract_content() 186 | output_path = extractor.save_to_json(output_file) 187 | 188 | end_time = datetime.now() 189 | processing_time = (end_time - start_time).total_seconds() 190 | 191 | return { 192 | 'success': True, 193 | 'message': f'成功提取 {len(content_list)} 个内容项', 194 | 'output_file': output_path, 195 | 'content_count': len(content_list), 196 | 'processing_time': processing_time 197 | } 198 | except Exception as e: 199 | return { 200 | 'success': False, 201 | 'message': str(e), 202 | 'output_file': None, 203 | 'content_count': 0 204 | } 205 | 206 | def main(): 207 | """主函数,处理用户输入""" 208 | print("\n=== HAR 文件内容提取工具 ===") 209 | 210 | # 获取 HAR 文件路径 211 | while True: 212 | har_path = input("\n请输入HAR文件路径: ").strip() 213 | if not har_path: 214 | print("错误:文件路径不能为空") 215 | continue 216 | if not os.path.exists(har_path): 217 | print("错误:文件不存在") 218 | continue 219 | if not har_path.lower().endswith('.har'): 220 | print("错误:文件必须是 .har 格式") 221 | continue 222 | break 223 | 224 | # 获取输出路径(可选) 225 | output_path = input("\n请输入输出文件路径(直接回车使用默认路径): ").strip() 226 | if not output_path: 227 | output_path = None 228 | print("使用默认输出路径") 229 | elif not output_path.lower().endswith('.json'): 230 | output_path = output_path + '.json' 231 | print(f"输出文件将保存为: {output_path}") 232 | 233 | # 处理文件 234 | print("\n开始处理...") 235 | result = process_har_file(har_path, output_path) 236 | 237 | # 打印结果 238 | print("\n=== 处理结果 ===") 239 | if result['success']: 240 | print(f"状态: 成功") 241 | print(f"输出文件: {result['output_file']}") 242 | print(f"提取内容数: {result['content_count']}") 243 | if 'processing_time' in result: 244 | print(f"处理时间: {result['processing_time']:.2f} 秒") 245 | else: 246 | print(f"状态: 失败") 247 | print(f"错误信息: {result['message']}") 248 | 249 | if __name__ == '__main__': 250 | try: 251 | main() 252 | except KeyboardInterrupt: 253 | print("\n\n程序已被用户中断") 254 | sys.exit(0) 255 | except Exception as e: 256 | print(f"\n程序发生错误: {str(e)}") 257 | sys.exit(1) 258 | finally: 259 | print("\n程序结束") --------------------------------------------------------------------------------