2025-07-25 18:56:54
======2025.07.25 23:00 更新======
原有提取链接的正则表达式不够严谨,自己使用时提取出现了问题,现进行修复。
// 使用更精确的正则表达式 const urlRegex = /(https?|ftp|file):\/\/[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]/g;
参考:https://www.cnblogs.com/speeding/p/5097790.html
======2025.07.25原文======
因为个人需求,经常需要从文本中提取链接转换成二维码使用,于是用AI写了一个小工具。
核心功能:提取文本中的单个链接转换成二维码
线上地址:https://lab.lklog.cn/QRcode/
代码如下:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>链接转二维码</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; text-align: center; } h1 { margin: 20px 0; color: #333; } textarea { width: 100%; height: 150px; padding: 10px; margin-bottom: 20px; border: 1px solid #ddd; border-radius: 4px; font-size: 16px; } button { background-color: #4CAF50; color: white; border: none; padding: 12px 24px; text-align: center; text-decoration: none; display: block; font-size: 16px; margin: 10px auto; cursor: pointer; border-radius: 4px; transition: background-color 0.3s; } button:hover { background-color: #45a049; } #qrcode { margin: 30px auto; padding: 20px; background-color: white; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); display: none; } .result-url { margin-top: 20px; word-break: break-all; color: #666; font-size: 14px; } .error-message { color: red; margin-top: 10px; display: none; } </style> <script src="https://cdn.jsdelivr.net/npm/[email protected]/qrcode.min.js"></script> </head> <body> <h1>链接转二维码</h1> <textarea id="textInput" placeholder="请粘贴包含单个链接的文本"></textarea> <button onclick="generateQRCode()">生成二维码</button> <div class="error-message" id="errorMessage"></div> <div id="qrcode"></div> <div class="result-url" id="resultUrl"></div> <script> function generateQRCode() { const textInput = document.getElementById('textInput').value.trim(); const urlRegex = /(https?:\/\/[^\s]+)/g; const matches = textInput.match(urlRegex); // 隐藏之前的结果和错误信息 document.getElementById('qrcode').style.display = 'none'; document.getElementById('resultUrl').textContent = ''; document.getElementById('errorMessage').style.display = 'none'; if (!textInput) { showError('请输入内容'); return; } if (!matches) { showError('未找到有效的HTTP/HTTPS链接'); return; } if (matches.length > 1) { showError('找到多个链接,请只输入一个链接'); return; } const link = matches[0]; // 显示提取的链接 document.getElementById('resultUrl').textContent = `提取的链接: ${link}`; // 生成二维码 const qr = qrcode(4, 'L'); qr.addData(link); qr.make(); const qrcodeDiv = document.getElementById('qrcode'); // 调整二维码大小为原来的60% qrcodeDiv.innerHTML = qr.createImgTag(6, 0); qrcodeDiv.style.display = 'inline-block'; } function showError(message) { const errorMessage = document.getElementById('errorMessage'); errorMessage.textContent = message; errorMessage.style.display = 'block'; } </script> </body> </html>
在线文本提取链接自动转换二维码工具最先出现在龙鲲博客。
2025-07-14 16:07:51
======2025.07.25日更新======
1、上传功能修复:原代码中const editor = doc.editor(side); 改成 const editor = doc.cm(side); (感谢友链@倦意博客指正)
2、部分样式调整:由于原代码内字体大小及颜色不够明显,线上部署版本进行了如下调整:
/* 新增右侧编辑器样式 */ .mergely-editor .mergely.ch.ina.rhs { color: #FFA500 !important; text-decoration: underline; } .CodeMirror { font-size: 1.125rem; /* 默认字体大小 */ }
======2025.07.14日原文======
因为经常使用在线文本对比,但不太放心使用公共的在线文本比对工具,于是使用AI编写了相关的功能,代码如下(若要实现离线使用,将静态资源文件全部替换为本地文件即可):
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>在线文本比对工具</title> <meta name="description" content="纯前端在线文本比对工具,无需网络即可使用。支持文本差异高亮显示、文件上传等功能。"> <link href="https://cdn.jsdelivr.net/npm/[email protected]/css/font-awesome.min.css" rel="stylesheet"> <script src="https://cdn.tailwindcss.com"></script> <link href="https://cdn.jsdelivr.net/npm/[email protected]/lib/codemirror.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/[email protected]/theme/material.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/[email protected]/lib/mergely.css" rel="stylesheet"> <script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/codemirror.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/[email protected]/addon/search/searchcursor.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/mergely.min.js"></script> <script> tailwind.config = { theme: { extend: { colors: { primary: '#4B5168', secondary: '#8D426C', tertiary: '#5199D3', accent: '#22C994', warning: '#F0C612', danger: '#D2465D', info: '#FFAB4D', }, fontFamily: { inter: ['Inter', 'system-ui', 'sans-serif'], }, } } } </script> <style type="text/tailwindcss"> @layer utilities { .content-auto { content-visibility: auto; } .text-shadow { text-shadow: 0 2px 4px rgba(0,0,0,0.1); } .bg-gradient-primary { background: linear-gradient(90deg, #8D426C 16%, #5199D3 16%, #5199D3 32%, #22C994 32%, #22C994 48%, #F0C612 48%, #F0C612 64%, #FFAB4D 64%, #FFAB4D 82%, #D2465D 82%); } .border-gradient { border-image: linear-gradient(90deg, #8D426C 16%, #5199D3 16%, #5199D3 32%, #22C994 32%, #22C994 48%, #F0C612 48%, #F0C612 64%, #FFAB4D 64%, #FFAB4D 82%, #D2465D 82%) 1; } } </style> </head> <body class="font-inter bg-gray-50 min-h-screen flex flex-col"> <!-- 顶部导航栏 --> <header class="bg-primary text-white shadow-md sticky top-0 z-50"> <div class="container mx-auto px-4 py-3 flex items-center justify-between"> <div class="flex items-center"> <i class="fa fa-file-code-o text-2xl mr-2 text-accent"></i> <h1 class="text-xl font-bold">文本比对工具</h1> </div> <button class="md:hidden text-white focus:outline-none" id="mobile-menu-button"> <i class="fa fa-bars text-xl"></i> </button> </div> <!-- 底部渐变线 --> <div class="h-1 bg-gradient-primary"></div> </header> <!-- 移动端菜单 (默认隐藏) --> <div class="md:hidden hidden bg-primary text-white shadow-lg absolute w-full z-40" id="mobile-menu"> <!-- 空内容,移除帮助和关于链接 --> </div> <!-- 主内容区 --> <main class="flex-grow container mx-auto px-4 py-8"> <div class="max-w-7xl mx-auto"> <!-- 工具标题和操作区 --> <div class="mb-6 flex flex-col md:flex-row md:items-center md:justify-between"> <div class="mb-4 md:mb-0"> <h2 class="text-[clamp(1.5rem,3vw,2.5rem)] font-bold text-gray-800 flex items-center"> <i class="fa fa-file-signature text-accent mr-3"></i> 在线文本比对工具 </h2> <p class="text-gray-600 mt-1">比较两个文本的差异,高亮显示修改、添加和删除的部分</p> </div> <div class="flex flex-wrap gap-2"> <button id="uploadFile" class="px-4 py-2 bg-warning hover:bg-warning/90 text-gray-800 rounded-lg shadow transition-all duration-200 flex items-center"> <i class="fa fa-cloud-upload-alt mr-2"></i>上传文件 </button> </div> </div> <!-- 文件上传区域 (默认隐藏) --> <div id="fileArea" class="mb-6 bg-gray-100 p-4 rounded-lg shadow-sm hidden transition-all duration-300"> <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div> <label for="lhsFile" class="block text-sm font-medium text-gray-700 mb-1">左侧文件</label> <input type="file" id="lhsFile" class="w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-medium file:bg-accent file:text-white hover:file:bg-accent/90"> </div> <div> <label for="rhsFile" class="block text-sm font-medium text-gray-700 mb-1">右侧文件</label> <input type="file" id="rhsFile" class="w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-medium file:bg-tertiary file:text-white hover:file:bg-tertiary/90"> </div> </div> </div> <!-- 文本比对区域 --> <div class="bg-white rounded-xl shadow-lg overflow-hidden transition-all duration-300 hover:shadow-xl"> <div id="compare1" class="w-full h-[60vh] min-h-[400px]"></div> </div> <!-- 功能说明卡片 --> <div class="mt-8 bg-white rounded-xl shadow-lg p-6 transition-all duration-300 hover:shadow-xl"> <h3 class="text-xl font-bold text-gray-800 mb-4 flex items-center"> <i class="fa fa-info-circle text-tertiary mr-2"></i>使用说明 </h3> <div class="grid grid-cols-1 md:grid-cols-2 gap-6"> <div class="flex items-start"> <div class="flex-shrink-0 bg-tertiary/10 p-3 rounded-full"> <i class="fa fa-edit text-tertiary text-xl"></i> </div> <div class="ml-4"> <h4 class="text-lg font-medium text-gray-800">直接输入文本</h4> <p class="mt-1 text-gray-600">在左右两个编辑器中直接输入或粘贴要比较的文本,系统会自动高亮显示差异。</p> </div> </div> <div class="flex items-start"> <div class="flex-shrink-0 bg-accent/10 p-3 rounded-full"> <i class="fa fa-upload text-accent text-xl"></i> </div> <div class="ml-4"> <h4 class="text-lg font-medium text-gray-800">上传文件</h4> <p class="mt-1 text-gray-600">点击"上传文件"按钮,选择要比较的两个文本文件(支持TXT、HTML、JSON等纯文本格式)。</p> </div> </div> <div class="flex items-start"> <div class="flex-shrink-0 bg-warning/10 p-3 rounded-full"> <i class="fa fa-search text-warning text-xl"></i> </div> <div class="ml-4"> <h4 class="text-lg font-medium text-gray-800">查看差异</h4> <p class="mt-1 text-gray-600">绿色标记表示新增内容,红色标记表示删除内容,黄色标记表示修改内容。</p> </div> </div> <div class="flex items-start"> <div class="flex-shrink-0 bg-info/10 p-3 rounded-full"> <i class="fa fa-mobile text-info text-xl"></i> </div> <div class="ml-4"> <h4 class="text-lg font-medium text-gray-800">响应式设计</h4> <p class="mt-1 text-gray-600">在任何设备上都能获得良好的使用体验,支持从手机到桌面的各种屏幕尺寸。</p> </div> </div> </div> </div> </div> </main> <!-- 页脚 --> <footer class="bg-primary text-white mt-12"> <!-- 顶部渐变线 --> <div class="h-1 bg-gradient-primary"></div> <div class="container mx-auto px-4 py-6"> <div class="flex justify-center items-center"> <p class="text-sm text-gray-300">© 2025 文本比对工具</p> </div> </div> </footer> <!-- 模态框 (用于提示信息) --> <div id="alertModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden"> <div class="bg-white rounded-lg shadow-xl p-6 max-w-md w-full mx-4 transform transition-all"> <div class="text-center"> <div id="modalIcon" class="mx-auto flex items-center justify-center w-16 h-16 rounded-full bg-green-100 mb-4"> <i class="fa fa-check text-green-500 text-2xl"></i> </div> <h3 id="modalTitle" class="text-lg font-medium text-gray-900 mb-2">操作成功</h3> <p id="modalMessage" class="text-gray-500 mb-6">您的操作已成功完成。</p> <button id="modalClose" class="px-4 py-2 bg-primary hover:bg-primary/90 text-white rounded-lg shadow transition-all duration-200"> 确定 </button> </div> </div> </div> <script> // 全局变量存储Mergely实例 let doc; document.addEventListener('DOMContentLoaded', function() { // 初始化Mergely实例 initMergely(); // 移动端菜单切换 document.getElementById('mobile-menu-button').addEventListener('click', function() { const mobileMenu = document.getElementById('mobile-menu'); mobileMenu.classList.toggle('hidden'); }); // 上传文件区域显示/隐藏 document.getElementById('uploadFile').addEventListener('click', function() { const fileArea = document.getElementById('fileArea'); fileArea.classList.toggle('hidden'); // 添加动画效果 if (!fileArea.classList.contains('hidden')) { fileArea.style.maxHeight = '0'; setTimeout(() => { fileArea.style.maxHeight = '200px'; }, 10); } else { fileArea.style.maxHeight = '200px'; setTimeout(() => { fileArea.style.maxHeight = '0'; }, 10); } }); // 处理文件上传 document.getElementById('lhsFile').addEventListener('change', function() { handleFileInput(this, 'lhs'); }); document.getElementById('rhsFile').addEventListener('change', function() { handleFileInput(this, 'rhs'); }); // 窗口大小改变时调整Mergely大小 window.addEventListener('resize', function() { if (doc) { doc.resize(); } }); }); // 初始化Mergely实例 function initMergely() { doc = new Mergely('#compare1', { width: 'auto', license: 'lgpl-separate-notice', cmsettings: { readOnly: false, lineWrapping: true, theme: 'material', lineNumbers: true }, lhs: setValue => setValue('欢迎使用文本比对工具\n请在此处输入或粘贴要比较的第一个文本'), rhs: setValue => setValue('Welcome to the text comparison tool\nPlease enter or paste the second text to compare here') }); // 同时保存到window对象以保持向后兼容性 window.doc = doc; } // 处理文件输入 function handleFileInput(input, side) { const file = input.files[0]; if (!file) return; // 检查文件大小 (限制为30MB) if (file.size > 30 * 1024 * 1024) { showAlert('上传内容不能大于 30 MB!', 'error'); return; } const reader = new FileReader(); reader.onload = e => { if (doc) { // 使用editor方法获取CodeMirror实例并设置值 const editor = doc.editor(side); if (editor) { editor.setValue(e.target.result); showAlert(`已成功加载文件: ${file.name}`, 'success'); } } }; reader.onerror = () => showAlert('读取文件时发生错误', 'error'); reader.readAsText(file, "UTF-8"); } // 显示提示框 function showAlert(message, type = 'success') { const modal = document.getElementById('alertModal'); const title = document.getElementById('modalTitle'); const msg = document.getElementById('modalMessage'); const icon = document.getElementById('modalIcon'); // 设置提示类型 if (type === 'success') { title.textContent = '操作成功'; icon.className = 'mx-auto flex items-center justify-center w-16 h-16 rounded-full bg-green-100 mb-4'; icon.innerHTML = '<i class="fa fa-check text-green-500 text-2xl"></i>'; } else if (type === 'error') { title.textContent = '操作失败'; icon.className = 'mx-auto flex items-center justify-center w-16 h-16 rounded-full bg-red-100 mb-4'; icon.innerHTML = '<i class="fa fa-times text-red-500 text-2xl"></i>'; } else if (type === 'warning') { title.textContent = '警告'; icon.className = 'mx-auto flex items-center justify-center w-16 h-16 rounded-full bg-yellow-100 mb-4'; icon.innerHTML = '<i class="fa fa-exclamation-triangle text-yellow-500 text-2xl"></i>'; } else if (type === 'info') { title.textContent = '信息'; icon.className = 'mx-auto flex items-center justify-center w-16 h-16 rounded-full bg-blue-100 mb-4'; icon.innerHTML = '<i class="fa fa-info-circle text-blue-500 text-2xl"></i>'; } // 设置提示消息 msg.textContent = message; // 显示模态框 modal.classList.remove('hidden'); modal.classList.add('flex'); // 添加动画效果 const modalContent = modal.querySelector('div'); modalContent.classList.add('scale-95', 'opacity-0'); setTimeout(() => { modalContent.classList.remove('scale-95', 'opacity-0'); modalContent.classList.add('scale-100', 'opacity-100'); }, 10); } // 关闭提示框 document.getElementById('modalClose').addEventListener('click', function() { const modal = document.getElementById('alertModal'); const modalContent = modal.querySelector('div'); // 添加关闭动画 modalContent.classList.remove('scale-100', 'opacity-100'); modalContent.classList.add('scale-95', 'opacity-0'); setTimeout(() => { modal.classList.add('hidden'); modal.classList.remove('flex'); }, 300); }); // 点击模态框背景关闭 document.getElementById('alertModal').addEventListener('click', function(e) { if (e.target === this) { document.getElementById('modalClose').click(); } }); </script> </body> </html>
目前代码运行良好,待实现一键清空文本框内容。
在线预览地址:https://lab.lklog.cn/duibis/
在线文本比对工具(附源码)最先出现在龙鲲博客。
2025-07-13 17:05:16
家里卫生间的锁有些不太灵,于是买了一把球形锁,打算自己换上。
在换的过程中发现原有球形锁的螺帽出现了不同程度的生锈,以及拧动过程中导致的滑丝,在搜索了诸多解决方案之后,发现了一个相对有效果的方法,这里记录一下,以备日后查阅。
使用热吹风对着螺丝吹5分钟以上,吹完之后迅速泼上冷水(冰水效果更佳),然后再尝试使用螺丝刀拧松。
原理就是热胀冷缩。在尝试此方法之前,我使用了网传的加电胶布,以及暴力拆除,均没有太多效果。
此次更换卫生间门把手,也让我得到了一些启发,第一就是在容易发生生锈的环境中,不一定要等到物品最终丧失功能再选择更换,完全可以提前更换,这样就可以避免等到真正丧失物理功能需要更换时,拆卸过程中螺丝滑丝等诸多问题。
第二就是为什么在更换卫生间球形锁的过程中,我会进入心流状态,短暂忘却了现实时间的流逝,一门心思只想如何完成更换,目前有诸多猜测,但无法形成完整结论,待进一步探讨。
第三,在安装完成之后,我意识到普通锁的内部结构其实并不是特别复杂,也没有特别的精妙,这似乎也应证了那一句,这世上本没有锁,偷的人多了,才有了锁,以及锁只防君子,防不了小人。
利用热胀冷缩解决螺丝滑丝问题最先出现在龙鲲博客。
2025-07-05 13:54:29
长话短说。需要观看一个没有字幕的外语视频,然后发现PotPlayer(以下简称:Pot)支持自动识别字幕并且可以实时翻译,这里记录一下折腾过程。
一共分为两步,第一步是自动识别字幕。
Pot内单击右键,选择字幕->创建有声字幕->创建有声字幕。
转换引擎:选择Whisper-Faster,点击下载按钮,下载引擎。
型号:选择small(更高版本对算力要求更高,视情况选择)。
型号由于众所周知的原因,无法直接进行下载,可以选择使用魔搭社区进行下载。
faster-whisper-small https://www.modelscope.cn/models/angelala00/faster-whisper-small/files
下载后点击型号两字,会跳转型号存放路径,将下载的文件移动至该文件即可。
然后点击开始按钮,即可开始生成当前语言的字幕文件。
第二步则是实时翻译,原理是借助翻译产品的API来实时进行翻译。
目前内置的翻译产品API除必应首年免费以外,其他均需收费使用。
PS:必应首年免费,注册也略微麻烦,非必要不推荐尝试。
这里有热心网友开发了国内百度翻译的插件,下载地址如下:
https://github.com/fjqingyou/PotPlayer_Subtitle_Translate_Baidu
文件下载之后移动至PotPlayer\Extension\Subtitle\Translate路径,重启Pot即可正常使用。
将百度翻译的API填入Pot内,即可正常开始实时翻译,由于国内产品注册没有难度,这里不做过多介绍。
路径:选择字幕->实时字幕翻译->实时字幕翻译设置。
个人使用之后感觉效果还可以,但是问题也很多,例如,电脑配置较低的情况下,字幕识别的速度相对来说比较慢,再者实时翻译可能存在接口的调用频率限制,例如限制在多少秒钟内只允许调用多少次,所以导致部分内容无法正常显示,当然也有可能是原字幕文件本身夹杂了多种语言,这种情况下,手动翻译都会出错,而API只是代替手动,自然而然也会出错了。
参考:
https://blog.csdn.net/duke_ding2/article/details/144973709
https://www.cnblogs.com/chasingdreams2017/p/18746470
http://www.potplayercn.com/course/potplayer-generate-subtitles-from-voice.html
2025-07-04 10:29:30
======2025.7.4 13:44 更新======
因为除了用B站学习以外,可能还会浏览一些其他视频,但其他视频并不需要跳转片头片尾。
所以,迭代这个版本,增加功能如下:
1、对指定BV号视频跳过片头片尾
2、可自定义每个视频的跳过时间
如果没有这两个需求,可以继续使用V1.2的版本。
PS:两个版本均兼容B站空降助手。
// ==UserScript== // [url=home.php?mod=space&uid=170990]@name[/url] B站指定视频片头片尾跳过 // [url=home.php?mod=space&uid=467642]@namespace[/url] [url=http://tampermonkey.net/]http://tampermonkey.net/[/url] // [url=home.php?mod=space&uid=1248337]@version[/url] 1.4 // @description 仅对指定BV号视频跳过片头片尾,可自定义每个视频的跳过时间 // [url=home.php?mod=space&uid=686208]@AuThor[/url] l9zp6 // [url=home.php?mod=space&uid=195849]@match[/url] [url=https://www.bilibili.com/video/]https://www.bilibili.com/video/[/url]* // @match [url=https://www.bilibili.com/bangumi/play/]https://www.bilibili.com/bangumi/play/[/url]* // [url=home.php?mod=space&uid=609072]@grant[/url] GM_getValue // @grant GM_setValue // @grant GM_addStyle // ==/UserScript== (function() { 'use strict'; // ========================== 配置区域 ========================== // 默认跳过时间(当未指定视频的自定义时间时使用) const DEFAULT_INTRO_SKIP_TIME = 0; // 默认不跳过片头 const DEFAULT_OUTRO_SKIP_TIME = 0; // 默认不跳过片尾 // 指定视频的自定义跳过时间(支持多个BV号) // 格式: 'BV号': { intro: 片头时间, outro: 片尾时间 } const CUSTOM_SKIP_TIMES = { 'BV14yzCYgE9Y': { intro: 10, outro: 5 }, // 示例1:自定义此BV号的跳过时间 'BV14yzCYgE9S': { intro: 5, outro: 3 }, // 示例2:添加更多BV号 // 可继续添加更多BV号配置... }; // ============================================================= // 存储状态信息 let lastVideoKey = ''; // 视频唯一标识 let lastUrl = ''; // 上次的完整URL let isWaitingForNextVideo = false; // 是否等待下一个视频 let lastProcessTime = 0; // 上次处理时间 let skipCooldown = false; // 跳过冷却标志 let forceProcess = false; // 强制处理标志 // 添加样式 GM_addStyle(` .skip-hint { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background-color: rgba(0, 0, 0, 0.7); color: white; padding: 10px 20px; border-radius: 5px; font-size: 18px; z-index: 10000; opacity: 0; transition: opacity 0.3s; } .skip-hint.show { opacity: 1; } `); // 创建提示元素 function createHintElement() { const hint = document.createElement('div'); hint.className = 'skip-hint'; document.body.appendChild(hint); return hint; } // 获取播放器元素 function getPlayer() { return document.querySelector('bwp-video, video'); } // 获取播放器容器 function getPlayerContainer() { return document.querySelector('#bilibili-player, .bpx-player-container'); } // 显示提示 function showHint(text) { let hint = document.querySelector('.skip-hint'); if (!hint) { hint = createHintElement(); } hint.textContent = text; hint.classList.add('show'); // 获取播放器容器 const playerContainer = getPlayerContainer(); // 检查是否全屏 const isFullscreen = document.fullscreenElement || document.webkitFullscreenElement; if (playerContainer && !isFullscreen) { // 非全屏状态,将提示添加到播放器容器内 playerContainer.appendChild(hint); // 重置位置样式 hint.style.top = '50%'; hint.style.left = '50%'; hint.style.transform = 'translate(-50%, -50%)'; } else { // 全屏状态,提示保持在body内 document.body.appendChild(hint); } setTimeout(() => { hint.classList.remove('show'); }, 2000); // 提示显示2秒 } // 获取当前视频的BV号 function getCurrentBVId() { const pathMatch = location.pathname.match(/\/video\/(BV\w+)/); return pathMatch ? pathMatch[1] : ''; } // 生成视频唯一标识(只考虑BV号和p参数) function generateVideoKey() { const bvid = getCurrentBVId(); if (!bvid) return ''; const pMatch = location.search.match(/[?&]p=(\d+)/); const pValue = pMatch ? `_p${pMatch[1]}` : ''; return `${bvid}${pValue}`; } // 获取当前视频的跳过时间配置 function getSkipTimes() { const bvid = getCurrentBVId(); // 如果是指定视频,使用自定义时间,否则使用默认时间 if (CUSTOM_SKIP_TIMES[bvid]) { console.log(`使用自定义跳过时间 for ${bvid}:`, CUSTOM_SKIP_TIMES[bvid]); return CUSTOM_SKIP_TIMES[bvid]; } return { intro: DEFAULT_INTRO_SKIP_TIME, outro: DEFAULT_OUTRO_SKIP_TIME }; } // 检查视频是否在指定列表中 function isVideoIncluded() { const bvid = getCurrentBVId(); return !!CUSTOM_SKIP_TIMES[bvid]; } // 获取视频标题 function getVideoTitle() { const titleElement = document.querySelector('h1.title, .media-info-title'); return titleElement ? titleElement.textContent.trim() : ''; } // 检查是否是新视频 function isNewVideo() { const currentVideoKey = generateVideoKey(); const currentUrl = location.href; let isNew = currentVideoKey !== lastVideoKey; // 如果URL相同但需要强制处理 if (!isNew && currentUrl === lastUrl && forceProcess) { console.log('强制处理相同URL的视频'); isNew = true; forceProcess = false; } if (isNew) { console.log('检测到新视频:', currentVideoKey); lastVideoKey = currentVideoKey; lastUrl = currentUrl; } else if (currentUrl !== lastUrl) { console.log('URL参数变化但视频未变:', currentUrl); lastUrl = currentUrl; } return isNew; } // 获取并更新记忆进度 function processMemoryProgress(player) { // 如果不是指定视频,直接返回 if (!isVideoIncluded()) return; const videoKey = generateVideoKey(); if (!videoKey) return; const memoryTime = GM_getValue(`bilibili_memory_${videoKey}`, 0); const { intro } = getSkipTimes(); // 如果记忆进度在片头时间之后,不做处理 if (memoryTime > intro) { console.log(`检测到记忆进度:${memoryTime}秒,超过片头时间${intro}秒,不跳过`); return; } // 如果是新视频且进度为0,跳转到片头跳过时间 if (player.currentTime <= intro) { console.log(`跳转到片头跳过时间:${intro}秒`); player.currentTime = intro; showHint(`已跳过${intro}秒片头`); } } // 检查片尾并跳过 function checkAndSkipOutro(player, duration) { // 如果不是指定视频,直接返回 if (!isVideoIncluded()) return; // 防止无限循环跳转 if (skipCooldown) return; const { outro } = getSkipTimes(); const currentTime = player.currentTime; // 检查是否在片尾区域 if (duration - currentTime <= outro) { // 防止在短时间内重复跳转 const now = Date.now(); if (now - lastProcessTime < 2000) { console.log('检测到重复跳转尝试,跳过此次操作'); return; } lastProcessTime = now; // 设置冷却期 skipCooldown = true; setTimeout(() => { skipCooldown = false; }, 2000); // 跳转到离片尾0.5秒处,确保能触发下一集 player.currentTime = duration - 0.5; console.log(`已跳过${outro}秒片尾,跳转到${duration - 0.5}秒`); showHint(`已跳过${outro}秒片尾`); // 标记为等待下一个视频,并设置强制处理 isWaitingForNextVideo = true; forceProcess = true; } } // 检查是否应该处理新视频 function shouldProcessNewVideo(player) { if (!player) return false; // 如果正在等待下一个视频且播放器已加载新视频 if (isWaitingForNextVideo && player.currentTime < 5) { console.log('检测到自动连播的新视频'); isWaitingForNextVideo = false; return true; } // 检查是否是新视频 return isNewVideo(); } // 主处理函数 function processVideo() { const player = getPlayer(); if (!player) return; // 检查是否应该处理新视频 if (shouldProcessNewVideo(player)) { console.log('检测到需要处理的新视频:', getVideoTitle()); // 处理记忆进度 processMemoryProgress(player); } // 保存进度 const videoKey = generateVideoKey(); if (videoKey) { GM_setValue(`bilibili_memory_${videoKey}`, player.currentTime); } // 检查片尾 if (player.duration && player.duration > 0) { checkAndSkipOutro(player, player.duration); } } // 初始化 function init() { console.log('B站片头片尾跳过脚本已加载'); // 监听播放器状态变化 const player = getPlayer(); if (player) { player.addEventListener('loadedmetadata', () => { console.log('播放器加载了新的元数据'); processVideo(); }); player.addEventListener('timeupdate', () => { // 播放器时间更新时检查 processVideo(); }); } // 监听URL变化 new MutationObserver(() => { console.log('检测到URL变化'); setTimeout(() => { processVideo(); }, 300); }).observe(document, { childList: true, subtree: true, attributes: true, attributeFilter: ['href'] }); // 定时检查 setInterval(() => { const player = getPlayer(); if (player && !player.paused) { processVideo(); } }, 1000); // 初始处理 setTimeout(processVideo, 1000); } // 等待页面加载完成 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();
======2025.7.4 原文======
众所周知,目前B站已经成为了最大的学习乐园之一,拥有海量的学习资源。
但在学习的过程中,很多的学习视频会考虑宣传等目的,在片头或者片尾加上与视频内容实质无关的片段,极其影响学习。
因为太久没写Javascript,以及不喜欢重复造轮子的想法,在尝试完市面上大部分的脚本之后。
我选择使用AI编写了脚本的大部分代码,目前已稳定支持全自动跳过固定片头片尾。
// ==UserScript== // @name B站片头片尾跳过 // @namespace http://tampermonkey.net/ // @version 1.2 // @description 自动跳过B站前10秒片头和后5秒片尾,支持任意URL参数 // @author l9zp6 // @match https://www.bilibili.com/video/* // @match https://www.bilibili.com/bangumi/play/* // @grant GM_getValue // @grant GM_setValue // @grant GM_addStyle // ==/UserScript== (function() { 'use strict'; // 配置参数 const INTRO_SKIP_TIME = 10; // 片头跳过时间(秒) const OUTRO_SKIP_TIME = 5; // 片尾跳过时间(秒) const SHOW_TIME = 2; // 提示显示时间(秒) // 存储上一次的视频信息 let lastVideoKey = ''; // 视频唯一标识(忽略无关参数) let lastUrl = ''; // 上次的完整URL let isWaitingForNextVideo = false; // 是否正在等待下一个视频 let lastProcessTime = 0; // 上次处理时间 let skipCooldown = false; // 跳过冷却标志 let forceProcess = false; // 强制处理标志 // 添加样式 GM_addStyle(` .skip-hint { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background-color: rgba(0, 0, 0, 0.7); color: white; padding: 10px 20px; border-radius: 5px; font-size: 18px; z-index: 10000; opacity: 0; transition: opacity 0.3s; } .skip-hint.show { opacity: 1; } `); // 创建提示元素 function createHintElement() { const hint = document.createElement('div'); hint.className = 'skip-hint'; document.body.appendChild(hint); return hint; } // 获取播放器元素 function getPlayer() { return document.querySelector('bwp-video, video'); } // 获取播放器容器 function getPlayerContainer() { return document.querySelector('#bilibili-player, .bpx-player-container'); } // 显示提示 function showHint(text) { let hint = document.querySelector('.skip-hint'); if (!hint) { hint = createHintElement(); } hint.textContent = text; hint.classList.add('show'); // 获取播放器容器 const playerContainer = getPlayerContainer(); // 检查是否全屏 const isFullscreen = document.fullscreenElement || document.webkitFullscreenElement; if (playerContainer && !isFullscreen) { // 非全屏状态,将提示添加到播放器容器内 playerContainer.appendChild(hint); // 重置位置样式 hint.style.top = '50%'; hint.style.left = '50%'; hint.style.transform = 'translate(-50%, -50%)'; } else { // 全屏状态,提示保持在body内 document.body.appendChild(hint); } setTimeout(() => { hint.classList.remove('show'); }, SHOW_TIME * 1000); } // 生成视频唯一标识(只考虑BV号和p参数) function generateVideoKey() { const pathMatch = location.pathname.match(/\/video\/(BV\w+)/); if (!pathMatch) return ''; const bvid = pathMatch[1]; const pMatch = location.search.match(/[?&]p=(\d+)/); const pValue = pMatch ? `_p${pMatch[1]}` : ''; return `${bvid}${pValue}`; } // 获取视频标题 function getVideoTitle() { const titleElement = document.querySelector('h1.title, .media-info-title'); return titleElement ? titleElement.textContent.trim() : ''; } // 检查是否是新视频 function isNewVideo() { const currentVideoKey = generateVideoKey(); const currentUrl = location.href; let isNew = currentVideoKey !== lastVideoKey; // 如果URL相同但需要强制处理 if (!isNew && currentUrl === lastUrl && forceProcess) { console.log('强制处理相同URL的视频'); isNew = true; forceProcess = false; } if (isNew) { console.log('检测到新视频:', currentVideoKey); lastVideoKey = currentVideoKey; lastUrl = currentUrl; } else if (currentUrl !== lastUrl) { console.log('URL参数变化但视频未变:', currentUrl); lastUrl = currentUrl; } return isNew; } // 获取并更新记忆进度 function processMemoryProgress(player) { const videoKey = generateVideoKey(); if (!videoKey) return; const memoryTime = GM_getValue(`bilibili_memory_${videoKey}`, 0); // 如果记忆进度在片头时间之后,不做处理 if (memoryTime > INTRO_SKIP_TIME) { console.log(`检测到记忆进度:${memoryTime}秒,超过片头时间,不跳过`); return; } // 如果是新视频且进度为0,跳转到片头跳过时间 if (player.currentTime <= INTRO_SKIP_TIME) { console.log(`跳转到片头跳过时间:${INTRO_SKIP_TIME}秒`); player.currentTime = INTRO_SKIP_TIME; showHint(`已跳过${INTRO_SKIP_TIME}秒片头`); } } // 检查片尾并跳过 function checkAndSkipOutro(player, duration) { // 防止无限循环跳转 if (skipCooldown) return; const currentTime = player.currentTime; // 检查是否在片尾区域 if (duration - currentTime <= OUTRO_SKIP_TIME) { // 防止在短时间内重复跳转 const now = Date.now(); if (now - lastProcessTime < 2000) { console.log('检测到重复跳转尝试,跳过此次操作'); return; } lastProcessTime = now; // 设置冷却期 skipCooldown = true; setTimeout(() => { skipCooldown = false; }, 2000); // 跳转到离片尾0.5秒处,确保能触发下一集 player.currentTime = duration - 0.5; console.log(`已跳过${OUTRO_SKIP_TIME}秒片尾,跳转到${duration - 0.5}秒`); showHint(`已跳过${OUTRO_SKIP_TIME}秒片尾`); // 标记为等待下一个视频,并设置强制处理 isWaitingForNextVideo = true; forceProcess = true; } } // 检查是否应该处理新视频 function shouldProcessNewVideo(player) { if (!player) return false; // 如果正在等待下一个视频且播放器已加载新视频 if (isWaitingForNextVideo && player.currentTime < 5) { console.log('检测到自动连播的新视频'); isWaitingForNextVideo = false; return true; } // 检查是否是新视频 return isNewVideo(); } // 主处理函数 function processVideo() { const player = getPlayer(); if (!player) return; // 检查是否应该处理新视频 if (shouldProcessNewVideo(player)) { console.log('检测到需要处理的新视频:', getVideoTitle()); // 处理记忆进度 processMemoryProgress(player); } // 保存进度 const videoKey = generateVideoKey(); if (videoKey) { GM_setValue(`bilibili_memory_${videoKey}`, player.currentTime); } // 检查片尾 if (player.duration && player.duration > OUTRO_SKIP_TIME) { checkAndSkipOutro(player, player.duration); } } // 初始化 function init() { console.log('B站片头片尾跳过脚本已加载'); // 监听播放器状态变化 const player = getPlayer(); if (player) { player.addEventListener('loadedmetadata', () => { console.log('播放器加载了新的元数据'); processVideo(); }); player.addEventListener('timeupdate', () => { // 播放器时间更新时检查 processVideo(); }); } // 监听URL变化 new MutationObserver(() => { console.log('检测到URL变化'); setTimeout(() => { processVideo(); }, 300); }).observe(document, { childList: true, subtree: true, attributes: true, attributeFilter: ['href'] }); // 定时检查 setInterval(() => { const player = getPlayer(); if (player && !player.paused) { processVideo(); } }, 1000); // 初始处理 setTimeout(processVideo, 1000); } // 等待页面加载完成 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();
本脚本是专门为了学习而编写的脚本,也只有在学习时使用该脚本才有可能有相应的效果,严禁将此脚本内容用于任何商业或非法用途,对于因违反此说明而产生的任何法律后果,用户需自行承担全部责任。
2025-06-24 22:54:42
晚上家里台式电脑在通过微信打开表格文件时,开始频繁出现“我们遇到了一个无法恢复的问题,您的文件已经自动保存到备份文件夹中,请勿担忧。如果此错误反复发生,请将错误报告发送给我们”。
在网上查找了相关的解决方案,例如关闭火绒的文件扫描、重置WPS的自带设置、安装WPS教育版,均无法很好的解决问题。
这里记录一下折中的解决方案:
安装WPS教育专版:https://ncre.neea.edu.cn/html1/report/1507/861-1.htm
优势:具备通用性,功能全面,不会出现教育版以及特制版本使用异常的问题,包括不限于过期、无法登录等。
劣势:没有WPS最新功能,UI相对最新版不够好看,可能不支持宏之类的高级功能(一般人也用不上)。