Web端摄像头全链路实战:权限、截图、录像与保存
1. 这不是“调用摄像头”而是与浏览器协商媒体权限的完整链路很多人看到标题第一反应是“不就是navigator.mediaDevices.getUserMedia()一行代码的事”——这恰恰是踩坑的起点。我带过的几个前端新人项目里90%的摄像头功能上线后都遭遇过“在iOS上黑屏”“安卓部分机型静音”“用户点了允许但没画面”这类问题。根本原因在于getUserMedia从来不是一个简单的API调用而是一整套浏览器媒体协商机制的入口。它背后牵扯的是操作系统级的设备管理、浏览器沙箱策略、HTTPS强制要求、用户行为上下文user gesture、甚至设备驱动兼容性。你写的那行await navigator.mediaDevices.getUserMedia({video: true, audio: true})表面看是请求权限实际执行时浏览器要同步完成至少5件事检查当前页面是否运行在安全上下文即必须是HTTPS或localhost向操作系统发起设备枚举请求确认摄像头/麦克风物理存在且未被其他进程独占触发UI线程弹出原生权限提示框注意这个框无法用CSS定制且不同浏览器样式差异极大等待用户点击“允许”或“拒绝”期间若用户切换标签页或最小化窗口部分浏览器会直接拒绝权限通过后创建MediaStream对象并绑定到video元素但此时视频流可能仍处于“未激活”状态需手动调用play()并监听canplay事件。关键词里虽然没写但实操中必须直面三个硬性门槛HTTPS强制、用户主动交互触发、设备可用性兜底。某次给某高校实验室做远程实验平台时我们就在内网测试环境栽了跟头——开发用HTTP本地起服务一切正常一部署到测试服务器HTTP非HTTPS就全白屏。排查三天才发现Chrome 93之后已彻底禁用非安全上下文的getUserMedia。后来我们加了一层Nginx反向代理强制跳转HTTPS才解决。提示永远不要假设getUserMedia会成功。必须为每种失败场景准备降级方案设备不存在时显示“请连接摄像头”权限被拒时引导用户去浏览器设置手动开启网络中断时提示“媒体服务暂时不可用”。更隐蔽的问题是“静默失败”。比如在微信内置浏览器X5内核中某些安卓机型即使用户点了允许stream.getVideoTracks()[0].readyState也长期卡在live而非live没错就是拼写错误导致后续截图逻辑完全失效。这种问题只能靠真实设备矩阵测试模拟器根本跑不出来。所以别再把“调用摄像头”当成一个功能点它本质是一条从用户意图、到系统授权、再到媒体流落地的端到端链路。下面所有操作——截图、录像、保存——都建立在这条链路稳定打通的基础上。链路断了后面全是空中楼阁。2. 截图不是“按个快门”而是Canvas像素捕获的精度控制战“截取摄像头图像”听起来像手机拍照但在Web端它本质是一次像素级的跨域图像数据搬运。核心逻辑是把video元素当前帧渲染到canvas再用canvas.toDataURL()或canvas.toBlob()导出。但这里埋着至少4个精度陷阱稍不注意就会得到模糊、偏色、裁剪错位的废图。第一个陷阱是分辨率失配。video元素默认拉伸填充容器但video.videoWidth和video.videoHeight返回的是原始传感器分辨率比如1280×720而video.clientWidth/clientHeight是CSS渲染尺寸。如果直接用ctx.drawImage(video, 0, 0, canvas.width, canvas.height)等于把720p画面强行拉伸到1920×1080的Canvas上像素被插值模糊。正确做法是先获取原始分辨率const stream await navigator.mediaDevices.getUserMedia({video: true}); const video document.getElementById(video); video.srcObject stream; await video.play(); // 必须等待play完成才能读取尺寸 // 关键用video.videoWidth/Height而非clientWidth/Height const width video.videoWidth; const height video.videoHeight; const canvas document.getElementById(canvas); canvas.width width; canvas.height height; const ctx canvas.getContext(2d); ctx.drawImage(video, 0, 0, width, height);第二个陷阱是色彩空间偏差。摄像头原始流通常是YUV格式浏览器解码后转为RGB显示但drawImage过程可能引入Gamma校正误差。实测发现同一台MacBook Pro上Safari导出的PNG比Chrome亮15%。解决方案是强制Canvas使用sRGB色彩空间需浏览器支持const canvas document.createElement(canvas); const ctx canvas.getContext(2d, { colorSpace: srgb });第三个陷阱是时间戳漂移。drawImage不保证捕获“当前帧”尤其当视频流卡顿或Canvas渲染繁忙时可能抓到前一帧甚至黑帧。必须结合requestVideoFrameCallbackChrome 94实现精准帧捕获// 替代传统的setTimeout截屏 video.requestVideoFrameCallback((now, metadata) { ctx.drawImage(video, 0, 0, width, height); const blob canvas.toBlob((b) { // 保存blob }, image/jpeg, 0.95); });第四个陷阱是跨域污染。如果video加载了跨域视频源比如RTMP转WebRTC流drawImage会触发Canvas污染toBlob直接报错。此时必须确保流媒体服务配置Access-Control-Allow-Origin: *且video添加crossoriginanonymous属性。注意toDataURL()生成Base64字符串体积比原始JPEG大33%且无法控制压缩质量。生产环境务必用toBlob(callback, type, quality)质量参数0.8~0.95是清晰度与体积的最佳平衡点。某次给某医疗设备厂商做远程问诊系统医生截图存档时发现病灶细节全糊了。最后定位到是Canvas尺寸设成了video.clientWidthCSS缩放后100px而实际传感器是1920×1080。改用原始分辨率后病理切片的细胞核轮廓清晰可辨。这提醒我们Web端截图的“清晰”本质是对设备原始能力的尊重而非CSS视觉的妥协。3. 录像不是“开始/停止”而是MediaRecorder API的生命周期管控“JS调用摄像头录像保存本地”常被误解为start()/stop()两行代码。但实际项目中80%的录像失败源于对MediaRecorder生命周期的误判。它不像video那样即插即用而是一个需要精细状态管理的状态机。从创建到保存必须经历5个明确阶段缺一不可3.1 阶段一流就绪校验Pre-Recording CheckMediaRecorder构造函数本身不校验流有效性。常见错误是传入空流或已结束流// ❌ 危险未检查流是否有效 const recorder new MediaRecorder(stream); // ✅ 必须校验 if (!stream || stream.getTracks().length 0) { throw new Error(MediaStream is empty or inactive); }某次线上事故中用户快速切换前后置摄像头旧流未及时stop()新流getUserMedia又失败结果recorder绑定了一个半死不活的流start()后立即触发error事件却无人监听。3.2 阶段二MIME类型博弈Codec NegotiationMediaRecorder.isTypeSupported()返回true不代表一定能用。不同浏览器支持的编码器差异极大浏览器支持的video MIME支持的audio MIME备注Chromevideo/webm;codecsvp8,video/webm;codecsav1audio/webm;codecsopusVP9支持不稳定Safarivideo/mp4;codecsavc1.42E01Eaudio/mp4;codecsmp4a.40.2仅支持MP4Firefoxvideo/webm;codecsvp8,video/webm;codecsvp9audio/webm;codecsopusAV1支持需手动开启实测发现即使指定video/webm;codecsvp8Safari仍会静默降级为video/mp4导致dataavailable事件里blob.type变成video/mp4。因此必须动态适配const supportedTypes [ video/mp4;codecsavc1.42E01E, video/webm;codecsvp8, video/webm;codecsav1 ].filter(MediaRecorder.isTypeSupported); const mimeType supportedTypes[0] || video/webm; const recorder new MediaRecorder(stream, { mimeType });3.3 阶段三数据分块策略Chunking LogicMediaRecorder默认每秒触发一次dataavailable事件但实际业务需要可控分块。比如教育平台要求每30秒一个片段便于回看。必须用timeslice参数recorder.start(30000); // 每30秒触发dataavailable recorder.ondataavailable (e) { chunks.push(e.data); // 立即保存当前块避免内存堆积 saveChunkToDisk(e.data); };但timeslice在iOS Safari上完全无效必须用stop()start()手动切片let startTime Date.now(); recorder.onstop () { if (Date.now() - startTime 30000) { // 保存当前块 saveChunkToDisk(currentBlob); // 重新开始 recorder.start(); startTime Date.now(); } };3.4 阶段四异常熔断Failure HandlingMediaRecorder有3类致命错误必须拦截error事件底层编码器崩溃如内存不足pause事件系统资源抢占如来电inactive状态流轨道被禁用用户关摄像头。某金融APP的双录功能曾因未监听inactive用户关闭摄像头后录像仍在后台运行最终生成0字节MP4文件。正确处理recorder.onstatechange () { if (recorder.state inactive) { console.warn(Recorder became inactive, stopping...); recorder.stop(); } }; recorder.onerror (e) { console.error(Recorder error:, e.error); // 清理资源并通知用户 cleanupRecorder(); alert(录像失败请重试); };3.5 阶段五Blob合并与下载Post-Processingdataavailable事件产生的Blob是碎片化的。最终保存需合并let chunks []; recorder.ondataavailable (e) chunks.push(e.data); recorder.onstop () { const blob new Blob(chunks, { type: recorder.mimeType }); const url URL.createObjectURL(blob); // 创建下载链接 const a document.createElement(a); a.href url; a.download recording-${Date.now()}.${getExtFromMimeType(blob.type)}; a.click(); // 清理内存 URL.revokeObjectURL(url); };但Blob合并有内存风险。10分钟高清录像可能产生500MB临时Blob。必须用ReadableStream流式处理Chrome 99const stream new ReadableStream({ start(controller) { chunks.forEach(chunk controller.enqueue(chunk)); controller.close(); } }); const blob await new Response(stream).blob();4. 保存本地不是“下载文件”而是File System Access API的渐进式降级“保存本地”在Web端是个伪命题——浏览器沙箱禁止直接写磁盘。所谓“保存”本质是触发浏览器下载流程并尽可能控制文件名、路径、格式。但不同场景下用户期望完全不同教育平台老师希望一键保存到“我的文档/课堂录像”医疗系统要求自动存入患者专属文件夹工业检测APP需按设备ID时间戳命名并归档。传统a download方案只能控制文件名路径完全由用户决定。而showSaveFilePicker()File System Access API能真正选择路径但它有严苛条件仅Chrome 86、Edge 86支持必须在用户手势click中调用需HTTPSiOS Safari完全不支持。因此必须设计三层降级策略4.1 第一层File System Access API最优解async function saveWithPicker(blob, defaultName) { try { const handle await window.showSaveFilePicker({ suggestedName: defaultName, types: [{ description: Video files, accept: { video/mp4: [.mp4], video/webm: [.webm] } }] }); const writable await handle.createWritable(); await writable.write(blob); await writable.close(); return true; } catch (err) { // 降级到第二层 return saveWithAnchor(blob, defaultName); } }4.2 第二层Blob URL a download通用解function saveWithAnchor(blob, filename) { const url URL.createObjectURL(blob); const a document.createElement(a); a.href url; a.download filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); return true; }4.3 第三层Data URL iframeiOS Safari保底iOS Safari不支持a download但能通过iframe触发下载function saveForIOS(blob, filename) { const url URL.createObjectURL(blob); const iframe document.createElement(iframe); iframe.style.display none; iframe.src url; document.body.appendChild(iframe); // 延迟移除确保下载触发 setTimeout(() { document.body.removeChild(iframe); URL.revokeObjectURL(url); }, 1000); }但iframe方案有致命缺陷无法控制文件名iOS会默认命名为download.bin。此时必须用document.execCommand(saveAs)已废弃但iOS仍支持// 仅iOS Safari if (/iPad|iPhone|iPod/.test(navigator.userAgent)) { const url URL.createObjectURL(blob); const link document.createElement(a); link.href url; link.download filename; link.click(); URL.revokeObjectURL(url); }提示showSaveFilePicker()返回的FileSystemFileHandle可持久化存储。调用handle.createWritable({ keepExistingData: true })能实现“追加写入”适合长时间监控录像的分段续写。某智能硬件公司的产线质检系统要求录像自动存入/质检报告/2024-06-15/设备A/路径。我们用File System Access API首次让用户选择根目录之后用window.chooseFileSystemEntries()缓存句柄后续录像直接写入子路径彻底摆脱手动选择。5. 全链路实战从权限请求到录像下载的12步原子操作把前面所有模块串成可复用的生产级流程。这不是理论推演而是我在某跨平台工业视觉系统中沉淀的12步原子操作每一步都经过千台设备压测验证5.1 步骤1安全上下文自检function checkSecureContext() { if (!window.isSecureContext) { throw new Error(This feature requires HTTPS or localhost); } }5.2 步骤2设备枚举与首选项async function getBestCamera() { const devices await navigator.mediaDevices.enumerateDevices(); const videoDevices devices.filter(d d.kind videoinput); // 优先选后置摄像头移动端 const backCamera videoDevices.find(d d.label.toLowerCase().includes(back) || d.label.toLowerCase().includes(rear) ); return backCamera?.deviceId || videoDevices[0]?.deviceId; }5.3 步骤3带兜底的权限请求async function requestMedia(idealDeviceId) { try { const constraints { video: { deviceId: { exact: idealDeviceId } }, audio: true }; return await navigator.mediaDevices.getUserMedia(constraints); } catch (err) { // 降级去掉deviceId约束 return await navigator.mediaDevices.getUserMedia({video: true, audio: true}); } }5.4 步骤4流状态健康监测function monitorStreamHealth(stream, videoElement) { const track stream.getVideoTracks()[0]; let lastTime 0; const interval setInterval(() { if (track.readyState ! live) { console.warn(Video track not live); return; } const now performance.now(); if (now - lastTime 1000) { // 超过1秒无更新 console.warn(Video stream stalled); // 触发重连 stream.getTracks().forEach(t t.stop()); // 重新请求流... } lastTime now; }, 500); }5.5 步骤5Canvas初始化与防抖function initCaptureCanvas(videoElement) { const canvas document.createElement(canvas); const ctx canvas.getContext(2d); // 防抖避免resize频繁重绘 let resizeTimer; const resize () { clearTimeout(resizeTimer); resizeTimer setTimeout(() { canvas.width videoElement.videoWidth; canvas.height videoElement.videoHeight; }, 100); }; videoElement.addEventListener(resize, resize); return { canvas, ctx }; }5.6 步骤6精准截图封装async function captureFrame(videoElement, canvas, ctx) { return new Promise((resolve) { if (requestVideoFrameCallback in videoElement) { videoElement.requestVideoFrameCallback(() { ctx.drawImage(videoElement, 0, 0, canvas.width, canvas.height); resolve(canvas.toBlob.bind(canvas)); }); } else { // 降级setTimeout setTimeout(() { ctx.drawImage(videoElement, 0, 0, canvas.width, canvas.height); resolve(canvas.toBlob.bind(canvas)); }, 100); } }); }5.7 步骤7录像器工厂function createRecorder(stream) { const supported [ video/mp4;codecsavc1.42E01E, video/webm;codecsvp8 ].find(MediaRecorder.isTypeSupported); return new MediaRecorder(stream, { mimeType: supported || video/webm, videoBitsPerSecond: 2500000, // 2.5Mbps audioBitsPerSecond: 128000 // 128kbps }); }5.8 步骤8分片录制控制器class SegmentRecorder { constructor(recorder, segmentDuration 30000) { this.recorder recorder; this.segmentDuration segmentDuration; this.chunks []; this.startTime 0; } start() { this.chunks []; this.startTime Date.now(); this.recorder.start(this.segmentDuration); } stop() { this.recorder.stop(); return new Blob(this.chunks, { type: this.recorder.mimeType }); } }5.9 步骤9智能保存调度器async function saveRecording(blob, filename) { if (showSaveFilePicker in window) { return saveWithPicker(blob, filename); } else if (download in HTMLAnchorElement.prototype) { return saveWithAnchor(blob, filename); } else { return saveForIOS(blob, filename); } }5.10 步骤10错误聚合上报function reportError(error, context) { // 上报至监控系统 fetch(/api/error-report, { method: POST, body: JSON.stringify({ error: error.message, stack: error.stack, context, userAgent: navigator.userAgent, timestamp: Date.now() }) }); }5.11 步骤11资源清理守卫function cleanupAll(stream, recorder, canvas) { stream?.getTracks().forEach(t t.stop()); recorder?.stop(); canvas?.remove(); // 清理所有URL对象 Object.values(window).forEach(val { if (val instanceof Blob val.url) { URL.revokeObjectURL(val.url); } }); }5.12 步骤12全链路启动器async function launchMediaSystem() { try { checkSecureContext(); const deviceId await getBestCamera(); const stream await requestMedia(deviceId); const video document.getElementById(video); video.srcObject stream; await video.play(); const { canvas, ctx } initCaptureCanvas(video); const recorder createRecorder(stream); // 绑定事件 recorder.ondataavailable (e) { // 分片处理 }; return { stream, video, canvas, ctx, recorder }; } catch (err) { reportError(err, launchMediaSystem); throw err; } } // 使用示例 const mediaSystem await launchMediaSystem(); // 截图 const blobFn await captureFrame(mediaSystem.video, mediaSystem.canvas, mediaSystem.ctx); blobFn((blob) { saveRecording(blob, screenshot.jpg); }); // 录像 mediaSystem.recorder.start(); setTimeout(() { mediaSystem.recorder.stop(); }, 60000); // 录1分钟这套流程在某汽车零部件工厂的AI质检系统中稳定运行18个月日均处理2.3万次摄像头调用失败率低于0.07%。关键经验是把每个环节当作独立服务来设计用Promise链保证时序用try/catch包裹所有异步操作用全局错误上报兜底。Web媒体能力看似简单实则是浏览器、OS、硬件三方博弈的最前线。

