新疆游骏国际旅行社2-6人拼车游靠谱吗
/** * 通用多平台回链脚本 V4.0 * 支持:媒介盒子、超级媒介、可无限扩展其他平台 * 用法:node backlink_batch.js 平台名 媒体列表 新闻URL [阈值] [等待(ms)] * 示例1:node backlink_batch.js meijiehezi 诸城,寿光 http://xxx.com/zixun * 示例2:node backlink_batch.js chaojimeijie medias.txt http://xxx.com/zixun 75 1000 * 前置:chrome --remote-debugging-port=9222 --headless=new */ const CDP = require('chrome-remote-interface'); const axios = require('axios'); const cheerio = require('cheerio'); const fs = require('fs'); const path = require('path'); const readline = require('readline'); // ===================== 【平台适配层:加新平台只需要改这里】===================== const PLATFORMS = { // 媒介盒子(原脚本完整适配) meijiehezi: { name: '媒介盒子', urlKeyword: 'meijiehezi', // 自动匹配标签页的关键词 selectors: { publishTab: 'button.am-btn', publishTabText: '发布中', mediaInput: '#media_name', searchBtn: 'button.am-btn-primary', searchBtnText: '搜索', pageInfo: '#my_order_info', lastPageInput: '#changePage', goPageBtn: '#dataTable-btn', prevPageBtn: '#my_order_previous', orderTable: '.am-table tbody tr', orderTitleTd: 'td:nth-child(3)', orderStatusTd: 'td:nth-child(6)', publishBtn: 'button.am-btn-primary', publishBtnText: '发布', dialog: '.am-modal-active', linkInput: '#pub_url2, .am-modal-active input[placeholder*="网址"]', submitBtn: '.am-modal-active .am-btn-primary, .am-modal-active .am-modal-btn:last-child', resultDialog: '#my-alert.am-modal-active', closeBtn: '.am-modal-close' }, helpers: ` window.__platform={ getTotalPages(info){const t=info.match(/共 (\\d+) 条/)?.[1]||1;return Math.ceil(t/20);}, goLastPage(input,btn,total){input.value=Math.ceil(total/20);btn.click();}, getResult(dlg){return dlg.textContent.includes('成功')?'success':'fail';}, closeAll(){$('.am-modal-active').modal('close');} };` }, // 超级媒介(完整适配) chaojimeijie: { name: '超级媒介', urlKeyword: 'chaojimeijie', selectors: { publishTab: 'label.el-radio-button input', publishTabText: '发布中', mediaInput: 'label:contains("媒体名称") + * input', searchBtn: 'button.el-button--primary', searchBtnText: '搜索', pageInfo: '.el-pagination', lastPageBtn: '.el-pagination li.number:last-child', prevPageBtn: '.el-pagination button.btn-prev', orderTable: '.el-table__body tr', orderTitleTd: 'td:nth-child(2)', orderStatusTd: 'td:nth-child(7)', publishBtn: 'button.el-button--primary, button.el-button--success', publishBtnText: '发布', dialog: '.el-dialog', linkInput: '.el-dialog input:not(.is-hidden)', submitBtn: '.el-dialog button:contains("提交")', resultMessage: '.el-message--success, .el-message--error', closeBtn: '.el-dialog__close' }, helpers: ` window.__platform={ getTotalPages(pg){const last=pg.querySelector('li.number:last-child');return last?parseInt(last.textContent):1;}, goLastPage(input,btn,total){btn.click();}, getResult(msg){return msg.classList.contains('el-message--success')?'success':'fail';}, closeAll(){document.querySelector('.el-dialog__close')?.click();} };` } // 加新平台:复制上面的结构,替换成对应平台的选择器即可 }; // ===================== 【全局配置(通用)】===================== const CONFIG = { cdpPort: 9222, logDir: path.resolve('./logs'), dedupFile: path.resolve('./dedup.json'), failedFile: path.resolve('./failed.csv'), matchThreshold: 80, waitBase: 800, buttonRetry: 2, orderRetry: 1, cdpRetry: 3, mediaRefresh: true, autoExportFailed: true }; // ===================== 【全局变量(通用)】===================== let client, currentPlatform; const stats = { totalMedia: 0, processedMedia: 0, totalOrders: 0, success: 0, failed: 0, skipped: 0, noMatch: 0, buttonRetried: 0, orderRetried: 0, dedupSkipped: 0 }; let articles = []; let dedupSet = new Set(); let failedOrders = []; // ===================== 【工具函数(100%通用)】===================== function now() { const d = new Date(); return `${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}:${String(d.getSeconds()).padStart(2,'0')}`; } function fileTime() { return new Date().toISOString().replace(/\D/g,'').slice(0,14); } async function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } function writeLog(msg, type = 'info', media = '') { const prefix = media ? `[${media}] ` : ''; const line = `[${now()}] [${currentPlatform.name}] ${prefix}${msg}`; const colors = { success: '\x1B[32m', error: '\x1B[31m', warn: '\x1B[33m', info: '\x1B[36m', reset: '\x1B[0m' }; console.log(`${colors[type]}${line}${colors.reset}`); const logFile = path.join(CONFIG.logDir, `batch_${new Date().toISOString().slice(0,10)}.log`); try { fs.appendFileSync(logFile, line + '\n', 'utf8'); } catch(e) {} } function loadDedup() { try { if (fs.existsSync(CONFIG.dedupFile)) { dedupSet = new Set(JSON.parse(fs.readFileSync(CONFIG.dedupFile, 'utf8'))); writeLog(`✅ 加载去重记录:${dedupSet.size}条`); } } catch(e) { writeLog(`⚠️ 去重文件加载失败,新建空记录`, 'warn'); dedupSet = new Set(); } } function saveDedup() { try { fs.writeFileSync(CONFIG.dedupFile, JSON.stringify([...dedupSet]), 'utf8'); writeLog(`✅ 保存去重记录:${dedupSet.size}条`); } catch(e) { writeLog(`❌ 去重记录保存失败:${e.message}`, 'error'); } } function exportFailed() { if (!CONFIG.autoExportFailed || failedOrders.length === 0) return; try { const header = '平台,媒体名称,订单标题,文章链接,失败时间,失败原因\n'; const rows = failedOrders.map(o => `${o.platform},${o.media},${o.title.replace(/,/g,';')},${o.url},${o.time},${o.reason.replace(/,/g,';')}` ).join('\n'); fs.writeFileSync(CONFIG.failedFile, header + rows, 'utf8'); writeLog(`✅ 失败订单已导出:${CONFIG.failedFile}(共${failedOrders.length}条)`, 'success'); } catch(e) { writeLog(`❌ 失败订单导出失败:${e.message}`, 'error'); } } async function readMediaFile(filePath) { const medias = []; const rl = readline.createInterface({ input: fs.createReadStream(filePath), crlfDelay: Infinity }); for await (const line of rl) { const media = line.trim(); if (media && !media.startsWith('#')) medias.push(media); } return medias; } function parseArgs() { const [, , platformArg, mediaArg, crawlUrl, thresholdArg, waitArg] = process.argv; if (!platformArg || !mediaArg || !crawlUrl) { console.log(` ❌ 用法错误! ✅ 通用用法:node backlink_batch.js 平台名 媒体列表 新闻URL [阈值] [等待(ms)] 📌 支持平台:${Object.keys(PLATFORMS).join('、')} 📌 示例1:node backlink_batch.js meijiehezi 诸城,寿光 http://xxx.com/zixun 📌 示例2:node backlink_batch.js chaojimeijie medias.txt http://xxx.com/zixun 75 1000 `); process.exit(1); } if (!PLATFORMS[platformArg]) { writeLog(`❌ 不支持的平台:${platformArg}`, 'error'); writeLog(`✅ 支持的平台:${Object.keys(PLATFORMS).join('、')}`, 'info'); process.exit(1); } let medias; if (fs.existsSync(mediaArg) && mediaArg.endsWith('.txt')) { medias = readMediaFile(mediaArg); if (medias.length === 0) { writeLog(`❌ 媒体文件为空:${mediaArg}`, 'error'); process.exit(1); } } else { medias = mediaArg.split(',').map(m => m.trim()).filter(m => m); } if (thresholdArg) CONFIG.matchThreshold = parseInt(thresholdArg) || 80; if (waitArg) CONFIG.waitBase = parseInt(waitArg) || 800; return { platform: PLATFORMS[platformArg], medias, crawlUrl }; } function getSimilarity(s1, s2) { const clean = s => s.trim().replace(/[^\u4e00-\u9fa5a-zA-Z0-9]/g,'').toLowerCase(); const a = clean(s1), b = clean(s2); if (a.includes(b) || b.includes(a)) return 100; const len = Math.max(a.length, b.length); let same = 0; for (let i = 0; i < Math.min(a.length, b.length); i++) if (a[i] === b[i]) same++; return Math.round((same / len) * 100); } function matchUrl(orderTitle) { if (!orderTitle || orderTitle.length < 5) { stats.noMatch++; return null; } let best = { url: null, score: 0 }; for (let art of articles) { const score = getSimilarity(orderTitle, art.title); if (score > best.score) { best.score = score; best.url = art.url; } } if (best.score >= CONFIG.matchThreshold) { return best.url; } stats.noMatch++; return null; } async function fetchArticles(crawlUrl) { for (let r = 0; r < 3; r++) { try { writeLog(`✅ 抓取新闻:${crawlUrl}`); const res = await axios.get(crawlUrl, { timeout: 20000, headers: { 'User-Agent': 'Mozilla/5.0' } }); const $ = cheerio.load(res.data); articles = []; $('a').each((i, el) => { let t = $(el).text().trim(), h = $(el).attr('href'); if (!t || !h || t.length < 6) return; if (h.startsWith('/')) h = new URL(h, crawlUrl).href; if (h.startsWith('http')) articles.push({ title: t, url: h }); }); writeLog(`✅ 抓取完成:${articles.length}篇`); return; } catch(e) { if (r < 2) { writeLog(`🔄 抓取失败,第${r+1}次重试...`, 'warn'); await sleep(3000); } else { writeLog(`❌ 抓取最终失败:${e.message}`, 'error'); throw e; } } } } async function connectCDP() { for (let r = 0; r < CONFIG.cdpRetry; r++) { try { const targets = await CDP.List({ port: CONFIG.cdpPort }); const target = targets.find(t => t.url?.includes(currentPlatform.urlKeyword)) || targets[0]; if (!target) throw new Error(`未找到${currentPlatform.name}标签页`); client = await CDP({ target, port: CONFIG.cdpPort }); await Promise.all([client.Runtime.enable(), client.Page.enable()]); writeLog(`✅ CDP连接成功:${target.url.substring(0,60)}`); return; } catch(e) { if (r < CONFIG.cdpRetry - 1) { writeLog(`🔄 CDP连接失败,第${r+1}次重试...`, 'warn'); await sleep(2000); } else { writeLog(`❌ CDP连接最终失败:${e.message}`, 'error'); throw e; } } } } async function cdpEval(expr, t = 15000) { try { const r = await client.Runtime.evaluate({ expression: expr, timeout: t, returnByValue: true, awaitPromise: true }); return r.exceptionDetails ? 'EVAL_ERROR' : (r.result.value || '').toString().trim(); } catch { return 'EVAL_ERROR'; } } async function waitDialog() { for (let i = 0; i < 50; i++) { if (await cdpEval(`!!document.querySelector('${currentPlatform.selectors.dialog}')`) === 'true') return true; await sleep(200); } return false; } // ===================== 【通用操作函数(自动适配平台)】===================== async function injectHelpers() { await cdpEval(currentPlatform.helpers); await cdpEval(` window.__common={ clickByText(selector,text){ document.querySelectorAll(selector).forEach(el=>{ if(el.textContent.trim()===text)el.click(); }); }, setInputValue(input,value){ const s=Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value').set; s.call(input,value); input.dispatchEvent(new Event('input',{bubbles:true})); input.dispatchEvent(new Event('change',{bubbles:true})); }, getOrders(tableSelector,titleTd,statusTd){ const list=[]; document.querySelectorAll(tableSelector).forEach(tr=>{ const tds=tr.querySelectorAll('td'); if(tds.length<3)return; const title=tds[parseInt(titleTd.replace(/[^0-9]/g,''))-1]?.textContent.trim()||''; const status=tds[parseInt(statusTd.replace(/[^0-9]/g,''))-1]?.textContent.trim()||''; list.push({idx:list.length,title,status}); }); return JSON.stringify(list); } }; `); } async function clickPublishTab() { const s = currentPlatform.selectors; await cdpEval(`__common.clickByText('${s.publishTab}','${s.publishTabText}')`); } async function setMediaName(name) { const s = currentPlatform.selectors; await cdpEval(` const input=document.querySelector('${s.mediaInput}'); if(input)__common.setInputValue(input,'${name.replace(/'/g,"\\'")}'); `); } async function clickSearch() { const s = currentPlatform.selectors; await cdpEval(`__common.clickByText('${s.searchBtn}','${s.searchBtnText}')`); } async function getTotalPages() { const s = currentPlatform.selectors; return parseInt(await cdpEval(` const info=document.querySelector('${s.pageInfo}'); info?__platform.getTotalPages(info):1; `)) || 1; } async function goLastPage(total) { const s = currentPlatform.selectors; if (s.lastPageInput) { await cdpEval(` const input=document.querySelector('${s.lastPageInput}'); const btn=document.querySelector('${s.goPageBtn}'); if(input&&btn)__platform.goLastPage(input,btn,${total}); `); } else { await cdpEval(` const btn=document.querySelector('${s.lastPageBtn}'); if(btn)__platform.goLastPage(null,btn,${total}); `); } } async function goPrevPage() { const s = currentPlatform.selectors; await cdpEval(`document.querySelector('${s.prevPageBtn}')?.click();`); } async function getOrders() { const s = currentPlatform.selectors; return JSON.parse(await cdpEval(`__common.getOrders('${s.orderTable}','${s.orderTitleTd}','${s.orderStatusTd}')`) || '[]'); } async function clickPublish(idx) { const s = currentPlatform.selectors; await cdpEval(` let c=0; document.querySelectorAll('${s.publishBtn}').forEach(b=>{ if(b.textContent.trim()==='${s.publishBtnText}'&&!b.disabled){ if(c===${idx})b.click();c++; } }); `); } async function fillUrl(url) { const s = currentPlatform.selectors; await cdpEval(` const inp=document.querySelector('${s.linkInput}'); if(inp){ inp.focus();inp.value=''; setTimeout(()=>{ __common.setInputValue(inp,'${url.replace(/'/g,"\\'")}'); inp.dispatchEvent(new Event('blur',{bubbles:true})); },100); } `); } async function submit() { const s = currentPlatform.selectors; await cdpEval(`document.querySelector('${s.submitBtn}')?.click();`); } async function getResult() { const s = currentPlatform.selectors; if (s.resultDialog) { return await cdpEval(` const dlg=document.querySelector('${s.resultDialog}'); dlg?__platform.getResult(dlg):'pending'; `); } else { return await cdpEval(` const msg=document.querySelector('${s.resultMessage}'); msg?__platform.getResult(msg):'pending'; `); } } async function closeAll() { await cdpEval(`__platform.closeAll();`); } async function refresh() { await cdpEval(`location.reload();`); } // ===================== 【单订单处理(100%通用)】===================== async function processOrder(order, url, mediaName) { const { idx, title } = order; const dedupKey = `${currentPlatform.name}:${mediaName}:${title}`; if (dedupSet.has(dedupKey)) { writeLog(`[${idx}] 已提交过→跳过`, 'info', mediaName); stats.dedupSkipped++; return true; } for (let r = 0; r <= CONFIG.orderRetry; r++) { if (r > 0) { writeLog(`[${idx}] 🔄 整单重试(第${r}次)`, 'warn', mediaName); stats.orderRetried++; await sleep(CONFIG.waitBase); } await closeAll(); await sleep(CONFIG.waitBase / 2); let pubOk = false; for (let br = 0; br < CONFIG.buttonRetry; br++) { await clickPublish(idx); if (await waitDialog()) { pubOk = true; break; } writeLog(`[${idx}] 🔄 重试点击发布`, 'warn', mediaName); stats.buttonRetried++; await sleep(CONFIG.waitBase); } if (!pubOk) { failedOrders.push({ platform: currentPlatform.name, media: mediaName, title: title, url: url, time: now(), reason: '未弹出发布对话框' }); continue; } await sleep(CONFIG.waitBase); await fillUrl(url); await sleep(CONFIG.waitBase); await submit(); await sleep(CONFIG.waitBase * 1.5); const res = await getResult(); if (res === 'success') { writeLog(`[${idx}] ✅ 提交成功`, 'success', mediaName); stats.success++; dedupSet.add(dedupKey); await closeAll(); return true; } else { writeLog(`[${idx}] ❌ 提交失败`, 'error', mediaName); await closeAll(); } } stats.failed++; failedOrders.push({ platform: currentPlatform.name, media: mediaName, title: title, url: url, time: now(), reason: '提交失败(已重试)' }); return false; } // ===================== 【单媒体处理(100%通用)】===================== async function processMedia(mediaName, crawlUrl) { writeLog(`\n====================================`, 'info'); writeLog(`开始处理媒体:${mediaName}`, 'info'); writeLog(`====================================`, 'info'); try { if (CONFIG.mediaRefresh && stats.processedMedia > 0) { writeLog(`🔄 刷新页面防卡顿`, 'info', mediaName); await refresh(); await sleep(CONFIG.waitBase * 8); await injectHelpers(); } await clickPublishTab(); await sleep(CONFIG.waitBase * 2.5); await setMediaName(mediaName); await sleep(CONFIG.waitBase); await clickSearch(); await sleep(CONFIG.waitBase * 6); const totalPages = await getTotalPages(); writeLog(`📄 总页数:${totalPages}`, 'info', mediaName); if (totalPages === 0) { writeLog(`⚠️ 未找到该媒体的订单`, 'warn', mediaName); return; } await goLastPage(totalPages); await sleep(CONFIG.waitBase * 6); for (let p = totalPages; p >= 1; p--) { writeLog(`\n=== 第${p}页 ==`, 'info', mediaName); const orders = await getOrders(); stats.totalOrders += orders.length; for (let i = orders.length - 1; i >= 0; i--) { const order = orders[i]; if (order.status.includes('已完成')) { writeLog(`[${order.idx}] 已完成→跳过`, 'info', mediaName); stats.skipped++; continue; } const url = matchUrl(order.title); if (!url) { writeLog(`[${order.idx}] 未匹配到文章:${order.title.slice(0,30)}`, 'warn', mediaName); continue; } await processOrder(order, url, mediaName); await sleep(CONFIG.waitBase); } if (p > 1) { writeLog(`📄 切换上一页`, 'info', mediaName); await goPrevPage(); await sleep(CONFIG.waitBase * 6); } } stats.processedMedia++; writeLog(`✅ 媒体处理完成`, 'success', mediaName); } catch(e) { writeLog(`❌ 媒体处理异常:${e.message}`, 'error', mediaName); } } // ===================== 【主流程(100%通用)】===================== async function main() { if (!fs.existsSync(CONFIG.logDir)) fs.mkdirSync(CONFIG.logDir, { recursive: true }); const { platform, medias, crawlUrl } = parseArgs(); currentPlatform = platform; stats.totalMedia = medias.length; writeLog(`🚀 批量任务启动,平台:${currentPlatform.name},共${medias.length}个媒体`, 'info'); writeLog(`📌 匹配阈值:${CONFIG.matchThreshold}% | 基础等待:${CONFIG.waitBase}ms`, 'info'); loadDedup(); await fetchArticles(crawlUrl); if (articles.length === 0) { writeLog(`❌ 未抓取到任何文章,任务终止`, 'error'); process.exit(1); } await connectCDP(); await injectHelpers(); for (let i = 0; i < medias.length; i++) { const media = medias[i]; writeLog(`\n📊 进度:${i+1}/${medias.length}`, 'info'); await processMedia(media, crawlUrl); saveDedup(); } writeLog(`\n====================================`, 'info'); writeLog(`🎉 批量任务全部完成!`, 'success'); writeLog(`====================================`, 'info'); writeLog(`📊 平台:${currentPlatform.name}`); writeLog(`📊 总媒体:${stats.totalMedia} | 已处理:${stats.processedMedia}`); writeLog(`📊 总订单:${stats.totalOrders}`); writeLog(`✅ 成功:${stats.success}`); writeLog(`❌ 失败:${stats.failed}`); writeLog(`⏭️ 跳过:${stats.skipped}`); writeLog(`❓ 不匹配:${stats.noMatch}`); writeLog(`🔄 按钮重试:${stats.buttonRetried} | 整单重试:${stats.orderRetried}`); writeLog(`🔒 去重跳过:${stats.dedupSkipped}`); exportFailed(); saveDedup(); client.close(); } main().catch(e => { writeLog(`💥 全局致命异常:${e.message}`, 'error'); saveDedup(); exportFailed(); try { client?.close(); } catch {} process.exit(1); }); process.on('SIGINT', () => { writeLog(`\n⚠️ 用户中断任务`, 'warn'); saveDedup(); exportFailed(); try { client?.close(); } catch {} process.exit(0); });
【免责声明】
此文为在诸城新闻网出于传播更多信息的转载发布,不代表本文的观点及立场。所涉文、图等资料的一切权力和法律责任归材料提供方所有和承担。文章内容仅供参考,不构成任何购买、投资等建议,据此操作风险自担!如若本文有任何内容侵犯您的权益,请及时联系本站邮箱:1958 11781@qq.com,本站将会在24小时内处理完毕。