【油猴脚本】生成纯元素CSS选择器(附开发笔记)
脚本介绍
用途
常见的CSS选择器:
#gatsby-focus-wrapper > div > div > div.Box-nv15kw-0.Flex-arghxi-0.layout___StyledFlex-sc-1qhwq3g-0.iUtsKT.fNYvIR > div.Box-nv15kw-0.iiMOdu > div > div > div > div:nth-child(8) > div > div
经过脚本转换后的CSS选择器:
body > div > div:nth-child(1) > div > div > div:nth-child(2) > div:nth-child(1) > div > div > div > div:nth-child(8) > div > div
可以发现转换后的CSS选择器只包含元素,这就是这个脚本的目的。
注意:
准备转换的CSS选择器不能包含找不到实际元素的伪类和伪元素,如:hover、::after等,因为脚本需要用JS的document.querySelector函数在网页中找到对应的元素。只要是正常利用浏览器复制得到的CSS选择器都行。CSS选择器种类参见:https://developer.mozilla.org/zh-CN/docs/Learn/CSS/Building_blocks/Selectors
网页中iframe里的元素不适用,因为每个嵌入的浏览上下文(embedded browsing context)都有自己的会话历史记录 (session history)和DOM 树。而CSS的作用域是相同的document,因此不能直接用CSS选择器获取到iframe里的元素。参见:
https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/iframe
https://stackoverflow.com/questions/21842373
补充:利用浏览器复制CSS选择器
首先打开开发者模式
Windows和Linux:ctrl+ shift+i
Mac:command + ⌘ + i
点击下图标识的图标
鼠标移动到需要选择的网页元素上,可以发现右边的开发者工具中会出现你选中的元素。
右键点击开发者工具中出现的元素然后按照图中顺序选择复制selector
安装地址
脚本发布在greasyfork:https://greasyfork.org/zh-CN/scripts/460714
脚本Github仓库文件地址:https://github.com/coycs/Greasy-Fork/blob/main/%E7%94%9F%E6%88%90%E7%BA%AF%E5%85%83%E7%B4%A0CSS%E9%80%89%E6%8B%A9%E5%99%A8/main.js,如果你只是需要用这个脚本就忽略这个地址,去greasyfork安装就可以用了。
使用
(1)有一定的编程基础并且不想安装油猴脚本
获取到目标元素的选择器后,作为函数调用参数填入指定位置,复制全部代码粘贴在浏览器的控制台:
const genTagSelector = (selector) => {let targetNode = document.querySelector(selector);let tagSelector = "";while (targetNode.nodeName != "HTML") {const parentNode = targetNode.parentNode;const childNodes = Array.from(parentNode.childNodes).filter(node => node.nodeName != "#text" && node.nodeName != "#comment");const tagName = targetNode.tagName.toLowerCase();let nthIndex = 0;// 判断父元素下目标元素的标签名是否唯一if (childNodes.filter(node => node.tagName.toLowerCase() == tagName).length == 1) {if (parentNode.nodeName != "HTML") {tagSelector = ` > ${tagName}` + tagSelector;} else {tagSelector = `html > ${tagName}` + tagSelector;}} else {// 获取nthIndex的序号for (let i = 0; i < childNodes.length; i++) {if (childNodes[i] === targetNode) {nthIndex = i + 1;break;}};if (parentNode.nodeName != "HTML") {tagSelector = ` > ${tagName}:nth-child(${nthIndex})` + tagSelector;} else {tagSelector = `html > ${tagName}:nth-child(${nthIndex})` + tagSelector;}}targetNode = parentNode;}return tagSelector;
}// 函数传入选择器字符串
// 如:genTagSelector("body > div > p")
genTagSelector("")
(2)直接使用油猴脚本
按照图中步骤先点击油猴脚本图标(对应1),然后点击该脚本菜单的按钮“输入选择器”(对应2)。
出现对话框后在输入框中输入想要转换的CSS选择器,然后点击“确定”按钮。
随后出现一个新的对话框,复制对话框中的内容然后点击“确定”按钮关闭对话框。
开发笔记
开发背景
小小的油猴脚本也需要对于前端基础知识的了解很深入。
开发这个脚本的想法源于自己想要用puppeteer来将https://docs.npmjs.com/的页面转成pdf来仔细阅读做笔记。使用puppeteer的过程中需要用到CSS选择器来需要获取到侧边栏中目录的链接。直接用浏览器复制到的CSS选择器如下:
#gatsby-focus-wrapper > div > div > div.Box-nv15kw-0.Flex-arghxi-0.layout___StyledFlex-sc-1qhwq3g-0.iUtsKT.fNYvIR > div.Box-nv15kw-0.iiMOdu > div > div > div > div:nth-child(8) > div > div > div:nth-child(3) > div
很容易发现没有几个id,class名也有一些生成的内容在里面,我担心这种CSS选择器的稳定性,我不想在下次运行脚本前还有很大的可能性需要重新复制一下CSS选择器。
于是我想网页dom结构一般是稳定的,那么只要选择器不涉及class类和id就满足需要了。我首先想到的是用xpath,因为它也可以直接在浏览器复制,比如:
/html/body/div/div[1]/div/div[1]
但自己对xpath了解不多,简单学习一下发现相比于平常使用的CSS选择器来说要麻烦很多,不符合开发习惯,也容易出错,于是还是选择CSS选择器,xpath的使用可以去这里https://developer.mozilla.org/zh-CN/docs/Web/XPath/Introduction_to_using_XPath_in_JavaScript学习。
既然我不想慢慢分析dom结构写出不涉及class类和id的CSS选择器但又需要用到,那就写一个脚本来辅助生成吧。
基本思路
主要的思路是先用浏览器复制到元素的选择器,接下来在程序中获取到其父元素,再在父元素中循环用nth-child来判断各个子元素是否是目标元素,这样就可以找到nth-child的index,就这样循环往复直到body元素。这个思路的灵感来自开发其他项目时查询到的stackflow回答。
按这个思路应该可以用递归,最后判断一下元素的父元素是否是body元素就可以了。但是我想到网页结构嵌套可能比较多,使用递归会不会导致效率低和栈溢出呢?考虑后决定使用循环比较稳妥。
根据上面的思路,简单写出了核心的根据CSS选择器生成纯元素CSS选择器的函数:
const genTagSelector = (selector) => {let targetNode = document.querySelector(selector);let tagSelector = "";while (targetNode.nodeName != "BODY") {const parentNode = targetNode.parentNode;const tagName = targetNode.tagName.toLowerCase();const tagNameNodes = Array.from(parentNode.querySelectorAll(tagName));let nthIndex = 0;for (let i = 0; i < tagNameNodes.length; i++) {if (tagNameNodes[i] === targetNode) {nthIndex = i + 1;break;}};if (parentNode.nodeName != "BODY") {tagSelector = ` > ${tagName}:nth-child(${nthIndex})` + tagSelector;} else {tagSelector = `body > ${tagName}:nth-child(${nthIndex})` + tagSelector;}targetNode = parentNode;}return tagSelector;}
解决问题
:nth-child
初步写出核心函数后简单测试一下:
genTagSelector("#gatsby-focus-wrapper > div > div > div.Box-nv15kw-0.Flex-arghxi-0.layout___StyledFlex-sc-1qhwq3g-0.iUtsKT.fNYvIR > div.Box-nv15kw-0.iiMOdu > div > div > div > div:nth-child(8) > div > div > div:nth-child(3) > div")
输出结果为:
body > div:nth-child(1) > div:nth-child(1) > div:nth-child(1) > div:nth-child(1) > div:nth-child(11) > div:nth-child(1) > div:nth-child(1) > div:nth-child(1) > div:nth-child(1) > div:nth-child(15) > div:nth-child(1) > div:nth-child(1) > div:nth-child(3) > div:nth-child(1)
验证以下发现没有找到目标元素:
经过不断删减选择器测试发现直到这里是有效的:
body > div:nth-child(1) > div:nth-child(1) > div:nth-child(1)
这样也是有效的:
body > div:nth-child(1) > div:nth-child(1) > div:nth-child(1) > div
但这样就无效了:
body > div:nth-child(1) > div:nth-child(1) > div:nth-child(1) > div:nth-child(1)
我对于:nth-child的认知可以以div:nth-child(1)为例,就是选中某个元素下的所有div元素中的排在第一个的元素。
但按照我的思路来看程序应该不会出现这种错误才对,于是我思考是否是自己对于:nth-child的理解不到位呢?
查询了MDN文档https://developer.mozilla.org/zh-CN/docs/Web/CSS/:nth-child发现自己确实存在误解:
:nth-child(an+b) 这个 CSS 伪类首先找到所有当前元素的兄弟元素,然后按照位置先后顺序从 1 开始排序。
结合一个例子可以更好理解:
span:nth-child(1)
表示父元素中子元素为第一的并且名字为 span 的标签被选中
那么以a:nth-child(b)为例,就是先找到a的所有兄弟元素,按照先后排序,然后找到b位置的为a的元素。
这样看来我原来的理解的确是错误的,那么根据正确的理解写CSS选择器来验证一下:
body > div:nth-child(1) > div:nth-child(1) > div:nth-child(1)>div:nth-child(2)
可以发现符合正确理解对应的预期结果。
这时候我又想到了:first-child,如果我用div:first-child能成功获取到目标元素吗?因为按照我对:first-child的理解和:nth-child最初的理解是一致的,就是获取到父元素下的所有div元素中的排在第一个的元素。测试一下:
body > div:nth-child(1) > div:nth-child(1) > div:nth-child(1) > div:first-child
可以发现没有成功获取到,那么说明我的理解有误,怀疑和:nth-child如出一辙。
查询一下MDN文档:https://developer.mozilla.org/zh-CN/docs/Web/CSS/:first-child
也是获取一组兄弟元素,而不是仅仅:first-child前面的元素,以c:first-child为例。那么按照正确理解来解释就是获取到c的所有兄弟元素,按照先后排序,然后获取到第一位置为c的元素。测试验证一下:
body > div:nth-child(1) > div:nth-child(1) > div:nth-child(1) > a:first-child
可以发现成功获取到,验证了自己的想法。
既然已经纠正了对于:nth-child的理解,那修改一下代码:
const genTagSelector = (selector) => {let targetNode = document.querySelector(selector);let tagSelector = "";while (targetNode.nodeName != "BODY") {const parentNode = targetNode.parentNode;const childNodes = Array.from(parentNode.childNodes);const tagName = targetNode.tagName.toLowerCase();let nthIndex = 0;for (let i = 0; i < childNodes.length; i++) {if (childNodes[i] === targetNode) {nthIndex = i + 1;break;}};if (parentNode.nodeName != "BODY") {tagSelector = ` > ${tagName}:nth-child(${nthIndex})` + tagSelector;} else {tagSelector = `body > ${tagName}:nth-child(${nthIndex})` + tagSelector;}targetNode = parentNode;}return tagSelector;}
节点与nodeName
当我用上面的代码来测试自己简单写的网页时发现又出现了问题。
自己写的网页html代码:
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head><body><div><p>p1</p><p>p2</p><div>div1</div><div>div2</div></div>
</body></html>
渲染出的网页:
选择元素测试:
验证结果:
可以发现没有正确找到目标元素,而且:nth-child内的序号都找到了6,但很明显网页元素没有那么多啊,按照上面的思路怎么会错误得这么离谱。
在仔细检查了各个元素的childNodes后,我发现了问题所在。
body内明明只有一个div元素,哪里来的两个text元素?查看这个text元素的属性后我明白了原因:
根据data属性值可以知道是换行符产生了这个节点,根据这个nodeName属性值"#text"查询MDN文档发现这是一个文本节点https://developer.mozilla.org/zh-CN/docs/Web/API/Node/nodeName
https://developer.mozilla.org/zh-CN/docs/Web/API/Node/nodeType
但这不也是一个节点吗,按照我之前的代码来说应该也没有问题啊?
待我仔细再去查看:nth-child的概念后发现了问题:
注意:nth-child找的是元素节点,文本节点不是元素节点,按照我的代码去分析就是找到了所有的节点包括文本节点,但:nth-child找的是元素节点,那么这两个的数量就有可能不一样,又可能一样的情况就是全部是元素节点,比如源代码中body内容都在一行里:
既然二者的数量有可能不一致,那确定的:nth-child的序号就有可能出错,而对我自己写的网页测试就是出现的这样的错误。要解决这个问题就要让确定:nth-child序号时所有的子节点都是元素节点。修改后代码如下:
const genTagSelector = (selector) => {let targetNode = document.querySelector(selector);let tagSelector = "";while (targetNode.nodeName != "BODY") {const parentNode = targetNode.parentNode;const childNodes = Array.from(parentNode.childNodes).filter(node => node.nodeName != "#text" && node.nodeName != "#comment");const tagName = targetNode.tagName.toLowerCase();let nthIndex = 0;for (let i = 0; i < childNodes.length; i++) {if (childNodes[i] === targetNode) {nthIndex = i + 1;break;}};if (parentNode.nodeName != "BODY") {tagSelector = ` > ${tagName}:nth-child(${nthIndex})` + tagSelector;} else {tagSelector = `body > ${tagName}:nth-child(${nthIndex})` + tagSelector;}targetNode = parentNode;}return tagSelector;
}
如果仔细看可以发现代码中还排除了"#comment",也就是注释节点,但我一开始觉得印象里网页中好像没有注释内容,也就忽略了,但在测试出错后才发现确实存在注释节点:
生成内容较长
使用上面核心函数生成的CSS选择器像这样:
body > div:nth-child(1) > div:nth-child(1) > div:nth-child(1) > div:nth-child(2) > div:nth-child(2) > div:nth-child(1) > div:nth-child(1) > div:nth-child(1) > div:nth-child(1) > div:nth-child(8) > div:nth-child(1) > div:nth-child(2) > div:nth-child(3) > div:nth-child(2)
可以发现除了body,其他部分都带有:nth-child,但分析网页发现有的元素的子元素只有一个,那么就可以直接用这个子元素的标签名选择,而不不需要:nth-child(1)。因此代码就需要判断一下父元素的子元素是否只有一个,优化后的代码如下:
const genTagSelector = (selector) => {let targetNode = document.querySelector(selector);let tagSelector = "";while (targetNode.nodeName != "BODY") {const parentNode = targetNode.parentNode;const childNodes = Array.from(parentNode.childNodes).filter(node => node.nodeName != "#text" && node.nodeName != "#comment");const tagName = targetNode.tagName.toLowerCase();let nthIndex = 0;// 判断父元素下目标元素的标签名是否唯一if (childNodes.filter(node => node.tagName.toLowerCase() == tagName).length == 1) {if (parentNode.nodeName != "BODY") {tagSelector = ` > ${tagName}` + tagSelector;} else {tagSelector = `body > ${tagName}` + tagSelector;}} else {// 获取nth-child的序号for (let i = 0; i < childNodes.length; i++) {if (childNodes[i] === targetNode) {nthIndex = i + 1;break;}};if (parentNode.nodeName != "BODY") {tagSelector = ` > ${tagName}:nth-child(${nthIndex})` + tagSelector;} else {tagSelector = `body > ${tagName}:nth-child(${nthIndex})` + tagSelector;}}targetNode = parentNode;}return tagSelector;}
简化后的CSS选择器:
body > div > div:nth-child(1) > div > div > div:nth-child(2) > div:nth-child(1) > div > div > div > div:nth-child(8) > div > div > div:nth-child(3) > div
可以发现确实是更简单一点。
整合油猴
既然核心函数写好了,那么整合进油猴内就主要解决交互使用的问题了,主要考虑的就是怎么将生成后内容复制进剪切板。
一开始的想法是想要在生成后自动复制到剪切板,这样用户用得就比较方便自然。
查询后https://www.zhangxinxu.com/wordpress/2021/10/js-copy-paste-clipboard/发现常用的有两种方法:document.execCommand,navigator.clipboard.writeText。
但查询后发现document.execCommand已经不推荐使用了:
https://developer.mozilla.org/zh-CN/docs/Web/API/Document/execCommand
navigator.clipboard.writeText也存在兼容性和需要在HTTPS的问题:
https://developer.mozilla.org/zh-CN/docs/Web/API/Clipboard
而且无法想象的是safari浏览器会出现什么错误,各种厂商自己的浏览器又会出现什么错误。“能够使用”和“可能出错”哪个更重要?肯定是“能够使用”,那么就要放弃自动复制的思路,选择最原始的方式,直接展示,然后用户自己复制。
我最先想到的是Window.alert方法,查看了一下兼容性,发现很好,那为什么不用呢:
https://caniuse.com/?search=alert
其他内容就是和油猴扩展本身有关的了,内容比较简单,最后脚本的全部代码如下:
// ==UserScript==
// @name 生成纯元素CSS选择器
// @namespace coycs.com
// @version 1.0.0
// @description generate a element-only CSS selector
// @author coycs
// @match http://*/*
// @match https://*/*
// @grant GM_registerMenuCommand
// @run-at document-end
// @license MIT
// ==/UserScript==(function () {"use strict";// 根据选择器生成元素选择器const genTagSelector = (selector) => {let targetNode = document.querySelector(selector);let tagSelector = "";while (targetNode.nodeName != "BODY") {const parentNode = targetNode.parentNode;const childNodes = Array.from(parentNode.childNodes).filter(node => node.nodeName != "#text" && node.nodeName != "#comment");const tagName = targetNode.tagName.toLowerCase();let nthIndex = 0;// 判断父元素下目标元素的标签名是否唯一if (childNodes.filter(node => node.tagName.toLowerCase() == tagName).length == 1) {if (parentNode.nodeName != "BODY") {tagSelector = ` > ${tagName}` + tagSelector;} else {tagSelector = `body > ${tagName}` + tagSelector;}} else {// 获取nth-child的序号for (let i = 0; i < childNodes.length; i++) {if (childNodes[i] === targetNode) {nthIndex = i + 1;break;}};if (parentNode.nodeName != "BODY") {tagSelector = ` > ${tagName}:nth-child(${nthIndex})` + tagSelector;} else {tagSelector = `body > ${tagName}:nth-child(${nthIndex})` + tagSelector;}}targetNode = parentNode;}return tagSelector;}// 转换选择器const tranSelector = () => {const promptContent = window.prompt("请将原始的选择器粘贴在下面");// 判断粘贴内容是否合格if (promptContent === null) {// 点击取消按钮return;} else if (promptContent === "") {// 输入框内容为空时点击确定window.alert("内容为空!");} else if (!document.querySelector(promptContent)) {// 选择器无效window.alert("请检查选择器是否正确!");} else {const tagSelector = genTagSelector(promptContent);window.alert(tagSelector);}}GM_registerMenuCommand("输入选择器", tranSelector, "t");
})();
【油猴脚本】生成纯元素CSS选择器(附开发笔记)相关推荐
- 【油猴脚本】改变网页代码块的字体样式/美化LeetCode代码文字显示(CSS;设置@font-face和font-family)
美化网页的代码字体 脚本安装地址: 参考:改变网页代码块的字体样式 更新日志 V0.1 更新时间:2021年9月20日23:04:41 更新功能: 1.改变LeetCode.CSDN.博客园嵌入的代码 ...
- 在油猴脚本中添加css样式的方法
由于项目要求,需要在系统页面注入dom元素,且对这些注入的元素在UI界面层有美观度要求,就避免不了要对其CSS样式优化. 通常在油猴脚本中添加CSS样式的方法如下: 一.引入外部css文件 // @r ...
- Tampermonkey(油猴)脚本编写快速入门
目录 油猴脚本概述 脚本注释/注解 脚本权限 grant 添加新脚本 自定义网页倒计时 网页浏览离开黑屏保护 微博视频下载助手 华为云工作项列表突出展示工作项 Greasy Fork 发布脚本 油猴脚 ...
- 记录一次油猴脚本开发的Demo(入门级)、开发过程
记录一次开发油猴脚本的demo 前言:之前听别人讲油猴脚本怎么怎么地,怎么怎么样,一直以为是个很难的东西,所以在上周五的时候,就自己看了一下,这个东西也不太难.主要还是js玩的6就行了.当然了我的js ...
- 【教程、无技术含量】简单的油猴脚本编写教程
不建议阅读者: 前端大神 想要深入学习(涉及到分析挖掘调用网站中js算法/自己写算法)油猴脚本的读者[备注:楼主也不会,楼主也很绝望啊...] 阅读以下内容所需知识: javascript/jQuer ...
- 我的第一个油猴脚本--微博超话自动签到
简介 用户脚本是一段代码,它们能够优化您的网页浏览体验.安装之后,有些脚本能为网站添加新的功能,有些能使网站的界面更加易用,有些则能隐藏网站上烦人的部分内容.其中常见的有 油猴插件.ChromeExt ...
- 自己动手编写一个在线保存百度谷歌搜索关键词历史记录的油猴脚本
标题快捷导航 如何通过Web技术实现我们的需求 需要的技术栈 油猴脚本的编写 浏览器扩展的编写 后台部分 小结 如何通过Web技术实现我们的需求 相信大家只要会一点前端和后端基础的,一看到这个标题就有 ...
- 自用chrome+油猴脚本,使用迅雷下载百度云大文件,一键离线下载
油猴是有名的火狐浏览器插件(Greasemonkey),当然也有Chrome版本(tampermonkey),甚至IE.Safari.Opera都有--虽然这些插件是由不同的开发者开发出来的,界面也可 ...
- 从零快速编写一个油猴脚本
Tampermonkey,又称 Greasemonkey 油猴脚本,是一款免费的浏览器扩展,可用于管理用户脚本,它本质上是对浏览器接口的二次封装 油猴脚本可用于更改页面布局样式.完成页面自动化.去广告 ...
最新文章
- 智源-AI Time 5 | 无人驾驶距离我们还有多远?(活动报名)
- java语言提供结构_java学习之语句结构
- springboot pom文件指定jdk_Spring Boot 入门
- 第三章用sql语句操作数据
- LeetCode 451 根据字符出现频率排序
- @Component,@Service等注解是如何被解析的?
- pytorch保存模型参数
- 补发《超级迷宫》站立会议八
- 爱情保卫战 - 爱情保鲜剂 语录收集
- 利用canvas打造一个炫酷的粒子背景
- 【深度学习6】对比学习(Contrastive Learning)入门
- mfc将图形涂满颜色,(c++)使用顺序栈
- 牛客网C语言 算学分绩
- linux批量删除指定名称的文件夹
- 最新流量卡官网介绍单页源码
- OOA/OOD/OOP细讲
- RTL-SDR 学习——什么是RTL-SDR(1)
- 什么邮箱发送邮件不进垃圾箱,邮件进垃圾箱了是什么原因怎么办?
- QT的基本使用(一):计算器界面的简易设计及其简单功能实现
- java aop 环绕通知_SpringAOP 环绕通知避坑
热门文章
- JetBrain Activate
- Gestures//手势
- 模拟退火(Simulated Annealing, SA)算法简介与MATLAB实现
- Wallpaper Engine 提取/导出原壁纸
- Angular基础知识学习记录
- Apache Sedona 常见问题解答 (FAQ)
- Android 屏幕录像教程
- HPL HPCG benchmark test
- 如何做出微软风格的 PPT?
- pinyin4j把中文句子(含有多音字字母)转成拼音(二维数组递归求所有组合情况返回list)算法实现!