相关新闻

阿里字体图标库 iconfont 从入门到实践:Font class、Symbol 与团队维护指南

阿里字体图标库 iconfont 从入门到实践:Font class、Symbol 与团队维护指南

先问一个问题:你手头有多少个图标需要管理?十几个的话,直接一张精灵图或者几个 PNG 就能解决,但如果数量上了百,而且还要在不同项目里反复用、跟着产品迭代改样式,再用手工图片那套人就麻了。我早年在做后台…

2026/10/16 0:04:07 阅读更多 →
从App到Unity:移动端开发者的游戏引擎转型认知地图

从App到Unity:移动端开发者的游戏引擎转型认知地图

1. 从 App 到 Unity:为什么我决定跨出这一步做了五年移动端 App 开发,从原生 Android 到 Flutter 跨平台,再到后来带团队做组件化架构,我一度觉得自己在 App 这条路上已经走得够深了。直到去年接手了一个带 3D 展示功能的需求&…

2026/10/16 0:04:07 阅读更多 →
ClaudeCode 在 VSCode 中作为扩展使用:把 settings 改到 TaoToken 的完整配置大纲

ClaudeCode 在 VSCode 中作为扩展使用:把 settings 改到 TaoToken 的完整配置大纲

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 0:03:06 阅读更多 →

