diff --git a/background.js b/background.js new file mode 100644 index 0000000..16cf195 --- /dev/null +++ b/background.js @@ -0,0 +1,82 @@ +console.log("background run"); +let jobs = {}; + +// Establece una conexion para escuchar los comandos entrantes +chrome.runtime.onConnect.addListener(function (port) { + port.onMessage.addListener(async function (params, sender) { + const [tab] = await chrome.tabs.query({ + active: true, + currentWindow: true, + }); + + //no encontro la tab + if (!tab) { + console.log("error al conseguir la tab"); + return; + } + console.log("tab found"); + + //establece un puerto con el content script + let portContentScript = chrome.tabs.connect(tab.id, { + name: "bg-content_script", + }); + + // si el comando que llega es "start" ... (desde indexjs) + if (params.cmd === "start") { + //envia el comando 'scrap' al contentscript + console.log("envia scrap a contetn"); + portContentScript.postMessage({ cmd: "scrap" }); + } + + // si el comando que esta recibiendo es stop (indexjs) + if (params.cmd === "stop") { + //envia el comando stop al contentscript + portContentScript.postMessage({ cmd: "stop" }); + } + + // si el comando que recibe es saveInfo (contentscript) + if (params.cmd === "saveInfo") { + // guarda la info entrante en la variable global jobs + const { jobsInfo } = params; + + //acomoda el nuevo conjunto de jobsInformation a lo que ya se tiene ? + jobsInfo.forEach((job) => { + const { location, salary } = job; + + // si no existe esa localidad en jobs + if (!jobs[location]) { + //crea la localidad y le asigna un nuevo arreglo con el salario y la cantidad 1 + jobs[location] = [{ salary, count: 1 }]; + } else { + //ya existe esa localidad + let bool = false; + + jobs[location].forEach((u) => { + //si ese salario ya existe + if (u.salary == salary) { + bool = true; + u.count++; //aumenta el contador + } + }); + + //en caso no existia ese salario + if (!bool) { + //pushea el nuevo salario con count en 1 + jobs[location].push({ salary, count: 1 }); + } + } + }); + } + + // si el comando que recbie es saveInLocalStorage ((contentscript)) + if (params.cmd === "saveInLocalStorage") { + //guarda jobs en el localStorage + chrome.storage.local.set({ + jobsAnalysis: JSON.stringify({ data: jobs }), + }); + + //resetea la variable donde estaba guardando los trabajos + jobs = {}; + } + }); +}); diff --git a/contentscript.js b/contentscript.js new file mode 100644 index 0000000..e574af4 --- /dev/null +++ b/contentscript.js @@ -0,0 +1,85 @@ +// nos devuelve un array de objetos con la info importante de cada aviso +function jobCardsInfo() { + //obtiene todos los elementos cuyos id comienzan con "jobcard" + // los recibe como nodos y con el spread operator convertimos el array de Nodes a un array de Elements + const jobCards = [...document.querySelectorAll("div[id*=jobcard]")]; + + // extraemos solo la informacion que queremos + return jobCards.map((card) => { + // Del cardJob extraemos: fecha, titulo del trabajo, salario + const [{ innerText: date }, { innerText: title }, { innerText: salary }] = + card.children[1].children[0].children; // card.children me da 2 elementos, pero la info esta en el 2do. Y de ese 2do elemento necesito su primer hijo. Y finalmente los hijos de este ultimo + + // Del cardJob extramos: localidad + const locations = [...card.querySelectorAll("a[title*=Empleos]")]; + // locations me devuelve un array de 2 elementos. La localidad y el estado + // pero hay cards que no tienen localidad, solamente estado + + // por ello pongo el reverse()[0] + let [{ title: location } = { title: "" }] = + locations.reverse(); + + // si el card no tiene location le asignamos "" y luego al imprimir en el front le pondremos "Sin especificar localidad" + if (location === "") { + return { title, salary, location }; + } + //location = location.slice(11); // le quitamos el "empleos en " + return { title, salary, location: location.slice(11) }; + }); +} + +// establece una conexion con el background +//const buttonNext = () => document.querySelector("li[class*=next]"); // +const element = document.querySelector("div[class*=jobCardContainer]"); +const portBackground = chrome.runtime.connect({ + name: "content_script-background", +}); + +let mutation = null; +chrome.runtime.onConnect.addListener(function (port) { + port.onMessage.addListener(({ cmd }) => { + if (cmd === "scrap") { + console.log("scrapeando"); + mutation = new MutationObserver(() => { + const buttonNext = document.querySelector("li[class*=next]"); + console.log("buttonNext", buttonNext); + if (buttonNext) { + console.log("existe el button next "); + const nextPage = buttonNext.className.includes("disabled"); + + //si es que no tiene la clase disabled + if (nextPage === false) { + const jobsInfo = jobCardsInfo(); + + //envia la info al background para que la vaya sumando + portBackground.postMessage({ + cmd: "saveInfo", + jobsInfo, + }); + buttonNext.click(); + } else { + mutation && mutation.disconnect(); + portBackground.postMessage({ cmd: "saveInLocalStorage" }); + } + } + //alertar que no hubo boton (por implementar) + }); + + mutation.observe(element, { subtree: true, childList: true }); + + const jobsInfo = jobCardsInfo(); + + portBackground.postMessage({ + cmd: "saveInfo", + jobsInfo, + }); + + document.querySelector("li[class*=next]").click(); + } + + if (cmd === "stop") { + mutation && mutation.disconnect(); + portBackground.postMessage({ cmd: "saveInLocalStorage" }); + } + }); +}); diff --git a/images/occ.png b/images/occ.png new file mode 100644 index 0000000..f5ae218 Binary files /dev/null and b/images/occ.png differ diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..4ed731a --- /dev/null +++ b/manifest.json @@ -0,0 +1,23 @@ +{ + "manifest_version": 3, + "name": "jobs-scraping", + "author": "https://github.com/GersoZz", + "version": "1.0.0", + "description": "Scraping Jobs Offers", + "permissions": ["tabs", "storage", "activeTab", "scripting"], + "background": { + "service_worker": "./background.js" + }, + "action": { + "default_popup": "./popup/index.html" + }, + "icons": { + "32": "./images/occ.png" + }, + "content_scripts": [ + { + "matches": ["https://www.occ.com.mx/*"], + "js": ["./contentscript.js"] + } + ] +} diff --git a/popup/index.html b/popup/index.html new file mode 100644 index 0000000..efbb9c9 --- /dev/null +++ b/popup/index.html @@ -0,0 +1,45 @@ + + + + + + + + + + + + Document + + +

