Skip to content

Commit

Permalink
refactor: some gramatical enhancements and added new cover image.
Browse files Browse the repository at this point in the history
  • Loading branch information
llorenspujol committed Oct 14, 2023
1 parent ab92669 commit fd914bb
Show file tree
Hide file tree
Showing 2 changed files with 91 additions and 86 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ seo:

El web scraping pot semblar una tasca senzilla, però hi ha molts reptes a superar. En aquest blog, ens endinsarem en com fer "scraping" a LinkedIn per extreure ofertes de feina. Per fer això, utilitzarem [Puppeteer](https://pptr.dev/) i [RxJS](https://rxjs.dev/). L'objectiu és assolir web scraping d'una manera declarativa, modular i escalable.

## Entenent el Web scraping
## Què és el web scraping?

El web scraping és una tècnica d'extracció de dades utilitzada per recopilar informació de llocs web. Consisteix en un procés automatitzat d'obtenció de dades de pàgines web, com ara text, imatges, enllaços i més, per a després emmagatzemar o processar aquestes dades per a diversos propòsits.

Expand Down Expand Up @@ -75,30 +75,32 @@ This translation maintains the meaning of the original English text in Catalan.

Aquesta és la part central d'aquest bloc, on ens submergim en el procés d'accés a les ofertes de treball de LinkedIn, analitzant el contingut HTML i recuperant les dades d'ofertes de feina en format JSON.

### 1- Construint la URL per Navegar per les Ofertes de Treball de LinkedIn
### 1- Construeix l'URL per navegar fins a la pàgina d'ofertes de feina de LinkedIn

Per accedir a les ofertes de treball de LinkedIn, necessitem construir una URL utilitzant la funció `urlQueryPage`:
Per accedir a les ofertes de feina de LinkedIn, necessitem construir una URL utilitzant la funció `urlQueryPage`:

```ts:src/linkedin.ts
export const urlQueryPage = (search: ScraperSearchParams) =>
`https://linkedin.com/jobs-guest/jobs/api/seeMoreJobPostings/search?keywords=${search.searchText}
&start=${search.nPage * 25}${search.locationText ? '&location=' + search.locationText : ''}`
```

Aquesta funció de generació de URL és un pas crucial en el nostre procés, ja que ens permet navegar per les ofertes de treball de LinkedIn amb els criteris de cerca específics definits per `searchText`, `pageNumber`, i opcionalment `locationText`.
En aquest cas, ja he realitzat la investigació prèvia per trobar aquesta URL. El nostre objectiu és trobar una URL que puguem parametritzar amb els nostres paràmetres de cerca desitjats.
Per a aquest exemple, els nostres paràmetres de cerca seran `searchText`, `pageNumber` i opcionalment `locationText`.

Exemples de url poden ser:
1. <a href="https://www.linkedin.com/jobs-guest/jobs/api/seeMoreJobPostings/search?keywords=Angular&start=0" target="_blank">https://www.linkedin.com/jobs-guest/jobs/api/seeMoreJobPostings/search?keywords=Angular&start=0</a>
2. <a href="https://www.linkedin.com/jobs-guest/jobs/api/seeMoreJobPostings/search?keywords=React&location=Barcelona&start=0" target="_blank">https://www.linkedin.com/jobs-guest/jobs/api/seeMoreJobPostings/search?keywords=React&location=Barcelona&start=0</a>
3. <a href="https://www.linkedin.com/jobs-guest/jobs/api/seeMoreJobPostings/search?keywords=python&start=0" target="_blank">https://www.linkedin.com/jobs-guest/jobs/api/seeMoreJobPostings/search?keywords=python&start=0</a>


### 2- Navegant a la URL i Extraient Dades de les Ofertes
Amb la nostra URL objectiu identificada, podem procedir amb les dues accions principals requerides:
### 2- Navega a l'URL i extreu les ofertes de feina

1. Navegar a la URL de les Ofertes de Treball: Aquest pas implica dirigir la nostra eina de "scraping" web a la URL on estan allotjades les ofertes de treball.
Amb la nostra URL identificada, podem procedir amb les dues accions principals requerides:

2. Extreure dades de les ofertes de feina i convertint-les a JSON: Un cop estem a la pàgina d'ofertes de treball, utilitzarem tècniques de "scraping" web per extreure les dades de les ofertes i retornar-les en format JSON.
1. Navegar a la URL on hi ha les Ofertes de Treball: Aquest pas implica dirigir la nostra eina de "scraping" web a la URL on estan les ofertes de treball.

2. Extreure dades de les ofertes de feina i convertint-les a JSON: Un cop estem a la pàgina d'ofertes de feina, utilitzarem tècniques de "scraping" web per extreure les dades de les ofertes i retornar-les en format JSON.

```ts:src/linkedin.ts

Expand Down Expand Up @@ -235,11 +237,11 @@ export function getJobsFromLinkedinPage(page: Page): Observable<JobInterface[]>

```

El codi proporcionat extreu efectivament tota la informació de treball disponible de la pàgina. Encara que el codi no és molt estètic, aconsegueix la feina, que és típic per a codi de "scraping" web.
El codi proporcionat extreu tota la informació de les ofertes de feina de la pàgina. Encara que el codi no és molt estètic, aconsegueix la feina, que és típic per a codi de "scraping" web.

> En un context de programació estàndard, generalment és aconsellable descompondre el codi en funcions més petites i aïllades per millorar la llegibilitat i la mantenibilitat. No obstant això, quan es tracta de codi executat dins de `page.evaluate` en Puppeteer, estem una mica limitats perquè aquest codi s'executa en la instància de Puppeteer (Chrome), no en el nostre entorn Node.js. Per tant, tot el codi ha de ser autocontingut dins de la crida de `page.evaluate`. L'única excepció aquí són les variables (com `stacks` en el nostre cas), que poden passar-se com a arguments a `page.evaluate`, sempre que no continguin funcions o objectes complexos que no es puguin serialitzar.
> En un context de programació normal, generalment és aconsellable descompondre el codi en funcions més petites i aïllades per millorar-ne la llegibilitat i la mantenibilitat. No obstant això, quan es tracta de codi executat dins de `page.evaluate` en Puppeteer, estem una mica limitats perquè aquest codi s'executa en la instància de Puppeteer (Chrome), no en el nostre entorn Node.js. Per tant, tot el codi ha de ser autocontingut dins de la crida de `page.evaluate`. L'única excepció aquí són les variables (com `stacks` en el nostre cas), que poden passar-se com a arguments a `page.evaluate`, sempre que no continguin funcions o objectes complexos que no es puguin serialitzar.
En aquest cas, l'únic component desafiant "per escrapejar"(to scrape) és la informació del salari, ja que implica convertir un format de text com '$65,000.00 - $90,000.00' en valors de salari mínim i màxim separats.
En aquest cas, l'únic component desafiant per passar de HTML text a JSON és la informació del salari, ja que implica convertir un format de text com '$65,000.00 - $90,000.00' en dos valors numèrics de salari mínim i màxim separats.
A més, hem encapsulat tot el codi dins d'un bloc try/catch per gestionar els errors de manera elegant. Encara que actualment registrem els errors a la consola, és aconsellable considerar la implementació d'un mecanisme per emmagatzemar aquests registres d'error en disc. Aquesta pràctica es torna particularment important perquè les pàgines web sovint experimenten canvis, cosa que necessita actualitzacions freqüents al codi de l'anàlisi HTML.


Expand All @@ -249,39 +251,41 @@ Finalment, és important remarcar que sempre utilitzem els operadors `defer` i `
defer(() => fromPromise(myPromise()))
```

Aquesta estratègia és la més recomanada ja que funciona de manera fiable en tots els escenaris. Les Promeses són "eager", mentre que els Observables són "lazy" i només s'inicien quan algú s'hi subscriu. L'operador `defer` ens permet convertir a "lazy" una Promesa.
Aquesta estratègia és la més recomanada ja que funciona de manera fiable en tots els escenaris. Les Promeses són "eager", mentre que els Observables són "lazy" i només s'inicien quan algú s'hi subscriu. L'operador `defer` ens permet convertir a "lazy" una Promesa. Per més informació pots anar a aquest [enllaç](https://stackoverflow.com/questions/39319279/convert-promise-to-observable/69360357#69360357)


### 3- Afegir un Bucle Asíncron per Iterar a través de Totes les Pàgines
### 3- Afegeix un bucle asíncron per iterar a través de totes les pàgines

En el pas anterior, hem après com obtenir totes les dades de les ofertes de treball d'una pàgina de LinkedIn. Ara, el que volem fer és utilitzar aquest codi tantes vegades com sigui possible per recopilar tantes dades com puguem. Per aconseguir-ho, primer necessitem iterar a través de totes les pàgines disponibles:


```ts:src/linkedin.ts
export function getJobsFromPageRecursive(page: Page, searchParams: ScraperSearchParams): Observable<ScraperResult> {
return getJobsFromLinkedinPage(page, searchParams).pipe(
function getJobsFromAllPages(page: Page, initSearchParams: ScraperSearchParams): Observable<ScraperResult> {
const getJobs$ = (searchParams: ScraperSearchParams) => goToLinkedinJobsPageAndExtractJobs(page, searchParams).pipe(
map((jobs): ScraperResult => ({jobs, searchParams} as ScraperResult)),
catchError(error => {
console.error('error', error);
return of({jobs: [], searchParams})
}),
switchMap(({jobs}) => {
console.log(`Linkedin - Query: ${searchParams.searchText}, Location: ${searchParams.locationText}, Page: ${searchParams.nPage}, nJobs: ${jobs.length}, url: ${urlQueryPage(searchParams)}`);
console.error(error);
return of({jobs: [], searchParams: searchParams})
})
);

return getJobs$(initSearchParams).pipe(
expand(({jobs, searchParams}) => {
console.log(`Linkedin - Query: ${searchParams.searchText}, Location: ${searchParams.locationText}, Page: ${searchParams.pageNumber}, nJobs: ${jobs.length}, url: ${urlQueryPage(searchParams)}`);
if (jobs.length === 0) {
return EMPTY;
} else {
return concat(of({jobs, searchParams}), getJobsFromPageRecursive(page, {...searchParams, nPage: searchParams.nPage++}));
return getJobs$({...searchParams, pageNumber: searchParams.pageNumber + 1});
}
})
);
}

```
El codi anterior és un bucle asíncron creat amb recursió.
El codi anterior incrementa el número de pàgina fins que arribem a una pàgina on no hi ha ofertes de feina (que seria l'última pàgina). Per realitzar aquest bucle en RxJS, utilitzem l'operador `expand`, que projecta recursivament cada valor de la font (source) a un Observable que es fusiona en l'Observable de sortida. La seva funcionalitat està ben explicada [aquí](https://ncjamieson.com/understanding-expand/).

> En RxJS, no podem utilitzar un bucle amb la paraula clau `for` com ho fem amb await/async. Hem d'utilitzar un bucle recursiu en el seu lloc. Encara que inicialment pugui semblar una limitació, en un context asíncron, aquest mètode resulta ser més avantatjós en nombroses situacions
Podríem implementar això utilitzant Promises en lloc d'Observables? Absolutament, aquí teniu el codi equivalent escrit amb Promises:
Així doncs, com seria el codi equivalent utilitzant Promeses? Aquí en tenim un exemple:

```typescript
export async function getJobsFromAllPages(page: Page, searchParams: ScraperSearchParams): Promise<ScraperResult> {
Expand Down Expand Up @@ -309,7 +313,7 @@ export async function getJobsFromAllPages(page: Page, searchParams: ScraperSearc
}

```
Aquest codi realitza accions gairebé idèntiques a les que utilitza Observables, però amb una diferència crítica: només emet quan totes les pàgines han acabat el seu processament. En canvi, la implementació que fa ús d'Observables emet després de cada pàgina. Crear un "stream" de dades és vital en aquest cas perquè volem gestionar les ofertes de treball tan aviat com es resolguin.
Aquest codi és gairebé equivalent al basat en Observables, amb una diferència crítica: només emet quan totes les pàgines han acabat de processar. En canvi, la implementació utilitzant Observables emet després de cada pàgina. Crear un "stream" és crucial en aquest cas perquè volem gestionar les ofertes de feina tan aviat siguin resoltes.

Certament, podríem introduir la nostra lògica després de la línia:

Expand All @@ -318,7 +322,7 @@ const jobs = await getJobsFromLinkedinPage(page, searchParams);

/* Handle the jobs here */
```
...però això acoblaria innecessàriament el nostre codi de "scraping" amb la part que gestiona les dades de les ofertes de treball (un cas comú serà guardar les ofertes de treball en una base de dades).
...però això acoblaria innecessàriament el nostre codi de "scraping" amb la part que gestiona les dades de les ofertes de feina. Gestionar les dades de les ofertes pot implicar algunes transformacions, crides a alguna API, i finalment, guardar les dades a una base de dades.

Així doncs, en aquest exemple veiem clarament un dels molts avantatges que els Observables ofereixen respecte a les Promeses.

Expand Down Expand Up @@ -367,25 +371,24 @@ export function getJobsFromLinkedin(browser: Browser): Observable<ScraperResult>

Aquest codi iterarà a través de diversos paràmetres de cerca, un a la vegada, i recuperarà ofertes de feina per a cada combinació de `searchText` i `locationText`.

**🎉 Felicitats! Ara sou capaços de fer "scraping" a LinkedIn i a qualsevol altre pàgia web! 🎉**

Tot i això, hi ha alguns desafiaments a superar per a un "scraping" consistent a LinkedIn.
**🎉 Felicitats! Ara ja sou capaços de fer "scraping" a LinkedIn i a qualsevol altre pàgia web! 🎉**

Tot i això, LinkedIn, igual que moltes altres pàgines web, té tècniques per prevenir el web scraping. Anem a veure com solventar-les 👇.


## Errors Comuns al fer "Scraping" a LinkedIn
## Errors comuns al fer "scraping" a LinkedIn

Si executeu el codi proporcionat, ràpidament us trobareu amb nombrosos errors de LinkedIn, fent difícil fer "scraping" amb èxit d'una quantitat significativa d'informació. Hi ha dos errors comuns que necessitem abordar:
Si executem el codi proporcionat, ràpidament ens trobarem amb nombrosos errors de LinkedIn, fent difícil fer "scraping" amb èxit d'una quantitat significativa d'informació. Hi ha dos errors comuns que necessitem abordar:

#### 1- Resposta "status code" 429
Aquesta resposta pot ocorrer durant el scraping, i significa que esteu fent massa sol·licituds. Si us trobeu amb aquest error, considereu reduir la velocitat en què es duen a terme les peticions fins que desaparegui.
Aquesta resposta pot passar durant el scraping, i significa que estem fent massa sol·licituds en un petit període de temps. Si ens trobem amb aquest error hem de reduir la velocitat en què es duen a terme les peticions fins que desaparegui.

#### 2- Authwall de LinkedIn
De tant en tant, LinkedIn us pot redirigir a un authwall en lloc de la pàgina desitjada. Quan apareix l'authwall, l'única opció és esperar una mica més abans de fer la pròxima sol·licitud.
De tant en tant, LinkedIn ens pot redirigir a un authwall en lloc de la pàgina desitjada. Quan apareix l'authwall, l'única opció és esperar una mica més abans de fer la pròxima sol·licitud.

### Com superar aquests errors de manera efectiva

Aquests errors han de ser gestionats a la funció `getJobsFromLinkedinPage`, ampliarem aquesta funció i separarem el codi de "scraping" html en una altra funció anomenada `getLinkedinJobsFromJobsPage`. El codi és així:
Aquests errors han de ser gestionats a la funció `getJobsFromLinkedinPage`, ampliem aquesta funció i separarem el codi de "scraping" html en una altra funció anomenada `getLinkedinJobsFromJobsPage`. El codi és així:

```ts:src/linkedin.ts
const AUTHWALL_PATH = 'linkedin.com/authwall';
Expand Down Expand Up @@ -448,7 +451,7 @@ function waitForJobSearchCard(page: Page) {
}
```

En aquest codi, abordem els errors esmentats anteriorment, és a dir, l'error de resposta 429 i el problema de l'authwall. Superar aquests errors és crucial per assegurar l'èxit a llarg termini del scraping web de LinkedIn.
En aquest codi, abordem els errors esmentats anteriorment, és a dir, l'error de resposta 429 i el problema de l'authwall. Superar aquests errors és molt important per tenir èxit durant l'scraping web a LinkedIn.

Per gestionar aquests errors, el codi utilitza una estratègia de reintents personalitzada implementada per la funció [`retryStrategyByCondition`](https://github.com/llorenspujol/linkedin-jobs-scraper/blob/12b48449d773800bada82cbbfc09f76af5ac9289/src/scraper.utils.ts#L33):

Expand Down Expand Up @@ -479,14 +482,14 @@ export const retryStrategyByCondition = ({maxRetryAttempts = 3, scalingDuration
};
```

Aquesta estratègia, bàsicament, augmenta el temps entre cada reintent després d'un fracàs, permetent que el codi sigui més resistent quan s'enfronta a aquests errors comuns de LinkedIn. Aquest mecanisme de reintent ajuda a gestionar els desafiaments associats amb el scraping de LinkedIn i millora la fiabilitat global del procés.
Aquesta estratègia augmenta el temps entre cada intent després d'un error. D'aquesta manera, ens assegurem que esperarem prou temps perquè LinkedIn ens permeti fer sol·licituds de nou

> Nota: És important ser conscient que LinkedIn pot posar a la llista negra la nostra adreça IP, i simplement esperar més temps pot no ser una solució efectiva. Per mitigar aquest problema potencial i reduir l'ocurrència d'errors, una pràctica recomanada és implementar una rotació d'IP a intervals regulars. Com a exemple, utilitzar una VPN com a proxy i canviar periòdicament entre diferents ubicacions geogràfiques presenta una solució fàcil i efectiva a aquest repte.
> Nota: És important ser conscient que LinkedIn pot posar a la llista negra la nostra adreça IP, i simplement esperar més temps pot no ser una solució efectiva. Per mitigar aquest problema potencial i reduir els errors, una pràctica recomanada és fer una rotació d'IPs de tant en tant.

## Paraules Finals
## Paraules finals

El web scraping pot violar freqüentment els termes de servei d'un lloc web. Sempre reviseu i respecteu l'arxiu robots.txt d'un lloc web i els seus Termes de Servei. En aquest cas, aquest codi hauria de ser utilitzat NOMÉS amb fins docents i de hobby. LinkedIn prohibeix específicament qualsevol extracció de dades del seu lloc web; podeu llegir més [aquí](https://www.linkedin.com/legal/crawling-terms).
El web scraping pot violar freqüentment els termes de servei d'un lloc web. Sempre reviseu i respecteu l'arxiu robots.txt d'un lloc web i els seus Termes de Servei. En aquest cas, **aquest codi ha de ser utilitzat NOMÉS amb fins docents i de hobby**. LinkedIn prohibeix específicament qualsevol extracció de dades del seu lloc web; podeu llegir més [aquí](https://www.linkedin.com/legal/crawling-terms).

Recomano utilitzar el web scraping per a l'aprenentatge, l'ensenyament i projectes de hobby. Sempre recordeu de respectar el lloc web, eviteu llançar massa sol·licituds i assegureu-vos que les dades s'utilitzen de manera respectuosa.

Expand Down
Loading

0 comments on commit fd914bb

Please sign in to comment.