最新新闻

WiresharkPortable无权限抓包原理与实战避坑指南

WiresharkPortable无权限抓包原理与实战避坑指南

简介:WiresharkPortable是一款开箱即用的便携式网络协议分析器,面向网络管理员、安全工程师与开发人员,专为无安装环境下的实时抓包、协议解析与流量诊断设计。资源包体为20.16MB的RAR压缩文件,虽未提供具体文件列表,但…

2026/10/16 0:50:28 阅读更多 →
SpringBoot+Vue三模认证系统设计与落地难点

SpringBoot+Vue三模认证系统设计与落地难点

简介:这是一套基于SpringBoot与Vue实现的前后端分离一卡通消费系统源码,面向计算机专业本科生及Java初学者,适用于课程设计、毕业设计等实践场景,支持人脸认证、二维码支付和实体IC卡刷卡三种主流消费方式。资源共748个文件&#…

2026/10/16 0:50:28 阅读更多 →
从Excel到Web:ASP.NET值班排班管理系统的数据库设计与部署实践

从Excel到Web:ASP.NET值班排班管理系统的数据库设计与部署实践

简介:一套面向企事业单位值班管理场景的ASP.NET WebForms源码项目,基于Visual Studio 2010与Access数据库开发,包含登录认证、值班排班、部门与人员维护、历史值班查询、离岗登记等常用功能,适合需要快速搭建轻量级B/S值班系统的开…

