2026年9月10日星期四

把 WordPress 官方历史版本保存下来:一次 886 个版本的归档记录


把 WordPress 官方历史版本保存下来:一次 886 个版本的归档记录

最近又折腾了一件比较“老”的事情:把 WordPress 官方提供的历史版本尽可能完整地保存下来。 豆子一直比较喜欢保存一些以前的软件版本。很多软件在当年到处都能下载,但过了几年以后,原来的下载地址可能没了,第三方网站也可能消失。等真正想找的时候,反而不一定还能找到。 WordPress 也是一样。 现在大家安装 WordPress,基本都是直接下载最新版,很少有人会去关心十几年前的版本。不过对豆子来说,这些历史版本本身就是互联网发展过程中的一部分,所以还是想把它们保存下来。

先从官方历史版本页面开始

WordPress 官方其实一直保留着历史版本下载页面: https://wordpress.org/download/releases/ 这个页面基本可以看作 WordPress 官方历史版本档案库,里面能够找到从早期版本一直到现在的正式版本。 不过页面里面除了正式发布版本之外,还能看到 Beta、RC、Alpha、Nightly 以及 MU 等版本。 豆子的需求比较简单:只保存正式发布的 WordPress ZIP 安装包。 所以 Beta、RC、Alpha、Nightly、MU 这些版本就不考虑了。

为什么没有一个一个手动下载

最开始其实也想过手动下载。 但是看了一下版本数量以后就放弃了。 WordPress 从早期版本一直发展到现在,正式版本加起来已经是几百个。如果一个一个点下载,再一个一个建立目录保存,实在太麻烦了。 既然官方页面本身就有完整的下载链接,那就干脆让程序自己把链接找出来,然后自动下载。 豆子的要求也没有那么复杂:
  • 只要正式版本
  • 只下载 ZIP
  • 按照 1.X、2.X、3.X 这样的大版本分类
  • 保持官方原始文件名
  • 已经下载过的文件不要重复下载
  • 下载失败以后可以重新运行程序继续下载
这样基本就够用了。

先统计一下到底有多少正式版本

在真正开始下载之前,豆子先写了一个简单的 PowerShell 正则,把官方页面中的 WordPress ZIP 文件名提取出来。 核心匹配规则其实很简单:
$pattern = 'wordpress-([0-9]+(?:\.[0-9]+)*(?:-(?:platinum|gold))?)\.zip'
这里特意保留了 -platinum-gold 这种历史版本名称。 例如:
wordpress-1.0-platinum.zip
wordpress-1.2.zip
wordpress-2.0.zip
wordpress-4.9.26.zip
wordpress-6.8.2.zip
wordpress-7.1.zip
然后按照主版本号进行统计。

最终统计结果

实际从 WordPress 官方历史版本页面中统计出来,一共有: 886 个正式版本 ZIP 文件。 按照大版本分类以后是:
版本 数量
1.X 9
2.X 41
3.X 150
4.X 357
5.X 217
6.X 105
7.X 6
合计 886
看到这个数量的时候还是有点意思的。 尤其是 4.X,一个大版本就有 357 个正式 ZIP,确实不少。 当然,这里统计的是正式版本,并不是 WordPress 历史上所有曾经出现过的开发版本。

有些版本中间没有连续编号

在统计的时候还发现了一些版本号并不是完全连续的情况。 例如:
3.9.37
3.9.39
中间没有 3.9.38。 还有:
5.0.4
5.0.6
中间没有 5.0.5。 6.5 这一系列也存在类似情况。 刚开始还以为是自己的程序漏掉了版本,后来重新检查官方页面,确认这些确实就是官方当前保存的正式发布版本。 所以这种情况不用自己补,也没有必要为了“连续编号”去找第三方文件。 既然目标是做官方历史版本归档,那就按照官方实际存在的文件保存即可。

目录结构也尽量简单

