{Linkedin} Faire du Publipostage à l’aide de la fonction « Recherche »

Bonjour à tous !

Aujourd'hui j'ai le cœur léger, j'ai enfin mis fin à 14 mois de pénibles échanges par avocat interposé avec mes ex-associé ! C'est terminé, j'ai enfin la tête hors de l'eau et je vais pouvoir avancer !
Je n'ai contractuellement pas le droit de vous raconter les détails trépidant de cette aventure, ni même de citer le nom de la société en cause, mais je vous le dis : c'est fini !

Aujourd'hui je vais pouvoir me concentrer sur l'avenir ! Notamment à l'aide de ma VAE qui me validera officiellement un niveau Bac + 4, grâce à 8 années d'expériences très concentrés tant sur des projets professionnels que personnels !

J'espère que vous appréciez un peu le nouveau format de mes articles et que l'ajout de vidéo de démonstration vous plait ! J'ai eu l'occasion de voir quelques personnes s'abonner à la chaîne et ça ... c'est super chouette !

Je me tâte à ouvrir une boite postale pour les lecteurs parmi vous qui souhaiterais m'envoyer des bricoles pour l'écriture de mes articles ! Qu'en pensez-vous ? Qu'auriez vous envie de m'envoyer ?!?
J'avais prévu de faire un appels au don fin 2018, mais j'ai été assez pris et finalement j'ai laissé tomber ...

 

Linkedin, nous voilà !

C'est donc là le sujet de l'article si vous avez bien lu le titre ! Il ne faudrait pas traîner trop sur l'introduction ^^
Alors voilà, j'étais entrain d’errer dans le couloir des idées d'articles foireuses .... lorsque soudain la lumière c'est allumé !
Une personne vient me voir et me demande de lui construire un outil qui permettrait ... d'extraire des données Linkedin ... pour ceux qui connaissent le sujet, il s'agit d'écrire un crawler !
L'objectif est bien entendu de réaliser une opération de publipostage !

Ni une ni deux, j'attaque donc l'écriture d'un script !

 

La recherche Linkedin

Linkedin, charge progressivement le contenu de ses pages, et met en place des protections assez sympa pour lutter contre les crawlers !
Il faudra donc réaliser un script qui simule un comportement humain, ni trop rapide, ni trop lent ...
Qui génère des défilements de pages, pour charger le contenu, qui clic sur les boutons suivants, précédents, ...

Néanmoins, comme vous le démontre la capture d'écran ci-dessus ... Linkedin dispose du nombre de page que vous consultez ... par heure / par jour .. ou par mois, il ne faut donc pas être trop gourmand ^^
Pour obtenir la capture ci-dessus, je vous avoue que j'ai pas mal forcé, il s'agit là de plusieurs centaines de profils que mon script à consulter lors de mes différents tests !

 

Mon script va donc parcourir chaque page et extraire les liens vers les différents profils.

Pour chaque profil, je vais démarrer un traitement différé, celui-ci va donc parcourir des profils en arrière plan, pendant qu'il extrait les liens vers les profils.

 

Les profils Linkedin - #Partie Technique

C'est là que j'ai eu pas mal de fil à retordre ...
Si vous êtes un peu curieux, vous pourrez regarder le code source de la page d'un profil Linkedin !
Toutes les données sont là ... mais dans n'importe quel ordre dans des objets qui font références à d'autres objets, avec des identifiants d'objets, de position dans la page, des types de contenus ....

Les données sont exposées entre balise <code></code>, et sont au format JSON, vous pourrez donc faire une récupération de ce qu'il y a entre toutes les balises <code> et le parser, vous obtiendrez pour chacun, un objet ... toutes les données que vous cherchez se trouvent dans la propriété "included" sous la forme d'un tableau d'objets ...

En parcourant tous les objets contenus dans le tableau "included", j'ai constaté que les données qui m’intéressaient "firstName", "lastName", "locationName"  ('Nom',Prénom', 'localisation') , possédait une propriétés $type dont la valeur se trouvait être l'une de celle-ci :

  • com.linkedin.voyager.identity.profile.Profile
  • com.linkedin.voyager.identity.profile.Position
  • com.linkedin.voyager.identity.shared.MiniProfile

