Blog

Como buscar links dentro de documentos de MS Office con PowerShell

Alguna vez te has visto en la necesidad de verificar si tus documentos de Microsoft Office contienen links apuntando a rutas locales? Yo tampoco, pero hace unos días un usuario, el cual estaba moviendo carpetas y archivos hacia un nuevo folder de red, envió un correo preguntando si era posible saber cuales archivos tienen links de este tipo, ya que, al mover los documentos hacia otras carpetas los links se romperán . Ahora no suena tan loco, cierto?

Recuerdo que hace unos meses un amigo me comento que ellos tuvieron un problema similar, creo que en la empresa donde el trabaja, migraron documentos a otro sistema de archivos y los links dentro de los archivos obviamente se rompieron, lo mismo paso con correos que tenían links hacia el sistema de archivos viejo.

Pues bien, haciendo uso de mi mejor “Skill” la que es “Googlear” me puse a investigar si había una manera de manipular o al menos leer archivos de Word desde PowerShell.
Y gracias a que alguien ya habia hecho la misma pregunta en StackOverFlow me puse manos a la obra para hacer lo mismo y además agregar “mi toque mágico” al Script ya que el Script que cree es capaz de buscar dentro de archivos de Word, Excel y PowerPoint.
Además, hice uso de “PowerShell Runspaces” para hacer el Script “multihilo”.

El Script es capaz de obtener los archivos del tipo especificado en una ruta determinada (incluyendo subfolders) y buscar por links que apunten a algún recurso local (ejemplo: \\carpeta_local\), ignorando links apuntando a sitios web.
También exporta los resultados obtenidos a un archivo CSV(por cada tipo de archivo) donde se especifica la ruta completa del archivo, el texto del enlace y la dirección a la cual apunta el enlace.

