Site icon Anakeyn

Récupérer les liens contextuels de son site Web avec R

Dans cet article nous verrons comment récupérer les liens contextuels, c’est à dire les liens présents dans la zone éditoriale (ou de contenu) sur les pages de son site.

Pour être plus explicite, sur l’image suivante :

Zone liens contextuels

Il s’agit de la zone en rouge qui contient un contenu spécifique à la page. Les autres zones : entête, menus, barres latérales, pied de page … étant le plus souvent répétées sur toutes les pages.

On considère que cette zone, spécifique à la page, contient l’information la plus importante. De la même façon, et comme on le suppose depuis de nombreuses années, les liens de cette zone seraient considérés comme plus importants, du point de vue SEO, que les liens des autres zones de la page, appelés liens « structurels ».

Pour vous faire une idée, voici ce que dit Olivier Andrieu (spécialiste SEO bien connu) à propos des liens contextuels et structurels dans un article datant déjà de 2007.

Par ailleurs, en vous focalisant sur les liens contextuels, il vous sera plus facilement possible de tester si le maillage interne de votre site répond aux critères de la structure en « silos » ou en « cocons sémantiques ». La notion de cocon sémantique a été développée par Laurent Bourrelly (autre spécialiste SEO bien connu).

Afin de récupérer ces liens contextuels, nous allons modifier le crawler « NetworkRcrawler » que nous avions présenté précédemment dans notre article « Créer un graphe de site Web avec R ».

Par ailleurs, dans cette exemple nous testerons notre site https://www.anakeyn.com – au hasard 🙂 .

Logiciel R

Comme dans nos autres articles traitant de R, nous vous invitons à télécharger Le Logiciel R sur ce site https://cran.r-project.org/, ainsi que l’environnement de développement RStudio ici : https://www.rstudio.com/products/rstudio/download/.

Logiciel Gephi

Ici, nous avons décidé d’utiliser Gephi pour la partie « Graphe » de l’article. Gephi est gratuit et open source et vous pouvez le télécharger sur Gephi.org. Gephi est compatible avec Windows, Mac OS X et Linux.

Code Source

Vous pouvez copier/coller les morceaux de code source dans un script R pour les tester.

Vous pouvez aussi récupérer gratuitement le code source en entier dans notre boutique à l’adresse : https://www.anakeyn.com/boutique/produit/script-r-recuperation-liens-contextuels/

Parties non modifiés

La partie chargement de l’environnement ne change pas :

#' 
#'
#'
#Packages à installer une fois.
#install.packages("Rcrawler")
#install.packages("igraph")
#install.packages("foreach")
#install.packages("doParallel")
#install.packages("data.table")
#install.packages("gdata")
#install.packages("xml2")
#install.packages("httr")
#Bibliothèques à charger.
library(Rcrawler)  #Notamment pour Linkparamsfilter...
library(doParallel) #Notamment pour parallel::makeCluster	
library(data.table)  #Notamment pour %like% %in% ...
library(igraph) #Notamment graph.data.frame() ...
library(xml2) #Notamment pour read_html
library(httr)  #pour GET, content ...
#

Ainsi que  les fonctions NetworkRobotParser

#' NetworkRobotParser modifie RobotParser qui  générait une erreur d'encoding on rajoute MyEncod.
#' RobotParser fetch and parse robots.txt
#'
#' This function fetch and parse robots.txt file of the website which is specified in the first argument and return the list of correspending rules .
#' @param website character, url of the website which rules have to be extracted  .
#' @param useragent character, the useragent of the crawler
#' @return
#' return a list of three elements, the first is a character vector of Disallowed directories, the third is a Boolean value which is TRUE if the user agent of the crawler is blocked.
#' @import httr
#' @export
#'
#' @examples
#'
#' RobotParser("http://www.glofile.com","AgentX")
#' #Return robot.txt rules and check whether AgentX is blocked or not.
#'
#'
NetworkRobotParser <- function(website, useragent, Encod="UTF-8") {
  URLrobot<-paste(website,"/robots.txt", sep = "")
  bots<-GET(URLrobot, user_agent("Mozilla/5.0 (Windows NT 6.3; WOW64; rv:42.0) Gecko/20100101 Firefox/42.0"),timeout(5))
  #PR Ajout de Encod
  MyEncod <- trimws(gsub("charset=", "", unlist(strsplit(bots$headers$'content-type', ";"))[2]))
  if (is.null(MyEncod) || is.na(MyEncod) ) MyEncod <- Encod
  bots<-as.character(content(bots, as="text", encoding = MyEncod))  #pour éviter erreur d encoding
  write(bots, file = "robots.txt")
  bots <- readLines("robots.txt") # dans le repertoire du site
  if (missing(useragent)) useragent<-"NetworkRcrawler"
  useragent <- c(useragent, "*")
  ua_positions <- which(grepl( "[Uu]ser-[Aa]gent:[ ].+", bots))
  Disallow_dir<-vector()
  allow_dir<-vector()
  for (i in 1:length(useragent)){
    if (useragent[i] == "*") useragent[i]<-"\\*"
    Gua_pos <- which(grepl(paste("[Uu]ser-[Aa]gent:[ ]{0,}", useragent[i], "$", sep=""),bots))
    if (length(Gua_pos)!=0 ){
      Gua_rules_start <- Gua_pos+1
      Gua_rules_end <- ua_positions[which(ua_positions==Gua_pos)+1]-1
      if(is.na(Gua_rules_end)) Gua_rules_end<- length(bots)
      Gua_rules <- bots[Gua_rules_start:Gua_rules_end]
      Disallow_rules<-Gua_rules[grep("[Dd]isallow",Gua_rules)]
      Disallow_dir<-c(Disallow_dir,gsub(".*\\:.","",Disallow_rules))
      allow_rules<-Gua_rules[grep("^[Aa]llow",Gua_rules)]
      allow_dir<-c(allow_dir,gsub(".*\\:.","",allow_rules))
    }
  }
  if ("/" %in% Disallow_dir){
    Blocked=TRUE
    print ("This bot is blocked from the site")} else{ Blocked=FALSE }
  
  Rules<-list(Allow=allow_dir,Disallow=Disallow_dir,Blocked=Blocked )
  return (Rules)
}