Lorsqu'il s'agit du nom et du prénom, il faut juste ignorer les données qui contiennent les termes suivant :

  • Actu
  • Récap
  • Linkedin
  • Linkedin Récap

 

Quant à l'entreprise ... il s'agit simplement de récupérer tous les objets qui contiennent la propriété "companyName" (nom de l'entreprise) en faisant un tri à l'aide des dates "startDate" "endDate" contenu dans l'objet à travers la propriété "timePeriod".
Me concernant, j'ai simplement récupérer les dates les plus récentes, et lorsque je n'avais pas de date de fin, j'y mettait 2042 ;)

 

Script de téléchargement des résultats d'une recherche Linkedin

 

Pour utiliser le script suivant, il vous faudra réaliser les opérations suivantes dans cet ordre :

  1. Faites une recherche Linkedin
  2. Ouvrez la console développeur à l'aide de la touche F12 ou par "clic droit" > "inspecter"
  3. Copiez-collez le code ci-dessous
  4. Validez avec la touche "entrée"
  5. Patientez !

 

/*
		(c) Dyrk - 2019 / 2020
		Linkedin Search Extractor 
*/

var results = [], d = document, h = d.getElementsByTagName('html')[0], 
	AllowedType  = ['com.linkedin.voyager.identity.profile.Profile','com.linkedin.voyager.identity.profile.Position', 'com.linkedin.voyager.identity.shared.MiniProfile'],
	endTreatments = function(){
		/*
			Génére un fichier CSV  à partir des données stockées dans la variables  "results"
		*/
		setTimeout(function(){
            var doc = d.createElement('a'), txt = "";
           results.map(profil => {
				if (!profil.firstName) return -1;
				['firstName','lastName','title','locationName','companyName','startDate','endDate'].map(item=> (txt+= profil[item] +';'));
                txt+="\n";
            });
            doc.setAttribute('TARGET','BLANK');
            doc.setAttribute('download','extract.csv');
            doc.href = 'data:text/csv,'+encodeURIComponent(txt)
            doc.click();
        }, 10000);
		return console.log('terminé - Enregistrement en cours');
}, scrollBottom = function(x){
	/*
		La fonction scrollBottom s'appelle recursivement
		mais temporise chacune de ses recursions
		lorsqu'elle effectué x scroll, elle démarre le traitement collectResult
	*/
	h.scrollTop =  h.scrollTop + 250;
	setTimeout(x <= 0 ? collectResult : scrollBottom.bind(null, --x), 1000);
}, collectProfilInfo = function(url){
	/*
		Effectue une requête ajax sur le profil, et collecte les différentes infos 
		Nom, Prénom, localisation, dernière entreprise, ... 
	*/
	profil  = new XMLHttpRequest();
profil.addEventListener('load', (function(url,e){
	var profil = {}, dom = new DOMParser(), test = false;
    doc = dom.parseFromString(e.target.response, 'text/html');
    datas = doc.getElementsByTagName('code');
	for (var i in datas){
		if (typeof datas[i]!= 'object') continue;
		if (datas[i].textContent.indexOf('firstName') ==-1  && datas[i].textContent.indexOf('lastName') ==-1) continue;
		data = JSON.parse(datas[i].textContent).included;
		data.map((el) => ['firstName','lastName', 'locationName', 'companyName'].map(key=>{
			if (el[key]  && AllowedType.indexOf(el['$type']) != -1 && 
				['actu','récap', 'linkedin','linkedin récap'].indexOf(el[key].toLowerCase())==-1){
					if (['firstName','lastName'].indexOf(key) !=-1 && el['publicIdentifier'] != /.*\/(.*?)\/$/.exec(url)[1])
						 return el;
					profil[key] = el[key];
            	}
			if (el[key] &&  key == 'companyName'){ 
				el.timePeriod.endDate = typeof el.timePeriod.endDate != "undefined" ? el.timePeriod.endDate : {"year":2042,"month":12}
				el.timePeriod.startDate.month = typeof el.timePeriod.startDate.month != "undefined"  ? el.timePeriod.startDate.month : 01;
				startDate = el.timePeriod.startDate.year + (el.timePeriod.startDate.month > 9 ? el.timePeriod.startDate.month : '0'+el.timePeriod.startDate.month);
				endDate 	= el.timePeriod.endDate.year +""+ (el.timePeriod.endDate.month > 9 ? el.timePeriod.endDate.month  : '0'+el.timePeriod.endDate.month);
				if (!profil.endDate || parseInt(profil.endDate) < parseInt(endDate)){
					profil[key] = el[key];
					profil.startDate = startDate;
					profil.endDate   = endDate;
					profil.title	 = el.title
				}
			}
			return el;
        }));
	};
	results.push(profil);
	console.log(profil);
	}).bind(null, url));
	profil.open('GET', url);
	profil.send(null);
}, collectResult = function(){
	/*
		"collectResult" permet de récupérer les liens de chacun des profils d'une page de recherche
		Pour chaque lien récolté il démarre un traitement différé de collecte des infos du profil "collectProfilInfo"
		Le traitement s'arrête sur la dernière page, et appelle "endTreatments" qui générera un fichier csv
	
	*/
	 var lst = d.getElementsByClassName('search-result__wrapper'), urlProfil;
    for (var i in lst){
        if (typeof lst[i] != 'object' || lst[i].getAttribute('done')) continue;
		try{
			urlProfil = lst[i].getElementsByTagName('a')[0].getAttribute('href');
			if (urlProfil != '#') setTimeout(collectProfilInfo.bind(null, urlProfil), 1000);
        	lst[i].setAttribute('done','done');
        } catch (e){};
    }
	bt = d.getElementsByClassName('artdeco-pagination__button--next')[0];
	try {
		listPage = d.getElementsByClassName('artdeco-pagination__indicator artdeco-pagination__indicator--number');
		lastPage = listPage[listPage.length-1].textContent.trim();
		currPage = /page=([0-9]*)/.exec(d.location);
		if ((currPage ? currPage[1] : 1)== lastPage) return endTreatments();
		bt.click();
    } catch (e){
		console.log("Error ",e);
		 return endTreatments();
    }
	setTimeout(scrollBottom.bind(null, 4), 1000);
	
};
/* 
Linkedin charge les pages progressivement
Il faut donc scroller jusqu'en bas pour obtenir la page entière.
scrollBottom va générer x scroll
*/
scrollBottom(4);

 

 

Vidéo de démonstration

Pensez à prendre 5 minutes pour vous abonnez si vous appréciez le fait que j'ajoute du contenu vidéo.
Je ne touche pour l'instant pas d'argent avec Youtube, mais peut-être un jour cela me permettra d'en récupérer quelques revenus pour investir dans du matériels pour mes articles ;)

 

 

 

Deviner des adresses emails

Souvent les adresses mails d'une entreprise sont formatée selon une certaine norme.
Si je m'appel Dave Hill et que je bosse pour l'entreprise bidule, on peut imaginer :

  • d.hill@bidule.com
  • dave.hill@bidule.com
  • dh@bidule.com
  • dave-hill@bidule.com
  • dave.h@bidule.com
  • ....

Quelques recherches sur Internet, vous permettrons de trouver une ou deux adresses mails de l'entreprise dont vous pourrez vous inspirer pour deviner le bon formatage d'adresse email.

 

Législation

Bien que le challenge de cet article soit divertissant, il est toutefois à noter qu'il est interdit de faire du crawling à des fins commerciales sans le consentement de la personne que vous allez démarcher.*

 

Conclusion

Linkedin, freine assez bien l'utilisation de bot, cet effort est à poursuivre, mais vos données sont accessibles publiquement, et n'importe qui avec un peu (beaucoup) d'analyse ... pourra récupérer vos données et les utiliser à des fins commerciales.
Souvent lorsque l'on me démarche, et que je ne vois pas la trame de fond Linkedin, je prend le temps de répondre à la personne en l'interrogeant sur la manière dont il s'est procuré mon contact !

D'ailleurs, petite technique pour savoir si un chasseur de tête vous a repéré par le bouche à oreille ou par Linkedin ... mettez un faux nom / prénom sur Linkedin, et sur tous les sites professionnels.

 

Bonne journée à tous !

 

Partagez ce contenu

Laisser une réponse

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *