),需剥离 nth-of-type 以实现批量提取同行元素
let isGenericWrapper = isBatchField && (depth === 0 || (depth === 1 && (!current.className || current.tagName === 'LI')));
if (!isGenericWrapper && current.id && !current.id.includes('__spider')) { sel += '#' + CSS.escape(current.id); hasId = true; }
let hasClass = false;
if (!isGenericWrapper && !hasId && current.className) {
let classStr = typeof current.className === 'string' ? current.className : (current.className.baseVal || '');
let classes = classStr.trim().split(/\s+/).filter(c => c && !c.includes('spider-highlight') && !/^[a-zA-Z0-9\-_]{8,}$/.test(c));
if (classes.length > 0) { sel += '.' + CSS.escape(classes[0]); hasClass = true; }
}
if (hasId) { path.unshift(sel); break; }
let nth = 1, sibling = current;
while (sibling = sibling.previousElementSibling) { if (sibling.tagName === current.tagName) nth++; }
if (isAbsolute || nth > 1 || !hasClass) {
if (!isGenericWrapper) { sel += `:nth-of-type(${nth})`; }
}
path.unshift(sel); current = current.parentNode; depth++;
}
return path.join(' > ');
}
function refreshDialog(isEditMode = false) {
document.querySelectorAll('.spider-highlight').forEach(el => el.classList.remove('spider-highlight'));
if(currentElement) currentElement.classList.add('spider-highlight');
UI.chkAbsolute.checked = useAbsoluteSelector; let stopNode = null;
if (currentFilterPicking) { UI.fieldHint.textContent = `【树状筛选 - 提取${currentFilterPicking.type==='n'?'名称':'参数值'}】`; }
else { const fSchema = TAB_SCHEMAS[currentTab].fields.find(x => x.id === currentPickingFieldId); UI.fieldHint.textContent = `【${fSchema.name}】`; if (fSchema && !fSchema.global && spiderConfig[currentTab].container) stopNode = spiderConfig[currentTab].container.node; }
if (currentPickingFieldId === 'container' && !currentFilterPicking) {
UI.pathControls.style.display = 'flex'; UI.btnParentNode.style.display = 'block'; UI.btnChildNode.style.display = 'block';
let absSel = getSelector(currentElement, null, useAbsoluteSelector);
if(!isEditMode) tempExtraction = { type: 'container', absSel: absSel };
UI.cssPath.textContent = '容器: ' + tempExtraction.absSel;
UI.attrContainer.innerHTML = '
已锁定外层框架
如不准确可点击扩大或重选
';
} else {
let generatedSel = currentElement ? getSelector(currentElement, stopNode, useAbsoluteSelector) : '';
if(!isEditMode) {
if(!currentElement) tempExtraction = { type: 'custom', key: '', val: '', sel: '', filters: [] };
else tempExtraction = { type: tempExtraction?.type || 'text', key: tempExtraction?.key || '', val: '', sel: generatedSel, filters: tempExtraction?.filters || [] };
}
if (!currentElement) { UI.pathControls.style.display = 'none'; UI.cssPath.textContent = '路径: [无 (采用自定义固定值)]'; UI.btnParentNode.style.display = 'none'; UI.btnChildNode.style.display = 'none'; }
else { UI.pathControls.style.display = 'flex'; UI.cssPath.textContent = tempExtraction.sel ? '路径: ' + tempExtraction.sel : '路径: (当前节点本身)'; UI.btnParentNode.style.display = 'block'; UI.btnChildNode.style.display = 'block'; }
let htmlStr = `
`;
if (currentElement) {
htmlStr += `
`;
htmlStr += `
`;
htmlStr += `
`;
Array.from(currentElement.attributes).forEach(attr => { let isChecked = (tempExtraction.type==='attr' && tempExtraction.key===attr.name) ? 'checked' : ''; htmlStr += `
`; });
} else { htmlStr += `
提示:您选择了手动输入固定值。
如需提取请点击重选。
`; }
UI.attrContainer.innerHTML = htmlStr;
shadow.querySelectorAll('input[name="sp_ex"]').forEach(r => r.addEventListener('change', (e) => {
if(e.target.value === 'text') tempExtraction.type = 'text'; else if (e.target.value === 'custom') tempExtraction.type = 'custom'; else if (e.target.value === 'html') tempExtraction.type = 'html'; else if (e.target.value === 'outerhtml') tempExtraction.type = 'outerhtml'; else { tempExtraction.type = 'attr'; tempExtraction.key = e.target.value.split(':')[1]; }
}));
const customInput = shadow.querySelector('#sp_custom_input');
if (customInput) { customInput.onclick = () => { shadow.querySelector('input[value="custom"]').checked = true; tempExtraction.type = 'custom'; }; customInput.oninput = (e) => { tempExtraction.val = e.target.value; if (shadow.querySelector('input[value="custom"]').checked === false) { shadow.querySelector('input[value="custom"]').checked = true; tempExtraction.type = 'custom'; } }; }
}
renderFilters();
}
function renderFilters() {
let isContainer = currentPickingFieldId === 'container';
if (isContainer && !currentFilterPicking) { UI.filterCard.style.display = 'none'; return; }
UI.filterCard.style.display = 'flex';
if (!tempExtraction || !tempExtraction.filters || tempExtraction.filters.length === 0) { UI.filterList.innerHTML = ''; UI.filterList.style.display = 'none'; return; }
UI.filterList.style.display = 'flex'; let html = ''; let delBtnHtml = `
`;
tempExtraction.filters.forEach((flt, idx) => {
if (flt.type === 'replace') { html += `
替换➞${delBtnHtml.replace('f_del"', `f_del" data-idx="${idx}"`)}
`; }
else if (flt.type === 'split') { let dIdx = (parseInt(flt.index)||0)+1; html += `
分割取第段${delBtnHtml.replace('f_del"', `f_del" data-idx="${idx}"`)}
`; }
else if (flt.type === 'between') { html += `
取中间与${delBtnHtml.replace('f_del"', `f_del" data-idx="${idx}"`)}
`; }
else if (flt.type === 'regex') { html += `
`; }
else if (flt.type === 'urldecode') { html += `
URL 解码${delBtnHtml.replace('f_del"', `f_del" data-idx="${idx}"`)}
`; }
else if (flt.type === 'remove_space') { html += `
去空白换行${delBtnHtml.replace('f_del"', `f_del" data-idx="${idx}"`)}
`; }
else if (flt.type === 'prepend') { html += `
前缀${delBtnHtml.replace('f_del"', `f_del" data-idx="${idx}"`)}
`; }
else if (flt.type === 'append') { html += `
后缀${delBtnHtml.replace('f_del"', `f_del" data-idx="${idx}"`)}
`; }
else if (flt.type === 'remove_html') { html += `
去HTML${delBtnHtml.replace('f_del"', `f_del" data-idx="${idx}"`)}
`; }
});
UI.filterList.innerHTML = html;
shadow.querySelectorAll('.f_from').forEach(el => el.oninput = (e) => tempExtraction.filters[e.target.dataset.idx].from = e.target.value);
shadow.querySelectorAll('.f_to').forEach(el => el.oninput = (e) => tempExtraction.filters[e.target.dataset.idx].to = e.target.value);
shadow.querySelectorAll('.f_sep').forEach(el => el.oninput = (e) => tempExtraction.filters[e.target.dataset.idx].sep = e.target.value);
shadow.querySelectorAll('.f_left').forEach(el => el.oninput = (e) => tempExtraction.filters[e.target.dataset.idx].left = e.target.value);
shadow.querySelectorAll('.f_right').forEach(el => el.oninput = (e) => tempExtraction.filters[e.target.dataset.idx].right = e.target.value);
shadow.querySelectorAll('.f_pattern').forEach(el => el.oninput = (e) => tempExtraction.filters[e.target.dataset.idx].pattern = e.target.value);
shadow.querySelectorAll('.f_text').forEach(el => el.oninput = (e) => tempExtraction.filters[e.target.dataset.idx].text = e.target.value);
shadow.querySelectorAll('.f_index').forEach(el => el.oninput = (e) => { let h = parseInt(e.target.value); if(isNaN(h)) h=1; tempExtraction.filters[e.target.dataset.idx].index = Math.max(0, h - 1); });
shadow.querySelectorAll('.f_del').forEach(el => el.onclick = (e) => { tempExtraction.filters.splice(e.target.closest('.f_del').dataset.idx, 1); renderFilters(); });
shadow.querySelectorAll('.sp_magic_btn').forEach(btn => {
btn.onclick = (e) => {
e.stopPropagation(); const tBtn = e.target.closest('.sp_magic_btn'); const menu = shadow.querySelector('#rmenu_' + tBtn.dataset.idx); const isShow = menu.style.display === 'flex';
shadow.querySelectorAll('.sp_regex_menu').forEach(m => m.style.display = 'none');
if (!isShow) { menu.style.display = 'flex'; const cardRect = UI.filterCard.getBoundingClientRect(); const btnRect = tBtn.getBoundingClientRect(); if (cardRect.bottom - btnRect.bottom < 110) { menu.style.top = 'auto'; menu.style.bottom = 'calc(100% + 4px)'; } else { menu.style.top = 'calc(100% + 4px)'; menu.style.bottom = 'auto'; } }
};
});
shadow.querySelectorAll('.sp_regex_menu_item').forEach(item => { item.onclick = (e) => { e.stopPropagation(); tempExtraction.filters[e.target.dataset.idx].pattern = e.target.dataset.val; renderFilters(); }; });
}
shadow.addEventListener('click', () => { shadow.querySelectorAll('.sp_regex_menu').forEach(m => m.style.display = 'none'); });
shadow.querySelector('#sp_btn_add_replace').onclick = () => { tempExtraction.filters.push({type: 'replace', from:'', to:''}); renderFilters(); };
shadow.querySelector('#sp_btn_add_split').onclick = () => { tempExtraction.filters.push({type: 'split', sep:'', index:0}); renderFilters(); };
shadow.querySelector('#sp_btn_add_between').onclick = () => { tempExtraction.filters.push({type: 'between', left:'', right:''}); renderFilters(); };
shadow.querySelector('#sp_btn_add_regex').onclick = () => { tempExtraction.filters.push({type: 'regex', pattern:''}); renderFilters(); };
shadow.querySelector('#sp_btn_add_urldecode').onclick = () => { tempExtraction.filters.push({type: 'urldecode'}); renderFilters(); };
shadow.querySelector('#sp_btn_add_nospace').onclick = () => { tempExtraction.filters.push({type: 'remove_space'}); renderFilters(); };
shadow.querySelector('#sp_btn_add_prepend').onclick = () => { tempExtraction.filters.push({type: 'prepend', text:''}); renderFilters(); };
shadow.querySelector('#sp_btn_add_append').onclick = () => { tempExtraction.filters.push({type: 'append', text:''}); renderFilters(); };
shadow.querySelector('#sp_btn_add_nohtml').onclick = () => { tempExtraction.filters.push({type: 'remove_html'}); renderFilters(); };
// [折叠控制]
UI.btnParentNode.onclick = () => {
if (!currentElement || currentElement.tagName === 'BODY') return;
let boundNode = document;
if (!currentFilterPicking && TAB_SCHEMAS[currentTab].isList && currentPickingFieldId !== 'container') {
let conf = spiderConfig[currentTab]; let fSchema = TAB_SCHEMAS[currentTab].fields.find(x => x.id === currentPickingFieldId);
if (!fSchema.global && conf.container && conf.container.node) boundNode = conf.container.node;
}
if (currentElement === boundNode) { alert('[提示] 已达到约束边界!'); return; }
elementHistory.push(currentElement); currentElement = currentElement.parentElement; refreshDialog(false);
UI.dialogBox.classList.add('collapsed'); UI.btnRestore.style.display = 'block'; UI.dialogTitle.style.display = 'none'; UI.fieldHint.style.display = 'inline-block'; UI.fieldHint.style.margin = '0';
};
UI.btnChildNode.onclick = () => {
if (elementHistory.length === 0) { alert('[提示] 已经是底层的节点了!'); return; }
currentElement = elementHistory.pop(); refreshDialog(false);
UI.dialogBox.classList.add('collapsed'); UI.btnRestore.style.display = 'block'; UI.dialogTitle.style.display = 'none'; UI.fieldHint.style.display = 'inline-block'; UI.fieldHint.style.margin = '0';
};
UI.btnRestore.onclick = () => {
UI.dialogBox.classList.remove('collapsed'); UI.btnRestore.style.display = 'none'; UI.dialogTitle.style.display = 'block'; UI.fieldHint.style.display = 'inline-block'; UI.fieldHint.style.marginTop = '6px';
};
function closeDialog() {
if(currentElement) currentElement.classList.remove('spider-highlight');
UI.dialogBox.classList.remove('show');
setTimeout(() => {
UI.dialogOverlay.style.display = 'none'; resetPanelTitle();
UI.panel.classList.remove('picking-mode'); UI.panel.style.cursor = 'default';
currentPickingFieldId = null; tempExtraction = null; currentFilterPicking = null;
UI.dialogBox.classList.remove('collapsed'); // Reset
if (currentTab === 'filter') renderFilterTab(); else renderTaskList();
}, 300);
}
shadow.querySelector('#sp_btn_cancel').onclick = closeDialog;
// [核心提取保存:全自动全域映射引擎]
shadow.querySelector('#sp_btn_confirm').onclick = () => {
let safeFilters = tempExtraction.filters ? JSON.parse(JSON.stringify(tempExtraction.filters)) : [];
let eData = { type: tempExtraction.type, val: tempExtraction.val, key: tempExtraction.key, filters: safeFilters };
if (currentFilterPicking) {
let cIdx = currentFilterPicking.classIdx; let fIdx = currentFilterPicking.filterIdx; let t = currentFilterPicking.type;
let currItem = spiderConfig.filter.list[cIdx].items[fIdx];
let newSel = tempExtraction.type === 'custom' ? '' : tempExtraction.sel;
currItem[t] = { sel: newSel, extract: eData };
// 🎯 核心全域同步映射:如果当前维度属于批量嗅探出来的,且选择器包含基准特征,则强行覆盖其兄弟维度的配置
if (currItem.batchId && currItem.baseSel && newSel && newSel.startsWith(currItem.baseSel)) {
let relativeSel = newSel.substring(currItem.baseSel.length); // 切割出相对偏移路径 (例如: > li > a)
spiderConfig.filter.list[cIdx].items.forEach(sib => {
// 找到跟自己同属一批被嗅探出来、但 baseSel 不同的兄弟,强行套用相对路径和清洗规则
if (sib.batchId === currItem.batchId && sib !== currItem && sib.baseSel) {
sib[t] = { sel: sib.baseSel + relativeSel, extract: JSON.parse(JSON.stringify(eData)) };
}
});
}
saveConfig(); closeDialog(); return;
}
const conf = spiderConfig[currentTab];
if (currentPickingFieldId === 'container') { conf[currentPickingFieldId].sel = tempExtraction.absSel; conf[currentPickingFieldId].node = currentElement; }
else { conf[currentPickingFieldId].sel = tempExtraction.type === 'custom' ? '' : tempExtraction.sel; conf[currentPickingFieldId].extract = eData; }
if (conf._reuse) conf._reuse = null;
saveConfig(); closeDialog();
};
// =========================================================================
// === [BLOCK 6: 终极代码生成与编译引擎] ===
// =========================================================================
function applyFilters(val, filters) {
let res = val || ''; if (typeof res !== 'string') res = String(res);
if (!filters || filters.length === 0) return res.trim();
filters.forEach(flt => {
if (flt.type === 'replace' && flt.from) res = res.split(flt.from).join(flt.to || '');
else if (flt.type === 'split' && flt.sep) { let idx = parseInt(flt.index)||0; res = res.split(flt.sep)[idx] !== undefined ? res.split(flt.sep)[idx] : ''; }
else if (flt.type === 'between' && flt.left && flt.right) { if(res.includes(flt.left)){ res = res.split(flt.left)[1].split(flt.right)[0] || ''; } else { res = ''; } }
else if (flt.type === 'urldecode') { try { res = decodeURIComponent(res); } catch(e){} }
else if (flt.type === 'regex' && flt.pattern) { try { let m = res.match(new RegExp(flt.pattern)); res = m ? m[0] : ''; } catch(e) {} }
else if (flt.type === 'remove_space') res = res.replace(/[\s\u3000]+/g, '');
else if (flt.type === 'prepend') res = (flt.text || '') + res;
else if (flt.type === 'append') res = res + (flt.text || '');
else if (flt.type === 'remove_html') res = res.replace(/<[^>]+>/g, '');
});
return res.trim();
}
function genFiltersCode(vName, filters, indNum) {
if (!filters || filters.length === 0) return '';
let iStr = ' '.repeat(indNum); let c = '';
filters.forEach(flt => {
if (flt.type === 'replace' && flt.from) c += `${iStr}${vName} = String(${vName}).split('${flt.from.replace(/'/g, "\\'")}').join('${(flt.to||'').replace(/'/g, "\\'")}');\n`;
else if (flt.type === 'split' && flt.sep) c += `${iStr}${vName} = String(${vName}).split('${flt.sep.replace(/'/g, "\\'")}')[${parseInt(flt.index)||0}] || '';\n`;
else if (flt.type === 'between' && flt.left && flt.right) c += `${iStr}if(String(${vName}).includes('${flt.left.replace(/'/g, "\\'")}')){ ${vName} = String(${vName}).split('${flt.left.replace(/'/g, "\\'")}')[1].split('${flt.right.replace(/'/g, "\\'")}')[0] || ''; } else { ${vName} = ''; }\n`;
else if (flt.type === 'urldecode') c += `${iStr}try { ${vName} = decodeURIComponent(${vName}); } catch(e) {}\n`;
else if (flt.type === 'regex' && flt.pattern) c += `${iStr}${vName} = String(${vName}).match(/${flt.pattern.replace(/\\/g, "\\\\").replace(/\//g, "\\/")}/)?.[0] || '';\n`;
else if (flt.type === 'remove_space') c += `${iStr}${vName} = String(${vName}).replace(/[\\s\\u3000]+/g, '');\n`;
else if (flt.type === 'prepend') c += `${iStr}${vName} = '${(flt.text||'').replace(/'/g, "\\'")}' + String(${vName});\n`;
else if (flt.type === 'append') c += `${iStr}${vName} = String(${vName}) + '${(flt.text||'').replace(/'/g, "\\'")}';\n`;
else if (flt.type === 'remove_html') c += `${iStr}${vName} = String(${vName}).replace(/<[^>]+>/g, '');\n`;
});
return c;
}
function generateParseListCodeStr(tabName) {
let conf = spiderConfig[tabName]; let schema = TAB_SCHEMAS[tabName];
let c = `function parseVideoList() {\n var vod = [];\n var items = document.querySelectorAll('${conf.container.sel}');\n items.forEach(function(item) {\n`;
let vodKeys = [];
schema.fields.forEach(f => {
if (f.id === 'container' || f.global || !conf[f.id].extract) return;
let data = conf[f.id];
if (data.extract.type === 'custom') { c += ` var ${f.id} = '${(data.extract.val||'').replace(/'/g, "\\'")}';\n`; }
else {
let queryCode = data.sel ? `item.querySelector('${data.sel}')` : `item`;
let exCode = data.extract.type === 'text' ? `?.textContent` : data.extract.type === 'html' ? `?.innerHTML` : data.extract.type === 'outerhtml' ? `?.outerHTML` : `?.getAttribute('${data.extract.key}')`;
c += ` var ${f.id} = ${queryCode}${exCode} || '';\n`;
}
c += genFiltersCode(f.id, data.extract.filters, 8); c += ` ${f.id} = String(${f.id}).trim();\n`; vodKeys.push(f.id);
});
c += ` var _item_data = { ${vodKeys.map(k => `${k}: ${k}`).join(', ')} };\n`;
c += ` if (Object.values(_item_data).some(function(v) { return v !== ''; })) { vod.push(_item_data); }\n });\n return vod;\n}\n`;
return c;
}
function buildStaticFilterConfig() {
let fConf = { class: [], filters: {} };
spiderConfig.filter.list.forEach(cls => {
if (!cls.val || cls.val.trim() === '') return;
let cId = cls.val.trim();
if (cls.name) fConf.class.push({ type_id: cId, type_name: cls.name.trim() });
let fArr = [];
cls.items.forEach((item, fIdx) => {
let k = item.key || 'key_' + fIdx; let nm = item.name || '未知';
let vList = [];
let nNodes = item.n && item.n.extract && item.n.extract.type !== 'custom' && item.n.sel ? document.querySelectorAll(item.n.sel) : [];
let vNodes = item.v && item.v.extract && item.v.extract.type !== 'custom' && item.v.sel ? document.querySelectorAll(item.v.sel) : [];
let mx = Math.max(item.n && item.n.extract && item.n.extract.type === 'custom' ? 1 : nNodes.length, item.v && item.v.extract && item.v.extract.type === 'custom' ? 1 : vNodes.length);
for(let i=0; i
0) fArr.push({ key: k, name: nm, value: vList });
});
if (fArr.length > 0) fConf.filters[cId] = fArr;
});
return fConf;
}
shadow.querySelector('#sp_run_test').onclick = () => {
UI.outHeader.style.display = 'flex'; const codeTabEl = shadow.querySelector('[data-target="sp_code_output"]');
if (currentTab === 'global') {
codeTabEl.textContent = '完整脚本'; let g = spiderConfig.global;
let fullCode = `/**\n * Spider UI 规则\n * @config\n * debug: true\n * returnType: dom\n */\n\nvar BASE = '${g.base || ''}';\n\nfunction init(cfg) {\n return {};\n}\n\n`;
fullCode += `// 1. 路由配置\nvar spider = {\n routes: {\n`;
const fmt = (str) => "'" + str.replace(/'/g, "\\'").replace(/\{BASE\}/g, "' + BASE + '") + "'";
if (g.home) fullCode += ` homeContent: function() {\n return ${fmt(g.home)};\n },\n homeVideoContent: function() {\n return ${fmt(g.home)};\n },\n`;
if (g.category) {
let s = g.category.replace(/'/g, "\\'").replace(/\{BASE\}/g, "' + BASE + '").replace(/\{tid\}/g, "' + tid + '").replace(/\{pg\}/g, "' + pg + '");
fullCode += ` categoryContent: function(tid, pg, filter, extend) {\n var url = '${s}';\n`;
if (g.cat_pg1) fullCode += ` if (parseInt(pg) === 1) { url = url.replace(/-1\\.html/g, '.html').replace(/\\/page\\/1/g, ''); }\n`;
fullCode += ` return url;\n },\n`;
}
if (g.detail) {
let detUrl = g.detail.replace(/'/g, "\\'").replace(/\{BASE\}/g, "' + BASE + '").replace(/\{id\}/g, "' + id + '");
fullCode += ` detailContent: function(ids) {\n var id = Array.isArray(ids) ? ids[0] || '' : ids || '';\n return '${detUrl}';\n },\n`;
}
if (g.search) {
let sUrl = g.search.replace(/'/g, "\\'").replace(/\{BASE\}/g, "' + BASE + '").replace(/\{wd\}/g, "' + encodeURIComponent(wd) + '").replace(/\{pg\}/g, "' + pg + '");
fullCode += ` searchContent: function(wd, pg) {\n var url = '${sUrl}';\n`;
if (g.search_pg1) fullCode += ` if (parseInt(pg) === 1) { url = url.replace(/-1\\.html/g, '.html').replace(/\\/page\\/1/g, ''); }\n`;
fullCode += ` return url;\n }\n`;
}
fullCode += ` }\n};\n\n`; fullCode = fullCode.replace(/,\n \}/g, '\n }');
fullCode += `// 2. 数据提取\nfunction homeContent(filter) {\n`;
let fConfig = buildStaticFilterConfig();
fullCode += ` var filterConfig = ${JSON.stringify(fConfig, null, 4)};\n return filterConfig;\n}\n\n`;
let genF = [];
function addP(tN) {
if (!spiderConfig[tN].container || !spiderConfig[tN].container.sel) return null;
let bd = generateParseListCodeStr(tN).replace(/function parseVideoList\(\) \{\n/, '').replace(/\}$/, '');
let wrap = `function _temp_name_() {\n${bd}}\n`;
for(let z of genF) { if (z.body.replace(/_temp_name_/g,'') === wrap.replace(/_temp_name_/g,'')) return z.name; }
let nm = 'parseVideoList' + (genF.length ? genF.length + 1 : '');
if(genF.length===0) nm = 'parseVideoList'; genF.push({ name: nm, body: wrap.replace(/_temp_name_/g, nm) }); return nm;
}
let hn = addP('home'); fullCode += `function homeVideoContent() {\n return { list: ${hn ? hn+'()' : '[]'} };\n}\n\n`;
let cn = addP('category'); fullCode += `function categoryContent(tid, pg, filter, extend) {\n`;
['pagecount', 'limit', 'total'].forEach(k => {
if (spiderConfig.category[k] && spiderConfig.category[k].extract) {
let pd = spiderConfig.category[k];
if (pd.extract.type === 'custom') fullCode += ` var _${k}_raw = '${(pd.extract.val||'').replace(/'/g, "\\'")}';\n`;
else {
let pq = pd.sel ? `document.querySelector('${pd.sel}')` : `document`; let xC = pd.extract.type === 'text' ? `?.textContent` : pd.extract.type === 'html' ? `?.innerHTML` : pd.extract.type === 'outerhtml' ? `?.outerHTML` : `?.getAttribute('${pd.extract.key}')`;
fullCode += ` var _${k}_raw = ${pq}${xC} || '';\n`;
}
fullCode += ` var _${k} = _${k}_raw;\n`; fullCode += genFiltersCode(`_${k}`, pd.extract.filters, 4); fullCode += ` _${k} = parseInt(String(_${k}).match(/\\d+/)?.[0]) || 0;\n\n`;
}
});
fullCode += ` var vod = ${cn ? cn+'()' : '[]'};\n return {\n code: 1,\n msg: "数据列表",\n page: typeof pg !== 'undefined' ? pg : 1,\n pagecount: ${spiderConfig.category.pagecount?.extract ? '_pagecount' : '99'},\n limit: ${spiderConfig.category.limit?.extract ? '_limit' : 'vod.length'},\n total: ${spiderConfig.category.total?.extract ? '_total' : 'vod.length'},\n list: vod\n };\n}\n\n`;
fullCode += `function detailContent(ids) {\n var detail = {};\n`;
TAB_SCHEMAS.detail.fields.forEach(f => {
if (f.isPlay || !spiderConfig.detail[f.id].extract) return;
let dt = spiderConfig.detail[f.id];
if (dt.extract.type === 'custom') fullCode += ` detail.${f.id} = '${(dt.extract.val||'').replace(/'/g, "\\'")}';\n`;
else { let qc = `document.querySelector('${dt.sel}')`; let xC = dt.extract.type === 'text' ? `?.textContent` : dt.extract.type === 'html' ? `?.innerHTML` : dt.extract.type === 'outerhtml' ? `?.outerHTML` : `?.getAttribute('${dt.extract.key}')`; fullCode += ` detail.${f.id} = ${qc}${xC} || '';\n`; }
fullCode += genFiltersCode(`detail.${f.id}`, dt.extract.filters, 4); fullCode += ` detail.${f.id} = String(detail.${f.id}).trim();\n\n`;
});
let dConf = spiderConfig.detail;
if (dConf.vod_play_title && dConf.vod_play_url && dConf.vod_play_title.extract && dConf.vod_play_url.extract) {
fullCode += ` var playFroms = []; var playUrls = [];\n`;
fullCode += ` var fromNodes = ${dConf.vod_play_from && dConf.vod_play_from.extract && dConf.vod_play_from.extract.type !== 'custom' && dConf.vod_play_from.sel ? `document.querySelectorAll('${dConf.vod_play_from.sel}')` : `[]`};\n`;
fullCode += ` var titleNodes = ${dConf.vod_play_title.extract.type !== 'custom' && dConf.vod_play_title.sel ? `document.querySelectorAll('${dConf.vod_play_title.sel}')` : `[]`};\n`;
fullCode += ` var urlNodes = ${dConf.vod_play_url.extract.type !== 'custom' && dConf.vod_play_url.sel ? `document.querySelectorAll('${dConf.vod_play_url.sel}')` : `[]`};\n`;
fullCode += ` var baseNodes = titleNodes.length > 0 ? titleNodes : urlNodes; var epGroups = []; var currentGroup = []; var lastContainer = null;\n for(var j=0; j 0) epGroups.push(currentGroup);\n var maxLen = fromNodes.length > 0 ? Math.min(fromNodes.length, epGroups.length) : epGroups.length;\n for(var i=0; i 0) { playFroms.push(fromName); playUrls.push(epList.join('#')); }\n }\n detail.vod_play_from = playFroms.join('$$$'); detail.vod_play_url = playUrls.join('$$$');\n`;
}
fullCode += ` return { list: [detail] };\n}\n\n`;
let sn = addP('search');
fullCode += `function searchContent(wd, pg) {\n`;
['pagecount', 'limit', 'total'].forEach(k => {
if (spiderConfig.search[k] && spiderConfig.search[k].extract) {
let pd = spiderConfig.search[k];
if (pd.extract.type === 'custom') fullCode += ` var _${k}_raw = '${(pd.extract.val||'').replace(/'/g, "\\'")}';\n`;
else {
let pq = pd.sel ? `document.querySelector('${pd.sel}')` : `document`; let xC = pd.extract.type === 'text' ? `?.textContent` : pd.extract.type === 'html' ? `?.innerHTML` : pd.extract.type === 'outerhtml' ? `?.outerHTML` : `?.getAttribute('${pd.extract.key}')`;
fullCode += ` var _${k}_raw = ${pq}${xC} || '';\n`;
}
fullCode += ` var _${k} = _${k}_raw;\n`; fullCode += genFiltersCode(`_${k}`, pd.extract.filters, 4); fullCode += ` _${k} = parseInt(String(_${k}).match(/\\d+/)?.[0]) || 0;\n\n`;
}
});
fullCode += ` var vod = ${sn ? sn+'()' : '[]'};\n return {\n code: 1,\n msg: "搜索结果",\n page: typeof pg !== 'undefined' ? pg : 1,\n pagecount: ${spiderConfig.search.pagecount?.extract ? '_pagecount' : '99'},\n limit: ${spiderConfig.search.limit?.extract ? '_limit' : 'vod.length'},\n total: ${spiderConfig.search.total?.extract ? '_total' : 'vod.length'},\n list: vod\n };\n}\n\n`;
if (genF.length > 0) { fullCode += `// 3. 公共解析\n`; genF.forEach(f => { fullCode += `${f.body}\n`; }); }
UI.codeOutput.value = fullCode; UI.testResult.textContent = "[成功] 全局编译完成!"; UI.outTabs[1].click(); return;
}
// ==== [局部测试与方法生成] ====
codeTabEl.textContent = currentTab === 'filter' ? '静态配置' : '方法代码';
const schema = TAB_SCHEMAS[currentTab], conf = spiderConfig[currentTab]; let tabCode = '';
try {
let liveResult = null;
if (currentTab === 'filter') {
let sData = buildStaticFilterConfig(); liveResult = sData;
tabCode = `// 首页静态过滤配置\nfunction homeContent(filter) {\n var filterConfig = ${JSON.stringify(sData, null, 4)};\n return filterConfig;\n}\n`;
} else if (schema.isList) {
let resArray = [];
if (conf.container && conf.container.sel) {
const items = document.querySelectorAll(conf.container.sel);
for(let i=0; i {
if (f.id === 'container' || f.global || !conf[f.id].extract) return;
let rawVal; if (conf[f.id].extract.type === 'custom') rawVal = conf[f.id].extract.val;
else { let targetEl = conf[f.id].sel ? item.querySelector(conf[f.id].sel) : item; rawVal = conf[f.id].extract.type === 'text' ? targetEl?.textContent : conf[f.id].extract.type === 'html' ? targetEl?.innerHTML : conf[f.id].extract.type === 'outerhtml' ? targetEl?.outerHTML : targetEl?.getAttribute(conf[f.id].extract.key); }
obj[f.id] = applyFilters(rawVal, conf[f.id].extract.filters); if (obj[f.id] !== '') _isEmpty = false;
});
if (!_isEmpty) resArray.push(obj);
}
}
let globalData = {};
['pagecount', 'limit', 'total'].forEach(k => {
if (conf[k] && conf[k].extract) {
let rawVal; if (conf[k].extract.type === 'custom') rawVal = conf[k].extract.val;
else { let el = conf[k].sel ? document.querySelector(conf[k].sel) : document; rawVal = conf[k].extract.type === 'text' ? el?.textContent : conf[k].extract.type === 'html' ? el?.innerHTML : conf[k].extract.type === 'outerhtml' ? el?.outerHTML : el?.getAttribute(conf[k].extract.key); }
let finalVal = applyFilters(rawVal, conf[k].extract.filters); let parsedNum = parseInt(String(finalVal).match(/\d+/)?.[0]);
if (!isNaN(parsedNum)) globalData[k] = parsedNum; else globalData[k] = `[异常] "${finalVal}"`;
}
});
liveResult = (currentTab === 'category' || currentTab === 'search') ? { code: 1, msg: "数据列表", page: "pg (运行时传入)", pagecount: globalData.pagecount || 99, limit: globalData.limit !== undefined ? globalData.limit : resArray.length, total: globalData.total !== undefined ? globalData.total : resArray.length, list: resArray } : { msg: `匹配到 ${resArray.length} 个项目`, list: resArray };
if (currentTab === 'home') { tabCode += `function homeVideoContent() {\n var vod = parseVideoList();\n return { list: vod };\n}\n\n`; }
else if (currentTab === 'category' || currentTab === 'search') {
let fName = currentTab === 'category' ? 'categoryContent(tid, pg, filter, extend)' : 'searchContent(wd, pg)';
tabCode += `function ${fName} {\n`;
['pagecount', 'limit', 'total'].forEach(k => {
if (conf[k] && conf[k].extract) {
let pd = conf[k];
if (pd.extract.type === 'custom') { tabCode += ` var _${k}_raw = '${(pd.extract.val||'').replace(/'/g, "\\'")}';\n`; }
else {
let pq = pd.sel ? `document.querySelector('${pd.sel}')` : `document`;
let pExCode = pd.extract.type === 'text' ? `?.textContent` : pd.extract.type === 'html' ? `?.innerHTML` : pd.extract.type === 'outerhtml' ? `?.outerHTML` : `?.getAttribute('${pd.extract.key}')`;
tabCode += ` var _${k}_raw = ${pq}${pExCode} || '';\n`;
}
tabCode += ` var _${k} = _${k}_raw;\n`; tabCode += genFiltersCode(`_${k}`, pd.extract.filters, 4);
tabCode += ` _${k} = parseInt(String(_${k}).match(/\\d+/)?.[0]) || 0;\n\n`;
}
});
tabCode += ` var vod = parseVideoList();\n return {\n code: 1,\n msg: "列表",\n page: typeof pg !== 'undefined' ? pg : 1,\n pagecount: ${conf.pagecount?.extract ? '_pagecount' : '99'},\n limit: ${conf.limit?.extract ? '_limit' : 'vod.length'},\n total: ${conf.total?.extract ? '_total' : 'vod.length'},\n list: vod\n };\n}\n\n`;
}
if (conf.container && conf.container.sel) tabCode += generateParseListCodeStr(currentTab);
} else {
let detailObj = {};
schema.fields.forEach(f => {
if (f.isPlay || !conf[f.id].extract) return;
let rawVal; if (conf[f.id].extract.type === 'custom') rawVal = conf[f.id].extract.val;
else { let el = document.querySelector(conf[f.id].sel); rawVal = conf[f.id].extract.type === 'text' ? el?.textContent : conf[f.id].extract.type === 'html' ? el?.innerHTML : conf[f.id].extract.type === 'outerhtml' ? el?.outerHTML : el?.getAttribute(conf[f.id].extract.key); }
detailObj[f.id] = applyFilters(rawVal, conf[f.id].extract.filters);
});
if (conf.vod_play_title && conf.vod_play_url && conf.vod_play_title.extract && conf.vod_play_url.extract) {
let playFroms = []; let playUrls = [];
let fromNodes = conf.vod_play_from?.extract?.type !== 'custom' && conf.vod_play_from?.sel ? document.querySelectorAll(conf.vod_play_from.sel) : [];
let titleNodes = conf.vod_play_title.extract.type !== 'custom' && conf.vod_play_title.sel ? document.querySelectorAll(conf.vod_play_title.sel) : [];
let urlNodes = conf.vod_play_url.extract.type !== 'custom' && conf.vod_play_url.sel ? document.querySelectorAll(conf.vod_play_url.sel) : [];
let baseNodes = titleNodes.length > 0 ? titleNodes : urlNodes; let epGroups = []; let currentGroup = []; let lastContainer = null;
for(let j=0; j 0) epGroups.push(currentGroup);
let maxLen = fromNodes.length > 0 ? Math.min(fromNodes.length, epGroups.length) : epGroups.length;
for(let i=0; i 0) { playFroms.push(fromName); playUrls.push(epList.join('#')); }
}
detailObj.vod_play_from = playFroms.join('$$$'); detailObj.vod_play_url = playUrls.join('$$$');
}
liveResult = { msg: '详情页解析成功', list: [detailObj] };
tabCode += `function detailContent(ids) {\n var detail = {};\n`;
schema.fields.forEach(f => {
if (f.isPlay || !conf[f.id].extract) return;
let dt = conf[f.id];
if (dt.extract.type === 'custom') { tabCode += ` detail.${f.id} = '${(dt.extract.val||'').replace(/'/g, "\\'")}';\n`; }
else { let qc = `document.querySelector('${dt.sel}')`; let exCode = dt.extract.type === 'text' ? `?.textContent` : dt.extract.type === 'html' ? `?.innerHTML` : dt.extract.type === 'outerhtml' ? `?.outerHTML` : `?.getAttribute('${dt.extract.key}')`; tabCode += ` detail.${f.id} = ${qc}${exCode} || '';\n`; }
tabCode += genFiltersCode(`detail.${f.id}`, dt.extract.filters, 4); tabCode += ` detail.${f.id} = String(detail.${f.id}).trim();\n\n`;
});
if (conf.vod_play_title && conf.vod_play_url && conf.vod_play_title.extract && conf.vod_play_url.extract) {
tabCode += ` var playFroms = []; var playUrls = [];\n var fromNodes = ${conf.vod_play_from && conf.vod_play_from.extract && conf.vod_play_from.extract.type !== 'custom' && conf.vod_play_from.sel ? `document.querySelectorAll('${conf.vod_play_from.sel}')` : `[]`};\n var titleNodes = ${conf.vod_play_title.extract.type !== 'custom' && conf.vod_play_title.sel ? `document.querySelectorAll('${conf.vod_play_title.sel}')` : `[]`};\n var urlNodes = ${conf.vod_play_url.extract.type !== 'custom' && conf.vod_play_url.sel ? `document.querySelectorAll('${conf.vod_play_url.sel}')` : `[]`};\n var baseNodes = titleNodes.length > 0 ? titleNodes : urlNodes; var epGroups = []; var currentGroup = []; var lastContainer = null;\n for(var j=0; j 0) epGroups.push(currentGroup);\n var maxLen = fromNodes.length > 0 ? Math.min(fromNodes.length, epGroups.length) : epGroups.length;\n for(var i=0; i 0) { playFroms.push(fromName); playUrls.push(epList.join('#')); }\n }\n detail.vod_play_from = playFroms.join('$$$');\n detail.vod_play_url = playUrls.join('$$$');\n\n`;
}
tabCode += ` return { list: [detail] };\n}\n`;
}
UI.testResult.textContent = typeof liveResult === 'string' ? liveResult : JSON.stringify(liveResult, null, 2);
UI.codeOutput.value = tabCode; UI.outTabs[0].click();
} catch(e) {
UI.testResult.textContent = '[错误] 提取出错: ' + e.message; UI.outTabs[0].click();
}
};
// =========================================================================
// === [BLOCK 7: 面板开关与系统启动] ===
// =========================================================================
UI.fab.onclick = () => {
UI.panel.style.display = 'flex';
setTimeout(() => UI.panel.classList.add('show'), 10);
UI.fab.style.display = 'none';
if (currentTab === 'filter') renderFilterTab(); else renderTaskList();
};
shadow.querySelector('#sp_close_panel').onclick = () => {
UI.panel.classList.remove('show');
setTimeout(() => {
UI.panel.style.display = 'none';
UI.fab.style.display = 'flex';
togglePicking(false);
resetPanelTitle();
UI.panel.classList.remove('picking-mode');
UI.panel.style.cursor = 'default';
currentPickingFieldId = null;
currentFilterPicking = null;
}, 300);
};
console.log("[成功] Spider-UI V44 启动完毕 (局部热更 + 完美筛选树状版)。");
})();