El Script luce de la siguiente manera:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
[CmdletBinding()]
param(
    [Parameter(Mandatory=$true, Position = 0)]
        [ValidateNotNullOrEmpty()]
        [string]$Path,
    [Parameter(Mandatory=$true, Position = 1, HelpMessage ="Document Type")]
        [ValidateNotNullOrEmpty()]
        [ValidateSet("word","excel","powerpoint")]
        [string[]]$DocType

)
if(Test-Path -Path $Path){
    #Create a RunspacePool to open multiple instances of PowerShell
    $RunspacePool = [runspacefactory]::CreateRunspacePool()
    $RunspacePool.Open()
    $jobs = New-Object System.Collections.ArrayList
    foreach($doc in $DocType){
        #Get list of files
        $Script:files = $null
        $Script:filetype = ""
        $Handle = $null
        switch ($doc.ToUpper()) {
            "WORD" {
                Write-Host "Retrieving $doc files";
                $Script:files = Get-ChildItem -Path $Path -Include *.doc* -Recurse | Select-Object -Property FullName
                $Script:filetype = "WORD"
                Write-Host "Found: $(@($Script:files).Count) $doc files";
                break
            }
            "EXCEL" {
                Write-Host "Retrieving $doc files";
                $Script:files = Get-ChildItem -Path $Path -Include *.xls* -Recurse | Select-Object -Property FullName
                $Script:filetype = "EXCEL"
                Write-Host "Found: $(@($Script:files).Count) $doc files";
                break
            }
            "POWERPOINT" {
                Write-Host "Retrieving $doc files";
                $Script:files = Get-ChildItem -Path $Path -Include *.ppt* -Recurse | Select-Object -Property FullName
                $Script:filetype = "POWERPOINT"
                Write-Host "Found: $(@($Script:files).Count) $doc files";
                break
            }
            Default {
                Write-Host "File extension not valid";
                break
            }
        }
       
        if($null -ne $Script:files){
            #Open a new PowerShell instance for each extension
            Write-Host "Current file to be processed: $Script:files"
            $ps = [powershell]::Create()
            $ps.RunspacePool = $RunspacePool
            [void]$ps.AddScript({
                param (
                    [psobject]$ListOfFiles,
                    [string]$DocType
                )
                "DocType:$DocType Found these items: " + $ListOfFiles >> "C:\Temp\logs.txt"
                #Logic for each type of extension
                switch ($DocType) {
                    "WORD" {
                        "Processing WORD files" >> "C:\Temp\logs.txt"
                        $Word = New-Object -ComObject Word.application
                        $Word.Visible = $false
                        foreach($file in $ListOfFiles){
                            $document = $Word.Documents.Open($file.FullName)
                            $links = @($document.Hyperlinks | Where-Object { $_.Address -NotLike "http*" -and $_.Address -notlike "mailto*" })
                            "WORD File: $($file.FullName) - Links: $links" >>  "C:\Temp\logs.txt"
                            $links | Select-Object @{n="File";e={$file.FullName}},@{n="Sheet";e={""}}, @{n="TextToDisplay";e={if([string]::IsNullOrEmpty($_.TextToDisplay)){$_.Range.Text}else{$_.TextToDisplay}}}, @{n="Address";e={if([string]::IsNullOrEmpty($_.Address)){ "Possible Attachment"}else{$_.Address.ToString()}}} | Export-Csv -Path "C:\Temp\Report-DOC.csv" -NoTypeInformation -Append
                            $document.close()
                        }
                        #Removing WORD APP
                        $Word.Quit()
                        break
                    }
                     "POWERPOINT" {
                        "Processing PowerPoint files" >> "C:\Temp\logs.txt"
                         $Ppt = New-Object -ComObject Powerpoint.application
                         foreach($file in $ListOfFiles){
                            $presentation = $Ppt.Presentations.Open($file.FullName)
                            foreach($slide in $presentation.Slides){
                                $links = @($slide.Hyperlinks | Where-Object { $_.Address -NotLike "http*" -and $_.Address -notlike "mailto*" })
                                "PowerPoint File: $($file.FullName) - Links: $links" >>  "C:\Temp\logs.txt"
                                $links | Where-Object -Property Address -NotLike "http*" | Select-Object @{n="File";e={$file.FullName}},@{n="Sheet";e={$slide.Name}}, @{n="TextToDisplay";e={if([string]::IsNullOrEmpty($_.TextToDisplay)){$_.Range.Text}else{$_.TextToDisplay}}}, @{n="Address";e={if([string]::IsNullOrEmpty($_.Address)){"Possible Attachment"}else{$_.Address.ToString()}}} | Export-Csv -Path "C:\Temp\Report-PPT.csv" -NoTypeInformation -Append
                                $presentation.close()
                            }
                         }
                         #Removing PowerPoint APP
                         $Ppt.Quit()
                         break
                    }
                    "EXCEL" {
                        "Processing Excel files" >> "C:\Temp\logs.txt"
                        $Excel = New-Object -com Excel.Application;
                        $Excel.Visible = $false;
                        foreach($file in $ListOfFiles){
                            $Workbook = $Excel.Workbooks.Open($file.FullName)
                            foreach($sheet in $Workbook.Sheets){
                                $links = @($sheet.Hyperlinks | Where-Object { $_.Address -NotLike "http*" -and $_.Address -notlike "mailto*" })
                                "Excel File: $($file.FullName) - Links: $links" >>  "C:\Temp\logs.txt"
                                $links | Where-Object -Property Address -NotLike "http*" | Select-Object @{n="File";e={$file.FullName}},@{n="Sheet";e={$sheet.Name}}, @{n="TextToDisplay";e={if([string]::IsNullOrEmpty($_.TextToDisplay)){$_.Range.Text}else{$_.TextToDisplay}}}, @{n="Address";e={if([string]::IsNullOrEmpty($_.Address)){"Possible Attachment"}else{$_.Address.ToString()}}} | Export-Csv -Path "C:\Temp\Report-XLS.csv" -NoTypeInformation -Append
                            }
                            $Workbook.close()
                        }
                        #Removing Excel APP
                        $Excel.Quit()
                        break
                    }
                    Default {
                        Write-Host "This is a not valid DocType: $DocType" >> "C:\Temp\logs.txt";
                        break
                    }
                }
            })
            [void]$ps.AddParameter("ListOfFiles",$Script:files)
            [void]$ps.AddParameter("DocType",$Script:filetype)
            $Handle = $ps.BeginInvoke()
            $temp = '' | Select-Object PowerShell,Handle
            $temp.PowerShell = $ps
            $temp.handle = $Handle
            [void]$jobs.Add($Temp)

        }else{
            Write-Host "$doc : Files not found"
        }
    }
        do{
            Start-Sleep -seconds 1
            [int]$ProcessInProgress = @($jobs | Where-Object {$_.handle.iscompleted -ne 'Completed'}).Count
            Write-Host ("Remaining Jobs: $ProcessInProgress")
        }while($ProcessInProgress -ne 0)
        $jobs | ForEach-Object {
            $_.powershell.EndInvoke($_.handle)
            $_.PowerShell.Dispose()
        }

}else {
    Write-Host "The folder: $Path doesn't exists"
}

El Script recibe dos parametros:

  • Path: Indica el directorio que se desea escanear
  • DocType: Indica los tipos de documentos a escanear, siendo los únicos valores validos
    • word
    • excel
    • powerpoint

Suponiendo que has guardado el Script con el siguiente nombre: Tobal-EresLaPolla.ps1
La manera de ejecutar el Script puede ser de la siguiente manera:

  • .\Tobal-EresLaPolla.ps1 -Path “C:\FolderConArchivos” -Doctype “Word”
  • .\Tobal-EresLaPolla.ps1 -Path “C:\FolderConArchivos” -Doctype “Word”, “excel”,”powerpoint”

De esa manera se ejecutará el Script y creara un nuevo “Runspace” por cada tipo de documento, de esta manera la aplicación puede ejecutar los procesos en paralelo.
Los reportes generados se guardan en la carpeta: “C:\Temp”, pero tu puedes editar esa parte para que guarde los archivos donde tu quieras.

Si tienes algun comentario y/o duda [email protected] en los comentarios.

Saludos!!!

Leave a Reply

Your email address will not be published. Required fields are marked *

%d bloggers like this: