您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Exporta jogadores no mercado (todas as páginas) em CSV com atributos e metadados. Robusto a temas/idiomas, ignora Forma.
// ==UserScript== // @name MZ - Exportar Transferências (CSV) // @namespace MZTools // @version 0.1.0 // @description Exporta jogadores no mercado (todas as páginas) em CSV com atributos e metadados. Robusto a temas/idiomas, ignora Forma. // @author MZ Tools // @match *://*.managerzone.com/* // @icon https://www.managerzone.com/favicon.ico // @run-at document-idle // @grant none // @license MIT // ==/UserScript== (function () { 'use strict'; /** ========================== * CONFIG / LABELS / HELPERS * ========================== */ // Mapeamento de labels dos atributos → chave no CSV // Aceita variações em PT/EN/ES e ignora maiúsc/minúsc. const ATTR_LABELS = { // Físicos / mentais (ajuste se seu idioma estiver diferente) 'velocidade': 'Velocidade', 'speed': 'Velocidade', 'resist': 'Resistência', 'stamina': 'Resistência', 'intelig': 'Inteligência', 'intelligence': 'Inteligência', 'passe': 'Passe', 'passing': 'Passe', 'chute': 'Chute', 'shooting': 'Chute', 'cabece': 'Cabeceio', 'heading': 'Cabeceio', 'controle': 'Controle de Bola', 'ball control': 'Controle de Bola', 'marcação': 'Marcação', 'marking': 'Marcação', 'posicion': 'Posicionamento', 'positioning': 'Posicionamento', 'tackle': 'Desarme', 'desarme': 'Desarme', 'corrida com a bola': 'Corrida com Bola', 'dribbling': 'Corrida com Bola', 'finalização': 'Finalização', 'finishing': 'Finalização', 'cruzamento': 'Cruzamento', 'crossing': 'Cruzamento', 'experi': 'Experiência', 'experience': 'Experiência', // goleiro (se aparecer nos cards) 'goleiro': 'Goleiro', 'keeper': 'Goleiro' }; // Labels para estrelas (alto1, baixo2) e velocidade de treino const STAR_LABELS = { // “alto1” e “baixo2” são nomes de categorias no seu fluxo; aqui salvamos como colunas alto1: ['alto', 'high', 'alto1'], baixo2: ['baixo', 'low', 'baixo2'], treino: ['velocidade de treino', 'training speed', 'velocidad de entrenamiento'] }; // Funções auxiliares const $ = (sel, root = document) => root.querySelector(sel); const $$ = (sel, root = document) => Array.from(root.querySelectorAll(sel)); const normText = (t) => (t || '').replace(/\s+/g, ' ').trim(); const onlyDigits = (t) => (t || '').replace(/[^\d]/g, ''); const toNumber = (t) => { if (t == null) return null; const n = String(t).replace(/[^\d.,-]/g, '').replace(/\./g, '').replace(',', '.'); const v = parseFloat(n); return Number.isFinite(v) ? v : null; }; // CSV safe const csvCell = (v) => { if (v == null) return ''; const s = String(v); return /[;"\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s; }; // Tenta ler número de "bolas" (total de pontos/níveis) exibidos no card function parseTotalBolas(container) { // Heurística 1: badges/bolinhas como <span class="ball ..."> repetidas const balls = $$('.ball', container); if (balls.length) return balls.length; // Heurística 2: texto tipo "Total de bolas: 123" const byText = $$('.player-attr, .small, .tiny, .text-muted', container) .map(el => normText(el.textContent)) .find(t => /total.*bolas|balls total|total.*pontos/i.test(t)); if (byText) { const n = toNumber(onlyDigits(byText)); if (n) return n; } return ''; } // Conta estrelas num bloco (checa <i class="fa-star">, <img alt="*">, etc.) function countStarsIn(node) { if (!node) return ''; const starLike = $$('i[class*="star"], svg[class*="star"], img[alt*="star"], img[src*="star"]', node); if (starLike.length) { // Se houver half-stars, ajuste aqui (ex.: class*="half"); por ora, contamos inteiras. return starLike.length; } // Fallback: procura “★” const txt = normText(node.textContent); const stars = (txt.match(/★/g) || []).length; return stars || ''; } // Encontra um bloco próximo cujo label contem determinado termo function findBlockByLabel(root, labelCandidates) { const nodes = $$('*', root).filter(el => { const t = normText(el.textContent).toLowerCase(); return labelCandidates.some(lab => t.includes(lab)); }); return nodes[0] || null; } // Lê “alto/baixo” determinante a partir de rótulos contendo (1) ou (2) function parseAltoBaixoDeterminate(attrList) { // attrList: array de {label, value, rawLabel} // Regra: se o label contém "1", determinante = 'alto1'; se contém "2", 'baixo2'. // Salva também o NOME do atributo que carregou o 1 ou 2. let det = ''; let atributo = ''; for (const row of attrList) { const raw = (row.rawLabel || '').toLowerCase(); if (/\b1\b|\(1\)/.test(raw)) { det = '1'; atributo = row.label; break; } if (/\b2\b|\(2\)/.test(raw)) { det = '2'; atributo = row.label; break; } } return { alto_baixo: det, atributo_determinante: atributo }; } // Extrai ID do jogador a partir de links function extractPlayerId(container) { const a = $('a[href*="player_id="]', container) || $('a[href*="players&"]', container); if (!a) return ''; const m = a.href.match(/player_id=(\d+)/); return m ? m[1] : ''; } // Extrai nome (a partir do link do jogador ou título do card) function extractName(container) { const a = $('a[href*="player_id="]', container); if (a && a.textContent) return normText(a.textContent); // fallback: o primeiro H-tag com texto const h = $('h3, h4, .player-name, .name', container); return h ? normText(h.textContent) : ''; } // Extrai idade e temporada de nascimento a partir de textos frequentes no card function extractAgeAndSeason(container) { const textNodes = $$('.small, .tiny, .text-muted, .player-meta, .details', container) .map(el => normText(el.textContent)); let idade = ''; let temporada = ''; for (const t of textNodes) { // Exemplos: "Idade: 23 (Nasc.: temp 95)", "Age: 23 (Born: season 95)" const mAge = t.match(/idade[:\s]*?(\d{1,2})|age[:\s]*?(\d{1,2})/i); if (mAge) idade = mAge[1] || mAge[2] || idade; const mSeason = t.match(/temp(?:orad[ao])?[:\s]*?(\d{2,3})|season[:\s]*?(\d{2,3})/i); if (mSeason) temporada = mSeason[1] || mSeason[2] || temporada; } return { idade, temporada_nascimento: temporada }; } // Extrai prazo final (deadline) e valor solicitado function extractDeadlineAndPrice(container) { const txts = $$('*', container).map(el => normText(el.textContent)); // Prazo final const tPrazo = txts.find(t => /prazo|deadline|fecha.*em|ends/i.test(t)) || ''; // ex.: "Prazo final: 2025-09-30 18:00", "Deadline: in 3h 14m", etc. let prazo_final = ''; const mDate = tPrazo.match(/\b(\d{4}-\d{2}-\d{2}.*?)$/) || tPrazo.match(/(\d{1,2}[:h]\d{2}.*)/); if (mDate) prazo_final = mDate[1]; // Valor solicitado const tPreco = txts.find(t => /valor|preço|asking|ask/i.test(t)) || ''; // números com milhar/ponto e decimal/vírgula const mMoney = tPreco.match(/([\d\.\,]+)/); const valor_solicitado = mMoney ? mMoney[1] : ''; return { prazo_final, valor_solicitado }; } // Extrai tabela/lista de atributos do card function extractAttributes(container) { const rows = []; // Pega linhas que parecem "Label: valor" ou estão lado a lado const possibleRows = $$('li, .row, .attr, .attribute, tr', container); for (const r of possibleRows) { const t = normText(r.textContent).toLowerCase(); // encontre um label conhecido const key = Object.keys(ATTR_LABELS).find(k => t.includes(k)); if (!key) continue; // label “cru” para detecção de (1)/(2) const rawLabelNode = $('*', r) || r; const rawLabel = normText(rawLabelNode.textContent); // tenta achar número do atributo no mesmo bloco let val = ''; // 1) números explícitos const m = rawLabel.match(/(\d{1,2})\s*$/) || normText(r.textContent).match(/(\d{1,2})\s*$/); if (m) val = m[1]; // 2) bolinhas (nível visual) if (!val) { const dots = $$('i[class*="dot"], span[class*="dot"], .ball', r).length; if (dots) val = String(dots); } rows.push({ label: ATTR_LABELS[key], value: val, rawLabel: rawLabel }); } // Consolida num objeto {Atributo: valor} const out = {}; for (const row of rows) { out[row.label] = row.value; } // Determina alto/baixo e atributo determinante const det = parseAltoBaixoDeterminate(rows); return { attrs: out, ...det }; } // Extrai estrelas para alto1, baixo2, velocidade de treino function extractStars(container) { const findAndCount = (labels) => { const node = findBlockByLabel(container, labels); return countStarsIn(node); }; return { estrelas_alto1: findAndCount(STAR_LABELS.alto1.map(s => s.toLowerCase())), estrelas_baixo2: findAndCount(STAR_LABELS.baixo2.map(s => s.toLowerCase())), estrelas_vel_treino: findAndCount(STAR_LABELS.treino.map(s => s.toLowerCase())) }; } // Seleciona “cards/linhas” de jogadores nos resultados function selectPlayerCards() { // Heurísticas comuns: // - tabela: linhas com link ?player_id=... // - cards: divs com link do jogador const byTable = $$('tr').filter(tr => $('a[href*="player_id="]', tr)); if (byTable.length) return byTable; const byCard = $$('div, li').filter(div => $('a[href*="player_id="]', div)); return byCard; } // Extrai 1 jogador a partir de um card/linha function scrapeOne(container) { const id = extractPlayerId(container); const nome = extractName(container); const { idade, temporada_nascimento } = extractAgeAndSeason(container); const total_bolas = parseTotalBolas(container); const { prazo_final, valor_solicitado } = extractDeadlineAndPrice(container); const { attrs, alto_baixo, atributo_determinante } = extractAttributes(container); const { estrelas_alto1, estrelas_baixo2, estrelas_vel_treino } = extractStars(container); return { ID: id, Nome: nome, Idade: idade, 'Temporada Nascimento': temporada_nascimento, 'Total de Bolas': total_bolas, 'Prazo Final': prazo_final, 'Valor Solicitado': valor_solicitado, // Atributos (ignorando Forma) Velocidade: attrs['Velocidade'] || '', 'Resistência': attrs['Resistência'] || '', Inteligência: attrs['Inteligência'] || '', Passe: attrs['Passe'] || '', Chute: attrs['Chute'] || '', Cabeceio: attrs['Cabeceio'] || '', 'Controle de Bola': attrs['Controle de Bola'] || '', Marcação: attrs['Marcação'] || '', Posicionamento: attrs['Posicionamento'] || '', Desarme: attrs['Desarme'] || '', 'Corrida com Bola': attrs['Corrida com Bola'] || '', Finalização: attrs['Finalização'] || '', Cruzamento: attrs['Cruzamento'] || '', Experiência: attrs['Experiência'] || '', // Estrelas 'Estrelas Alto1': estrelas_alto1, 'Estrelas Baixo2': estrelas_baixo2, 'Velocidade de Treinamento (★)': estrelas_vel_treino, // Determinante alto/baixo 'Alto/Baixo (1/2)': alto_baixo, 'Atributo Determinante': atributo_determinante }; } // Constrói CSV com separador ';' function toCSV(rows) { const headers = [ 'ID','Nome','Idade','Temporada Nascimento','Total de Bolas','Prazo Final','Valor Solicitado', 'Velocidade','Resistência','Inteligência','Passe','Chute','Cabeceio','Controle de Bola','Marcação', 'Posicionamento','Desarme','Corrida com Bola','Finalização','Cruzamento','Experiência', 'Estrelas Alto1','Estrelas Baixo2','Velocidade de Treinamento (★)', 'Alto/Baixo (1/2)','Atributo Determinante' ]; const lines = [ headers.map(csvCell).join(';'), ...rows.map(r => headers.map(h => csvCell(r[h])).join(';')) ]; return lines.join('\n'); } // Dispara download function download(name, text) { const blob = new Blob([text], { type: 'text/csv;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = name; document.body.appendChild(a); a.click(); setTimeout(() => { URL.revokeObjectURL(url); a.remove(); }, 250); } /** ========================== * SCRAPER (PÁGINA ATUAL) * ========================== */ function scrapeCurrentPage() { const cards = selectPlayerCards(); const rows = []; for (const c of cards) { try { const row = scrapeOne(c); if (row.ID || row.Nome) rows.push(row); } catch (e) { // silencioso, segue o baile } } return rows; } /** ========================== * PAGINAÇÃO (TODAS AS PÁGINAS) * ========================== * * Estratégia: usamos ajaxSearchOffset se existir; caso não exista no escopo, * simulamos cliques nos links de paginação que tenham offset. */ async function scrapeAllPages() { const aggregate = []; const seen = new Set(); // Se a função global existir, iteramos offsets razoáveis (até 1000 registros) const ajaxFn = window.ajaxSearchOffset; if (typeof ajaxFn === 'function') { // Detecta quantos offsets existem na paginação atual (lê links) const getOffsetsFromDOM = () => $$('a[href*="offset="], a.page-link, .pagination a') .map(a => { const m = (a.getAttribute('onclick') || '').match(/ajaxSearchOffset\((\d+)\)/) || (a.href || '').match(/offset=(\d+)/); return m ? parseInt(m[1], 10) : null; }) .filter(v => Number.isInteger(v)); const offsets = Array.from(new Set([0, ...getOffsetsFromDOM()])).sort((a,b)=>a-b); for (const off of offsets) { await goOffset(off); await waitForResultsMutation(); const pageRows = scrapeCurrentPage(); for (const r of pageRows) { const key = r.ID || r.Nome + '|' + r['Prazo Final'] + '|' + r['Valor Solicitado']; if (!seen.has(key)) { seen.add(key); aggregate.push(r); } } } return aggregate; } // Fallback: sem ajaxSearchOffset — tentamos clicar nos links de paginação const pagerLinks = $$('a[href*="offset="], a.page-link, .pagination a'); if (!pagerLinks.length) { // Sem paginador → só atual return scrapeCurrentPage(); } // Primeira página aggregate.push(...scrapeCurrentPage()); // Demais páginas (abrindo em AJAX se for SPA, ou navegando) for (const a of pagerLinks) { if (a.click) a.click(); await waitForResultsMutation(); aggregate.push(...scrapeCurrentPage()); } return dedupe(aggregate); function dedupe(arr) { const out = []; const s = new Set(); for (const r of arr) { const key = r.ID || r.Nome + '|' + r['Prazo Final'] + '|' + r['Valor Solicitado']; if (!s.has(key)) { s.add(key); out.push(r); } } return out; } function waitForResultsMutation() { return new Promise(resolve => { const container = document.body; const obs = new MutationObserver((muts) => { // heurística: quando houver muitos links de players, deve ter carregado const count = $$('a[href*="player_id="]').length; if (count > 0) { obs.disconnect(); setTimeout(resolve, 100); // pequeno buffer } }); obs.observe(container, { childList: true, subtree: true }); // timeout de segurança setTimeout(() => { obs.disconnect(); resolve(); }, 4000); }); } async function goOffset(n) { try { const ret = ajaxFn(n); // Se ajaxFn não retornar Promise, aguarda um pequeno tempo if (ret && typeof ret.then === 'function') await ret; await new Promise(r => setTimeout(r, 200)); } catch (e) { await new Promise(r => setTimeout(r, 300)); } } } /** ========================== * UI (PAINEL FLUTUANTE) * ========================== */ function injectPanel() { if ($('#mz-export-panel')) return; const panel = document.createElement('div'); panel.id = 'mz-export-panel'; panel.innerHTML = ` <div style=" position: fixed; right: 14px; bottom: 14px; z-index: 99999; background: rgba(15, 17, 23, 0.9); color: #fff; padding: 12px 14px; border-radius: 12px; box-shadow: 0 6px 18px rgba(0,0,0,.25); font: 13px/1.3 system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, sans-serif;"> <div style="font-weight:600;margin-bottom:8px;">MZ • Exportar Transferências</div> <div style="display:flex; gap:6px; flex-wrap:wrap; margin-bottom:8px;"> <button id="mz-btn-page" style="padding:6px 10px;border-radius:8px;border:none;cursor:pointer;">Coletar (página)</button> <button id="mz-btn-all" style="padding:6px 10px;border-radius:8px;border:none;cursor:pointer;">Coletar (todas páginas)</button> <button id="mz-btn-dl" style="padding:6px 10px;border-radius:8px;border:none;cursor:pointer;" disabled>Baixar CSV</button> </div> <div id="mz-status" style="opacity:.85">Aguardando…</div> </div> `; document.body.appendChild(panel); let buffer = []; $('#mz-btn-page').addEventListener('click', () => { try { const rows = scrapeCurrentPage(); buffer = rows; $('#mz-status').textContent = `Coletados (página): ${rows.length}`; $('#mz-btn-dl').disabled = rows.length === 0; } catch (e) { $('#mz-status').textContent = `Erro: ${e.message || e}`; } }); $('#mz-btn-all').addEventListener('click', async () => { $('#mz-status').textContent = 'Coletando todas as páginas…'; try { buffer = await scrapeAllPages(); $('#mz-status').textContent = `Coletados (todas páginas): ${buffer.length}`; $('#mz-btn-dl').disabled = buffer.length === 0; } catch (e) { $('#mz-status').textContent = `Erro: ${e.message || e}`; } }); $('#mz-btn-dl').addEventListener('click', () => { if (!buffer.length) return; const csv = toCSV(buffer); const stamp = new Date().toISOString().slice(0,19).replace(/[:T]/g,'-'); download(`mz_transferencias_${stamp}.csv`, csv); }); } /** ========================== * BOOT * ========================== */ function isTransferPage() { const href = location.href; return /[?&]p=transfer(&|$)/i.test(href) || /\/transfer/i.test(href); } function boot() { if (!isTransferPage()) return; injectPanel(); } // Espera DOM if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', boot); } else { boot(); } })();