Aller au contenu

Optimisation des basiques SEO avec Python

Cet article fait suite à celui-ci, qui permet de lancer et récupérer des fichiers produits par Screaming Frog (Internal HTML en particulier).

(Disclaimer : je ne prétends pas réinventer la poudre : d’autres l’ont déjà fait)

En l’occurrence, on va commencer à interpréter ces résultats et produire des livrables sous Excel pour la todolist du client.

Je commence par garder une partie des colonnes qui m’intéressent :

columns_to_keep = [
        "Adresse", "Type de contenu", "Code HTTP", "Title 1", "Longueur du Title 1", "Largeur en pixels du Title 1", "Meta Description 1", "Longueur de la Meta Description 1", "Largeur en pixels de la Meta Description 1", "H1-1", "Longueur du H1-1", "H1-2", "Longueur du H1-2", "Meta Robots 1", "Meta Refresh 1", "Élément de lien en version canonique 1", "Nombre de mots", "Ratio texte", "Crawl profondeur", "Profondeur du dossier", "Liens entrants", "Liens entrants uniques", "Liens entrants JS uniques", "% du total", "Liens sortants", "Liens sortants uniques", "Liens sortants JS uniques", "Liens sortants externes", "Liens sortants externes uniques", "Liens sortants JS externes uniques", "URL de redirection", "Type de redirection", "Adresse codée en URL"
    ]

Bien évidemment, c’est à vous de sélectionner parmi la liste des colonnes ce qui vous intéresse, ici j’ai volontairement conservé une liste de colonne assez vaste pour que vous voyiez qu’on peut vraiment tout récupérer.

Et si votre système est en langue anglaise, les colonnes n’auront pas le même nom… Pensez donc à actualiser cette liste !

Ensuite, comme je reste dans l’environnement de l’article précédent, j’ai un fichier hosts.txt qui va déterminer l’emplacement des fichiers importants (puisque ma structure est la suivante):

hosts.txt (liste de sites)
/{host}/
/{host}/SFDatas/
/{host}/SFDatas/internal_html.csv

Et du coup l’idée c’est de traiter le fichier csv pour avoir une liste de recommandations pour les basiques SEO, et ne pas perdre de temps à exécuter cette partie quand on peut complètement l’automatiser 😄

Je crée également une fonction pour traiter le fichier internal

# cwd : current working directory

def internal_process(host, cwd, columns_to_keep):
    if host:
        file_path = find_file(os.path.join(cwd, host), "interne_html.csv")
    else:
        print(f"Host vide, exit : {host}")
        return
    if not file_path:
        return
    
    df = pd.read_csv(file_path)
    available_columns = [col for col in columns_to_keep if col in df.columns]
    if not available_columns:
        print(f"Aucune des colonnes spécifiées n'est présente dans le fichier : {file_path}")
        return
    
    df = df[available_columns]
    dirpath = os.path.dirname(file_path)
    output_file_path = os.path.join(dirpath, f'{host}_filtré.csv')
    stats_output_file = os.path.join(dirpath, f'{host}_statistiques.csv')
    comments_output_file = os.path.join(dirpath, f'{host}_commentaires.csv')
    df.to_csv(output_file_path, index=False)
    print(f"Le fichier filtré a été sauvegardé sous le nom {output_file_path}")
    df = process_internal_html(df)
    
    stats_df = pd.DataFrame({
        'Code HTTP': df['Code HTTP'].value_counts() if 'Code HTTP' in df.columns else [],
        'Crawl profondeur': df['Crawl profondeur'].value_counts() if 'Crawl profondeur' in df.columns else [],
        'duplication title H1': df['duplication title H1'].value_counts() if 'duplication title H1' in df.columns else [],
        'taille de H1': df['taille de H1'].value_counts() if 'taille de H1' in df.columns else [],
        'unicité de H1': df['unicité de H1'].value_counts() if 'unicité de H1' in df.columns else [],
        'H1 manquant': df['H1 manquant'].value_counts() if 'H1 manquant' in df.columns else [],
        'taille de Title': df['taille de Title'].value_counts() if 'taille de Title' in df.columns else [],
        'unicité de Title': df['unicité de Title'].value_counts() if 'unicité de Title' in df.columns else [],
        'Title manquant': df['Title manquant'].value_counts() if 'Title manquant' in df.columns else [],
        'taille de Meta Description': df['taille de Meta Description'].value_counts() if 'taille de Meta Description' in df.columns else [],
        'unicité de Meta Description': df['unicité de Meta Description'].value_counts() if 'unicité de Meta Description' in df.columns else [],
        'Meta Description manquante': df['Meta Description manquante'].value_counts() if 'Meta Description manquante' in df.columns else [],
    })
    stats_df.to_csv(stats_output_file)
    print(f"Le fichier de statistiques a été sauvegardé sous le nom {stats_output_file}")
    comments_df = df[['Adresse'] + [col for col in df.columns if col.startswith('code de réponse') or
                                                    col.startswith('profondeur de crawl') or
                                                    col.startswith('duplication title H1') or
                                                    col.startswith('taille de H1') or
                                                    col.startswith('unicité de H1') or
                                                    col.startswith('H1 manquant') or
                                                    col.startswith('taille de Title') or
                                                    col.startswith('unicité de Title') or
                                                    col.startswith('Title manquant') or
                                                    col.startswith('taille de Meta Description') or
                                                    col.startswith('unicité de Meta Description') or
                                                    col.startswith('Meta Description manquante')]]
    comments_df = deduplicate(comments_df, 'Adresse')
    comments_df.to_csv(comments_output_file, index=False)
    print(f"Le fichier de commentaires a été sauvegardé sous le nom {comments_output_file}")
    
    return comments_output_file, stats_output_file



