// ==UserScript== // @name 企业信息要素摘录(企查查/天眼查/启信宝)@极客律师 // @namespace corp-info-extract // @version 0.1 // @description QCC/天眼查/启信宝:提取公司名称/统一社会信用代码/地址/法定代表人+核心职务(口径A),默认复制格式化文字;Alt 复制JSON。 // @match https://www.qcc.com/firm/* // @match https://www.tianyancha.com/company/* // @match https://www.qixin.com/company/* // @run-at document-end // @grant GM_setClipboard // @grant GM_notification // ==/UserScript== (function () { 'use strict'; /** ========================= * Utils * =========================*/ const sleep = (ms) => new Promise(r => setTimeout(r, ms)); function normText(s) { return (s ?? '') .toString() .replace(/\u00A0/g, ' ') .replace(/\s+/g, ' ') .trim(); } // 旧版:如果不匹配人名,会返回原字符串(对“注册资本”这种会误伤) function cleanPersonName(raw) { const s = normText(raw); const m = s.match(/^[\u4e00-\u9fff·]{2,20}/); return m ? m[0] : s; } // 新增:严格人名(不匹配则返回空,避免“1000万人民币”被当做人名) function cleanPersonNameStrict(raw) { const s = normText(raw); const m = s.match(/^[\u4e00-\u9fff·]{2,20}/); return m ? m[0] : ''; } function isPersonLike(s) { const t = normText(s); if (!t) return false; // 必须以中文姓名开头 if (!/^[\u4e00-\u9fff·]{2,20}/.test(t)) return false; // 排除明显的资金/单位词 if (/(人民币|万元|亿元|美元|港元|欧元|注册资本|实缴资本|万|亿)/.test(t)) return false; return true; } function isUsccLike(s) { const t = normText(s).toUpperCase(); return /^[0-9A-Z]{18}$/.test(t); } async function waitFor(fn, { timeout = 20000, interval = 200 } = {}) { const start = Date.now(); while (Date.now() - start < timeout) { try { const v = fn(); if (v) return v; } catch (_) {} await sleep(interval); } return null; } function notify(text, title = '企业信息摘录') { try { if (typeof GM_notification === 'function') { GM_notification({ title, text, timeout: 2500 }); } else { console.info(`[${title}] ${text}`); } } catch (e) { console.info(`[${title}] ${text}`); } } async function copyToClipboard(text) { try { if (typeof GM_setClipboard === 'function') { GM_setClipboard(text, 'text'); return true; } } catch (e) {} try { await navigator.clipboard.writeText(text); return true; } catch (e) { return false; } } function queryByXPath(xpath, root = document) { try { return document.evaluate(xpath, root, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; } catch (e) { return null; } } /** ========================= * Generic helpers (for TYC/QIXIN) * =========================*/ function normalizeLabel(label) { return normText(label).replace(/[::\s]/g, ''); } // 更“严格”的 label->value:优先从 table 行里找“标签=精确命中” function getValueByLabelFromTablesStrict(label, { root = document.body, preferSelectors = ['.copy-value', '.val', '.value', 'a', 'span', 'div'], validate = null, } = {}) { const key = normalizeLabel(label); if (!key) return ''; const tables = Array.from(root.querySelectorAll('table')); for (const table of tables) { const trs = Array.from(table.querySelectorAll('tr')); for (const tr of trs) { const cells = Array.from(tr.querySelectorAll('th,td')); for (let i = 0; i < cells.length - 1; i++) { const cellText = normalizeLabel(cells[i].textContent || ''); // 严格:必须等于 key(或等于 key + 若干空白/冒号) if (cellText !== key) continue; const valCell = cells[i + 1]; // 优先 selector for (const sel of preferSelectors) { const vEl = valCell.querySelector(sel); const v = normText(vEl ? vEl.textContent : ''); if (v && (!validate || validate(v))) return v; } const v2 = normText(valCell.textContent); if (v2 && (!validate || validate(v2))) return v2; } } } return ''; } function getCompanyNameGeneric() { const h1 = document.querySelector('h1'); if (h1) { const t = normText(h1.textContent); if (t && t.length <= 80) return t; } const og = document.querySelector('meta[property="og:title"]'); if (og && og.content) return normText(og.content); const title = normText(document.title); return title .replace(/\s*-\s*企查查\s*$/i, '') .replace(/\s*-\s*天眼查\s*$/i, '') .replace(/\s*-\s*启信宝\s*$/i, '') .trim(); } /** ========================= * Positions parsing + normalization (口径A) * =========================*/ function parsePositions(raw) { let s = normText(raw); s = s.replace(/\d{4}-\d{2}-\d{2}.*$/, '').trim(); s = s.replace(/[,、]/g, ','); s = s.replace(/\/+/g, ','); const parts = s.split(',').map(normText).filter(Boolean); const out = []; for (const p of parts) if (!out.includes(p)) out.push(p); return out; } function positionsToPhrase(positions, opts = {}) { const { useCompanyPrefix = false, gmWord = '总经理', drop = new Set(['财务负责人']), } = opts; const norm = []; for (const p0 of (positions || [])) { let p = normText(p0); if (!p) continue; if (p === '经理') p = gmWord; if (p === '总经理') p = gmWord; if (drop.has(p)) continue; if (!norm.includes(p)) norm.push(p); } const has = (x) => norm.includes(x); let phrase = ''; if (has('董事长')) { phrase = has(gmWord) ? `董事长兼${gmWord}` : '董事长'; } else if (has('执行董事')) { phrase = has(gmWord) ? `执行董事兼${gmWord}` : '执行董事'; } else if (has('董事')) { phrase = has(gmWord) ? `董事兼${gmWord}` : '董事'; } else if (has(gmWord)) { phrase = gmWord; } else { phrase = norm[0] || ''; } if (!phrase) return ''; return useCompanyPrefix ? `该公司${phrase}` : phrase; } async function ensurePeopleSectionVisibleGeneric() { const targets = ['主要人员', '主要成员', '高管', '高管信息', '人员']; const els = Array.from(document.querySelectorAll('a,span,div,button')) .filter(el => { const t = normText(el.textContent); return targets.some(k => t.includes(k)); }); els.sort((a, b) => normText(a.textContent).length - normText(b.textContent).length); if (els[0]) { try { els[0].click(); } catch (e) {} await sleep(500); } } function extractLegalRepFromPeopleTables(legalRepNameHint = '') { const hint = cleanPersonNameStrict(legalRepNameHint); const tables = Array.from(document.querySelectorAll('table')); for (const table of tables) { // header texts let headerCells = Array.from(table.querySelectorAll('thead th')); if (!headerCells.length) { const firstRow = table.querySelector('tr'); if (firstRow) headerCells = Array.from(firstRow.querySelectorAll('th')); } if (!headerCells.length) continue; const headerTexts = headerCells.map(th => normText(th.textContent)); const idxName = headerTexts.findIndex(t => t === '姓名' || t.includes('姓名') || t.includes('名称')); const idxPos = headerTexts.findIndex(t => t.includes('职务') && t.includes('任期')) !== -1 ? headerTexts.findIndex(t => t.includes('职务') && t.includes('任期')) : (headerTexts.findIndex(t => t === '职务' || t.includes('职务')) !== -1 ? headerTexts.findIndex(t => t === '职务' || t.includes('职务')) : headerTexts.findIndex(t => t === '职位' || t.includes('职位'))); if (idxName < 0 || idxPos < 0) continue; const rows = Array.from(table.querySelectorAll('tbody tr')).length ? Array.from(table.querySelectorAll('tbody tr')) : Array.from(table.querySelectorAll('tr')); for (const tr of rows) { const tds = Array.from(tr.querySelectorAll('td')); if (tds.length <= Math.max(idxName, idxPos)) continue; const nameCell = tds[idxName]; const posCell = tds[idxPos]; const a = nameCell.querySelector('a'); const name = cleanPersonNameStrict(a ? a.textContent : nameCell.textContent); if (!name) continue; const isLegalRepByBadge = normText(nameCell.textContent).includes('法定代表人') || Array.from(nameCell.querySelectorAll('*')).some(n => normText(n.textContent) === '法定代表人'); const isLegalRepByName = hint && name === hint; if (!isLegalRepByBadge && !isLegalRepByName) continue; const positions = parsePositions(normText(posCell.textContent)); return { name, positions }; } } return { name: '', positions: [] }; } /** ========================= * QCC adapter (完全沿用你旧版策略,避免串值) * =========================*/ function getValueFromQccInfoTable(label, { tableSelector = 'table.ntable', labelCellSelector = 'td.tb', valueSelector = '.copy-value', } = {}) { const tables = Array.from(document.querySelectorAll(tableSelector)); for (const table of tables) { const labelTds = Array.from(table.querySelectorAll(labelCellSelector)); const keyTd = labelTds.find(td => normText(td.textContent).includes(label)); if (!keyTd) continue; const tr = keyTd.closest('tr'); if (!tr) continue; const tds = Array.from(tr.querySelectorAll('td')); const idx = tds.indexOf(keyTd); const valTd = tds[idx + 1]; if (!valTd) continue; const vEl = valTd.querySelector(valueSelector); const v = normText(vEl ? vEl.textContent : valTd.textContent); if (v) return v; } return ''; } async function buildPayloadQCC() { await waitFor(() => getValueFromQccInfoTable('统一社会信用代码') || document.querySelector('table.ntable'), { timeout: 20000 }); const company_name = getCompanyNameGeneric(); const uscc = getValueFromQccInfoTable('统一社会信用代码'); const address = getValueFromQccInfoTable('注册地址') || getValueFromQccInfoTable('通讯地址') || getValueFromQccInfoTable('地址'); // 严格人名:如果这里误抓到“注册资本”,会被清空,然后转而从人员表里找 const legalRepRaw = getValueFromQccInfoTable('法定代表人'); const legal_rep_name_hint = cleanPersonNameStrict(legalRepRaw); await ensurePeopleSectionVisibleGeneric(); await waitFor(() => document.querySelector('table tr'), { timeout: 12000 }); const lr = extractLegalRepFromPeopleTables(legal_rep_name_hint); const finalLegalRepName = cleanPersonName(lr.name || legal_rep_name_hint); const positions = Array.isArray(lr.positions) ? lr.positions : []; return { company_name, uscc, address, legal_representative: { name: finalLegalRepName, positions }, source: { site: 'qcc', url: location.href, captured_at: new Date().toISOString() } }; } /** ========================= * Qixin adapter (启信宝) * =========================*/ async function buildPayloadQixin() { await waitFor(() => document.body && /统一社会信用代码|社会信用代码|法定代表人|注册地址/.test(document.body.innerText || ''), { timeout: 25000 }); const company_name = getCompanyNameGeneric(); let uscc = getValueByLabelFromTablesStrict('统一社会信用代码', { validate: isUsccLike }) || getValueByLabelFromTablesStrict('社会信用代码', { validate: isUsccLike }); if (!uscc) { const idNumberElement = queryByXPath('//*[@id="__nuxt"]//*[contains(text(),"统一社会信用代码") or contains(text(),"社会信用代码")]'); // fallback: try nearby if (idNumberElement && idNumberElement.parentElement) { const v = normText(idNumberElement.parentElement.textContent).match(/[0-9A-Z]{18}/i); if (v) uscc = v[0].toUpperCase(); } } let address = getValueByLabelFromTablesStrict('注册地址') || getValueByLabelFromTablesStrict('地址'); // 法定代表人:强校验(必须像人名) let legalRepRaw = getValueByLabelFromTablesStrict('法定代表人', { validate: isPersonLike }) || getValueByLabelFromTablesStrict('法人', { validate: isPersonLike }); // fallback:你给的 XPath(保留) if (!legalRepRaw) { const legalRepElement = queryByXPath('//*[@id="__nuxt"]//a[contains(@href,"/human/") or contains(@href,"/people/")][1]'); legalRepRaw = normText(legalRepElement ? legalRepElement.textContent : ''); if (!isPersonLike(legalRepRaw)) legalRepRaw = ''; } const legal_rep_name_hint = cleanPersonNameStrict(legalRepRaw); await ensurePeopleSectionVisibleGeneric(); await waitFor(() => document.querySelector('table tr') || (legal_rep_name_hint && (document.body.innerText || '').includes(legal_rep_name_hint)), { timeout: 15000 }); const lr = extractLegalRepFromPeopleTables(legal_rep_name_hint); const finalLegalRepName = cleanPersonName(lr.name || legal_rep_name_hint); const positions = Array.isArray(lr.positions) ? lr.positions : []; return { company_name, uscc, address, legal_representative: { name: finalLegalRepName, positions }, source: { site: 'qixin', url: location.href, captured_at: new Date().toISOString() } }; } /** ========================= * Tianyancha adapter (天眼查) * =========================*/ async function buildPayloadTyc() { await waitFor(() => document.body && /统一社会信用代码|社会信用代码|法定代表人|注册地址/.test(document.body.innerText || ''), { timeout: 30000 }); const company_name = getCompanyNameGeneric(); const uscc = getValueByLabelFromTablesStrict('统一社会信用代码', { validate: isUsccLike }) || getValueByLabelFromTablesStrict('社会信用代码', { validate: isUsccLike }); const address = getValueByLabelFromTablesStrict('注册地址') || getValueByLabelFromTablesStrict('企业地址') || getValueByLabelFromTablesStrict('地址'); // 法定代表人:强校验 let legalRepRaw = getValueByLabelFromTablesStrict('法定代表人', { validate: isPersonLike }) || getValueByLabelFromTablesStrict('法人代表', { validate: isPersonLike }) || getValueByLabelFromTablesStrict('法人', { validate: isPersonLike }); // 二次兜底:从页面文本中抓“法定代表人:张三” if (!legalRepRaw) { const text = document.body ? (document.body.innerText || '') : ''; const m = text.match(/法定代表人[::]\s*([\u4e00-\u9fff·]{2,20})/); if (m) legalRepRaw = m[1]; } const legal_rep_name_hint = cleanPersonNameStrict(legalRepRaw); await ensurePeopleSectionVisibleGeneric(); await waitFor(() => document.querySelector('table tr') || (legal_rep_name_hint && (document.body.innerText || '').includes(legal_rep_name_hint)), { timeout: 15000 }); const lr = extractLegalRepFromPeopleTables(legal_rep_name_hint); const finalLegalRepName = cleanPersonName(lr.name || legal_rep_name_hint); const positions = Array.isArray(lr.positions) ? lr.positions : []; return { company_name, uscc, address, legal_representative: { name: finalLegalRepName, positions }, source: { site: 'tianyancha', url: location.href, captured_at: new Date().toISOString() } }; } /** ========================= * Dispatcher * =========================*/ const SITE = (() => { const h = location.host; if (h.includes('qcc.com')) return 'qcc'; if (h.includes('tianyancha.com')) return 'tyc'; if (h.includes('qixin.com')) return 'qixin'; return 'unknown'; })(); async function buildPayload() { if (SITE === 'qcc') return buildPayloadQCC(); if (SITE === 'qixin') return buildPayloadQixin(); if (SITE === 'tyc') return buildPayloadTyc(); throw new Error('Unsupported site: ' + location.host); } /** ========================= * Output format (your target) * =========================*/ function formatResultText(payload) { const name = payload.company_name || ''; const addr = payload.address || ''; const uscc = payload.uscc || ''; const lrName = payload.legal_representative?.name || ''; const posArr = payload.legal_representative?.positions || []; const rolePhrase = positionsToPhrase(posArr, { useCompanyPrefix: false, gmWord: '总经理', drop: new Set(['财务负责人']), }); const line1 = `${name},住所地:${addr},统一社会信用代码:${uscc}。`; const line2 = rolePhrase ? `法定代表人:${lrName},${rolePhrase}。` : `法定代表人:${lrName}。`; return `${line1}\n${line2}`; } function warnIfMissing(payload) { const missing = []; if (!payload.company_name) missing.push('公司名称'); if (!payload.uscc) missing.push('统一社会信用代码'); if (!payload.address) missing.push('地址'); if (!payload.legal_representative?.name) missing.push('法定代表人'); if (missing.length) notify(`部分字段为空:${missing.join('、')}(可能需登录/VIP/反爬限制/未展开模块)`); } /** ========================= * UI + Hotkeys * =========================*/ function addFloatingButton(run) { const btn = document.createElement('button'); btn.textContent = '复制信息'; btn.setAttribute('type', 'button'); btn.title = '默认复制格式化文字;按住 Alt 点击复制 JSON。快捷键:Alt+Shift+C(文字) / Alt+Shift+J(JSON)'; btn.style.cssText = [ 'position:fixed', 'right:16px', 'bottom:16px', 'z-index:999999', 'padding:10px 12px', 'border-radius:10px', 'border-radius:10px', 'border:1px solid rgba(0,0,0,.15)', 'background:#fff', 'box-shadow:0 8px 20px rgba(0,0,0,.12)', 'cursor:pointer', 'font-size:14px', 'line-height:1', ].join(';'); btn.addEventListener('click', (e) => run({ mode: e.altKey ? 'json' : 'text' })); document.body.appendChild(btn); } function addHotkeys(run) { window.addEventListener('keydown', (e) => { if (!(e.altKey && e.shiftKey)) return; if (e.key === 'C' || e.key === 'c') { e.preventDefault(); run({ mode: 'text' }); } if (e.key === 'J' || e.key === 'j') { e.preventDefault(); run({ mode: 'json' }); } }, true); } async function runCopy({ mode = 'text' } = {}) { try { notify(`正在提取(${SITE})…`); const payload = await buildPayload(); warnIfMissing(payload); const json = JSON.stringify(payload, null, 2); const text = formatResultText(payload); const toCopy = mode === 'json' ? json : text; const ok = await copyToClipboard(toCopy); if (ok) { notify(mode === 'json' ? '已复制 JSON' : '已复制格式化文字'); console.info('[Corp payload]', payload); console.info('[Corp formatted]', text); } else { notify('复制失败:已输出到控制台(Console)'); console.info('[Corp copy]', toCopy); alert('复制失败(可能被浏览器限制)。已输出到控制台,请从 Console 手动复制。'); } } catch (e) { console.error('[Corp] error', e); notify('提取失败:请看控制台错误'); alert('提取失败:请打开控制台查看错误信息(F12 -> Console)。'); } } // init addFloatingButton(runCopy); addHotkeys(runCopy); })();