ET Networking Normalisation :

#' 
#'
#' NetworkLinkNormalization :modification de LinkNormalization : 
#' on ne renvoie pas des liens uniques mais multiples : le dédoublonnement des liens doit se faire lors de 
#' l'étude du  réseau avec igraph.
#' correction aussi pour les liens avec # et mailto:, callto: et tel: qui étaient renvoyés.
#'
#' A function that take a URL _charachter_ as input, and transforms it into a canonical form.
#' @param links character, the URL to Normalize.
#' @param current character, The URL of the current page source of the link.
#' @return
#' return the simhash as a nmeric value
#' @author salim khalilc corrigé par Pierre Rouarch
#' @details
#' This funcion call an external java class  
#' @export
#'
#' @examples
#'
#' # Normalize a set of links
#'
#' links<-c("http://www.twitter.com/share?url=http://glofile.com/page.html",
#'          "/finance/banks/page-2017.html",
#'          "./section/subscription.php",
#'          "//section/",
#'          "www.glofile.com/home/",
#'          "glofile.com/sport/foot/page.html",
#'          "sub.glofile.com/index.php",
#'          "http://glofile.com/page.html#1"
#'                    )
#'
#' links<-LinkNormalization(links,"http://glofile.com" )
#'
#'
NetworkLinkNormalization<-function(links, current){
  protocole<-strsplit(current, "/")[[c(1,1)]]
  base <- strsplit(gsub("http://|https://", "", current), "/")[[c(1, 1)]]
  base2 <- strsplit(gsub("http://|https://|www\\.", "", current), "/")[[c(1, 1)]]
  rlinks<-c();
  #base <- paste(base, "/", sep="")
  
  for(t in 1:length(links)){
    if (!is.null(links[t]) && length(links[t]) == 1){  #s'il y a qq chose
      if (!is.na(links[t])){ #pas NA
       if(substr(links[t],1,2)!="//"){ #si ne commence pas par //
          if(sum(gregexpr("http", links[t], fixed=TRUE)[[1]] > 0)<2) {  #Si un seul http
            # remove spaces
            if(grepl("^\\s|\\s+$",links[t])) { 
              links[t]<-gsub("^\\s|\\s+$", "", links[t] , perl=TRUE)
            }
            #if starts with # remplace par url courante
            if (substr(links[t],1,1)=="#"){
              links[t]<- current }   #on est sut la même page (PR)
            #if starts with / add base 
            if (substr(links[t],1,1)=="/"){
              links[t]<-paste0(protocole,"//",base,links[t]) }
            #if starts with ./ add base
            if (substr(links[t],1,2)=="./") {
              # la url current se termine par /
              if(substring(current, nchar(current)) == "/"){
                links[t]<-paste0(current,gsub("\\./", "",links[t]))
                # si non
              } else {
                links[t]<-paste0(current,gsub("\\./", "/",links[t]))
              }
            }
            
            if(substr(current,1,10)=="http://www" || substr(current,1,11)=="https://www") {  #si on a un protocole + www  sur la page courante.
              if(substr(links[t],1,10)!="http://www" && substr(links[t],1,11)!="https://www" && substr(links[t],1,8)!="https://" && substr(links[t],1,7)!="http://" ){
                if (substr(links[t],1,3)=="www") {
                  links[t]<-paste0(protocole,"//",links[t])
                } else {
                  #tests liens particulier sans protocole http://
                  if(substr(links[t],1,7)!="mailto:" && substr(links[t],1,7)!="callto:" && substr(links[t],1,4)!="tel:") {  
                  links[t]<-paste0(protocole,"//www.",links[t])
                  }
                  
                }
              }
            }else {   #à priori pas de http sans www dans current
              if(substr(links[t],1,7)!="http://" && substr(links[t],1,8)!="https://" ){
                #test liens cas particuliers sans protocole http://
                if(substr(links[t],1,7)!="mailto:" && substr(links[t],1,7)!="callto:" && substr(links[t],1,4)!="tel:") {  
                  links[t]<-paste0(protocole,"//",links[t])
                }
               }
            }
            if(grepl("#",links[t])){links[t]<-gsub("\\#(.*)","",links[t])}  #on vire ce qu'il y a derrière le #
            
            rlinks <- c(rlinks,links[t])  #ajout du lien au paquet de liens
          }
        }
      }
    }
  }
  #rlinks<-unique(rlinks)  #NON : garder tous les liens,  pas d'unicité.
  return (rlinks)
}

Récupération des liens contextuels avec NetworkRcrawler

Dans cette partie, il s’agit d’indiquer au crawler d’aller récupérer les liens contextuels dans des zones spécifiques des pages.

Heureusement pour nous, les différentes zones des pages de sites Web sont repérables par des balises dans le code HTML des pages (enfin en général…).

Pour revenir au schéma précédent d’une page, la zone qui nous intéresse peut être indiquée par une balise spécifique.

Balise zone liens contextuels

Sur l’image la balise qui indique la zone où trouver les liens contextuels et qui nous intéresse démarre à la balise « <div id= »main »> ».