def process_internal_html(df):
    df['code de réponse'] = ""
    df['profondeur de crawl'] = ""
    df['duplication title H1'] = ""
    df['taille de H1'] = ""
    df['unicité de H1'] = ""
    df['H1 manquant'] = ""
    df['taille de Title'] = ""
    df['unicité de Title'] = ""
    df['Title manquant'] = ""
    df['taille de Meta Description'] = ""
    df['unicité de Meta Description'] = ""
    df['Meta Description manquante'] = ""
    df['Meta Robots'] = ""
    if 'Code HTTP' in df.columns:
        df.loc[(df['Code HTTP'] >= 200) & (df['Code HTTP'] < 300), 'code de réponse'] = ''
        df.loc[(df['Code HTTP'] >= 300) & (df['Code HTTP'] < 400), 'code de réponse'] = 'Redirection'
        df.loc[(df['Code HTTP'] >= 400) & (df['Code HTTP'] < 500), 'code de réponse'] = 'Erreur 4xx'
        df.loc[(df['Code HTTP'] >= 500) & (df['Code HTTP'] < 600), 'code de réponse'] = 'Erreur 5xx'
    if 'Crawl profondeur' in df.columns:
        df.loc[(df['Crawl profondeur'] == 4) | (df['Crawl profondeur'] == 5), 'profondeur de crawl'] = 'url peu accessible'
        df.loc[df['Crawl profondeur'] >= 6, 'profondeur de crawl'] = 'url trop profonde'
    if 'Title 1' in df.columns and 'H1-1' in df.columns:
        df.loc[df['Title 1'] == df['H1-1'], 'duplication title H1'] = 'Titre identique à H1'
    if 'Longueur du H1-1' in df.columns:
        df.loc[df['Longueur du H1-1'] > 70, 'taille de H1'] = 'H1 trop long'
        df.loc[df['Longueur du H1-1'] < 30, 'taille de H1'] = 'H1 trop court'
    if 'H1-1' in df.columns:
        df.loc[df['H1-1'].duplicated(keep=False), 'unicité de H1'] = 'H1 non unique'
        df.loc[df['H1-1'].isnull(), 'H1 manquant'] = 'H1 vide'
    if 'Longueur du Title 1' in df.columns:
        df.loc[df['Longueur du Title 1'] > 60, 'taille de Title'] = 'Title trop long'
        df.loc[df['Longueur du Title 1'] < 40, 'taille de Title'] = 'Title trop court'
    if 'Title 1' in df.columns:
        df.loc[df['Title 1'].duplicated(keep=False), 'unicité de Title'] = 'Title non unique'
        df.loc[df['Title 1'].isnull(), 'Title manquant'] = 'Title vide'
    if 'Longueur de la Meta Description 1' in df.columns:
        df.loc[df['Longueur de la Meta Description 1'] > 160, 'taille de Meta Description'] = 'Meta Description trop longue'
        df.loc[df['Longueur de la Meta Description 1'] < 120, 'taille de Meta Description'] = 'Meta Description trop courte'
    if 'Meta Description 1' in df.columns:
        df.loc[df['Meta Description 1'].duplicated(keep=False), 'unicité de Meta Description'] = 'Meta Description non unique'
        df.loc[df['Meta Description 1'].isnull(), 'Meta Description manquante'] = 'Meta Description vide'
    if 'Meta Robots 1' in df.columns:
        df.loc[df['Meta Robots 1'] == "noindex", 'Meta Robots'] = 'noindex'
    
    return df

