// ==UserScript== // @name 网页结构采集器(通用) // @namespace https://local.cxbank.icu/probe // @version 2.0.0 // @description 任意网页点一下,采集页面结构 / 点选元素 / 网络请求 / 存储概览,可上传或本地导出。只读,不修改页面。 // @author ZZ // @match *://*/* // @grant none // @run-at document-idle // ==/UserScript== (function () { 'use strict'; var API = 'https://api.cxbank.icu'; var VERSION = '2.0.0'; var MAX_NODES = 1500; var MAX_CLASSES = 150; var MAX_BYTES = 380 * 1024; var MAX_NET = 40; var out = { meta: {}, picks: [], videos: [], audios: [], iframes: [], inputs: [], buttons: [], forms: [], tables: [], dialogs: [], keywords: [], classes: {}, ids: [], outline: [], errors: [], net: [], storage: {}, perf: {}, }; var cls = function (el) { try { return String(el.className || '').trim().slice(0, 180); } catch (e) { return ''; } }; var txt = function (el, n) { try { return String(el.textContent || '').replace(/\s+/g, ' ').trim().slice(0, n || 60); } catch (e) { return ''; } }; function sel(el) { if (!el || !el.tagName) return ''; if (el.id) return '#' + el.id; var c = cls(el).split(/\s+/).filter(Boolean).slice(0, 2); if (c.length) return el.tagName.toLowerCase() + '.' + c.join('.'); return el.tagName.toLowerCase(); } function path(el) { var arr = [], n = el; for (var i = 0; i < 6 && n && n.tagName; i++) { arr.unshift(sel(n) + (n.getAttribute && n.getAttribute('onclick') ? '[onclick]' : '')); n = n.parentElement; } return arr.join(' > '); } function reset() { ['picks', 'videos', 'audios', 'iframes', 'inputs', 'buttons', 'forms', 'tables', 'dialogs', 'keywords', 'ids', 'outline', 'errors'].forEach(function (k) { out[k] = []; }); out.classes = {}; } /* ================= 网络请求记录(只记 URL/状态,不记 body) ================= */ var netOn = false; function hookNet() { if (netOn) return; netOn = true; var of = window.fetch; if (typeof of === 'function') { window.fetch = function (input, init) { var url = '', method = 'GET'; try { url = typeof input === 'string' ? input : (input && input.url) || ''; method = (init && init.method) || (input && input.method) || 'GET'; } catch (e) {} var t0 = Date.now(); var p = of.apply(this, arguments); p.then(function (r) { pushNet(method, url, r.status, Date.now() - t0); }) .catch(function () { pushNet(method, url, 0, Date.now() - t0); }); return p; }; } var oo = window.XMLHttpRequest && window.XMLHttpRequest.prototype.open; var os = window.XMLHttpRequest && window.XMLHttpRequest.prototype.send; if (oo && os) { window.XMLHttpRequest.prototype.open = function (m, u) { try { this.__pm = m; this.__pu = u; this.__pt = Date.now(); } catch (e) {} return oo.apply(this, arguments); }; window.XMLHttpRequest.prototype.send = function () { var self = this; try { self.addEventListener('loadend', function () { pushNet(self.__pm || 'GET', self.__pu || '', self.status, Date.now() - (self.__pt || Date.now())); }); } catch (e) {} return os.apply(this, arguments); }; } } function pushNet(method, url, status, ms) { try { if (!url || String(url).indexOf('data:') === 0) return; out.net.push({ m: String(method).toUpperCase().slice(0, 6), u: String(url).slice(0, 160), s: status, ms: ms }); if (out.net.length > MAX_NET) out.net.shift(); } catch (e) {} } hookNet(); /* ---------- 结构骨架 ---------- */ var SKIP = { SCRIPT: 1, STYLE: 1, NOSCRIPT: 1, PATH: 1, DEFS: 1, META: 1, LINK: 1, HEAD: 1 }; function walk(el, depth) { if (!el || !el.tagName || SKIP[el.tagName]) return; if (out.outline.length >= MAX_NODES || depth > 7) return; var c = cls(el); var interesting = !!el.id || !!c || ['VIDEO', 'AUDIO', 'IFRAME', 'INPUT', 'BUTTON', 'FORM', 'TABLE', 'UL', 'OL', 'LI', 'CANVAS', 'SVG', 'TEXTAREA'].indexOf(el.tagName) >= 0; if (interesting) { out.outline.push({ d: depth, s: sel(el), t: txt(el, 40), n: el.children ? el.children.length : 0 }); } if (el.children) for (var i = 0; i < el.children.length; i++) walk(el.children[i], depth + 1); } /* ---------- 各类元素 ---------- */ function scanMedia(doc) { try { doc.querySelectorAll('video').forEach(function (v) { out.videos.push({ src: String(v.currentSrc || v.src || '').slice(0, 160), dur: Math.round(v.duration || 0), now: Math.round(v.currentTime || 0), paused: v.paused, rate: v.playbackRate, muted: v.muted, w: v.offsetWidth, h: v.offsetHeight, path: path(v), }); }); doc.querySelectorAll('audio').forEach(function (a) { out.audios.push({ src: String(a.currentSrc || a.src || '').slice(0, 140), path: path(a) }); }); doc.querySelectorAll('iframe,frame').forEach(function (f) { out.iframes.push({ src: String(f.src || '').slice(0, 160), id: String(f.id || ''), cls: cls(f), path: path(f) }); }); } catch (e) { out.errors.push('media:' + e.message); } } function scanControls(doc) { try { doc.querySelectorAll('input,textarea,select').forEach(function (el) { if (out.inputs.length >= 60) return; out.inputs.push({ tag: el.tagName.toLowerCase(), type: String(el.type || ''), name: String(el.name || ''), id: String(el.id || ''), ph: String(el.placeholder || '').slice(0, 40), cls: cls(el), path: path(el), }); }); doc.querySelectorAll('button,[role=button],a.btn,.btn,input[type=submit],input[type=button]').forEach(function (el) { if (out.buttons.length >= 60) return; out.buttons.push({ tag: el.tagName.toLowerCase(), text: txt(el, 30), cls: cls(el), id: String(el.id || ''), type: String(el.type || ''), path: path(el), }); }); doc.querySelectorAll('form').forEach(function (f) { if (out.forms.length >= 12) return; out.forms.push({ action: String(f.action || '').slice(0, 120), method: String(f.method || ''), inputs: f.querySelectorAll('input,textarea').length, path: path(f), }); }); doc.querySelectorAll('table').forEach(function (t) { if (out.tables.length >= 8) return; var ths = []; t.querySelectorAll('th').forEach(function (th) { if (ths.length < 8) ths.push(txt(th, 20)); }); out.tables.push({ cls: cls(t), headers: ths, rows: t.querySelectorAll('tr').length, path: path(t) }); }); } catch (e) { out.errors.push('controls:' + e.message); } } function scanOverlays(doc) { try { var els = doc.querySelectorAll('div,section,aside'); for (var i = 0; i < els.length && out.dialogs.length < 15; i++) { var el = els[i], cs; try { cs = getComputedStyle(el); } catch (e) { continue; } var z = parseInt(cs.zIndex, 10); if ((cs.position === 'fixed' || cs.position === 'absolute') && z > 500 && el.offsetWidth > 180 && el.offsetHeight > 60) { out.dialogs.push({ cls: cls(el), z: z, size: el.offsetWidth + 'x' + el.offsetHeight, text: txt(el, 60), path: path(el), }); } } } catch (e) {} } var KW = /题目|试题|选项|答案|答题|作业|考试|测验|章节|课程|课时|播放|视频|进度|任务点|已完成|验证码|登录|提交|保存/; function scanKeywords(doc) { try { var all = doc.querySelectorAll('*'); for (var i = 0; i < all.length && i < 8000; i++) { var el = all[i]; if (el.children.length > 6) continue; var t = txt(el, 40); if (!t || t.length < 2 || !KW.test(t)) continue; if (out.keywords.length >= 60) break; out.keywords.push({ tag: el.tagName.toLowerCase(), cls: cls(el), text: t, path: path(el) }); } } catch (e) {} } function scanClasses(doc) { try { var all = doc.querySelectorAll('*'); for (var i = 0; i < all.length && i < 8000; i++) { var c = cls(all[i]); if (!c) continue; c.split(/\s+/).forEach(function (t) { if (t.length < 3 || t.length > 40) return; out.classes[t] = (out.classes[t] || 0) + 1; }); } var entries = Object.keys(out.classes).map(function (k) { return [k, out.classes[k]]; }); entries.sort(function (a, b) { return b[1] - a[1]; }); var top = {}; entries.slice(0, MAX_CLASSES).forEach(function (e) { top[e[0]] = e[1]; }); out.classes = top; } catch (e) {} try { var ids = [], all2 = doc.querySelectorAll('[id]'); for (var k = 0; k < all2.length && ids.length < 80; k++) ids.push(all2[k].id); out.ids = ids; } catch (e) {} } /* ---------- 框架 / 存储 / 性能 ---------- */ function framework() { var f = []; try { if (window.Vue) f.push('Vue' + (window.Vue.version ? ' ' + window.Vue.version : '')); if (window.React || document.querySelector('[data-reactroot],#root,#__next')) f.push('React'); if (window.jQuery || window.$) f.push('jQuery' + (window.jQuery && window.jQuery.fn && window.jQuery.fn.jquery ? ' ' + window.jQuery.fn.jquery : '')); if (document.querySelector('[class*=el-]')) f.push('ElementUI'); if (document.querySelector('[class*=ant-]')) f.push('AntDesign'); if (document.querySelector('[class*=van-]')) f.push('Vant'); if (document.querySelector('[class*=layui-]')) f.push('LayUI'); if (document.querySelector('[class*=vjs-]')) f.push('video.js'); if (window.player || window.videoPlayer) f.push('player对象'); } catch (e) {} return f; } function scanStorage() { var o = { local: {}, session: {}, cookie: {} }; try { var ls = window.localStorage; o.local.count = ls.length; var size = 0; for (var i = 0; i < ls.length; i++) { var k = ls.key(i) || ''; size += k.length + String(ls.getItem(k) || '').length; } o.local.sizeKB = Math.round(size / 1024); o.local.keys = []; for (var j = 0; j < ls.length && o.local.keys.length < 25; j++) o.local.keys.push(ls.key(j)); } catch (e) { o.local.err = e.message; } try { var ss = window.sessionStorage; o.session.count = ss.length; o.session.keys = []; for (var m = 0; m < ss.length && o.session.keys.length < 25; m++) o.session.keys.push(ss.key(m)); } catch (e) { o.session.err = e.message; } try { o.cookie.count = String(document.cookie || '').split(';').filter(function (s) { return s.trim(); }).length; } catch (e) {} return o; } function scanPerf() { var o = {}; try { o.nodes = document.getElementsByTagName('*').length; o.resources = performance.getEntriesByType ? performance.getEntriesByType('resource').length : -1; var n = performance.getEntriesByType ? performance.getEntriesByType('navigation')[0] : null; if (n) { o.domReadyMs = Math.round(n.domContentLoadedEventEnd || 0); o.loadMs = Math.round(n.loadEventEnd || 0); } o.shadowRoots = 0; var all = document.querySelectorAll('*'); for (var i = 0; i < all.length && i < 5000; i++) if (all[i].shadowRoot) o.shadowRoots++; o.canvas = document.querySelectorAll('canvas').length; } catch (e) {} return o; } /* ---------- 采集 ---------- */ function collect() { out.meta = { url: String(location.href).slice(0, 300), title: String(document.title || '').slice(0, 120), ready: document.readyState, ua: String(navigator.userAgent || '').slice(0, 160), screen: screen.width + 'x' + screen.height, viewport: window.innerWidth + 'x' + window.innerHeight, at: new Date().toLocaleString('zh-CN'), probeVer: VERSION, frameworks: framework(), lang: String(document.documentElement.lang || ''), }; reset(); scanMedia(document); scanControls(document); scanOverlays(document); scanKeywords(document); scanClasses(document); walk(document.body, 0); out.storage = scanStorage(); out.perf = scanPerf(); /* 同源 iframe 递归(最多 3 个) */ try { var fr = document.querySelectorAll('iframe'); var done = 0; for (var i = 0; i < fr.length && done < 3; i++) { var d = null; try { d = fr[i].contentDocument; } catch (e) { d = null; } if (!d || !d.body) continue; done++; scanMedia(d); scanControls(d); } } catch (e) { out.errors.push('iframe:' + e.message); } return out; } /* ---------- 点选元素 ---------- */ var picking = false; var pickHint = null; function startPick() { picking = true; if (pickHint) pickHint.style.display = 'block'; document.body.style.cursor = 'crosshair'; say('点选模式:点页面上任意元素,会记录它的选择器和结构(Esc 取消)'); } function stopPick() { picking = false; if (pickHint) pickHint.style.display = 'none'; document.body.style.cursor = ''; } document.addEventListener('click', function (e) { if (!picking) return; e.preventDefault(); e.stopPropagation(); var el = e.target; var info = { tag: el.tagName ? el.tagName.toLowerCase() : '', id: String(el.id || ''), cls: cls(el), text: txt(el, 60), path: path(el), kids: el.children ? Array.prototype.slice.call(el.children, 0, 8).map(function (c) { return c.tagName.toLowerCase() + '.' + cls(c).split(/\s+/).slice(0, 2).join('.'); }) : [], attrs: {}, }; ['type', 'name', 'href', 'src', 'onclick', 'role', 'aria-label', 'data-id'].forEach(function (k) { var v = el.getAttribute && el.getAttribute(k); if (v) info.attrs[k] = String(v).slice(0, 80); }); out.picks.push(info); say('已记录:' + info.tag + (info.id ? '#' + info.id : '') + (info.cls ? '.' + info.cls.split(/\s+/)[0] : '')); stopPick(); renderPre(); }, true); document.addEventListener('keydown', function (e) { if (e.key === 'Escape' && picking) { stopPick(); say('已取消点选'); } }, true); /* ---------- 界面 ---------- */ var box = document.createElement('div'); box.style.cssText = 'position:fixed;right:16px;bottom:110px;z-index:2147483647;' + 'font:13px/1.6 -apple-system,"Microsoft YaHei",sans-serif;background:rgba(255,255,255,.97);' + 'color:#1c1c1e;border:.5px solid rgba(0,0,0,.1);border-radius:14px;' + 'box-shadow:0 10px 30px rgba(0,0,0,.18);padding:12px 14px;width:320px;display:none;' + '-webkit-backdrop-filter:blur(16px);backdrop-filter:blur(16px)'; box.innerHTML = '