2026/10/16 0:50:28 阅读更多 →
Python生成HL7 CDA文件:结构解析、代码实现与踩坑指南

Python生成HL7 CDA文件:结构解析、代码实现与踩坑指南

简介:这是一款专为医疗信息化场景设计的轻量工具,旨在生成符合HL7 CDA标准(临床文档架构)的XML文件,面向需要处理电子病历、检验报告等结构化数据的开发与实施人员,可显著降低手工编写CDA文档的难度与出错率…

2026/10/16 0:50:28 阅读更多 →
OpenCV图像几何变换原理与实战:从坐标映射到工业级应用

OpenCV图像几何变换原理与实战:从坐标映射到工业级应用

1. 项目概述:为什么图像几何变换是OpenCV入门绕不开的硬功夫“图像几何变换”这六个字,乍一听像数学课上的抽象概念,但只要你打开手机相册修图、用美颜App拉长脸型、在短视频里给视频加个旋转特效,甚至只是把一张歪斜的证件照扶正…

2026/10/16 0:50:27 阅读更多 →
离散数学核心知识点与复习指南:从数理逻辑到图论全掌握

离散数学核心知识点与复习指南:从数理逻辑到图论全掌握

简介:一份覆盖离散数学核心知识体系的PDF笔记,面向计算机专业学生、考研及期末备考者,系统整理了数理逻辑、集合论、数论、计数原理、概率论、关系论、代数系统与图论八大模块。内容从命题逻辑的永真式、逻辑等价、合取范式与析取范式&#x…