Globalement, que fait cette fonction ?

Elle crée un fichier {host}_commentaires.csv (host étant le nom du site) qui commente les éléments suivants:

  • les codes de réponses
  • la profondeur des pages
  • la taille, présence, unicité des H1, Title, & Meta Description
  • La duplication des Title & H1

Et un fichier {host}_statistiques.csv qui rassemble les statistiques de tous les éléments commentés.

Ensuite, je vais traiter le fichier all_outlinks.csv avec quelques fonctions :

def outlinks_process(host, cwd, columns_to_keep):
    if host:
        file_path_internal = find_file(os.path.join(cwd, host), "interne_html.csv")
        file_path_links = find_file(os.path.join(cwd, host), "tous_les_liens_sortants.csv")
    else:
        print(f"Host vide, exit : {host}")
        return
    if not file_path_internal or not file_path_links:
        return
    
    df_internal = pd.read_csv(file_path_internal)
    df_internal = get_redir_desti(df_internal)
    
    df_links = pd.read_csv(file_path_links)
    df_links = df_links[df_links.apply(lambda row: extract_host(row['Destination']) == host and extract_host(row['Source']) == host, axis=1)]
    df_links = process_outgoing_links(df_links, df_internal, host)
    
    dirpath = os.path.dirname(file_path_internal)
    output_file_path_links = os.path.join(dirpath, f'{host}_liens_sortants_filtres.csv')
    df_links.to_csv(output_file_path_links, index=False)
    print(f"Le fichier des liens sortants filtré a été sauvegardé sous le nom {output_file_path_links}")
    
    return output_file_path_links


def process_outgoing_links(df_links, df_internal, host):
    columns_to_keep_links = [
        "Type", "Source", "Destination", "Texte Alt", "Ancrage", "Code de statut", "Suivre", "Chemin du lien", "Position du lien", "Origine du lien"
    ]
    df_links = df_links[columns_to_keep_links]
    df_links = df_links[df_links['Type'] == 'Hyperlien']
    df_links = df_links[~df_links['Code de statut'].between(200, 299)]
    df_links = df_links[df_links.apply(lambda row: extract_host(row['Destination']) == host and extract_host(row['Source']) == host, axis=1)]
    df_links['Commentaire'] = ""
    df_links = df_links.merge(df_internal[['Adresse', 'URL de redirection']], left_on='Destination', right_on='Adresse', how='left')
    df_links.loc[df_links['Code de statut'].between(300, 399), 'Commentaire'] = \
        df_links.apply(lambda row: f"Redirections : maillez directement l'url cible {row['URL de redirection']} de la redirection dans {row['Source']}", axis=1)
    df_links.loc[df_links['Code de statut'].between(400, 499), 'Commentaire'] = \
        'Erreurs, corrigez le lien, remettez l\'url ou redirigez la'
    df_links.loc[df_links['Code de statut'].between(500, 599), 'Commentaire'] = \
        'Erreurs serveurs, à investiguer'
    df_links = df_links[['Source', 'Destination', 'Texte Alt', 'Ancrage', 'Code de statut', 'Commentaire']]
    return df_links

Que fait ce code ? Il crée un fichier “liens sortants filtrés” qui ne conserve que les liens en 3xx, 4xx et 5xx et les commente pour indiquer les actions à prendre :

Sur les 3xx il indique la cible de la redirection pour pointer vers la page à mailler au lieu de celle en redirection en interne du site.

Sur les 4xx et les 5xx, il informe que le lien est cassé.

Enfin, un code en plus permet de regrouper les informations importantes sur un Excel :

def assemble_to_excel(to_assemble, cwd, host, titles_to_assemble):
    foodict = {}
    for i in range(len(to_assemble)):
        if to_assemble[i] is None:
            print(f"Le fichier {titles_to_assemble[i]} n'existe pas.")
            return
        else:
            foodict[titles_to_assemble[i]] = pd.read_csv(to_assemble[i])
    
    output_excel_path = os.path.join(cwd, host, f'{host}_assembled.xlsx')
    
    with pd.ExcelWriter(output_excel_path) as writer:
        for sheet_name, df in foodict.items():
            df.to_excel(writer, sheet_name=sheet_name, index=False)
    
    print(f"Les fichiers CSV ont été assemblés dans le fichier {output_excel_path}")

