🐥

PowerShell:ファイルの一括ダウンロードとリネーム

に公開

とあるサイトからファイルを一括でダウンロードして、ファイルのリネームをしたかったので作ってみた。

概要

大量のファイルがアップされているサイトで、リンクテキストとダウンロードされるファイル名が異なるので、リンクテキストにリネームするもの。

処理の流れ

  1. ダウンロードするサイトを指定
  2. リンクを取得
  3. ファイルを一個ずつダウンロード
  4. ファイルをリンクテキストにリネーム

完成したスクリプト

FileDL_RN.ps1
<#
.SYNOPSIS
    指定したWebサイトから特定の拡張子ファイル(PDF, XLSX等)を一括ダウンロードし、
    リンクのテキスト(または属性値)をファイル名として安全にリネームして保存する。
#>

#-----------------------------------------------------------------------------
# 【設定項目】ダウンロード対象の拡張子を指定(カンマ区切りで追加・削除可能)
$TargetExtensions = @("pdf", "xlsx", "zip", "docx") 
#-----------------------------------------------------------------------------

# 【重要:プロキシ環境およびSSL/TLSエラー回避のネットワーク設定】
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
[System.Net.WebRequest]::DefaultWebProxy.Credentials = [System.Net.CredentialCache]::DefaultCredentials

# URL形式のチェック用Function
function IsAbsoluteUrl([string] $url){
    return [System.Uri]::IsWellFormedUriString($url, [System.UriKind]::Absolute)
}

# 堅牢化された安全なファイル名生成Function
Function Get-SafeFileName {
    param([String]$Name)
    if ([string]::IsNullOrWhiteSpace($Name)) { return "NoTitle" }

    $invalidChars = [IO.Path]::GetInvalidFileNameChars() -join ''
    $re = "[{0}]" -f [RegEx]::Escape($invalidChars)
    $cleanName = $Name -replace $re, ''
    $cleanName = $cleanName -replace "[\r\n\t]", ''
    $cleanName = $cleanName.Trim().TrimEnd('.')

    if ([string]::IsNullOrWhiteSpace($cleanName)) { return "InvalidName" }
    if ($cleanName -match "^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$") { $cleanName = "_" + $cleanName }
    if ($cleanName.Length -gt 100) { $cleanName = $cleanName.Substring(0, 100) }

    return $cleanName
}

#-----------------------------------------------------------------------------
# メイン処理開始
#-----------------------------------------------------------------------------

# 動的に正規表現パターンを生成
$regexPattern = "\.(" + ($TargetExtensions -join "|") + ")$"
Write-Host "抽出対象の拡張子パターン: $regexPattern" -ForegroundColor DarkGray

$TargetUrl = Read-Host "(ファイルを取得したいサイトのURLを入力してください)"
if (-not (IsAbsoluteUrl($TargetUrl))) {
    Write-Error "$TargetUrl は正しい形式のURLではありません。"
    exit
}
$uri = New-Object System.Uri ($TargetUrl)

# フォルダ選択ダイアログ表示
Add-Type -AssemblyName System.Windows.Forms
$FolderBrowser = New-Object System.Windows.Forms.FolderBrowserDialog -Property @{
    RootFolder = "Desktop"
    Description = "ファイルを保存するフォルダを選択してください"
}

if ($FolderBrowser.ShowDialog() -ne [System.Windows.Forms.DialogResult]::OK) {
    Write-Warning "フォルダ選択がキャンセルされました。"
    exit
}
$SavePath = $FolderBrowser.SelectedPath

try {
    Write-Host "サイト情報を取得中..." -ForegroundColor Cyan
    $response = Invoke-WebRequest -Uri $uri -UseBasicParsing
    
    # 文字コードの動的判定
    $charset = "UTF-8"
    if ($response.Headers["Content-Type"] -match "charset=([^\s;]+)") {
        $charset = $matches[1]
    } elseif ($response.Content -match 'charset\s*=\s*["'']?([a-zA-Z0-9\-_]+)') {
        $charset = $matches[1]
    }

    # リンクの抽出
    $links = $response.Links | Where-Object { $_.href -match $regexPattern -and -not [string]::IsNullOrWhiteSpace($_.href) }

    if ($null -eq $links -or $links.Count -eq 0) {
        Write-Warning "指定された拡張子($($TargetExtensions -join ', '))のファイルが見つかりません。"
        exit
    }

    Write-Host "合計 $($links.Count) 件の対象ファイルを発見。ダウンロードを開始します。"

    foreach ($link in $links) {
        $OriginalFileName = Split-Path $link.href -Leaf
        
        if (IsAbsoluteUrl($link.href)) {
            $DownloadUrl = New-Object System.Uri ($link.href)
        } else {
            $DownloadUrl = New-Object System.Uri ($uri, $link.href)
        }

        # ---------------------------------------------------------
        # スマート・エンコーディング処理(破壊的変換の防止)
        # ---------------------------------------------------------
        if ($response.Headers["Content-Type"] -match "charset=") {
            $a_Tag = $link.outerHTML
        } else {
            $bytes = [System.Text.Encoding]::GetEncoding('ISO-8859-1').GetBytes($link.outerHTML)
            try {
                $a_Tag = [System.Text.Encoding]::GetEncoding($charset).GetString($bytes)
            } catch {
                $a_Tag = [System.Text.Encoding]::GetEncoding('UTF-8').GetString($bytes)
            }
        }

        # ---------------------------------------------------------
        # ファイル名抽出ロジック
        # ---------------------------------------------------------
        $rawText = ""

        # 優先順位1: テキスト情報を最優先
        $innerText = $a_Tag -replace "<[^>]+>", " "
        $innerText = [System.Net.WebUtility]::HtmlDecode($innerText)
        $innerText = $innerText -replace "[\r\n\t]", " "
        $innerText = $innerText -replace "\s+", " " 
        $innerText = $innerText.Trim()

        if (-not [string]::IsNullOrWhiteSpace($innerText)) {
            $rawText = $innerText
        } else {
            # 優先順位2: テキストが空の場合のみ属性値(aria-label, title, alt)を探す
            if ($a_Tag -match 'aria-label\s*=\s*(["''])(.*?)\1') {
                $rawText = $matches[2]
            } elseif ($a_Tag -match 'title\s*=\s*(["''])(.*?)\1') {
                $rawText = $matches[2]
            } elseif ($a_Tag -match '<img[^>]+alt\s*=\s*(["''])(.*?)\1') {
                $rawText = $matches[2]
            }
        }

        # 最終的な無害化
        $SafeBaseName = Get-SafeFileName -Name $rawText
        $Extension = [System.IO.Path]::GetExtension($OriginalFileName)

        # 保存先パスの決定(同名ファイル回避)
        $NewFileName = "${SafeBaseName}${Extension}"
        $OutFilePath = Join-Path $SavePath $NewFileName
        $counter = 1

        while (Test-Path -Path $OutFilePath) {
            $NewFileName = "${SafeBaseName}_${counter}${Extension}"
            $OutFilePath = Join-Path $SavePath $NewFileName
            $counter++
        }

        # ダウンロード実行
        Write-Host "DL中: $NewFileName" -NoNewline
        Invoke-WebRequest -Uri $DownloadUrl.AbsoluteUri -OutFile $OutFilePath
        Write-Host " [完了]" -ForegroundColor Green
    }
    Write-Host "すべての処理が完了しました。" -ForegroundColor Cyan

} catch {
    Write-Error "処理中にエラーが発生しました。`n詳細: $($_.Exception.Message)"
}

参考

とても参考にさせていただきました。
https://qiita.com/hiron2225/items/eb5cbf18fed27b96a622
https://www.hi-matic.org/diary/?20190105

Discussion