Bon, dans la réalité, c’est un petit peu plus complexe et il se peut que les zones que vous recherchez n’aient pas toutes les mêmes balises selon les pages. Pour les rechercher il faut afficher le code source de votre page dans un navigateur (en général en tapant « Ctrl+U »).

Voici par exemple le code source HTML de la page d’accueil d’Anakeyn :

html anakeyn

J’ai surligné en bleu la balise que j’ai repérée et qui m’intéresse : « <main id= »main » class= »site-main » role= »main »> »

Vous devrez vérifier sur les différents types de pages : Accueil, catégories, articles, listes, achats… quels sont les balises qui vous intéressent pour récupérer les liens contextuels.

Dans le cas d’Anakeyn nous en avons identifiées 4. Ce n’est finalement pas beaucoup car le site est divisé en 3 sites : l’actuel et 2 sites anciens et qui ont chacun un thème différent de WordPress.

Au final les balises sélectionnées sont les suivantes :

Revenons au code source de NetworkRcrawler, pour identifier les zones à rechercher nous avons ajouté une variable XPathLinksAreaNodes contenant les balises à rechercher au format
xPath

#' 
#' NetworkRcrawler  (modification de Rcrawler par Pierre Rouarch pour limiter le nombre de page crawlées et éviter
#' une attente trop longue) :
#' NetworkRcrawler a pour objectif de créer un réseau de pages de site exploitable par iGraph
#' 
#' Version 1.01  
#' Ajout du paramètre XPathLinksAreaNodes  :  (vecteur de chaines de caractères au format xpath ) pour indiquer les zones pour rechercher des liens.
#' # attention on ne parse qu'une seule zone, le repérage se fait dans l'ordre déterminé par le vecteur.
#' Exemple : c( "main" , "//div[@class=\"main-container\"]" , #"//main[@class=\"main\"]")
#' 
#'  a vous d'identifier les zones "principales" que vous souhaitez cibler pour éviter les 
#'  liens de menus et de bas de page par exemple
#'  Attention si la zone ciblée est vide sur une page les liens sont récupérés 
#'  sur toute la page (pour éviter un arrêt du crawl)
#' Version 1.0
#' 
#' #' Modification vs Rcrawler :
#' Ajout du paramètre MaxPagesParsed   pour limiter les pages "parsées" et y passer la nuit
#' Ajout du Paramètres IndexErrPages  pour récupérér des pages avec status autre que 200
#' On utilise GET() plutôt que LinkExtractor car LinkExtractor ne nous renvoyait pas les  infos voulues 
#' notamment les redirections.
#' Récupération du contenu de la page dans pkg.env$GraphNodes plutôt que passer par des fichiers externes
#' La valeur de pkg.env$GraphEdges$Weight est à 1 et non pas à la valeur du level de la page comme précédemment.
#' 
#' 
#' Simplification vs Rcrawler : 
#' Suppression de l'enregistrement des fichiers *.html pour gagner en performance
#' Nous avons aussi supprimer les paramètres d'extraction qui ne nous semblent pas pertinents à ce 
#' stade : Comme le contenu est passé à travers de NetwNodes$Content les extractions de contenus 
#' peuvent(doivent?) se faire en dehors de la construction du Réseau de pages à proprement parlé.
#' 
#' Paramètres conservés vs Rcrawler
#' @param Website character, the root URL of the website to crawl and scrape.
#' @param no_cores integer, specify the number of clusters (logical cpu) for parallel crawling, by default it's the numbers of available cores.
#' @param no_conn integer, it's the number of concurrent connections per one core, by default it takes the same value of no_cores.
#' @param MaxDepth integer, repsents the max deph level for the crawler, this is not the file depth in a directory structure, but 1+ number of links between this document and root document, default to 10.
#' @param RequestsDelay integer, The time interval between each round of parallel http requests, in seconds used to avoid overload the website server. default to 0.
#' @param Obeyrobots boolean, if TRUE, the crawler will parse the website\'s robots.txt file and obey its rules allowed and disallowed directories.
#' @param Useragent character, the User-Agent HTTP header that is supplied with any HTTP requests made by this function.it is important to simulate different browser's user-agent to continue crawling without getting banned.
#' @param Encod character, set the website caharacter encoding, by default the crawler will automatically detect the website defined character encoding.
#' @param Timeout integer, the maximum request time, the number of seconds to wait for a response until giving up, in order to prevent wasting time waiting for responses from slow servers or huge pages, default to 5 sec.
#' @param URLlenlimit integer, the maximum URL length limit to crawl, to avoid spider traps; default to 255.
#' @param urlExtfilter character's vector, by default the crawler avoid irrelevant files for data scraping such us xml,js,css,pdf,zip ...etc, it's not recommanded to change the default value until you can provide all the list of filetypes to be escaped.
#' @param ignoreUrlParams character's vector, the list of Url paremeter to be ignored during crawling .
#' @param NetwExtLinks boolean, If TRUE external hyperlinks (outlinks) also will be counted on Network edges and nodes.
#' Paramètre  ajouté 
#' @param MaxPagesParsed integer,  Maximum de pages à Parser  (Ajout PR)
#' @param XPathLinksAreaNodes character, xpath,  si l'on veut cibler la zone de la page ou récupérér les liens  (Ajout PR)
#'        Attention si la zone n'est pas trouvé tous les liens de la page sont récupérés.
#' 
#' 
#' @return
#'
#' The crawling and scraping process may take a long time to finish, therefore, to avoid data loss 
#' in the case that a function crashes or stopped in the middle of action, some important data are 
#' exported at every iteration to R global environment:
#'
#' - NetwNodes : Dataframe with alls hyperlinks and parameters of pages founded. 
#' - NetwEdges : data.frame representing edges of the network, with these column : From, To, Weight (1) and Type (1 for internal hyperlinks 2 for external hyperlinks).
#'
#' @details
#'
#' To start NetworkRcrawler (or Rcrawler) task you need the provide the root URL of the website you want to scrape, it can 
#' be a domain, a subdomain or a website section (eg. http://www.domain.com, http://sub.domain.com or 
#' http://www.domain.com/section/). The crawler then will go through all its internal links. 
#' The process of a crawling is performed by several concurrent processes or nodes in parallel, 
#' So, It is recommended to use R 64-bit version.
#'
#' For more tutorials about RCrawler check https://github.com/salimk/Rcrawler/
#'
#' For scraping complexe character content such as arabic execute Sys.setlocale("LC_CTYPE","Arabic_Saudi Arabia.1256") then set the encoding of the web page in Rcrawler function.
#'
#' If you want to learn more about web scraper/crawler architecture, functional properties and implementation using R language, Follow this link and download the published paper for free .
#'
#' Link: http://www.sciencedirect.com/science/article/pii/S2352711017300110
#'
#' Dont forget to cite Rcrawler paper:
#' Khalil, S., & Fakir, M. (2017). RCrawler: An R package for parallel web crawling and scraping. SoftwareX, 6, 98-106.
#'
#' @examples
#'
#' \dontrun{
#'  NetworkRcrawler(Website ="http://www.example.com/", no_cores = 4, no_conn = 4)
#'  #Crawl, index, and store web pages using 4 cores and 4 parallel requests