En l’occurrence, l’idée, c’est que le fichier des recommandations et commentaires soit plus accessible et lisible pour l’utilisateur final. Il est donc livré directement dans le dossier {host}.

Le code complet :

import pandas as pd
import os
from urllib.parse import urlparse

def find_file(root_directory, filename):
    if not filename or not root_directory:
        print("Le nom du fichier ou le répertoire racine est invalide")
        return None
    for dirpath, dirnames, filenames in os.walk(root_directory):
        if filename in filenames:
            return os.path.join(dirpath, filename)
    print(f"Le fichier {filename} n'a pas été trouvé dans {root_directory}")
    return None

def process_internal_html(df):
    df['code de réponse'] = ""
    df['profondeur de crawl'] = ""
    df['duplication title H1'] = ""
    df['taille de H1'] = ""
    df['unicité de H1'] = ""
    df['H1 manquant'] = ""
    df['taille de Title'] = ""
    df['unicité de Title'] = ""
    df['Title manquant'] = ""
    df['taille de Meta Description'] = ""
    df['unicité de Meta Description'] = ""
    df['Meta Description manquante'] = ""
    df['Meta Robots'] = ""
    if 'Code HTTP' in df.columns:
        df.loc[(df['Code HTTP'] >= 200) & (df['Code HTTP'] < 300), 'code de réponse'] = ''
        df.loc[(df['Code HTTP'] >= 300) & (df['Code HTTP'] < 400), 'code de réponse'] = 'Redirection'
        df.loc[(df['Code HTTP'] >= 400) & (df['Code HTTP'] < 500), 'code de réponse'] = 'Erreur 4xx'
        df.loc[(df['Code HTTP'] >= 500) & (df['Code HTTP'] < 600), 'code de réponse'] = 'Erreur 5xx'
    if 'Crawl profondeur' in df.columns:
        df.loc[(df['Crawl profondeur'] == 4) | (df['Crawl profondeur'] == 5), 'profondeur de crawl'] = 'url peu accessible'
        df.loc[df['Crawl profondeur'] >= 6, 'profondeur de crawl'] = 'url trop profonde'
    if 'Title 1' in df.columns and 'H1-1' in df.columns:
        df.loc[df['Title 1'] == df['H1-1'], 'duplication title H1'] = 'Titre identique à H1'
    if 'Longueur du H1-1' in df.columns:
        df.loc[df['Longueur du H1-1'] > 70, 'taille de H1'] = 'H1 trop long'
        df.loc[df['Longueur du H1-1'] < 30, 'taille de H1'] = 'H1 trop court'
    if 'H1-1' in df.columns:
        df.loc[df['H1-1'].duplicated(keep=False), 'unicité de H1'] = 'H1 non unique'
        df.loc[df['H1-1'].isnull(), 'H1 manquant'] = 'H1 vide'
    if 'Longueur du Title 1' in df.columns:
        df.loc[df['Longueur du Title 1'] > 60, 'taille de Title'] = 'Title trop long'
        df.loc[df['Longueur du Title 1'] < 40, 'taille de Title'] = 'Title trop court'
    if 'Title 1' in df.columns:
        df.loc[df['Title 1'].duplicated(keep=False), 'unicité de Title'] = 'Title non unique'
        df.loc[df['Title 1'].isnull(), 'Title manquant'] = 'Title vide'
    if 'Longueur de la Meta Description 1' in df.columns:
        df.loc[df['Longueur de la Meta Description 1'] > 160, 'taille de Meta Description'] = 'Meta Description trop longue'
        df.loc[df['Longueur de la Meta Description 1'] < 120, 'taille de Meta Description'] = 'Meta Description trop courte'
    if 'Meta Description 1' in df.columns:
        df.loc[df['Meta Description 1'].duplicated(keep=False), 'unicité de Meta Description'] = 'Meta Description non unique'
        df.loc[df['Meta Description 1'].isnull(), 'Meta Description manquante'] = 'Meta Description vide'
    if 'Meta Robots 1' in df.columns:
        df.loc[df['Meta Robots 1'] == "noindex", 'Meta Robots'] = 'noindex'
    
    return df

def internal_process(host, cwd, columns_to_keep):
    if host:
        file_path = find_file(os.path.join(cwd, host), "interne_html.csv")
    else:
        print(f"Host vide, exit : {host}")
        return
    if not file_path:
        return
    
    df = pd.read_csv(file_path)
    available_columns = [col for col in columns_to_keep if col in df.columns]
    if not available_columns:
        print(f"Aucune des colonnes spécifiées n'est présente dans le fichier : {file_path}")
        return
    
    df = df[available_columns]
    dirpath = os.path.dirname(file_path)
    output_file_path = os.path.join(dirpath, f'{host}_filtré.csv')
    stats_output_file = os.path.join(dirpath, f'{host}_statistiques.csv')
    comments_output_file = os.path.join(dirpath, f'{host}_commentaires.csv')
    df.to_csv(output_file_path, index=False)
    print(f"Le fichier filtré a été sauvegardé sous le nom {output_file_path}")
    df = process_internal_html(df)
    
    stats_df = pd.DataFrame({
        'Code HTTP': df['Code HTTP'].value_counts() if 'Code HTTP' in df.columns else [],
        'Crawl profondeur': df['Crawl profondeur'].value_counts() if 'Crawl profondeur' in df.columns else [],
        'duplication title H1': df['duplication title H1'].value_counts() if 'duplication title H1' in df.columns else [],
        'taille de H1': df['taille de H1'].value_counts() if 'taille de H1' in df.columns else [],
        'unicité de H1': df['unicité de H1'].value_counts() if 'unicité de H1' in df.columns else [],
        'H1 manquant': df['H1 manquant'].value_counts() if 'H1 manquant' in df.columns else [],
        'taille de Title': df['taille de Title'].value_counts() if 'taille de Title' in df.columns else [],
        'unicité de Title': df['unicité de Title'].value_counts() if 'unicité de Title' in df.columns else [],
        'Title manquant': df['Title manquant'].value_counts() if 'Title manquant' in df.columns else [],
        'taille de Meta Description': df['taille de Meta Description'].value_counts() if 'taille de Meta Description' in df.columns else [],
        'unicité de Meta Description': df['unicité de Meta Description'].value_counts() if 'unicité de Meta Description' in df.columns else [],
        'Meta Description manquante': df['Meta Description manquante'].value_counts() if 'Meta Description manquante' in df.columns else [],
    })
    stats_df.to_csv(stats_output_file)
    print(f"Le fichier de statistiques a été sauvegardé sous le nom {stats_output_file}")
    comments_df = df[['Adresse'] + [col for col in df.columns if col.startswith('code de réponse') or
                                                    col.startswith('profondeur de crawl') or
                                                    col.startswith('duplication title H1') or
                                                    col.startswith('taille de H1') or
                                                    col.startswith('unicité de H1') or
                                                    col.startswith('H1 manquant') or
                                                    col.startswith('taille de Title') or
                                                    col.startswith('unicité de Title') or
                                                    col.startswith('Title manquant') or
                                                    col.startswith('taille de Meta Description') or
                                                    col.startswith('unicité de Meta Description') or
                                                    col.startswith('Meta Description manquante')]]
    comments_df = deduplicate(comments_df, 'Adresse')
    comments_df.to_csv(comments_output_file, index=False)
    print(f"Le fichier de commentaires a été sauvegardé sous le nom {comments_output_file}")
    
    return comments_output_file, stats_output_file

def extract_host(url):
    parsed_url = urlparse(url)
    return parsed_url.netloc

def process_outgoing_links(df_links, df_internal, host):
    columns_to_keep_links = [
        "Type", "Source", "Destination", "Texte Alt", "Ancrage", "Code de statut", "Suivre", "Chemin du lien", "Position du lien", "Origine du lien"
    ]
    df_links = df_links[columns_to_keep_links]
    df_links = df_links[df_links['Type'] == 'Hyperlien']
    df_links = df_links[~df_links['Code de statut'].between(200, 299)]
    df_links = df_links[df_links.apply(lambda row: extract_host(row['Destination']) == host and extract_host(row['Source']) == host, axis=1)]
    df_links['Commentaire'] = ""
    df_links = df_links.merge(df_internal[['Adresse', 'URL de redirection']], left_on='Destination', right_on='Adresse', how='left')
    df_links.loc[df_links['Code de statut'].between(300, 399), 'Commentaire'] = \
        df_links.apply(lambda row: f"Redirections : maillez directement l'url cible {row['URL de redirection']} de la redirection dans {row['Source']}", axis=1)
    df_links.loc[df_links['Code de statut'].between(400, 499), 'Commentaire'] = \
        'Erreurs, corrigez le lien, remettez l\'url ou redirigez la'
    df_links.loc[df_links['Code de statut'].between(500, 599), 'Commentaire'] = \
        'Erreurs serveurs, à investiguer'
    df_links = df_links[['Source', 'Destination', 'Texte Alt', 'Ancrage', 'Code de statut', 'Commentaire']]
    return df_links

