10  Excel (.xls, .xlsx) Files // Fichiers Excel (.xls, .xlsx)

Author

Daniel Manrique-Castano

Published

December 18, 2025

10.1 Overview // Aperçu

Excel files are complex “containers” (Zip-compressed XML) that can hold much more than raw data.

//

Les fichiers Excel sont des « conteneurs » complexes (XML compressé au format Zip) qui peuvent contenir bien plus que de simples données brutes.

NoteCuration Goal // Objectif de la curation

Unpack the complex structure of Excel spreadsheets. Our objective is to identify hidden content, validate structural integrity, and detect external dependencies that may compromise the file’s future usability. // Décortiquons la structure complexe des feuilles de calcul Excel. Notre objectif est d’identifier le contenu caché, de vérifier l’intégrité structurelle et de détecter les dépendances externes susceptibles de compromettre l’utilisation future du fichier.

WarningIdentifying Risks // Identification des risques

Proprietary macros (VBA), external cell links that break upon transfer, and hidden sheets containing sensitive scratch work or PII are significant threats to data integrity and privacy. // Les macros propriétaires (VBA), les liens vers des cellules externes qui sont rompus lors du transfert et les feuilles masquées contenant des notes de travail sensibles ou des données à caractère personnel constituent des menaces importantes pour l’intégrité et la confidentialité des données.

This notebook combines two approaches:

  1. Structure Scan: Checking for macros, external links, and hidden sheets.
  2. Content Preview: Assessing if the file is a machine-readable table or a formatted report.

//

Ce cahier combine deux approches :

  1. Analyse de la structure : recherche de macros, de liens externes et de feuilles masquées.
  2. Aperçu du contenu : vérification du format du fichier (tableau lisible par une machine ou rapport formaté).

10.2 Setup // Configuration

We use readxl for data reading and tidyxl for deep structural inspection of modern Excel files.

//

Nous utilisons readxl pour la lecture des données et tidyxl pour l’analyse structurelle approfondie des fichiers Excel récents.

10.2.1 R Packages // Packages R

The following R packages are required. If you don’t have these packages, uncomment this code and run it once in your R console:

//

Les packages R suivants sont requis. Si vous ne disposez pas de ces packages, décommentez ce code et exécutez-le une fois dans votre console R :

Code
# install.packages(c("tidyverse", "readxl", "rstudioapi", "tidyxl"))

10.3 Load libraries // Charger les bibliothèques

Code
library(tidyverse)
library(readxl)     # Standard data reader
library(tidyxl)     # Structural inspector (Hidden sheets, formatting)
library(rstudioapi) # For directory selection

10.4 Select a target directory // Sélectionnez un répertoire de destination

This block allows for interactive selection of the image directory. If running in a non-interactive environment, it defaults to the path defined in the YAML header.

//

Ce bloc permet de sélectionner de manière interactive le répertoire contenant les images. En cas d’exécution dans un environnement non interactif, le chemin par défaut est celui défini dans l’en-tête YAML.

Code
# 1. Try to select interactively if in RStudio
if (interactive() && .Platform$OS.type == "windows") { 
  selected_dir <- rstudioapi::selectDirectory(caption = "Select Excel Directory")
} else {
  selected_dir <- NULL
}

# 2. Logic to determine final directory (Interactive vs Parameter)
if (!is.null(selected_dir)) {
  target_dir <- selected_dir
} else {
  target_dir <- params$target_dir
}

print(paste("Analyzing directory:", target_dir))
[1] "Analyzing directory: data/Inspect_xlsx/"

10.5 Find Excel Files // Rechercher les fichiers Excel

Now we scan the selected directory for all .xls and .xlsx files.

//

Nous allons maintenant rechercher tous les fichiers .xls et .xlsx dans le répertoire sélectionné.

Code
# Find all Excel files using a regex pattern
excel_files <- list.files(
  path = target_dir,
  pattern = "\\.xlsx?$", # Matches .xls or .xlsx
  recursive = TRUE,
  full.names = TRUE,
  ignore.case = TRUE
)

# Print the number of files found and show the first few paths
print(paste("Found", length(excel_files), "Excel files."))
[1] "Found 3 Excel files."
Code
head(excel_files)
[1] "data/Inspect_xlsx//41467_2025_65055_MOESM9_ESM.xlsx"
[2] "data/Inspect_xlsx//41467_2025_65141_MOESM4_ESM.xlsx"
[3] "data/Inspect_xlsx//41467_2025_65179_MOESM4_ESM.xlsx"

10.6 Structural and content inspection // Analyse structurelle et du contenu

In this section, we iterate through each file. For .xlsx files, we perform a deep scan for hidden content. For legacy .xls, we rely on basic extraction.

//

Dans cette section, nous parcourons chaque fichier. Pour les fichiers .xlsx, nous effectuons une analyse approfondie à la recherche de contenu masqué. Pour les anciens fichiers .xls, nous nous contentons d’une extraction de base.

Code
message("Generating Excel Structure Report...")

# Helper: Analyze Structure (Hidden Sheets, Links)
analyze_structure <- function(fp) {
  is_modern <- grepl("\\.xlsx$|\\.xlsm$", fp, ignore.case = TRUE)
  is_macro  <- grepl("\\.xlsm$", fp, ignore.case = TRUE)
  
  hidden_sheets <- "Unknown (Legacy)"
  has_ext_links <- "Unknown (Legacy)"
  
  if (is_modern) {
    tryCatch({
      # tidyxl allows us to see sheet state (visible/hidden)
      sheet_meta <- tidyxl::xlsx_sheet_names(fp)
      hidden_count <- sum(sheet_meta$state != "visible")
      hidden_sheets <- if (hidden_count > 0) paste0(hidden_count, " Hidden") else "None"
      
      # Check for external links (naive check in cells is slow, sticking to sheets/macros)
      # We could scan formulas, but for speed we focus on sheets here.
    }, error = function(e) {
      hidden_sheets <- "Scan Failed"
    })
  }
  
  return(list(
    Macros = is_macro,
    Hidden = hidden_sheets,
    Format = if (is_modern) "XML (Modern)" else "Binary (Legacy)"
  ))
}

# Main Loop
all_sheets_summary <- list()

for (file_path in excel_files) {
  file_name <- basename(file_path)
  
  # 1. Structural Scan
  struct <- analyze_structure(file_path)
  
  tryCatch({
    sheet_names <- readxl::excel_sheets(file_path)
    
    if (length(sheet_names) == 0) {
      all_sheets_summary[[length(all_sheets_summary) + 1]] <- tibble(
        FileName = file_name, SheetName = "(No Sheets)", Dimensions = "0 x 0",
        HeaderPreview = "(Empty)", IsMacro = struct$Macros, 
        HiddenSheets = struct$Hidden, Format = struct$Format, Status = "Empty"
      )
      next
    }
    
    # 2. Content Scan (Sheet by Sheet)
    for (sheet in sheet_names) {
      tryCatch({
        # Read minimal data for dimensions
        # Suppress messages about 'new names'
        full_data <- suppressMessages(read_excel(file_path, sheet = sheet, .name_repair = "minimal"))
        dims <- paste(dim(full_data), collapse = " x ")
        
        # Read header for preview
        preview <- suppressMessages(read_excel(file_path, sheet = sheet, n_max = 5, .name_repair = "minimal"))
        col_names <- colnames(preview)
        
        # Heuristic: Is it non-tabular? (Default columns ...1, ...2 or single column)
        is_messy <- any(grepl("^\\.\\.\\.\\d+$", col_names)) || length(col_names) <= 1
        
        header_str <- paste(head(col_names, 5), collapse = " | ")
        if (length(col_names) > 5) header_str <- paste(header_str, "...")
        
        all_sheets_summary[[length(all_sheets_summary) + 1]] <- tibble(
          FileName = file_name,
          SheetName = sheet,
          Dimensions = dims,
          HeaderPreview = header_str,
          IsMacro = struct$Macros,
          HiddenSheets = struct$Hidden,
          Format = struct$Format,
          LikelyNonTabular = is_messy,
          Status = "Success"
        )
        
      }, error = function(e) {
        all_sheets_summary[[length(all_sheets_summary) + 1]] <- tibble(
          FileName = file_name, SheetName = sheet, Dimensions = "Error",
          HeaderPreview = e$message, IsMacro = struct$Macros, 
          HiddenSheets = struct$Hidden, Format = struct$Format, 
          LikelyNonTabular = TRUE, Status = "Read Failed"
        )
      })
    }
  }, error = function(e) {
     all_sheets_summary[[length(all_sheets_summary) + 1]] <- tibble(
        FileName = file_name, SheetName = "Error", Dimensions = "Error",
        HeaderPreview = e$message, IsMacro = struct$Macros, 
        HiddenSheets = struct$Hidden, Format = struct$Format, 
        LikelyNonTabular = TRUE, Status = "File Failed"
      )
  })
}

summary_report <- bind_rows(all_sheets_summary)

# Display preview
print("--- Excel Structure Report ---")
[1] "--- Excel Structure Report ---"
Code
head(summary_report)
# A tibble: 6 × 9
  FileName        SheetName Dimensions HeaderPreview IsMacro HiddenSheets Format
  <chr>           <chr>     <chr>      <chr>         <lgl>   <chr>        <chr> 