豆子没有按照每一个小版本再建立目录,而是直接按照大版本分类。 最终目录结构就是这样:
WordPress 官方历史版本/
├── 1.X/
├── 2.X/
├── 3.X/
├── 4.X/
├── 5.X/
├── 6.X/
└── 7.X/
例如:
WordPress 官方历史版本/
├── 1.X/
│   ├── wordpress-1.0-platinum.zip
│   ├── wordpress-1.2.zip
│   └── ...
│
├── 2.X/
│   ├── wordpress-2.0.zip
│   ├── wordpress-2.0.1.zip
│   └── ...
│
├── 4.X/
│   ├── wordpress-4.9.zip
│   ├── wordpress-4.9.1.zip
│   └── ...
│
└── 7.X/
    ├── wordpress-7.0.zip
    └── wordpress-7.1.zip
这样以后自己找版本的时候也比较直观。

为什么没有继续使用 Cloudflare Worker 下载

其实最开始豆子是考虑过直接使用 Cloudflare Worker 来做的。 Worker 本身也确实可以访问 WordPress 官方下载地址,然后把文件下载下来,再进一步处理。 但是实际测试以后发现一个问题:前面十几个文件通常没什么问题,连续下载一段时间以后,就开始出现 Network Error。 这让豆子感觉 Worker 并不太适合拿来做这种连续、大量的历史文件下载任务。 毕竟这不是下载一个文件,而是一次要处理几百个 ZIP。 而且豆子的目标只是把文件保存下来,并不是一定要把所有事情都塞进 Cloudflare Worker。 所以后来干脆换了一个最简单的办法。

最后改成 Windows + curl 下载

Windows 现在本身就可以直接使用 curl.exe,不用为了这个小任务再安装 Python,也不用额外安装什么下载工具。 实际测试以后,直接从 Windows 下载反而非常稳定。 下载地址统一使用 WordPress 官方的:
https://downloads.wordpress.org/release/
例如:
https://downloads.wordpress.org/release/wordpress-6.8.2.zip
所以最后的下载流程就变得非常简单:
  1. 读取 WordPress 官方历史版本页面
  2. 自动找出正式版本 ZIP
  3. 提取版本号
  4. 根据主版本号决定保存到哪个目录
  5. 使用 curl.exe 下载
  6. 已经存在的文件直接跳过
  7. 下载失败自动重试
  8. 最后统计成功、跳过和失败数量

下载程序

为了以后重复使用,豆子把程序拆成了两个文件。 一个是启动程序:
WordPress官方历史版本下载.bat
另外一个是真正负责工作的 PowerShell: BAT 文件本身非常简单,主要就是调用 PowerShell:
@echo off
chcp 65001 >nul

title WordPress Official Release Archive Downloader

echo.
echo ============================================================
echo       WordPress Official Release Archive Downloader
echo ============================================================
echo.

powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0WordPress官方历史版本下载.ps1"

echo.
echo ============================================================
echo                         Finished
echo ============================================================
echo.

pause
WordPress官方历史版本下载.ps1

# ============================================================
# WordPress Official Historical Release Downloader
# ============================================================
# Source:
# https://wordpress.org/download/releases/
#
# Rules:
# - Official formal releases only
# - ZIP only
# - Exclude Beta / RC / Alpha / Nightly / MU
# - Archive 1.X - 7.X
# - Skip existing files
# - Retry failed downloads
# - Download directly from downloads.wordpress.org
# ============================================================

[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
$OutputEncoding = [System.Text.Encoding]::UTF8

$ErrorActionPreference = "Stop"

$BaseDir = Split-Path -Parent $MyInvocation.MyCommand.Path

$Root = Join-Path $BaseDir "WordPress 官方历史版本"

$ArchiveUrl = "https://wordpress.org/download/releases/"

Write-Host ""
Write-Host "============================================================"
Write-Host " WordPress Official Historical Release Downloader"
Write-Host "============================================================"
Write-Host ""

# ------------------------------------------------------------
# Create root directory
# ------------------------------------------------------------

if (!(Test-Path -LiteralPath $Root)) {

    New-Item `
        -ItemType Directory `
        -Path $Root `
        -Force | Out-Null
}

# ------------------------------------------------------------
# Create version directories
# ------------------------------------------------------------

for ($major = 1; $major -le 7; $major++) {

    $dir = Join-Path $Root ("{0}.X" -f $major)

    if (!(Test-Path -LiteralPath $dir)) {

        New-Item `
            -ItemType Directory `
            -Path $dir `
            -Force | Out-Null
    }
}