def deduplicate(df, column):
    if column in df.columns:
        df = df.drop_duplicates(subset=[column], keep='first')
    return df

def get_redir_desti(df_internal):
    necessary_columns = ['Adresse', 'Code HTTP', 'URL de redirection']
    available_columns = [col for col in necessary_columns if col in df_internal.columns]
    
    if len(available_columns) != len(necessary_columns):
        missing_columns = set(necessary_columns) - set(available_columns)
        print(f"Les colonnes suivantes sont manquantes dans le DataFrame : {missing_columns}")
        return pd.DataFrame()
    
    df_internal = df_internal.dropna(subset=['URL de redirection'])
    return df_internal[available_columns]

def outlinks_process(host, cwd, columns_to_keep):
    if host:
        file_path_internal = find_file(os.path.join(cwd, host), "interne_html.csv")
        file_path_links = find_file(os.path.join(cwd, host), "tous_les_liens_sortants.csv")
    else:
        print(f"Host vide, exit : {host}")
        return
    if not file_path_internal or not file_path_links:
        return
    
    df_internal = pd.read_csv(file_path_internal)
    df_internal = get_redir_desti(df_internal)
    
    df_links = pd.read_csv(file_path_links)
    df_links = df_links[df_links.apply(lambda row: extract_host(row['Destination']) == host and extract_host(row['Source']) == host, axis=1)]
    df_links = process_outgoing_links(df_links, df_internal, host)
    
    dirpath = os.path.dirname(file_path_internal)
    output_file_path_links = os.path.join(dirpath, f'{host}_liens_sortants_filtres.csv')
    df_links.to_csv(output_file_path_links, index=False)
    print(f"Le fichier des liens sortants filtré a été sauvegardé sous le nom {output_file_path_links}")
    
    return output_file_path_links

def assemble_to_excel(to_assemble, cwd, host, titles_to_assemble):
    foodict = {}
    for i in range(len(to_assemble)):
        if to_assemble[i] is None:
            print(f"Le fichier {titles_to_assemble[i]} n'existe pas.")
            return
        else:
            foodict[titles_to_assemble[i]] = pd.read_csv(to_assemble[i])
    
    output_excel_path = os.path.join(cwd, host, f'{host}_assembled.xlsx')
    
    with pd.ExcelWriter(output_excel_path) as writer:
        for sheet_name, df in foodict.items():
            df.to_excel(writer, sheet_name=sheet_name, index=False)
    
    print(f"Les fichiers CSV ont été assemblés dans le fichier {output_excel_path}")

def main():
    hosts_file = "hosts.txt"
    cwd = os.getcwd()
    columns_to_keep = [
        "Adresse", "Type de contenu", "Code HTTP", "Title 1", "Longueur du Title 1", "Largeur en pixels du Title 1",
        "Meta Description 1", "Longueur de la Meta Description 1", "Largeur en pixels de la Meta Description 1",
        "H1-1", "Longueur du H1-1", "H1-2", "Longueur du H1-2", "Meta Robots 1", "Meta Refresh 1", "Élément de lien en version canonique 1",
        "Nombre de mots", "Ratio texte", "Crawl profondeur", "Profondeur du dossier", "Liens entrants", "Liens entrants uniques",
        "Liens entrants JS uniques", "% du total", "Liens sortants", "Liens sortants uniques", "Liens sortants JS uniques", "Liens sortants externes",
        "Liens sortants externes uniques", "Liens sortants JS externes uniques", "URL de redirection", "Type de redirection", "Adresse codée en URL"
    ]
    
    with open(hosts_file, "r") as file:
        hosts = file.read().splitlines()
    
    for host in hosts:
        comments_output_file, stats_output_file = internal_process(host, cwd, columns_to_keep)
        output_file_path_links = outlinks_process(host, cwd, columns_to_keep)
        to_assemble = [comments_output_file, stats_output_file, output_file_path_links]
        titles_to_assemble = ['Commentaires', 'Statistiques', 'Liens sortants']
        assemble_to_excel(to_assemble, cwd, host, titles_to_assemble)

if __name__ == "__main__":
    main()

Et voilà !

Bien évidemment, vous pouvez corriger ce code, l’améliorer, pour ajouter des instructions pour les clients qui verront le livrable.