1 41467_2025_650… Fig2a-FA  48 x 5     White matter… FALSE   Unknown (Le… XML (…
2 41467_2025_650… Fig2b-FA  11042 x 3  Count_Greate… FALSE   Unknown (Le… XML (…
3 41467_2025_650… Fig2a-GM  139 x 5    Grey matter … FALSE   Unknown (Le… XML (…
4 41467_2025_650… Fig2b-GMV 11042 x 3  Count_Greate… FALSE   Unknown (Le… XML (…
5 41467_2025_650… Fig3b     82 x 8     Category | G… FALSE   Unknown (Le… XML (…
6 41467_2025_650… Fig3c     11 x 10    Category | I… FALSE   Unknown (Le… XML (…
# ℹ 2 more variables: LikelyNonTabular <lgl>, Status <chr>

10.7 Save results // Enregistrer les résultats

Code
output_dir <- "Results/Inspect_xlsx"
dir.create(output_dir, recursive = TRUE, showWarnings = FALSE)

output_file <- file.path(output_dir, paste0("Excel_Structure_", format(Sys.Date(), "%Y%m%d"), ".csv"))

write.csv(summary_report, output_file, row.names = FALSE)
print(paste("Report saved to:", output_file))
[1] "Report saved to: Results/Inspect_xlsx/Excel_Structure_20260703.csv"

10.8 Curation Insights // Aperçu de la curation

Use the generated report to identify high-risk files:

  • Macros (IsMacro = TRUE): Macros are a risk for security and prone to obsolescence. .xlsm files contain code that may not run in the future or may be blocked by security software. Curators can ask depositors to remove macros and save as .xlsx unless the code is essential (in which case, the code can be documented and stored as text).

  • Hidden Sheets: Hidden sheets often contain PII, messy scratchpads, or deprecated versions of the data. Curators can inspect these sheets manually, and if they are not needed, delete them to clean the dataset.

  • Likely Non-Tabular (LikelyNonTabular = TRUE): This implies that the sheet is likely a report, chart, or form, rather than a raw data table. These sheets generally cannot be converted to CSV automatically and require manual review. To keep tabular data tidy, graphs and images should be removed and saved separately.

//

Utilisez le rapport généré pour identifier les fichiers à haut risque :

  • Macros (IsMacro = TRUE) : Les macros constituent un risque pour la sécurité et sont susceptibles de devenir obsolètes. Les fichiers .xlsm contiennent du code qui pourrait ne plus fonctionner à l’avenir ou être bloqué par des logiciels de sécurité. Les conservateurs peuvent demander aux déposants de supprimer les macros et d’enregistrer les fichiers au format .xlsx, sauf si le code est essentiel (auquel cas, il peut être documenté et stocké sous forme de texte).

  • Feuilles masquées : Les feuilles masquées contiennent souvent des informations personnelles identifiables, des brouillons désordonnés ou des versions obsolètes des données. Les conservateurs peuvent inspecter ces feuilles manuellement et, si elles ne sont pas nécessaires, les supprimer pour nettoyer l’ensemble de données.

  • Probablement non tabulaire (LikelyNonTabular = TRUE) : Cela signifie que la feuille est probablement un rapport, un graphique ou un formulaire, plutôt qu’un tableau de données brutes. Ces feuilles ne peuvent généralement pas être converties automatiquement au format CSV et nécessitent une vérification manuelle. Pour que les données tabulaires restent ordonnées, les graphiques et les images doivent être supprimés et enregistrés séparément.


10.9 Additional Tools // Outils supplémentaires

  • LibreOffice Calc: Excellent open-source tool for batch converting legacy .xls files to modern open formats (.ods or .xlsx) via command line.

  • OpenRefine: is a tool for cleaning messy tabular data (e.g., untangling merged cells or fixing inconsistent headers).

  • VBA Remover: There are a set of different tools to strip VBA from Excel files to sanitize them. Several are available on-line.

//

  • LibreOffice Calc : excellent outil open source permettant de convertir par lots des fichiers .xls hérités vers des formats ouverts modernes (.ods ou .xlsx) via la ligne de commande.

  • OpenRefine : est un outil permettant de nettoyer des données tabulaires désordonnées (par exemple, démêler des cellules fusionnées ou corriger des en-têtes incohérents).

  • VBA Remover : il existe toute une série d’outils permettant de supprimer le VBA des fichiers Excel afin de les assainir. Plusieurs d’entre eux sont disponibles en ligne.

10.10 Using the Non-Interactive R Script // Utilisation du script R non interactif

For users who want to run this analysis on a server, in a batch job, or from the command line, here is a pure R script that performs the same process.

Download the R Script: Inspect_xlsx_Script.R

//

Pour les utilisateurs qui souhaitent exécuter cette analyse sur un serveur, dans le cadre d’un traitement par lots ou depuis la ligne de commande, voici un script R pur qui effectue le même processus.

Télécharger le script R : Inspect_xlsx_Script.R

10.10.1 Example HPC Submission Script // Exemple de script de soumission HPC

#!/bin/bash
#SBATCH --job-name=excel_check
#SBATCH --nodes=1
#SBATCH --ntasks=1
#SBATCH --time=00:30:00
#SBATCH --mem=8G
#SBATCH --output=logs/excel_check_%j.log

module load R

# Define target directory
TARGET_DIR="/scratch/user/project_data/financials"

# Prepare folders
mkdir -p Results/Inspect_xlsx
mkdir -p logs

# Run
echo "Starting Excel Inspection on $TARGET_DIR"
Rscript Inspect_xlsx_Script.R "$TARGET_DIR"