#'  NetworkRcrawler(Website = "http://www.example.com/", no_cores=8, no_conn=8, Obeyrobots = TRUE,
#'   Useragent="Mozilla 3.11")
#'   # Crawl and index the website using 8 cores and 8 parallel requests with respect to
#'   # robot.txt rules.
#'
#'   NetworkRcrawler(Website = "http://www.example.com/" , no_cores = 4, no_conn = 4, MaxPagesParsed=50)
#'   # Crawl the website using 4 cores and 4 parallel requests until the number of parsed pages reach 50
#'
#'   # Using Igraph for exmaple you can plot the network by the following commands
#'    library(igraph)
#'    network<-graph.data.frame(NetwEdges, directed=T)
#'    plot(network)
#'}
#'
#'
#' @author salim khalil modifié simplifié par Pierre Rouarch
#' @import foreach doParallel parallel data.table selectr
#' @export
#' @importFrom utils write.table
#' @importFrom utils flush.console
#'


NetworkRcrawler <- function(Website, no_cores, no_conn, MaxDepth = 10, RequestsDelay=0, Obeyrobots=FALSE,
                            Useragent, Encod, Timeout=5, URLlenlimit=255, urlExtfilter,
                            ignoreUrlParams = "", ManyPerPattern=FALSE,  NetwExtLinks=FALSE,
                            MaxPagesParsed=500, XPathLinksAreaNodes = "" ) {
  
  
  
  if (missing(no_cores)) no_cores<-parallel::detectCores()-1
  if (missing(no_conn)) no_conn<-no_cores
  if(missing(Useragent)) {Useragent="Mozilla/5.0 (Windows NT 6.3; WOW64; rv:42.0) Gecko/20100101 Firefox/42.0"}
  
  # Récupération encoding de la page d accueil du site
  if(missing(Encod)) {
    Encod<- Getencoding(Website)
    if (length(Encod)!=0){
      if(Encod=="NULL") Encod="UTF-8" ;
    } #/ if (length(Encod)!=0)
  } #/ if(missing(Encod))
  
  
  
  #Filtrer les documents/fichiers  non souhaités
  if(missing(urlExtfilter)) { 
    urlExtfilter<-c("flv","mov","swf","txt","xml","js","css","zip","gz","rar","7z","tgz","tar","z","gzip","bzip","tar","mp3","mp4","aac","wav","au","wmv","avi","mpg","mpeg","pdf","doc","docx","xls","xlsx","ppt","pptx","jpg","jpeg","png","gif","psd","ico","bmp","odt","ods","odp","odb","odg","odf") 
  } #/if(missing(urlExtfilter))
  
  
  #Récupération du nom de domaine seul 
  domain<-strsplit(gsub("http://|https://|www\\.", "", Website), "/")[[c(1, 1)]]
  
  #Lecture de Robot.txt
  if (Obeyrobots) {
    rules<-NetworkRobotParser(Website,Useragent, Encod = Encod)
    urlbotfiler<-rules[[2]]
    urlbotfiler<-gsub("^\\/", paste("http://www.",domain,"/", sep = ""), urlbotfiler , perl=TRUE)
    urlbotfiler<-gsub("\\*", ".*", urlbotfiler , perl=TRUE)
  } else {urlbotfiler=" "}
  
  
  
  
  pkg.env <- new.env()  #créé un nouvel environnement pour données locales
  #Création des variables pour la  data.frame des noeuds/pages pkg.env$GraphNodes
  Id<-vector() #Id de page 
  MyUrl<-vector() #Url de la page crawlé
  MyStatusPage<-vector() #Status de la page dans la boucle : discovered, crawled, parsed.
  Level <- numeric()  #Niveau de la page dans le site/réseau
  NbIntLinks <-numeric() #Nbre de liens internes sur la page si parsée
  NbExtLinks <-numeric() #Nbre de liens Externes sur la page si parsée
  HttpStat<-vector() #Statut http : 200, 404
  IntermediateHttpStat<-vector() #Statut http "intermédiaire" pour récupérer les redirections (301, 302)
  ContentType<-vector() #Type de contenu ie : text/html ...
  Encoding<-vector() #Encoding ie : UTF-8
  PageType<-vector()    #Type de page 1 interne, 2 externe sert aussi dans pkg.env$GraphEgdes
  AreaZoneParsed <- vector()  #Zone parsée
  HTMLContent <- vector() #Ajouté par PR -> Contenu html de la page 
  #PR ajout pkg.env$GraphNodes (contient les pages parsées, crawlées et non crawlées, internes et externes)
  pkg.env$GraphNodes<-data.frame(Id, MyUrl, MyStatusPage, Level, NbIntLinks, NbExtLinks, HttpStat, IntermediateHttpStat, ContentType, Encoding, PageType, AreaZoneParsed, HTMLContent)
  names(pkg.env$GraphNodes) <- c("Id","Url","MyStatusPage","Level","NbIntLinks", "NbExtLinks", "HttpStat", "IntermediateHttpStat", "ContentType","Encoding", "PageType", "AreaZoneParsed", "HTMLContent")
  
  #Création des variables pour la  data.frame des liens pkg.env$GraphEdges
  FromNode<-vector()  #Id de la page de départ
  ToNode<-vector() #Id de la page d'arrivée
  Weight<-vector()  #Poids du lien  ici on prendra = 1 
  AreaZoneParsed <- vector()  #Zone parsée
  pkg.env$GraphEgdes<-data.frame(FromNode,ToNode,Weight,PageType, AreaZoneParsed)  #Data.frame des noeuds. le poids du liens est le niveau de page d'arrivée, le Type interne externe. 
  names(pkg.env$GraphEgdes) <- c("From","To","Weight","PageType", "AreaZoneParsed") #Noms des variables de la data.frame.
  
  #Autres variables intermédiaires utiles.
  allpaquet<-list() #Contient les paquets de pages crawlées parsées.
  Links<-vector() #Liste des liens sur la page 
  
  
  
  #initialisation des noeuds/pages.
  pkg.env$GraphNodes[1,"Id"] <- 1   #initialisation Id 1ere page
  pkg.env$GraphNodes[1,"Url"] <- Website #initialisation Url fournie par nous.
  pkg.env$GraphNodes[1, "MyStatusPage"] <- "discovered"
  pkg.env$GraphNodes[1,"Level"]  <- 0  #initialisation (niveau de première page à 0)
  pkg.env$GraphNodes[1,"PageType"]  <- 1 #Page Interne.
  pkg.env$GraphNodes[1,"Encoding"]  <- Encod #Récupéré par GetEncoding ou forcé à UTF-8
  pkg.env$GraphNodes[1, "AreaZoneParsed"] <- "Accueil"
  #On force IndexErrPages pour récupérer les errreurs http
  IndexErrPages<-c(200, 300, 301, 302, 404, 403, 500, 501, 502, 503, NULL, NA, "")
  
  
  lev<-0   #Niveau du site à 0 (première page)
  LevelOut <- 0  #Niveau des pages du site atteint en retour de Get - mis à 0 pour pouvoir démarrer
  t<-1     #index de début de paquet de pages à parser (pour GET)
  i<-0     #index de pages parsées pour GET
  TotalPagesParsed <- 1  #Nombre total de pages parsées.
  
  
  
  #cluster initialisation pour pouvoir travailler sur plusieurs clusters en même temps .
  cl <- makeCluster(no_cores, outfile="") #création des clusters nombre no_cores fourni par nos soins.
  registerDoParallel(cl)
  clusterEvalQ(cl, library(httr))   #Pour la fonction GET
  
  
  
  
  
  ############################################################################################  
  #  Utilisation de GET() plutot que LinkExtractor :
  ############################################################################################  
  
  
  
  #Tant qu'il reste des pages à crawler  :
  while (t<=nrow(pkg.env$GraphNodes)) {
    
    
    # Calcul du nombre de pages à crawler !
    rest<-nrow(pkg.env$GraphNodes)-t  #Rest = nombre de pages restantes à crawler = nombre de pages - pointeur actuel de début de paquet 
    #Si le nombre de connections simultanées est inférieur au nombre de pages restantes à crawler.
    if (no_conn<=rest){  
      l<-t+no_conn-1    #la limite du prochain paquet de pages à crawler = pointeur actuel + nombre de connections - 1
    } else {
      l<-t+rest  #Sinon la limite = pointeur + reste 
    }
    
    
    
    #Délai
    if (RequestsDelay!=0) {
      Sys.sleep(RequestsDelay)
    }
    #Extraction d'un paquet de pages de t pointeur actuel à l limite 
    allGetResponse <- foreach(i=t:l,  .verbose=FALSE, .inorder=FALSE, .errorhandling='pass')  %dopar%
    {
      TheUrl <- pkg.env$GraphNodes[i,"Url"] #url de la page à crawler.
      GET(url = TheUrl, timeout = Timeout )
    } #/ foreach  
    
    #On regarde ce que l'on a récupéré de GET 
    for (s in 1:length(allGetResponse)) {
      TheUrl <- pkg.env$GraphNodes[t+s-1,"Url"] #t+s-1 pointeur courant dans GrapNodes
      
      if (!is.null(allGetResponse[[s]])) {  #Est-ce que l'on a une réponse pour cette  page ? 
        
        if (!is.null(allGetResponse[[s]]$status_code)) { #Est-ce que l'on a un status pour cette page ?
          
          #Recupération des données de la page crawlée et/ou parsée.
          #Si on n'avait pas déjà HttpStat
          if (is.null(pkg.env$GraphNodes[t+s-1, "HttpStat"]) || is.na(pkg.env$GraphNodes[t+s-1, "HttpStat"])) {
            if (!is.null(allGetResponse[[s]]$status_code) ) 
              pkg.env$GraphNodes[t+s-1, "HttpStat"] <- allGetResponse[[s]]$status_code
          }  #/if (is.null(pkg.env$GraphNodes[t+s-1, "HttpStat"]) || is.na(pkg.env$GraphNodes[t+s-1, "HttpStat"]))
          #pour les status de redirections.
          if (!is.null(allGetResponse[[s]]$all_headers[[1]]$status))  pkg.env$GraphNodes[t+s-1, "IntermediateHttpStat"] <- allGetResponse[[s]]$all_headers[[1]]$status
          # Si on n'avait pas déjà ContentType
          if (is.null(pkg.env$GraphNodes[t+s-1, "ContentType"]) || is.na(pkg.env$GraphNodes[t+s-1, "ContentType"])) 
          { pkg.env$GraphNodes[t+s-1, "ContentType"] <- trimws(unlist(strsplit(allGetResponse[[s]]$headers$'content-type', ";"))[1]) }
          # Si on n'avait pas déjà Encoding
          if (is.null(pkg.env$GraphNodes[t+s-1, "Encoding"]) || is.na(pkg.env$GraphNodes[t+s-1, "Encoding"])) 
          { pkg.env$GraphNodes[t+s-1, "Encoding"] <- trimws(gsub("charset=", "", unlist(strsplit(allGetResponse[[s]]$headers$'content-type', ";"))[2])) }
          
          #On récupère tout le contenu HTML
          MyEncod <- pkg.env$GraphNodes[t+s-1, "Encoding"]  #Verifie que l'on a un encoding auparavent
          if (is.null(MyEncod) || is.na(MyEncod) )  MyEncod <- Encod 
          
          pkg.env$GraphNodes[t+s-1, "HTMLContent"] <- content(allGetResponse[[s]], "text", encoding=MyEncod) #Récupere contenu HTML
          #Marque la page comme "crawlée"
          pkg.env$GraphNodes[t+s-1, "MyStatusPage"]  <- "crawled"
          #Niveau de cette page dans le réseau.
          LevelOut <- pkg.env$GraphNodes[t+s-1, "Level"]   #Level de la page crawlée
          
          
          #Parsing !!! Ici rechercher les liens  si c'est autorisé et page interne.
          if (MaxDepth>=LevelOut && TotalPagesParsed <= MaxPagesParsed &&  pkg.env$GraphNodes[t+s-1, "PageType"]==1) { #Récupérer des liens
            
            pkg.env$GraphNodes[t+s-1, "MyStatusPage"]  <- "parsed"
            TotalPagesParsed <- TotalPagesParsed + 1 #Total des pages parsées (pour la prochaine itération)
            AreaZoneParsed <- ""  #Zone parsé non définie
            x <- read_html(x = content(allGetResponse[[s]], "text"))  #objet html
            
            if (length(XPathLinksAreaNodes) != 0)  {
              for (i in 1:length(XPathLinksAreaNodes)) {
                LinksArea <-  xml2::xml_find_all(x, XPathLinksAreaNodes[i])
                if (length(LinksArea) != 0) 
                {
                  cat("XPathLinksAreaNodes founded",XPathLinksAreaNodes[i],"\n")
                  x <- LinksArea
                  AreaZoneParsed <- XPathLinksAreaNodes[i]  #Zone parsé repérée
                  break
                } #if length(linksArea)
              } #/for
            }  
            
            
            links<-xml2::xml_find_all(x, "//a/@href")  #trouver les liens 
            links<-as.vector(paste(links))   #Vectorisation des liens 
            links<-gsub(" href=\"(.*)\"", "\\1", links)  #on vire href
            
            
            #Va récupérer les liens normalisés.
            links<-NetworkLinkNormalization(links,TheUrl) #revient avec les protocoles http/https sauf liens mailto etc.
            #on ne conserve que les liens avec http
            links<-links[links %like% "http" ]
            # Ignore Url parameters
            links<-sapply(links , function(x) Linkparamsfilter(x, ignoreUrlParams), USE.NAMES = FALSE)
            # Link robots.txt filter
            if (!missing(urlbotfiler)) links<-links[!links %like% paste(urlbotfiler,collapse="|") ]
            
            #Récupération des liens internes et des liens externes 
            IntLinks <- vector()  #Vecteur des liens internes
            ExtLinks <- vector() #Vecteur des liens externes.
            
            if(length(links)!=0) {
              for(iLinks in 1:length(links)){
                if (!is.na(links[iLinks])){
                  #limit length URL to 255
                  if( nchar(links[iLinks])<=URLlenlimit) {
                    ext<-tools::file_ext(sub("\\?.+", "", basename(links[iLinks])))
                    #Filtre eliminer les liens externes , le lien source lui meme, les lien avec diese , 
                    #les types de fichier filtrer, les lien tres longs , les liens de type share
                    #if(grepl(domain,links[iLinks]) && !(links[iLinks] %in% IntLinks) && !(ext %in% urlExtfilter)){
                    #Finalement on garde les liens déjà dans dans la liste 
                    #(c'est à iGraph de voir si on souhaite simplifier le graphe)
                    
                    if(grepl(domain,links[iLinks]) && !(ext %in% urlExtfilter)){  #on n'enlève que les liens hors domaine et à filtrer.
                      #C'est un lien interne
                      IntLinks<-c(IntLinks,links[iLinks])
                      
                    } #/if(!(ext %in% urlExtfilter))
                    if(NetwExtLinks){ #/si je veux les liens externes en plus (les doublons seront gérés par iGraph)
                      
                      #if ( !grepl(domain,links[iLinks]) && !(links[iLinks] %in% ExtLinks) && !(ext %in% urlExtfilter)){
                      if ( !grepl(domain,links[iLinks])  && !(ext %in% urlExtfilter)){
                        ExtLinks<-c(ExtLinks,links[iLinks])
                      }  #/if ( !grepl(domain,links[iLinks]) && !(links[iLinks] %in% ExtLinks) && !(ext %in% urlExtfilter))
                    } #if(ExternalLInks)
                    
                  } # / if( nchar(links[iLinks])<=URLlenlimit) 
                } #/if (!is.na(links[iLinks]))
              } #/for(iLinks in 1:length(links))
            } #/if(length(links)!=0) 
            
            #Sauvegarde du  nombre de liens internes  sur la page parsée
            pkg.env$GraphNodes[t+s-1, "NbIntLinks"]  <- length(IntLinks)
            #Sauvegarde du  nombnre de liens externes  sur la page parsée
            pkg.env$GraphNodes[t+s-1, "NbExtLinks"]  <- length(ExtLinks)          
            
            #Sauvegarde des liens internes dans GraphNodes et GraphEdges :
            
            for(NodeElm in IntLinks) { #Sauvegarde des nouveaux noeuds dans GraphNodes et des liens dans GraphEdges
              
              #Ajout dans le graphe des Nodes des nouvelles pages internes découvertes 
              if (! (NodeElm  %in% pkg.env$GraphNodes[, "Url"] )) {
                NewIndexNodes <- nrow(pkg.env$GraphNodes) + 1
                pkg.env$GraphNodes[NewIndexNodes, "Id"] <- NewIndexNodes
                pkg.env$GraphNodes[NewIndexNodes, "Url"] <- NodeElm  #Récupération de l'URL.
                pkg.env$GraphNodes[NewIndexNodes, "MyStatusPage"] <- "discovered"  #Nouvelle page interne découverte.
                pkg.env$GraphNodes[NewIndexNodes, "Level"] <- LevelOut+1  #Niveau de la page parsée + 1 
                pkg.env$GraphNodes[NewIndexNodes, "PageType"] <- 1 #Page Interne
                pkg.env$GraphNodes[NewIndexNodes, "AreaZoneParsed"] <- AreaZoneParsed #code de zone parsée
              } #/ if (! (NodeElm  %in% pkg.env$GraphNodes[, "Url"] ))
              
              
              #Sauvegarde des liens.
              #position de la page de départ 
              posNodeFrom<-t+s-1 
              #position de la page d arrivée
              posNodeTo <- chmatch(NodeElm, pkg.env$GraphNodes[,"Url"])   
              #Insertion dans la data.frame des liens #type de lien 1 interne - Weight <- 1 (avant:  LevelOut +1)
              pkg.env$GraphEgdes[nrow(pkg.env$GraphEgdes) + 1,]<-c(posNodeFrom,posNodeTo,1,1, AreaZoneParsed)
              
            } #/for(NodeElm in IntLinks)
            
            
            #Insertion des liens externes dans GraphNodes et GraphEdges.
            
            for(NodeElm in ExtLinks){
              #Ajout dans le graphe des Nodes des nouvelles pages externes repérées 
              if (! (NodeElm  %in% pkg.env$GraphNodes[, "Url"] )) {  #Si n'existe pas insérer
                NewIndexNodes <- nrow(pkg.env$GraphNodes) + 1
                pkg.env$GraphNodes[NewIndexNodes, "Id"] <- NewIndexNodes
                pkg.env$GraphNodes[NewIndexNodes, "Url"] <- NodeElm
                pkg.env$GraphNodes[NewIndexNodes, "MyStatusPage"] <- "discovered"  #Nouvelle page externe découverte.
                pkg.env$GraphNodes[NewIndexNodes, "Level"] <- LevelOut+1  #Niveau de la page parsée + 1 
                pkg.env$GraphNodes[NewIndexNodes, "PageType"] <- "2" #Page Externe
                pkg.env$GraphNodes[NewIndexNodes, "AreaZoneParsed"] <- AreaZoneParsed #code de zone parsée
              } #/if (! (NodeElm  %in% pkg.env$GraphNodes[, "Url"] ))
              
              #Sauvegarde des liens.
              #position de la page de départ 
              posNodeFrom<-t+s-1 
              #position de la page d arrivée
              posNodeTo <- chmatch(NodeElm, pkg.env$GraphNodes[,"Url"])  #
              #Insertion dans la data.frame des liens #type de lien 2 externe, Weight <- 1 
              pkg.env$GraphEgdes[nrow(pkg.env$GraphEgdes) + 1,]<-c(posNodeFrom,posNodeTo,1,2, AreaZoneParsed)  
              
              
              
              
            }  #/for(NodeElm in ExtLinks)
            
            
            
            
          } #/if (MaxDepth>=LevelOut && TotalPagesParsed <= MaxPagesParsed &&  pkg.env$GraphNodes[t+s-1, "PageType"]==1)
          
        }   #/if (is.null(allGetResponse[[s]]$status_code)) 
        
        
      } #/if (!is.null(allGetResponse[[s]]))
      
    } #/for (s in 1:length(allGetResponse))
    
    cat("Crawl with GET  :",format(round((t/nrow(pkg.env$GraphNodes)*100), 2),nsmall = 2),"%  : ",t,"to",l,"crawled from ",nrow(pkg.env$GraphNodes)," Parsed:", TotalPagesParsed-1, "\n")
    
    t<-l+1  #Paquet suivant 
    
    #Sauvegarde des données vers l'environnement global.
    
    assign("NetwEdges", pkg.env$GraphEgdes, envir = as.environment(1) )  #Renvoie les données Edges vers env global
    assign("NetwNodes", pkg.env$GraphNodes, envir = as.environment(1) )  #Idem Nodes
    
  } #/while (t<=nrow(pkg.env$graphNodes)
  
  
  
  
  
  
  
  #Arret des clusters.
  
  stopCluster(cl)
  stopImplicitCluster()
  rm(cl)
  
  cat("+ Network nodes plus parameters are stored in a variable named : NetwNodes \n")
  cat("+ Network edges are stored in a variable named : NetwEdges \n")
  
  
} #/NetworkRcrawler



########################################################################################

Lancement du crawl sur Anakeyn.com

On indique tout d’abord la collection de balises que l’on souhaite tester dans une variable. Ici les balises on été traduites au format xPath :

#' 
#
myXPath <- c("//main[@id=\"main\"][@class=\"site-main\"][@role=\"main\"]", "//div[@class=\"entry-content article\"]", 
             "//div[@id=\"ot-main-container\"]", "//div[@id=\"main\"][@class=\"clearfix container\"]") 

Nota bene : le programme teste les balises dans l’ordre. Dès qu’il trouve la balise sur une page il récupère les liens sur cette page dans cette balise et passe à la page suivante sans tester les balises suivantes. On peut ainsi imaginer que l’on indique des balises du plus précis au moins précis si l’on veut récupérer des liens.

Attention toutefois, pour éviter que le crawler ne s’arrête trop tôt, si celui-ci ne trouve pas les balises recherchées, il renvoit tous les liens de la page. Comme le fichier de noeuds et le fichier de liens récupérés comportent une variable indiquant la balise sur laquelle le lien a été trouvé, à charge pour vous de conserver tel ou tel type de noeud ou lien qui vous intéresse.

Indication du site et lancement du Crawler

#
#
myWebsite = "https://www.anakeyn.com"
NetworkRcrawler (Website = myWebsite , no_cores = 8, no_conn = 8, 
          MaxDepth = 10,
          XPathLinksAreaNodes = myXPath, #pour crawler en priorité les zones de contenus contextuels.
          Obeyrobots = FALSE,
          Useragent = "Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko",
          NetwExtLinks = FALSE,
          MaxPagesParsed = 5000)