# ------------------------------------------------------------
# Read official archive
# ------------------------------------------------------------

Write-Host "[1] Reading official release archive..."
Write-Host ""

try {

    $response = Invoke-WebRequest `
        -Uri $ArchiveUrl `
        -UseBasicParsing `
        -TimeoutSec 60

    $html = $response.Content
}
catch {

    Write-Host ""
    Write-Host "[FAILED] Cannot read official archive."
    Write-Host $_.Exception.Message
    Write-Host ""

    pause
    exit 1
}

Write-Host "[OK] Official archive loaded."
Write-Host ""

# ------------------------------------------------------------
# Extract release ZIP filenames
#
# Important:
# Match filename instead of complete URL.
# This is the parser already verified successfully.
# ------------------------------------------------------------

$pattern = 'wordpress-([0-9]+(?:\.[0-9]+)*(?:-(?:platinum|gold))?)\.zip'

$matches = [regex]::Matches(
    $html,
    $pattern,
    [System.Text.RegularExpressions.RegexOptions]::IgnoreCase
)

$versions = @{}

foreach ($m in $matches) {

    $v = $m.Groups[1].Value.Trim()

    # Normal formal release
    if ($v -match '^\d+(?:\.\d+)*$') {

        $versions[$v] = $true

        continue
    }

    # Historical formal release
    if ($v -match '^\d+\.\d+-(?:platinum|gold)$') {

        $versions[$v] = $true
    }
}

# ------------------------------------------------------------
# Sort versions numerically
# ------------------------------------------------------------

$sortedVersions = $versions.Keys | Sort-Object {

    $n = $_ `
        -replace '-platinum$','' `
        -replace '-gold$',''

    [version]$n
}

$total = $sortedVersions.Count

Write-Host "============================================================"
Write-Host " Detection Result"
Write-Host "============================================================"
Write-Host ""

Write-Host ("Official release ZIP count: {0}" -f $total)

Write-Host ""

# ------------------------------------------------------------
# Statistics
# ------------------------------------------------------------

for ($major = 1; $major -le 7; $major++) {

    $count = @(
        $sortedVersions |
        Where-Object {
            $_ -match "^$major\."
        }
    ).Count

    Write-Host ("  {0}.X : {1}" -f $major, $count)
}

Write-Host ""

# ------------------------------------------------------------
# Download statistics
# ------------------------------------------------------------

$downloaded = 0
$skipped = 0
$failed = 0

$index = 0

Write-Host "============================================================"
Write-Host " Starting download"
Write-Host "============================================================"
Write-Host ""

# ------------------------------------------------------------
# Download each release
# ------------------------------------------------------------

foreach ($version in $sortedVersions) {

    $index++

    # Determine major version
    if ($version -notmatch '^(\d+)\.') {

        continue
    }

    $major = [int]$Matches[1]

    # Only archive 1.X - 7.X
    if ($major -lt 1 -or $major -gt 7) {

        continue
    }

    $folder = Join-Path `
        $Root `
        ("{0}.X" -f $major)

    $file = "wordpress-$version.zip"

    $target = Join-Path `
        $folder `
        $file

    $url = "https://downloads.wordpress.org/release/$file"

    Write-Host ""
    Write-Host "------------------------------------------------------------"
    Write-Host ("[{0}/{1}] {2}" -f $index, $total, $file)
    Write-Host "------------------------------------------------------------"

    # --------------------------------------------------------
    # Existing file
    # --------------------------------------------------------

    if (Test-Path -LiteralPath $target) {

        try {

            $size = (
                Get-Item -LiteralPath $target
            ).Length

            if ($size -gt 1024) {

                Write-Host "[SKIP] File already exists."
                $skipped++

                continue
            }

            Remove-Item `
                -LiteralPath $target `
                -Force `
                -ErrorAction SilentlyContinue
        }
        catch {

            Remove-Item `
                -LiteralPath $target `
                -Force `
                -ErrorAction SilentlyContinue
        }
    }

    # --------------------------------------------------------
    # Download
    # --------------------------------------------------------

    $success = $false

    for ($try = 1; $try -le 4; $try++) {

        Write-Host ("Download attempt {0}/4..." -f $try)

        try {

            & curl.exe `
                -L `
                --fail `
                --retry 2 `
                --retry-delay 5 `
                --connect-timeout 30 `
                --max-time 900 `
                --output "$target" `
                "$url"

            if (
                $LASTEXITCODE -eq 0 `
                -and (Test-Path -LiteralPath $target)
            ) {

                $size = (
                    Get-Item -LiteralPath $target
                ).Length

                if ($size -gt 1024) {

                    $success = $true

                    break
                }
            }
        }
        catch {

            # Retry below
        }

        Remove-Item `
            -LiteralPath $target `
            -Force `
            -ErrorAction SilentlyContinue

        if ($try -lt 4) {

            Write-Host "Waiting 5 seconds before retry..."

            Start-Sleep -Seconds 5
        }
    }

    # --------------------------------------------------------
    # Result
    # --------------------------------------------------------

    if ($success) {

        Write-Host "[OK] Download completed."

        $downloaded++
    }
    else {

        Write-Host "[FAILED] Download failed."

        $failed++
    }

    # --------------------------------------------------------
    # Delay between files
    # --------------------------------------------------------

    Start-Sleep -Seconds 5
}

# ------------------------------------------------------------
# Final report
# ------------------------------------------------------------

Write-Host ""
Write-Host "============================================================"
Write-Host " Download Finished"
Write-Host "============================================================"
Write-Host ""

Write-Host ("Official releases : {0}" -f $total)
Write-Host ("Downloaded        : {0}" -f $downloaded)
Write-Host ("Already existed   : {0}" -f $skipped)
Write-Host ("Failed            : {0}" -f $failed)

Write-Host ""
Write-Host "Archive directory:"
Write-Host $Root

Write-Host ""
Write-Host "Run this program again to retry failed or missing files."
Write-Host "Existing files will automatically be skipped."

Write-Host ""
Write-Host "============================================================"

pause
以后需要继续下载的时候,直接双击 BAT 就可以。 PowerShell 程序会自动检查哪些文件已经存在。 所以即使中途关掉电脑,或者某几个版本下载失败了,下一次重新运行也不用从头开始。

实际运行情况

目前实际测试下来,Windows 直接下载还是比较稳定的。 早期版本的 ZIP 文件非常小,所以下载速度很快。 运行过程中已经连续下载了几十个版本,没有出现错误。 当然,越往后面的版本文件越大,整个任务所需要的时间也会明显增加。 豆子在程序里给每个文件之间增加了几秒钟的间隔,并且单个文件支持多次重试。 这样做主要也是为了稳一点。 反正这些文件又不是着急马上用,慢一点没关系,能够完整保存下来就行。

为什么豆子还是想把这些老版本保存下来

其实豆子并没有打算把这些几百个 WordPress 版本全部重新安装一遍。 保存它们的意义更多还是在于留档。 以前很多软件都是这样。 在软件最流行的时候,到处都有下载地址,根本不会觉得某个版本有什么稀奇。 但是十几年以后,情况就完全不一样了。 官方网站可能已经不再提供下载,原来的第三方下载站可能关闭,论坛附件也可能失效。 到了那个时候,再想找一个十几年前的软件版本,往往比想象中困难得多。 所以看到官方还保留着完整的历史版本,豆子觉得还是值得顺手保存一份。

以后再慢慢整理

目前豆子的想法也很简单。 先把这些官方 ZIP 全部保存下来,放在本地,然后再同步到 Google Drive。 至于以后要不要进一步做版本索引、自动校验文件、建立网页档案之类的东西,暂时都不考虑。 先把东西留住。 整理其实只是顺手的事情。 对于这种历史软件归档来说,豆子觉得最重要的还是这一点:趁现在还能找到,就先保存一份。 以后哪天真的需要某个老版本的时候,至少不用再满互联网到处找。 而且这些东西本身也是 WordPress 发展历史的一部分。 从早期的 1.X,一直到现在的 7.X,能够把这么多年的版本完整地放在自己的硬盘和网盘里,看着其实也挺有意思。 这次就先把 WordPress 的历史版本保存下来。 以后有时间,再继续整理其他以前折腾过的软件。