吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 98|回复: 4
上一主题 下一主题
收起左侧

[Web逆向] Ai逆向同花顺网站采集数据

[复制链接]
跳转到指定楼层
楼主
13955925361 发表于 2026-9-19 01:28 回帖奖励
首先用的是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"

# &#9888;&#65039; 必须与 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

免费评分

参与人数 1吾爱币 +1 收起 理由
cm26408 + 1 我很赞同!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

沙发
reboot1105 发表于 2026-9-19 01:37
赞美AI大人。在二三年之前,这的确是无法想象的
3#
cm26408 发表于 2026-9-19 01:41
谢谢分享。我想做自动盯盘买卖,可是CODEX网络不稳定啊- -
4#
AE86zdm 发表于 2026-9-19 03:03
5#
kingbeast 发表于 2026-9-19 05:00
直接能用吗?
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-9-19 06:31

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表