Ici je souhaitais récupérer tous les liens donc j’ai mis MaxPagesParsed = 5000 pour être sûr. Mais attention cela peut être un peu long.

Nettoyage des pages et des liens.

Nous allons garder uniquement que les pages et des liens qui ont été trouvées dans les balises sélectionnées.

#' 
#
#On ne garde que les noeuds des zones ciblées.
NetwNodes1 <- NetwNodes[!(NetwNodes$AreaZoneParsed==""), ]
str(NetwNodes1)  #pour voir
#on ne garde dans le fichier de noeuds que les noeuds qui sont aussi dans les liens
NetwNodes2 <- NetwNodes1[(NetwNodes1$Id %in% NetwEdges$From) & (NetwNodes1$Id %in% NetwEdges$To),]
str(NetwNodes2) #pour voir
#on ne garde que les liens des zones ciblées.
NetwEdges1 <- NetwEdges[!(NetwEdges$AreaZoneParsed==""), ]
str(NetwEdges1) #pour voir
#on ne garde que les liens dont les zones From et To sont dans les noeuds.
NetwEdges2 <- NetwEdges1[(NetwEdges1$From %in%  NetwNodes2$Id ) & (NetwEdges1$To %in% NetwNodes2$Id  ),]
str(NetwEdges2) #pour voir