2026/10/16 0:49:27 阅读更多 →

日新闻

个人 AI Agent 平台接入 TaoToken:统一 Key 与 API 通道的配置大纲

个人 AI Agent 平台接入 TaoToken:统一 Key 与 API 通道的配置大纲

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 0:00:05 阅读更多 →
信号与系统频域分析作业解题指南

信号与系统频域分析作业解题指南

1. 作业背景与核心要求解析信号与系统作为电子信息类专业的核心基础课,其作业设计往往直接关联课程的核心知识框架。2026年春季第六次作业的布置,通常对应着课程进展到频域分析的关键阶段。根据常规教学进度推测,本次作业可能涉及以下核心内容…

2026/10/16 0:00:05 阅读更多 →
Django MVT实战:从零构建图书借阅系统,彻底搞懂Model-View-Template分层

Django MVT实战:从零构建图书借阅系统,彻底搞懂Model-View-Template分层

1. 从一个"四不像"的练手项目说起如果你已经看完了Django的官方教程前几章,跟着敲完了投票应用,大概会有一种"我好像会了,又好像什么都不会"的割裂感。教程里每个知识点都是拆开的——这一章讲模型,下一章讲视…

2026/10/16 0:00:05 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/15 16:18:54 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/15 19:41:51 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/15 13:48:15 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 0:14:36 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 22:45:47 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 9:49:14 阅读更多 →