Chapitre 2 Les données
Avant tout, il nous faut des données. Alors, dans le cadre d’un site web, les premières données
faciles à récupérer sont les URL
de pages. Pour cette étape, je me sers de screaming frog
8,
un crawler pour faire l’analyse SEO d’un site web. Je récupère les URL
mais aussi d’autres infos comme les
titres, etc.
2.1 Les packages
Nous aurons besoin d’un certains nombres de packages pour les différentes étapes de ce projet.
library(readxl)
library(tidyverse)
library(rvest)
library(janitor)
library(tidytext)
library(proustr)
library(stopwords)
library(glue)
library(lubridate)
library(wordcloud2)
library(ggraph)
library(tidygraph)
library(hrbrthemes)
library(kableExtra)
library(formattable)
library(ggrepel)
library(googleLanguageR)
2.2 1er nettoyage
On utilise la fonction read_excel()
pour lire le fichier et on sélectionne les variables qui nous
intéressent. Je garde spécifiquement les titres et les url sous forme encodée (important pour le
scraping plus loin), c’est-à-dire avec un encodage des caractères spéciaux comme les accents, etc.
crawl <- read_excel("all_URL.xlsx", skip = 1) %>%
select("Title 1", "URL Encoded Address") %>%
clean_names() %>%
rename("title" = title_1, url = url_encoded_address)
Petite subtilité, nous avons des URL en http
et https
. Après analyse, nous pouvons conserver
que les adresses en https. Nous avons aussi des URL avec un même titre pour plusieurs pages pour
une même contenu (elle se termine avec … | Page 4, … | Page 5, etc.) Nous les filtrons aussi.
title | url |
---|---|
adada | Tout sur la communication et la publicité au Luxembourg | Page 8 | https://www.adada.lu/page/8/ |
adada | Tout sur la communication et la publicité au Luxembourg | Page 7 | https://www.adada.lu/page/7/ |
adada | Tout sur la communication et la publicité au Luxembourg | Page 6 | https://www.adada.lu/page/6/ |
adada | Tout sur la communication et la publicité au Luxembourg | Page 5 | https://www.adada.lu/page/5/ |
adada | Tout sur la communication et la publicité au Luxembourg | Page 4 | https://www.adada.lu/page/4/ |
Ajoutons un ID pour chaque rang.
crawl2 <- crawl %>%
filter(str_detect(url, "https")) %>%
filter(!str_detect(title, "\\|\\sP")) %>%
rowid_to_column() #<- set id to the rows
Nous obtenons donc 1942 titres et URL.
2.3 Homogénéisation
Le premier problème rencontré avec les titres est l’homogénéité des mentions et références. Par
exemple, l’Agence VOUS
est parfois citée sous VOUS
, parfois agence VOUS
. Pareil pour IDP
(ID + P ou Ierace | Dechmann et Partners). Idem pour les clients
(BGL = BGL BNP, P&T = Post) et
j’en passe. Première étape, homogénéiser les noms et s’assurer que les noms composés restent ensemble
lors de la tokenisation9 que nous
verrons plus loin.
crawl3 <- crawl2 %>%
mutate_if(is.character, tolower) %>%
mutate(title = str_replace(title, pattern = "\\|[:space:]page[:space:].{1,2}", replacement = "")) %>%
mutate(title = str_replace_all(title, "\\([:digit:]\\)", "")) %>%
mutate(title = str_trim(title)) %>%
mutate(title = str_replace_all(title, "id\\+p", "idp")) %>%
mutate(title = str_replace_all(title, "ierace \\| dechmann \\+ partners", "idp")) %>%
mutate(title = str_replace_all(title, "ierace dechmann + partners", "idp")) %>%
mutate(title = str_replace_all(title, "agence vous", "agence_vous")) %>%
mutate(title = str_replace_all(title, "\\(l\\’agence\\) vous", "agence_vous")) %>%
mutate(title = str_replace_all(title, "concept factory", "concept_factory")) %>%
mutate(title = str_replace_all(title, "plan k", "plan_k")) %>%
mutate(title = str_replace_all(title, "plank", "plan_k")) %>%
mutate(title = str_replace_all(title, "binsfed", "binsfeld")) %>%
mutate(title = str_replace_all(title, "bunker palace", "bunker_palace")) %>%
mutate(title = str_replace_all(title, "l’alac", "l_alac")) %>%
mutate(title = str_replace_all(title, "l[:space:]alac", "l_alac")) %>%
mutate(title = str_replace_all(title, "[:space:]alac[:space:]", "l_alac")) %>%
mutate(title = str_replace_all(title, "fish and chips", "fish_and_chips")) %>%
mutate(title = str_replace_all(title, "101 studios", "101_studios")) %>%
mutate(title = str_replace_all(title, "101studios", "101_studios")) %>%
mutate(title = str_replace_all(title, "push the brand", "push_the_brand")) %>%
mutate(title = str_replace_all(title, "graphisterie générale", "graphisterie_générale")) %>%
mutate(title = str_replace_all(title, "human made", "human_made")) %>%
mutate(title = str_replace_all(title, "rose de claire", "rose_de_claire")) %>%
mutate(title = str_replace_all(title, "skill lab", "skill_lab")) %>%
mutate(title = str_replace_all(title, "maison moderne", "maison_moderne")) %>%
mutate(title = str_replace_all(title, "joe la pompe", "joe_la_pompe")) %>%
mutate(title = str_replace_all(title, "lemon event", "lemon_event")) %>%
mutate(title = str_replace_all(title, "groupe get", "groupe_get")) %>%
mutate(title = str_replace_all(title, "angels events agency", "angels_events_agency")) %>%
mutate(title = str_replace_all(title, "mad about soul", "mad_about_soul")) %>%
mutate(title = str_replace_all(title, "mad about", "mad_about")) %>%
mutate(title = str_replace_all(title, "keep contact", "keep_contact")) %>%
mutate(title = str_replace_all(title, "e-connect", "e_connect")) %>%
mutate(title = str_replace_all(title, "mikado publicis", "mikado_publicis")) %>%
mutate(title = str_replace_all(title, "mikado$", "mikado_publicis")) %>%
mutate(title = str_replace_all(title, "mikado[:space:]", "mikado_publicis ")) %>%
mutate(title = str_replace_all(title, "mikado,", "mikado_publicis,")) %>%
mutate(title = str_replace_all(title, "studio polenta", "studio_polenta")) %>%
mutate(title = str_replace_all(title, "piranha et petits poissons rouges", "piranha")) %>%
mutate(title = str_replace_all(title, "vidale & gloesener", "vidale_gloesener")) %>%
mutate(title = str_replace_all(title, "vidale-gloesener", "vidale_gloesener")) %>%
mutate(title = str_replace_all(title, "vidale gloesener", "vidale_gloesener")) %>%
mutate(title = str_replace_all(title, "quattro creative", "quattro_creative")) %>%
mutate(title = str_replace_all(title, "quattro[:space:]", "quattro_creative ")) %>%
mutate(title = str_replace_all(title, "shine a light", "shine_a_light ")) %>%
mutate(title = str_replace_all(title, "neon marketing technology", "neon")) %>%
mutate(title = str_replace_all(title, "intrépide studio", "intrepide_studio")) %>%
mutate(title = str_replace_all(title, "ing luxembourg", "ing_luxebourg")) %>%
mutate(title = str_replace_all(title, "[:space:]ing", " ing_luxebourg")) %>%
mutate(title = str_replace_all(title, "p&t", "post")) %>%
mutate(title = str_replace_all(title, "post luxembourg", "post")) %>%
mutate(title = str_replace_all(title, "bernard-massard", "bernard massard")) %>%
mutate(title = str_replace_all(title, "pall center", "pall")) %>%
mutate(title = str_replace_all(title, "vdl", "ville de luxembourg")) %>%
mutate(title = str_replace_all(title, "seat luxembourg", "losch")) %>%
mutate(title = str_replace_all(title, "volkswagen luxembourg", "losch")) %>%
mutate(title = str_replace_all(title, "audi luxembourg", "losch")) %>%
mutate(title = str_replace_all(title, "ministère du développement durable et des infrastructures", "mddi")) %>%
mutate(title = str_replace_all(title, "auchan cloche d or", "auchan")) %>%
mutate(title = str_replace_all(title, "détecteurs de fumée", "cgdis")) %>%
mutate(title = str_replace_all(title, "kpmg luxembourg", "kpmg")) %>%
mutate(title = str_replace_all(title, "[:space:]join[:space:]", " join luxembourg ")) %>%
mutate(title = str_replace_all(title, "^join[:space:]", "join luxembourg ")) %>%
mutate(title = str_replace_all(title, "mercedes-benz", "mercedes")) %>%
mutate(title = str_replace_all(title, "[:space:]bil[:space:]", " bil luxembourg ")) %>%
mutate(title = str_replace_all(title, "[:space:]leo[:space:]", " leo luxembourg ")) %>%
mutate(title = str_replace_all(title, "spuerkees", "bcee"))
Ouch! C’était long mais voilà une bonne chose de faite.
2.4 Vous avez dit VOUS ?
J’adore le nom de notre agence: VOUS. Mais vous voyez le second problème?
Comment identifier les titres avec le terme vous
ne correspondant pas à l’agence? Et bien là
aussi pas de solution miracle, il me faudra lire chaque titre et classifier le sens manuellement…
Voyons ce que cela donne sur la table 2.1.
#Extract a title index with the word "vous"
vous_index <- crawl3 %>%
filter(str_detect(title, "vous")) %>%
filter(!str_detect(title, "agence_vous")) %>%
pull(rowid)
#Analyse wich "vous" are not related to "agence vous"
no_vous_index <- c(89, 93, 247, 309, 348, 375, 518, 593, 594, 637, 819, 845, 856, 863, 907, 953, 1028, 1101, 1175, 1230, 1376, 1383, 1388, 1463, 1495, 1564, 1637, 1713, 1734, 1780, 1791, 1796, 1797, 1820, 1837, 1878, 1933)
#Keep index of "vous" related to "agence vous"
vous_is_agencevous_index <- vous_index[!vous_index %in% no_vous_index]
#Replace "vous" with "agence_vous"
vous_title <- crawl3[vous_is_agencevous_index, ] %>%
mutate(title = str_replace_all(title, "vous", "agence_vous"))
#bind
crawl4 <- crawl3 %>%
slice(-vous_is_agencevous_index) %>%
bind_rows(vous_title) %>%
mutate(title = str_replace_all(title, "’", " ")) %>%
mutate(title = str_replace_all(title, "'", " ")) %>%
arrange(rowid)
title |
---|
médecins du monde luxembourg oppose deux réalités dans sa campagne d été signée agence_vous |
agence_vous joue avec les codes de la série got pour la chambre des métiers |
foyer lance l assurance modulable mozaïk avec agence_vous |
join luxembourg déploie sa première campagne presse avec agence_vous. |
l agence_vous devient membre officiel de confrad |
Cela semble parfait.
2.5 Scraping du contenu
Avant de pouvoir faire une analyse, nous avons besoin de récupérer le contenu de chaque page. Comme
pour l’extraction des données
CIM10,
nous allons utiliser le package rvset
et la fonction safely
du
package purrr
pour envelopper (wrapping) la fonction de lecture de l’HTML. Cette procédure
permet de capturer d’éventuelles erreurs sans stopper le processus de lecture. En effet, si l’algorithme
tente de lire le contenu d’une page qui n’existe plus (lien mort donnant une erreur
40411), le processus s’arrêterait
alors qu’ici nous aurons une notification. La fonction nous donnera une liste en sortie.
Voilà. Après quelques minutes de patiente, l’entièreté du contenu est récupérée et pèse 5,2 Mb.
2.6 Fonctions d’extraction
Pour récupérer les catégories wordpress12 définies existantes, nous allons créer une fonction spécifique. Nous ferons de même pour la date de publication, les lovers, les commentaires et la date des commentaires.
# Extract category
extract_cat <- function(url) {
url %>%
html_node(".hentry") %>%
html_attr("class") %>%
str_extract("(category-[^\\s]*)") #regex selector
}
# Extract publication date
extract_date <- function(url) {
url %>%
html_node(".hentry") %>%
html_attr("class") %>%
str_extract_all("(?<=[ymdh])\\d+") %>%
ymd_h()
}
# Extract lovers
extract_lovers <- function(url) {
url %>%
html_nodes("#comments-list .comment-author") %>%
html_text(trim = TRUE)
}
# Extract comments
extract_comments <- function(url) {
url %>%
html_nodes("#comments-list .comment-content") %>%
html_text(trim = TRUE) %>%
str_replace_all("\\\n", " ")
}
# Extract Comment Date
extract_c_date <- function(url) {
url %>%
html_nodes("#comments-list .comment-meta") %>%
html_text(trim = TRUE) %>%
str_replace_all("\\D", "")
}
Nous pouvons procéder à l’extraction de notre liste de contenu (content). Nous pourrions avoir un
arrêt de la fonction en cas de contenu vide. Pour éviter cela, nous envelopperons les fonctions
sous la fonction safely
également que nous exécuterons sur le résultat de l’extraction de données
vu précédemment.
2.7 Extraction de variables
Bam! Nous avons nos 5 listes
, chacune longue de 1942
rangées (ce qui correspond à notre liste d’URL).
2.8 Préparation des 5 listes
Avant de rassembler le tout dans une seule table, nous allons nettoyer chaque liste, ajouter un
index (rowid) et transformer la liste en table via la fonction enframe
avec la bonne classe (chr,
POSIXct, etc).
page_category_tbl <- page_category %>%
map_chr("result", .default = NA) %>%
enframe(name = "rowid", value = "category")
#> Observations: 1,942
#> Variables: 2
#> $ rowid <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 1…
#> $ category <chr> "category-marques", "category-crea", "category-crea", "categ…
pub_date_tbl <- publication_date %>%
map("result", .default = NA) %>%
enframe(name = "rowid", value = "pub_date") %>%
unnest(pub_date)
#> Observations: 1,942
#> Variables: 2
#> $ rowid <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 1…
#> $ pub_date <dttm> 2019-06-13 06:00:00, 2018-04-18 11:00:00, 2019-08-14 16:00:…
lovers_tbl <- lovers %>%
map("result", .default = NA) %>%
enframe(name = "rowid", value = "lovers") %>%
unnest(lovers)
#> Observations: 3,198
#> Variables: 2
#> $ rowid <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18,…
#> $ lovers <chr> NA, "will de Luxembourg", NA, NA, NA, NA, NA, NA, NA, "David",…
Nous avons ici 3,198 observations. La raison est que nous avons plusieurs contributeurs (lovers) par pages.
comments_tbl <- comments %>%
map("result", .default = NA) %>%
enframe(name = "rowid", value = "comments") %>%
unnest(comments)
#> Observations: 3,198
#> Variables: 2
#> $ rowid <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 1…
#> $ comments <chr> NA, "Belle réalisation, c’est frais, c’est jeune, c’est symp…
comments_date_tbl <- comments_date %>%
map("result", .default = NA) %>%
enframe(name = "rowid", value = "comments_date") %>%
unnest(comments_date) %>%
mutate(comments_date = dmy_hm(comments_date))
#> Observations: 3,198
#> Variables: 2
#> $ rowid <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, …
#> $ comments_date <dttm> NA, 2018-04-23 17:57:00, NA, NA, NA, NA, NA, NA, NA, 2…
Sur cette dernière table, nous transformons les valeurs en POSIXct.
2.9 Joindre en 1 seule table
Nous allons à présent rassembler l’ensemble des tables en une seule. Pour rappel, nous avons 5
listes de 2 longueurs (1,942 et 3,198). J’utilise 2 fonctions: bind_cols
pour les listes de même
longueur et left_join
pour joindre les URL de page, les titres (table nommée crawl), la date
de publication, les catégories ensemble.
adada_tbl <- lovers_tbl %>%
bind_cols(comments_date_tbl[,2]) %>%
bind_cols(comments_tbl[,2]) %>%
left_join(crawl4, "rowid") %>%
left_join(page_category_tbl, "rowid") %>%
left_join(pub_date_tbl, "rowid") %>%
select(rowid, pub_date, category, title, lovers, comments, comments_date, url)
Vérifions.
#> Observations: 3,198
#> Variables: 8
#> $ rowid <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, …
#> $ pub_date <dttm> 2019-06-13 06:00:00, 2018-04-18 11:00:00, 2019-08-14 1…
#> $ category <chr> "category-marques", "category-crea", "category-crea", "…
#> $ title <chr> "kpmg plage 2019: kpmg persiste et signe avec takaneo",…
#> $ lovers <chr> NA, "will de Luxembourg", NA, NA, NA, NA, NA, NA, NA, "…
#> $ comments <chr> NA, "Belle réalisation, c’est frais, c’est jeune, c’est…
#> $ comments_date <dttm> NA, 2018-04-23 17:57:00, NA, NA, NA, NA, NA, NA, NA, 2…
#> $ url <chr> "https://www.adada.lu/2019/06/kpmg-plage-2019-kpmg-pers…
2.10 Enrichissement des données
Nous arrivons à la dernière étape de cette première partie et pas la moindre. Nous allons ajouter une colonne pour les agences mentionnées dans le titre de l’article, une colonne pour les clients et une colonne pour les personnes référencées. Ici pas de recette miracle, j’ai répertorié l’ensemble manuellement malgré une tentative infructueuse d’identification d’entités avec l’API NLP13 de Google (Nous reviendrons plus tard sur l’API).
J’ai donc enregistré 3 vecteurs pour les agences, le noms et prénoms et les clients 2.2.
|
|
|
2.11 Labellisation
Pour labéliser un titre avec le nom de l’agence, nous allons identifier la présence de celle-ci
avec la fonction str_which
. Si une mention est trouvée, la fonction mutate
enregistre comme
valeur dans une nouvelle colonne res
la référence de l’agence sous forme d’index (l’index
correspondant à la position de l’agence trouvée dans notre vecteur agency
). Si plusieurs agences
sont trouvées dans le titre, nous aurons une liste d’index pour ce titre particulier. C’est pour
cette raison que la fonction unnest
est présente dans notre manipulation. Nous créons ensuite une
nouvelle colonne avec comme sélecteur du nom notre colonne res
. Cela peut sembler complexe, mais
avec un peu de pratique cela va vous paraître évident. Nous répéterons cette procédure pour les
personnes et clients.
2.11.1 Identification des agences
2.11.2 Identification des clients
2.11.3 Identification des personnes
2.12 La table finalisée
adada_tbl3 <- adada_tbl2 %>%
mutate(res = map(title, ~ str_which(., people))) %>%
unnest(keep_empty = TRUE, res) %>%
mutate(people = people[res]) %>%
select(-res)
Nous avons une table avec 11 variables. Voici un extrait de chacune d’elle.
#> Observations: 3,839
#> Variables: 11
#> $ rowid <int> 1, 2, 3, 4, 5, 6, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 1…
#> $ pub_date <dttm> 2019-06-13 06:00:00, 2018-04-18 11:00:00, 2019-08-14 1…
#> $ category <chr> "category-marques", "category-crea", "category-crea", "…
#> $ title <chr> "kpmg plage 2019: kpmg persiste et signe avec takaneo",…
#> $ lovers <chr> NA, "will de Luxembourg", NA, NA, NA, NA, NA, NA, NA, N…
#> $ comments <chr> NA, "Belle réalisation, c’est frais, c’est jeune, c’est…
#> $ comments_date <dttm> NA, 2018-04-23 17:57:00, NA, NA, NA, NA, NA, NA, NA, N…
#> $ url <chr> "https://www.adada.lu/2019/06/kpmg-plage-2019-kpmg-pers…
#> $ agency <chr> "takaneo", "betocee", "agence_vous", "binsfeld", "plan_…
#> $ client <chr> "kpmg", "rosport", "médecins du monde", "enovos", "bell…
#> $ people <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
rowid | pub_date | category |
---|---|---|
827 | 2010-04-01 20:00:00 | category-communiques |
858 | 2018-05-17 07:00:00 | category-crea |
1635 | 2015-03-31 21:00:00 | category-crea |
477 | 2014-01-26 13:00:00 | category-crea |
1801 | 2011-05-12 10:00:00 | category-crea |
1877 | NA | category-crea |
218 | 2017-11-23 07:00:00 | category-communiques |
516 | 2017-03-28 16:00:00 | category-crea |
759 | 2017-02-01 09:00:00 | category-crea |
1229 | 2012-03-23 12:00:00 | category-crea |
rowid | title |
---|---|
1436 | luxembourg marketing & communication awards 2012 : présentation du nouveau règlement |
1003 | vanksen remporte 3 prix aux marketing & communication awards 2013 |
769 | wili signe l identité corporate du « innovation hub » de dudelange |
751 | 5 à sec fait une nouvelle fois appel à ribs |
1264 | a3com signe la communication de la 1ere édition du prix de musique quattropole |
1607 | bofferding au cœur du tour de france avec nvision |
861 | nouvelle adresse, nouveau départ pour atypical et kreutz & friends |
1687 | le mddi et la sécurité routière lancent une grande campagne de sensibilisation pour les pneus hiver avec mikado_publicis |
1444 | immotop.lu confie sa communication à l agence antidote |
1611 | emile weber promet des vacances surprenantes avec agence_vous |
rowid | comments_date | agency | client | people |
---|---|---|---|---|
323 | 2019-03-01 15:46:00 | NA | mudam | NA |
1687 | 2011-10-19 20:42:00 | mikado_publicis | sécurité routière | NA |
43 | NA | NA | mudam | NA |
915 | NA | binsfeld | battin | NA |
1846 | 2012-06-10 22:45:00 | comed | luxtram | NA |
53 | 2015-09-27 13:13:00 | human_made | NA | NA |
1165 | 2012-03-19 08:05:00 | NA | NA | NA |
1238 | 2012-02-16 07:46:00 | NA | axa | NA |
972 | 2011-08-03 22:13:00 | concept_factory | total luxembourg | NA |
1942 | 2011-06-22 13:41:00 | h2a | luxsecurity | NA |
rowid | lovers | comments |
---|---|---|
819 | georgette | y fait chaud ici non? |
1265 | NA | NA |
477 | Jeff | Déjà v(o)u(s) http://www.youtube.com/watch?v=316AzLYfAzw |
480 | NA | NA |
309 | Lucas | « On connaît assez bien le marché de la pub ici. » Bravo o / |
639 | Jean | Ben oui, la campagne Rosport à le goût de déja écrit et la campagne autopolis ressemble à une voiture d’occasion. Dans ce genre de concours il faut avoir un jury qui a une bonne culture publicitaire. |
1079 | castrol | 3 prix.. et le même soir… Quel talent ! mieux que Cannes Lions.. vous êtes plus forts que Burnett, Y&R. Je m’incline et vous prie d’accepter mes excuses. Mais je suis en droit de m’interroger ; Comment allez-vous gérer ce succès ? gardez bien à l’esprit que dans notre profession, le succès commercial est bien le seul qui vaille. Les succès d’estime ne conduisent jamais leur bénéficiaire qu’aux épinards sans beurre. SIgné la Conne ! |
1224 | NA | NA |
59 | NA | NA |
1229 | Priscillia | Beau visuel |
Bravo! Vous êtes arrivé à la fin de cette première partie.
Screaming Frog, A SEO Spider Tool, https://www.screamingfrog.co.uk/seo-spider/↩
Tokenisation, https://fr.wiktionary.org/wiki/tokenisation↩
CIM: Centre d’information sur les médias, https://www.cim.be/fr↩
Erreur 404, https://fr.wikipedia.org/wiki/Erreur_HTTP_404↩
Les catégories Wordpress, https://fr.support.wordpress.com/articles/categories/↩
Google, AI & Machine Learning Products, https://cloud.google.com/natural-language/?hl=fr↩