好友
阅读权限25
听众
最后登录1970-1-1
|
首先用的是qoder cn ,模型 Qwen3.8-Max,没啥原因,
主要就免费,在额度范围内,不出三轮对话就好了。
代码在附件里,一键运行就好。
另外rule和skills我也发在附件里了,真的是一滴都没有了。
jsvmp代码太大,已放压缩包
env环境:
[JavaScript] 纯文本查看 复制代码 /* * 同花顺 chameleon (hexin-v) 补环境
* ------------------------------------------------------------
* 目标:为 main(chameleon 反爬脚本) 提供尽量贴近真实 Chrome 的浏览器环境,
* 使其在 Node 中跑通并在 document.cookie 写入 hexin-v。
*
* 结构遵循补环境方法论:构造器 + prototype + 实例 + 描述符 + native toString 保护
* 使用方式:由 watch.js 在加载 main 之前 require 本文件。
*/
const globalObj = globalThis;
// ==================== 0. 可配置:站点 location ====================
// hexin-v 与域名相关,默认同花顺行情中心(q.10jqka.com.cn)。
const LOCATION = {
protocol: 'https:',
host: 'q.10jqka.com.cn',
hostname: 'q.10jqka.com.cn',
port: '',
pathname: '/index/index/board/all/field/zdf/order/desc/page/5/ajax/1/',
search: '',
hash: '',
origin: 'https://q.10jqka.com.cn',
href: 'https://q.10jqka.com.cn/index/index/board/all/field/zdf/order/desc/page/5/ajax/1/',
};
// 必须与请求头 User-Agent 完全一致:hexin-v 内部编码了 strhash(navigator.userAgent)
const USER_AGENT =
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' +
'(KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36';
// ==================== 1. native 函数保护 ====================
const rawToString = Function.prototype.toString;
const NATIVE_SYMBOL = Symbol('native_' + Math.random().toString(36).slice(2));
function markNative(fn, displayName, length) {
if (typeof fn !== 'function') return fn;
const name = displayName !== undefined ? displayName : (fn.name || '');
Object.defineProperty(fn, NATIVE_SYMBOL, {
value: 'function ' + name + '() { [native code] }',
configurable: true, enumerable: false, writable: true,
});
if (displayName !== undefined) {
Object.defineProperty(fn, 'name', { value: name, configurable: true });
}
if (length !== undefined) {
Object.defineProperty(fn, 'length', { value: length, configurable: true });
}
return fn;
}
// 统一 hook Function.prototype.toString,使被标记函数返回 [native code]
(function installToStringProtector() {
function protectedToString() {
if (typeof this === 'function' && this[NATIVE_SYMBOL]) {
return this[NATIVE_SYMBOL];
}
return rawToString.call(this);
}
markNative(protectedToString, 'toString', 0);
Object.defineProperty(Function.prototype, 'toString', {
value: protectedToString, configurable: true, writable: true, enumerable: false,
});
})();
// 定义一个 native 风格方法并挂到对象上
function defMethod(target, name, fn, length) {
markNative(fn, name, length !== undefined ? length : fn.length);
Object.defineProperty(target, name, {
value: fn, configurable: true, writable: true, enumerable: false,
});
return fn;
}
// 定义访问器属性(getter/setter),并保护其 toString
function defAccessor(target, name, get, set, enumerable) {
const desc = { configurable: true, enumerable: enumerable !== false };
if (get) { markNative(get, 'get ' + name, 0); desc.get = get; }
if (set) { markNative(set, 'set ' + name, 1); desc.set = set; }
Object.defineProperty(target, name, desc);
}
// ==================== 2. EventTarget / Node / Element 原型链 ====================
function EventTarget() { throw new TypeError("Illegal constructor"); }
defMethod(EventTarget.prototype, 'addEventListener', function addEventListener(type, cb, opts) {
if (!this.__listeners) this.__listeners = {};
(this.__listeners[type] || (this.__listeners[type] = [])).push(cb);
}, 3);
defMethod(EventTarget.prototype, 'removeEventListener', function removeEventListener(type, cb) {
if (!this.__listeners || !this.__listeners[type]) return;
this.__listeners[type] = this.__listeners[type].filter(f => f !== cb);
}, 2);
defMethod(EventTarget.prototype, 'dispatchEvent', function dispatchEvent(event) {
const list = this.__listeners && this.__listeners[event && event.type];
if (list) list.slice().forEach(cb => { try { cb.call(this, event); } catch (e) {} });
return true;
}, 1);
function Node() { throw new TypeError("Illegal constructor"); }
Node.prototype = Object.create(EventTarget.prototype);
Node.prototype.constructor = Node;
defMethod(Node.prototype, 'appendChild', function appendChild(child) { return child; }, 1);
defMethod(Node.prototype, 'insertBefore', function insertBefore(child) { return child; }, 2);
defMethod(Node.prototype, 'removeChild', function removeChild(child) { return child; }, 1);
function Element() { throw new TypeError("Illegal constructor"); }
Element.prototype = Object.create(Node.prototype);
Element.prototype.constructor = Element;
defMethod(Element.prototype, 'setAttribute', function setAttribute(k, v) { this[k] = v; }, 2);
defMethod(Element.prototype, 'getAttribute', function getAttribute(k) {
return Object.prototype.hasOwnProperty.call(this, k) ? this[k] : null;
}, 1);
defMethod(Element.prototype, 'removeAttribute', function removeAttribute(k) { delete this[k]; }, 1);
defMethod(Element.prototype, 'hasAttribute', function hasAttribute(k) {
return Object.prototype.hasOwnProperty.call(this, k);
}, 1);
function HTMLElement() { throw new TypeError("Illegal constructor"); }
HTMLElement.prototype = Object.create(Element.prototype);
HTMLElement.prototype.constructor = HTMLElement;
// ==================== 3. 伪 canvas 2d context ====================
function makeCanvasContext() {
const ctx = {
canvas: null,
fillStyle: '#000', strokeStyle: '#000', lineWidth: 1, font: '10px sans-serif',
globalAlpha: 1, globalCompositeOperation: 'source-over',
};
const noop = function () {};
['save', 'restore', 'scale', 'rotate', 'translate', 'transform', 'setTransform',
'clearRect', 'fillRect', 'strokeRect', 'beginPath', 'closePath', 'moveTo', 'lineTo',
'bezierCurveTo', 'quadraticCurveTo', 'arc', 'arcTo', 'rect', 'fill', 'stroke',
'clip', 'drawImage', 'putImageData'].forEach(m => defMethod(ctx, m, noop, 0));
defMethod(ctx, 'fillText', function fillText() {}, 4);
defMethod(ctx, 'strokeText', function strokeText() {}, 4);
defMethod(ctx, 'measureText', function measureText(t) {
return { width: (t ? String(t).length : 0) * 6, height: 10 };
}, 1);
defMethod(ctx, 'createLinearGradient', function () { return { addColorStop: function () {} }; }, 4);
defMethod(ctx, 'createRadialGradient', function () { return { addColorStop: function () {} }; }, 6);
defMethod(ctx, 'getImageData', function (x, y, w, h) {
return { data: new Uint8ClampedArray(Math.max(1, (w | 0) * (h | 0) * 4)), width: w | 0, height: h | 0 };
}, 4);
defMethod(ctx, 'createImageData', function (w, h) {
return { data: new Uint8ClampedArray(Math.max(1, (w | 0) * (h | 0) * 4)), width: w | 0, height: h | 0 };
}, 2);
return ctx;
}
function makeCanvasElement() {
const el = Object.create(HTMLElement.prototype);
el.nodeName = el.tagName = 'CANVAS';
el.width = 300; el.height = 150;
el.style = {};
let ctx2d = null;
defMethod(el, 'getContext', function getContext(type) {
if (type === '2d') {
if (!ctx2d) { ctx2d = makeCanvasContext(); ctx2d.canvas = el; }
return ctx2d;
}
if (type === 'webgl' || type === 'experimental-webgl' || type === 'webgl2') {
return makeWebGLContext();
}
return null;
}, 1);
defMethod(el, 'toDataURL', function toDataURL() {
return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+M8AAAMBAQDJ/pLvAAAAAElFTkSuQmCC';
}, 0);
defMethod(el, 'appendChild', function appendChild(c) { return c; }, 1);
return el;
}
function makeWebGLContext() {
const gl = {
VENDOR: 0x1F00, RENDERER: 0x1F01, VERSION: 0x1F02,
UNMASKED_VENDOR_WEBGL: 0x9245, UNMASKED_RENDERER_WEBGL: 0x9246,
};
defMethod(gl, 'getParameter', function getParameter(p) {
if (p === gl.VENDOR || p === gl.UNMASKED_VENDOR_WEBGL) return 'Google Inc. (NVIDIA)';
if (p === gl.RENDERER || p === gl.UNMASKED_RENDERER_WEBGL)
return 'ANGLE (NVIDIA, NVIDIA GeForce GTX 1060 Direct3D11 vs_5_0 ps_5_0, D3D11)';
if (p === gl.VERSION) return 'WebGL 1.0 (OpenGL ES 2.0 Chromium)';
return null;
}, 1);
defMethod(gl, 'getExtension', function getExtension() { return { UNMASKED_VENDOR_WEBGL: 0x9245, UNMASKED_RENDERER_WEBGL: 0x9246 }; }, 1);
defMethod(gl, 'getSupportedExtensions', function () { return ['WEBGL_debug_renderer_info']; }, 0);
return gl;
}
// ==================== 4. document ====================
const cookieJar = {}; // 存储所有 cookie,便于提取 hexin-v
Object.defineProperty(globalObj, '__cookieJar', { value: cookieJar, enumerable: false });
function makeElement(tagName) {
tagName = String(tagName || 'div').toLowerCase();
if (tagName === 'canvas') return makeCanvasElement();
const el = Object.create(HTMLElement.prototype);
el.nodeName = tagName.toUpperCase();
el.tagName = tagName.toUpperCase();
el.style = {};
el.childNodes = [];
el.attributes = {};
if (tagName === 'script') {
el.src = ''; el.async = false; el.type = '';
el.onload = null; el.onerror = null; el.onreadystatechange = null;
}
if (tagName === 'iframe') { el.src = ''; }
if (tagName === 'meta') { el.content = ''; el.charset = ''; }
defMethod(el, 'appendChild', function appendChild(c) {
if (c && c.tagName === 'SCRIPT' && typeof c.onload === 'function') {
// 补环境中脚本不会真正加载,保持静默(不触发 onload/onerror)
}
return c;
}, 1);
return el;
}
const headElement = makeElement('head');
const bodyElement = makeElement('body');
const documentElement = makeElement('html');
function Document() { throw new TypeError("Illegal constructor"); }
Document.prototype = Object.create(Node.prototype);
Document.prototype.constructor = Document;
const document = Object.create(Document.prototype);
Object.defineProperty(document, Symbol.toStringTag, { value: 'HTMLDocument', configurable: true });
document.documentElement = documentElement;
document.head = headElement;
document.body = bodyElement;
document.readyState = 'complete';
document.referrer = '';
document.title = '';
document.characterSet = 'UTF-8';
document.charset = 'UTF-8';
document.defaultCharset = 'UTF-8';
document.compatMode = 'CSS1Compat';
document.hidden = false;
document.visibilityState = 'visible';
document.domain = LOCATION.hostname;
document.URL = LOCATION.href;
document.documentURI = LOCATION.href;
document.location = null; // 稍后指向 location 实例
document.all = undefined;
// cookie 访问器:写入解析进 jar,读取拼接返回
defAccessor(document, 'cookie', function cookie() {
return Object.keys(cookieJar).map(k => k + '=' + cookieJar[k]).join('; ');
}, function cookie(val) {
val = String(val);
const first = val.indexOf(';');
const pair = (first === -1 ? val : val.slice(0, first));
const eq = pair.indexOf('=');
if (eq === -1) return;
const name = pair.slice(0, eq).trim();
const value = pair.slice(eq + 1).trim();
// 处理删除 cookie (过期时间)
if (/expires=Thu, 01 Jan 1970/i.test(val)) { delete cookieJar[name]; return; }
cookieJar[name] = value;
}, true);
defMethod(document, 'createElement', function createElement(tag) { return makeElement(tag); }, 1);
defMethod(document, 'createElementNS', function createElementNS(ns, tag) { return makeElement(tag); }, 2);
defMethod(document, 'createTextNode', function createTextNode(t) {
return { nodeName: '#text', nodeValue: t, textContent: t };
}, 1);
defMethod(document, 'getElementsByTagName', function getElementsByTagName(tag) {
tag = String(tag).toLowerCase();
if (tag === 'head') return [headElement];
if (tag === 'body') return [bodyElement];
if (tag === 'html') return [documentElement];
if (tag === 'script') return [];
if (tag === 'meta') return [];
return [];
}, 1);
defMethod(document, 'getElementsByClassName', function () { return []; }, 1);
defMethod(document, 'getElementById', function getElementById() { return null; }, 1);
defMethod(document, 'querySelector', function querySelector(sel) {
if (sel === 'head') return headElement;
if (sel === 'body') return bodyElement;
return null;
}, 1);
defMethod(document, 'querySelectorAll', function querySelectorAll() { return []; }, 1);
defMethod(document, 'addEventListener', EventTarget.prototype.addEventListener, 3);
defMethod(document, 'removeEventListener', EventTarget.prototype.removeEventListener, 2);
defMethod(document, 'dispatchEvent', EventTarget.prototype.dispatchEvent, 1);
defMethod(document, 'write', function write() {}, 0);
defMethod(document, 'appendChild', function appendChild(c) { return c; }, 1);
defMethod(document, 'insertBefore', function insertBefore(c) { return c; }, 2);
globalObj.document = document;
// ==================== 5. navigator ====================
function makePlugin(desc) {
const p = {
name: desc.name, description: desc.description, filename: desc.filename, length: desc.mimes.length,
};
desc.mimes.forEach((m, i) => { p[i] = m; });
return p;
}
function makeMimeType(m) {
return { type: m.type, description: m.description, suffixes: m.suffixes, enabledPlugin: null };
}
const pluginDefs = [
{ name: 'PDF Viewer', filename: 'internal-pdf-viewer', description: 'Portable Document Format',
mimes: [{ type: 'application/pdf', description: 'Portable Document Format', suffixes: 'pdf' }] },
{ name: 'Chrome PDF Viewer', filename: 'internal-pdf-viewer', description: 'Portable Document Format',
mimes: [{ type: 'application/pdf', description: 'Portable Document Format', suffixes: 'pdf' }] },
{ name: 'Chromium PDF Viewer', filename: 'internal-pdf-viewer', description: 'Portable Document Format',
mimes: [{ type: 'application/pdf', description: 'Portable Document Format', suffixes: 'pdf' }] },
{ name: 'Microsoft Edge PDF Viewer', filename: 'internal-pdf-viewer', description: 'Portable Document Format',
mimes: [{ type: 'application/pdf', description: 'Portable Document Format', suffixes: 'pdf' }] },
{ name: 'WebKit built-in PDF', filename: 'internal-pdf-viewer', description: 'Portable Document Format',
mimes: [{ type: 'application/pdf', description: 'Portable Document Format', suffixes: 'pdf' }] },
];
const pluginList = pluginDefs.map(makePlugin);
const mimeTypeList = [];
pluginList.forEach((p, i) => {
for (let j = 0; j < p.length; j++) {
const mt = makeMimeType(pluginDefs[i].mimes[j]);
mt.enabledPlugin = p;
mimeTypeList.push(mt);
}
});
// 让 plugins/mimeTypes 具备 namedItem/item/refresh 及 length
function makeCollection(arr) {
const c = Object.create(arr);
const coll = {};
arr.forEach((v, i) => { coll[i] = v; });
defMethod(coll, 'item', function item(i) { return arr[i] || null; }, 1);
defMethod(coll, 'namedItem', function namedItem(n) {
return arr.find(x => x.name === n || x.type === n) || null;
}, 1);
defMethod(coll, 'refresh', function refresh() {}, 0);
Object.defineProperty(coll, 'length', { value: arr.length, configurable: true, enumerable: false });
return coll;
}
function Navigator() { throw new TypeError("Illegal constructor"); }
const navigator = Object.create(Navigator.prototype);
Object.defineProperty(navigator, Symbol.toStringTag, { value: 'Navigator', configurable: true });
const navProps = {
userAgent: USER_AGENT,
appVersion: '5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36',
platform: 'Win32',
language: 'zh-CN',
languages: ['zh-CN', 'zh', 'en'],
vendor: 'Google Inc.',
vendorSub: '',
productSub: '20030107',
appName: 'Netscape',
appCodeName: 'Mozilla',
product: 'Gecko',
cookieEnabled: true,
doNotTrack: null,
msDoNotTrack: undefined,
maxTouchPoints: 0,
hardwareConcurrency: 8,
deviceMemory: 8,
onLine: true,
webdriver: false,
pdfViewerEnabled: true,
};
Object.keys(navProps).forEach(k => {
Object.defineProperty(navigator, k, { value: navProps[k], configurable: true, enumerable: true });
});
Object.defineProperty(navigator, 'plugins', { value: makeCollection(pluginList), configurable: true, enumerable: true });
Object.defineProperty(navigator, 'mimeTypes', { value: makeCollection(mimeTypeList), configurable: true, enumerable: true });
defMethod(navigator, 'javaEnabled', function javaEnabled() { return false; }, 0);
defMethod(navigator, 'sendBeacon', function sendBeacon() { return true; }, 2);
defMethod(navigator, 'getBattery', function getBattery() {
return Promise.resolve({ charging: true, chargingTime: 0, dischargingTime: Infinity, level: 1 });
}, 0);
globalObj.navigator = navigator;
// ==================== 6. location / history / screen ====================
function Location() { throw new TypeError("Illegal constructor"); }
const location = Object.create(Location.prototype);
['protocol', 'host', 'hostname', 'port', 'pathname', 'search', 'hash', 'origin', 'href'].forEach(k => {
defAccessor(location, k, function () { return LOCATION[k]; }, function (v) { LOCATION[k] = v; }, true);
});
defMethod(location, 'reload', function reload() {}, 0);
defMethod(location, 'replace', function replace() {}, 1);
defMethod(location, 'assign', function assign() {}, 1);
defMethod(location, 'toString', function toString() { return LOCATION.href; }, 0);
globalObj.location = location;
document.location = location;
function History() { throw new TypeError("Illegal constructor"); }
const history = Object.create(History.prototype);
Object.defineProperty(history, 'length', { value: 2, configurable: true, enumerable: true });
Object.defineProperty(history, 'state', { value: null, configurable: true, enumerable: true });
['back', 'forward', 'go', 'pushState', 'replaceState'].forEach(m => defMethod(history, m, function () {}, 2));
globalObj.history = history;
function Screen() { throw new TypeError("Illegal constructor"); }
const screen = Object.create(Screen.prototype);
Object.assign(screen, {
width: 1920, height: 1080, availWidth: 1920, availHeight: 1040,
colorDepth: 24, pixelDepth: 24, availLeft: 0, availTop: 0,
orientation: { angle: 0, type: 'landscape-primary' },
});
globalObj.screen = screen;
// ==================== 7. Storage ====================
function makeStorage(name) {
const store = {};
const s = {};
defAccessor(s, 'length', function () { return Object.keys(store).length; }, undefined, true);
defMethod(s, 'getItem', function getItem(k) {
k = String(k); return Object.prototype.hasOwnProperty.call(store, k) ? store[k] : null;
}, 1);
defMethod(s, 'setItem', function setItem(k, v) { store[String(k)] = String(v); }, 2);
defMethod(s, 'removeItem', function removeItem(k) { delete store[String(k)]; }, 1);
defMethod(s, 'clear', function clear() { for (const k in store) delete store[k]; }, 0);
defMethod(s, 'key', function key(i) { return Object.keys(store)[i] || null; }, 1);
Object.defineProperty(globalObj, '__' + name, { value: store, enumerable: false });
return s;
}
globalObj.localStorage = makeStorage('localStorage');
globalObj.sessionStorage = makeStorage('sessionStorage');
// ==================== 8. XMLHttpRequest / fetch ====================
function XMLHttpRequest() {
this.readyState = 0;
this.status = 0;
this.responseText = '';
this.response = '';
this._headers = {};
}
defMethod(XMLHttpRequest.prototype, 'open', function open(method, url) {
this._method = method; this._url = url; this.readyState = 1;
}, 5);
defMethod(XMLHttpRequest.prototype, 'send', function send() { this.readyState = 4; }, 1);
defMethod(XMLHttpRequest.prototype, 'setRequestHeader', function setRequestHeader(k, v) {
this._headers[k] = v;
}, 2);
defMethod(XMLHttpRequest.prototype, 'getResponseHeader', function getResponseHeader() { return null; }, 1);
defMethod(XMLHttpRequest.prototype, 'getAllResponseHeaders', function getAllResponseHeaders() { return ''; }, 0);
defMethod(XMLHttpRequest.prototype, 'addEventListener', EventTarget.prototype.addEventListener, 3);
defMethod(XMLHttpRequest.prototype, 'abort', function abort() {}, 0);
globalObj.XMLHttpRequest = XMLHttpRequest;
globalObj.Headers = function Headers(init) { this._h = Object.assign({}, init || {}); };
defMethod(globalObj.Headers.prototype, 'append', function append(k, v) { this._h[k] = v; }, 2);
defMethod(globalObj.Headers.prototype, 'set', function set(k, v) { this._h[k] = v; }, 2);
defMethod(globalObj.Headers.prototype, 'get', function get(k) { return this._h[k] || null; }, 1);
globalObj.Request = function Request(url, opts) { this.url = url; Object.assign(this, opts || {}); };
globalObj.fetch = function fetch() { return Promise.resolve({ status: 200, headers: new globalObj.Headers() }); };
markNative(globalObj.fetch, 'fetch', 1);
// ==================== 9. window 常用属性 / 构造器 ====================
globalObj.self = globalObj;
globalObj.top = globalObj;
globalObj.parent = globalObj;
globalObj.frames = globalObj;
globalObj.window = globalObj.window || globalObj;
globalObj.innerWidth = 1920;
globalObj.innerHeight = 937;
globalObj.outerWidth = 1920;
globalObj.outerHeight = 1040;
globalObj.devicePixelRatio = 1;
globalObj.screenX = 0;
globalObj.screenY = 0;
globalObj.pageXOffset = 0;
globalObj.pageYOffset = 0;
globalObj.EventTarget = EventTarget;
globalObj.Node = Node;
globalObj.Element = Element;
globalObj.HTMLElement = HTMLElement;
globalObj.Document = Document;
globalObj.Navigator = Navigator;
globalObj.Location = Location;
globalObj.History = History;
globalObj.Screen = Screen;
defMethod(globalObj, 'addEventListener', function addEventListener(type, cb, opts) {
if (!globalObj.__listeners) globalObj.__listeners = {};
(globalObj.__listeners[type] || (globalObj.__listeners[type] = [])).push(cb);
}, 3);
defMethod(globalObj, 'removeEventListener', function removeEventListener() {}, 2);
defMethod(globalObj, 'getComputedStyle', function getComputedStyle() { return {}; }, 1);
defMethod(globalObj, 'requestAnimationFrame', function requestAnimationFrame(cb) {
return setTimeout(() => cb(Date.now()), 16);
}, 1);
defMethod(globalObj, 'matchMedia', function matchMedia() {
return { matches: false, media: '', addListener() {}, removeListener() {}, addEventListener() {}, removeEventListener() {} };
}, 1);
defMethod(globalObj, 'scrollTo', function scrollTo() {}, 2);
// ==================== 10. 定时器:避免 setInterval 常驻挂起进程 ====================
const rawSetInterval = globalObj.setInterval;
if (typeof rawSetInterval === 'function') {
const wrapped = function setInterval(fn, ms) {
const t = rawSetInterval.apply(globalObj, arguments);
if (t && typeof t.unref === 'function') t.unref(); // 不阻塞进程退出
return t;
};
markNative(wrapped, 'setInterval', 2);
globalObj.setInterval = wrapped;
}
// getServerTime 会尝试 JSONP 加载脚本,补环境中不触发回调,保持静默即可
module.exports = {
cookieJar,
LOCATION,
USER_AGENT,
getHexinV() { return cookieJar['hexin-v'] || cookieJar['v'] || null; },
};
两个坑点:
[JavaScript] 纯文本查看 复制代码 /*
* run.js - 同花顺 hexin-v 生成入口
* ------------------------------------------------------------
* 流程:
* 1. 隐藏 Node 痕迹
* 2. 加载补环境(env.js),把 window/document/navigator/... 挂到 globalThis
* 3. 读取 main(chameleon) 源码,动态注入当前服务器时间(TOKEN_SERVER_TIME)
* —— main 里该值是保存时写死的,token 会编码它,过期会被服务端拒绝
* 4. 用 vm 在全局上下文执行 main,触发 chameleon 初始化 W(),写入 hexin-v
* 5. 从 cookie jar / localStorage 提取 token 并输出到 stdout(仅一行,便于 Python 解析)
*
* 运行:node run.js # 只打印 token
* node run.js --debug # 额外打印 cookie jar 到 stderr
*/
const _require = require;
const _fs = _require('fs');
const _vm = _require('vm');
const _path = _require('path');
// 在删除全局对象前先取好需要的引用
const _mainPath = _path.join(__dirname, 'main');
const _argv = process.argv.slice(2);
const _debug = _argv.indexOf('--debug') !== -1;
// 隐藏 Node 运行痕迹
delete global;
delete process;
delete require;
delete module;
delete exports;
delete __dirname;
delete __filename;
// 加载补环境
const env = _require('./env');
globalThis.window = globalThis.window || globalThis;
// 读取 main 源码并注入动态服务器时间(秒),保证 token 时间新鲜
let src = _fs.readFileSync(_mainPath, 'utf8');
src = src.replace(
/var\s+TOKEN_SERVER_TIME\s*=\s*[0-9.]+\s*;/,
'var TOKEN_SERVER_TIME = ' + (Date.now() / 1000) + ';'
);
// 在全局上下文执行(裸引用 window/document 走 globalThis)
_vm.runInThisContext(src, { filename: 'chameleon-main.js' });
// 提取 token
const token = env.getHexinV();
if (_debug) {
const jar = env.cookieJar || {};
console.error('==== COOKIE JAR ====');
Object.keys(jar).forEach(k => console.error(k + ' = ' + jar[k]));
console.error('==== localStorage(hexin-v) ====');
console.error(globalThis.__localStorage && globalThis.__localStorage['hexin-v']);
}
// stdout 仅输出 token,方便 Python 直接读取
console.log(token || '');
py调用
[Python] 纯文本查看 复制代码 # -*- coding: utf-8 -*-
"""
同花顺行情中心(q.10jqka.com.cn) 涨跌幅排行采集器
========================================================
功能:
1. 本地补环境(env.js)+chameleon(main) 通过 node 生成 hexin-v
2. 携带 hexin-v cookie 翻页请求行情接口
3. BeautifulSoup 解析表格 -> 结构化数据
4. 失败自动重试并刷新 token;token 按时间缓存复用(贴近浏览器行为)
5. 结果写入 CSV(utf-8-sig,Excel 可直接打开)
依赖:requests, beautifulsoup4, lxml
运行:
python 同花顺.py # 采集全部页(自动探测总页数)
python 同花顺.py --pages 5 # 只采集前 5 页
python 同花顺.py --field zdf --order desc --delay 0.5
"""
import argparse
import csv
import re
import subprocess
import sys
import time
from datetime import datetime
from pathlib import Path
import requests
from bs4 import BeautifulSoup
# Windows 控制台默认 GBK,强制 stdout 用 utf-8,避免中文/符号输出报错
try:
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
except Exception:
pass
BASE_DIR = Path(__file__).resolve().parent
NODE_SCRIPT = BASE_DIR / "run.js"
# ⚠️ 必须与 env.js 中补环境的 UA 完全一致:
# hexin-v 内部编码了 strhash(navigator.userAgent),不一致会被服务端判为异常
USER_AGENT = (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36"
)
# ==================== hexin-v 生成与缓存 ====================
def get_hexin_v() -> str:
"""调用 node run.js 生成一个全新的 hexin-v token(取 stdout 首行)。
Windows 编码陷阱:显式 encoding='utf-8' + errors='ignore',避免 GBK 控制台解码异常。
"""
result = subprocess.run(
["node", str(NODE_SCRIPT)],
cwd=str(BASE_DIR),
capture_output=True,
encoding="utf-8",
errors="ignore",
timeout=30,
)
if result.returncode != 0:
raise RuntimeError(f"node 执行失败: {result.stderr}")
lines = (result.stdout or "").strip().splitlines()
token = lines[0].strip() if lines else ""
if not token:
raise RuntimeError(f"未获取到 hexin-v,stdout={result.stdout!r} stderr={result.stderr!r}")
return token
class HexinVManager:
"""token 缓存管理器:按时间复用,超时或强制时重新生成。
真实浏览器里 hexin-v 会被复用一段时间(定期刷新),因此这里也做缓存,
既减少 node 进程开销,又更贴近真实访问行为,降低风控概率。
"""
def __init__(self, max_age: int = 60):
self._token = None
self._ts = 0.0
self.max_age = max_age # token 最长复用秒数
def token(self, force: bool = False) -> str:
now = time.time()
if force or self._token is None or (now - self._ts) > self.max_age:
self._token = get_hexin_v()
self._ts = now
return self._token
# ==================== 请求构造 ====================
def build_headers(referer: str) -> dict:
"""构造行情接口请求头(与浏览器实际请求一致)。"""
return {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,"
"image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
"Accept-Language": "zh-CN,zh;q=0.9",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"Pragma": "no-cache",
"Referer": referer,
"Sec-Fetch-Dest": "document",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Site": "same-origin",
"Upgrade-Insecure-Requests": "1",
"User-Agent": USER_AGENT,
"sec-ch-ua": '"Chromium";v="140", "Not=A?Brand";v="24", "Google Chrome";v="140"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"Windows"',
}
def build_url(page: int, field: str, order: str) -> str:
return (f"https://q.10jqka.com.cn/index/index/board/all/field/{field}/"
f"order/{order}/page/{page}/ajax/1/")
# ==================== 解析 ====================
def parse_board_html(html: str):
"""解析行情表格。
Returns:
(headers, rows, total_pages)
headers: 列名列表(取自 thead,动态适配列变化)
rows: 每行单元格文本组成的列表
total_pages: 总页数(取自 span.page_info 的 "1/279"),解析失败返回 1
"""
soup = BeautifulSoup(html, "lxml")
table = soup.select_one("table.m-table")
if table is None:
return [], [], 1
headers = [th.get_text(strip=True) for th in table.select("thead th")]
rows = []
for tr in table.select("tbody tr"):
cells = [td.get_text(strip=True) for td in tr.find_all("td")]
if cells:
rows.append(cells)
total_pages = 1
info = soup.select_one("span.page_info")
if info:
m = re.search(r"/\s*(\d+)", info.get_text(strip=True))
if m:
total_pages = int(m.group(1))
return headers, rows, total_pages
# ==================== 单页抓取(带重试) ====================
def fetch_page(session: requests.Session, page: int, mgr: HexinVManager,
field: str, order: str, retries: int = 3, timeout: int = 20):
"""抓取单页,失败自动重试并刷新 token。
成功判据:HTTP 200 且能解析出表格行(>0)。
Returns: html 文本;彻底失败返回 None。
"""
url = build_url(page, field, order)
headers = build_headers(url)
for attempt in range(1, retries + 1):
# 第 2 次起强制换新 token(旧 token 可能失效/被风控)
token = mgr.token(force=(attempt > 1))
try:
resp = session.get(url, headers=headers,
cookies={"hexin-v": token, "v": token}, timeout=timeout)
if resp.status_code == 200:
_, rows, _ = parse_board_html(resp.text)
if rows:
return resp.text
print(f" [warn] 第{page}页 200 但无数据(第{attempt}次),换 token 重试")
else:
print(f" [warn] 第{page}页 HTTP {resp.status_code}(第{attempt}次)")
except requests.RequestException as e:
print(f" [warn] 第{page}页请求异常(第{attempt}次): {e}")
time.sleep(1.0 * attempt) # 退避
return None
# ==================== 采集主流程 ====================
def crawl(field: str, order: str, max_pages: int, delay: float,
start_page: int, output: Path, token_max_age: int):
session = requests.Session()
session.headers.update({"User-Agent": USER_AGENT})
mgr = HexinVManager(max_age=token_max_age)
all_rows = []
headers = None
total_pages = None
page = start_page
consecutive_fail = 0
while True:
if max_pages and page > max_pages:
break
if total_pages and page > total_pages:
break
html = fetch_page(session, page, mgr, field, order)
if html is None:
consecutive_fail += 1
print(f" [error] 第{page}页彻底失败")
if consecutive_fail >= 3:
print(" [error] 连续 3 页失败,疑似被风控,停止采集")
break
page += 1
continue
consecutive_fail = 0
h, rows, tp = parse_board_html(html)
if headers is None:
headers = h
if total_pages is None:
total_pages = tp
print(f" 探测到总页数: {total_pages}")
all_rows.extend(rows)
print(f" 第 {page}/{total_pages} 页: {len(rows)} 行 (累计 {len(all_rows)})")
if not rows:
break
page += 1
if delay > 0:
time.sleep(delay)
if headers and all_rows:
save_csv(output, headers, all_rows)
print(f"\n完成: 共 {len(all_rows)} 行 -> {output}")
else:
print("\n未采集到任何数据,请检查 token 或网络。")
return all_rows
def save_csv(path: Path, headers: list, rows: list):
"""写入 CSV,utf-8-sig 保证 Excel 打开中文不乱码。"""
path.parent.mkdir(parents=True, exist_ok=True)
with open(path, "w", newline="", encoding="utf-8-sig") as f:
w = csv.writer(f)
w.writerow(headers)
w.writerows(rows)
def main():
parser = argparse.ArgumentParser(description="同花顺行情中心涨跌幅排行采集器")
parser.add_argument("--field", default="zdf", help="排序字段(默认 zdf=涨跌幅)")
parser.add_argument("--order", default="desc", choices=["desc", "asc"], help="排序方式")
parser.add_argument("--pages", type=int, default=0, help="最多采集页数(0=全部,自动探测总页数)")
parser.add_argument("--start", type=int, default=1, help="起始页码")
parser.add_argument("--delay", type=float, default=0.5, help="每页间隔秒数(礼貌抓取)")
parser.add_argument("--token-max-age", type=int, default=60, help="token 复用最长秒数")
parser.add_argument("--output", default="", help="输出 CSV 路径(默认自动命名)")
args = parser.parse_args()
if args.output:
output = Path(args.output)
else:
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
output = BASE_DIR / f"同花顺行情_{args.field}_{args.order}_{ts}.csv"
print(f"开始采集: field={args.field} order={args.order} "
f"start={args.start} pages={'全部' if args.pages == 0 else args.pages}")
crawl(args.field, args.order, args.pages, args.delay, args.start, output,
args.token_max_age)
if __name__ == "__main__":
main()
|
-
-
.qoder.zip
170.64 KB, 下载次数: 1, 下载积分: 吾爱币 -1 CB
售价: 3 CB吾爱币 [记录]
-
-
同花顺.zip
31.02 KB, 下载次数: 1, 下载积分: 吾爱币 -1 CB
免费评分
-
查看全部评分
|