Jobs Scraping

+ +
+ + +
+ +
+
+ Loading... +
+
+ +
+ + + + diff --git a/popup/index.js b/popup/index.js new file mode 100644 index 0000000..ecf54e7 --- /dev/null +++ b/popup/index.js @@ -0,0 +1,91 @@ +const btnStartScrap = document.getElementById("btnStartScrap"); +const btnStop = document.getElementById("btnStop"); +const spinner = document.getElementById("spinner"); +const results = document.getElementById("results"); + +const backgroundPort = chrome.runtime.connect({ name: "popup-background" }); + +// funcion para crear elementos con texto dentro +const createElementWithText = (elementType, innerString) => { + const element = document.createElement(elementType); + element.innerText = innerString; + return element; +}; + +// crea una nueva fila en la tabla con el elemento y la tabla que se le pasa +const addRowToTable = (element, tableBody) => { + const tr = document.createElement("tr"); + const td = createElementWithText("td", element.salary.replace("Mensual", "")); // Rango Salarial + const td2 = createElementWithText("td", element.count); // Cantidad de vcantes + + tr.append(td, td2); + return tableBody.append(tr); +}; + +/* crea una tabla con un Head por defecto y con el Body que le pasen */ +const createcontentTable = (tBody) => { + const table = document.createElement("table"); + table.setAttribute("class", "table"); + const tHead = document.createElement("thead"); + const tr = document.createElement("tr"); + tr.append( + createElementWithText("th", "Salario"), + createElementWithText("th", "Vacantes") + ); + + tHead.append(tr); + table.append(tHead, tBody); + return table; +}; + +const printAnalysis = () => { + chrome.storage.local.get("jobsAnalysis", (items) => { + if (typeof items.jobsAnalysis !== "undefined") { + results.innerHTML = ""; //limpia el div de resultados + const jobsObject = JSON.parse(items.jobsAnalysis); //convierte a Json el String guardado + const fragment = document.createDocumentFragment(); // fragment y vamos poniendo todo ahi al final solo habra un porceso para a;adir todo al dom + + // recorre el objeto + for (const key in jobsObject.data) { + const tBody = document.createElement("tbody"); + const localidad = document.createElement("h5"); + + const localidadStr = key === "" ? "Sin especificar localidad" : key; + localidad.innerText = ` ${localidadStr}`; + + jobsObject.data[key].forEach((el) => { + addRowToTable(el, tBody); // crea un elemento fila (con el) en tBody + }); + + const table = createcontentTable(tBody); + fragment.append(localidad, table); + results.appendChild(fragment); + } + } + }); +}; + +btnStartScrap.addEventListener("click", (e) => { + //limpia el div results + results.innerHTML = ``; + // manda el comando start al background + backgroundPort.postMessage({ cmd: "start" }); + + // muestra boton Stop y Spinner + btnStop.classList.remove("d-none"); + spinner.classList.remove("d-none"); + btnStartScrap.classList.add("d-none"); +}); + +chrome.storage.onChanged.addListener((e, a) => { + printAnalysis(); +}); + +printAnalysis(); + +btnStop.onclick = () => { + backgroundPort.postMessage({ cmd: "stop" }); + btnStop.classList.add("d-none"); + spinner.classList.add("d-none"); + btnStartScrap.classList.remove("d-none"); +}; diff --git a/popup/styles.css b/popup/styles.css new file mode 100644 index 0000000..5b643b5 --- /dev/null +++ b/popup/styles.css @@ -0,0 +1,9 @@ +body { + width: 600px; + height: auto; + padding: 20px; +} + +.title { + text-align: center; +}