Mise au format de Gephi

Afin de pouvoir récupérer les données dans Gephi je transforme les données avant de les sauvegarder au format .csv.
J’enlève aussi le code source des pages pour alléger les fichiers.

#' 
#
nohtmlNetwNodes <- NetwNodes2[,1:12]  #on enlève les codes sources des noeuds pour économiser de la place.
names(NetwEdges2)[1] <- "Source" #Changement du nom de  colonne From en Source 
names(NetwEdges2)[2] <- "Target"  #Changement du nom de  colonne To en Target
write.csv(NetwEdges2, file = "NetwEdgesAZP.csv", row.names=FALSE)
write.csv(nohtmlNetwNodes, file = "nohtmlNetwNodes.csv", row.names=FALSE)

Graphe dans Gephi

Pour l’utilisation de Gephi je vous renvoie à un article précédent : Créer un graphe de site Web avec Gephi

Ici nous allons récupérer le fichier de noeuds « nohtmlNetwNodes.csv » et le fichier de liens « NetwEdgesAZP.csv ».

Dans le graphe suivant, les couleurs indiquent les zones sur lesquels ont été récupérés les liens et la taille des noeuds est fonction du PageRank interne calculé.

L’algorithme de spatialisation choisi est « Force Atlas 2 ».

le site est divisée en 3 zones : en bleu la version actuelle du site, en magenta la version 1, en vert et rouge la version 2.

Graphe Anakeyn zones contextuelles

Comme vous pouvez le constater, même s’il existe plus ou moins 3 zones, le site est encore un peu « foutraque ». Nous sommes encore loin d’une organisation en « cocon sémantique ». Il reste plein de liens entre les différentes zones et chacune des zones ressemble à une « patate ».

Merci pour votre attention,

Si vous avez des remarques et des suggestions n’hésitez pas à les faire en commentaires.

A bientôt,

Pierre

Quitter la version mobile