📊

PowerShellを使ってZennの全記事URL(20万件以上)を一気に取得する

に公開

🎯 はじめに:Zennの全記事のURLを取得する方法を探した

最初は https://zenn.dev/articles?page=1 のようにページを順番に辿る方法を試しましたが、100ページまでしか表示されないため、全記事を取得することができませんでした。

そこで別の方法を探したところ、Zennがサイトマップを公開していることがわかりました。サイトマップとは、Webサイトの全ページを検索エンジンに知らせるためのファイルです。このサイトマップを解析することで、20万件以上の記事URLを効率的に取得できることがわかりました。

▼本記事のPowerShellを実行すると、Zennの全記事URLをファイルへ書き出します。
PowerShellでZennの全記事を取得

🗺️ サイトマップを利用したアプローチ

Zennは https://zenn.dev/sitemaps/_index.xml でサイトマップインデックスを公開しています。このファイルには、記事・本・ユーザー・トピックなどのサイトマップファイルへのリンクが含まれています。

<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <sitemap>
    <loc>https://zenn.dev/sitemaps/articles_1.xml.gz</loc>
    <lastmod>2025-03-08T12:00:00+09:00</lastmod>
  </sitemap>
  <sitemap>
    <loc>https://zenn.dev/sitemaps/articles_2.xml.gz</loc>
    <lastmod>2025-03-08T12:00:00+09:00</lastmod>
  </sitemap>
  <!-- 他のサイトマップファイル -->
</sitemapindex>

各サイトマップファイルはgzip圧縮されており、展開すると個別のURLリストが得られます。

💻 PowerShellスクリプトを作成した

以下のスクリプトは、サイトマップを解析してZennの全コンテンツURLを取得します。PowerShell 5.1以降で動作します。

PowerShellスクリプト(クリックで展開)
# Zennまるごとゲッター
# PowerShell 5.1 Compatible

param(
    [string]$OutputFile = "zenn_sitemap_data", # 拡張子は自動で付与されます
    [ValidateSet(\'csv\', \'json\', \'txt\')]
    [string]$OutputFormat = \'txt\' # パラメータ検証を追加
)

# --- 実行前処理 ---
Clear-Host
Write-Host "Zennまるごとゲッター" -ForegroundColor Green

# --- 設定 (ここを編集) ---
# trueに設定した種類のURLを取得します
$ScrapeConfig = @{
    article     = $true
    book        = $true
    user        = $true
    topic       = $true
    publication = $true
    static      = $false # 静的ページはデフォルトで除外
}

# 各リクエスト間の待機時間(ミリ秒)。サーバーへの負荷を軽減します。
[ValidateRange(0, [int]::MaxValue)]
[int]$RequestDelayMs = 500

# HTTPリクエスト時に送信するUser-Agent
$UserAgent = "ZennMarugotoGetter/2.0 (PowerShell Script)"

# -------------------------

# 指定されたバイト配列をgzip形式として展開し、文字列として返します。
function Expand-GzipContent {
    param([byte[]]$GzipBytes)
    try {
        $inputStream = New-Object System.IO.MemoryStream(,$GzipBytes)
        $gzipStream = New-Object System.IO.Compression.GzipStream($inputStream, [System.IO.Compression.CompressionMode]::Decompress)
        $outputStream = New-Object System.IO.MemoryStream
        $gzipStream.CopyTo($outputStream)
        
        # リソース管理の徹底
        $result = [System.Text.Encoding]::UTF8.GetString($outputStream.ToArray())
        $outputStream.Dispose()
        $gzipStream.Dispose()
        $inputStream.Dispose()
        
        return $result
    }
    catch {
        Write-Warning "Gzip展開エラー: $($_.Exception.Message)"
        return $null
    }
}

# 指定されたURLからデータをダウンロードします。失敗した場合はリトライします。
function Download-WithRetry {
    param(
        [string]$Uri,
        [int]$MaxRetries = 3,
        [int]$RetryDelaySec = 2
    )

    for ($attempt = 1; $attempt -le $MaxRetries; $attempt++) {
        try {
            # WebClient → Invoke-WebRequest移行
            $response = Invoke-WebRequest -Uri $Uri -UserAgent $UserAgent -UseBasicParsing -TimeoutSec 30
            return $response.Content
        }
        catch [System.Net.WebException] {
            # HTTP status codeの適切な処理
            if ($_.Exception.Response.StatusCode -eq 404 -or $_.Exception.Response.StatusCode -eq 403) {
                Write-Warning "アクセス不可 ($($_.Exception.Response.StatusCode)): $Uri (スキップします)"
                return $null
            }
        }
        catch {
            if ($attempt -lt $MaxRetries) {
                Write-Warning "ダウンロードに失敗しました (試行 $attempt/$MaxRetries)。$RetryDelaySec 秒後に再試行します..."
                Start-Sleep -Seconds $RetryDelaySec
            }
            else {
                Write-Error "$MaxRetries 回試行しましたが、$Uri のダウンロードに失敗しました。エラー: $($_.Exception.Exception.Message)"
                return $null
            }
        }
    }
    return $null
}

# --- メイン処理 ---
# .NETの圧縮ライブラリをロード
Add-Type -AssemblyName System.IO.Compression

# パフォーマンス改善のため、効率的なリストオブジェクトを使用
$results = New-Object System.Collections.Generic.List[psobject]
$sitemapIndexUrl = "https://zenn.dev/sitemaps/_index.xml"

try {
    # サイトマップインデックスを取得
    $indexContent = Download-WithRetry -Uri $sitemapIndexUrl
    if (-not $indexContent) { throw "サイトマップインデックスのダウンロードに失敗しました。" }
    
    # XML処理の堅牢性向上
    try {
        [xml]$indexXml = $indexContent
        if (-not $indexXml -or -not $indexXml.sitemapindex) {
            throw "無効なサイトマップインデックス構造です。"
        }
    }
    catch {
        Write-Error "サイトマップインデックスのXML解析に失敗しました: $($_.Exception.Message)"
        exit 1
    }
    
    $sitemapUrls = $indexXml.sitemapindex.sitemap.loc
    Write-Host "`n処理対象のサイトマップを $($sitemapUrls.Count) 件見つけました。" -ForegroundColor Cyan
    
    # 各サイトマップを処理
    $totalProcessed = 0
    foreach ($sitemapUrl in $sitemapUrls) {
        $totalProcessed++
        
        # プログレスバーを更新
        Write-Progress -Activity "サイトマップを処理中" -Status "[$totalProcessed/$($sitemapUrls.Count)] $sitemapUrl" -PercentComplete (($totalProcessed / $sitemapUrls.Count) * 100)

        # 設定に基づいて不要なサイトマップをスキップ
        $type = $sitemapUrl -replace ".+/(.+?)\\d*\\.(xml|gz).*\", "$1"
        if (-not $ScrapeConfig.ContainsKey($type) -or !$ScrapeConfig[$type]) {
            continue
        }

        # サーバーへの配慮
        Start-Sleep -Milliseconds $RequestDelayMs

        # サイトマップ本体をダウンロード(リトライ処理込み)
        $sitemapContent = Download-WithRetry -Uri $sitemapUrl
        if (-not $sitemapContent) { continue }
        
        # Gzip展開の処理
        if ($sitemapUrl.EndsWith(".gz")) {
            # バイナリデータとして再取得してGzip展開
            try {
                $binaryResponse = Invoke-WebRequest -Uri $sitemapUrl -UserAgent $UserAgent -UseBasicParsing -TimeoutSec 30
                $sitemapContent = Expand-GzipContent -GzipBytes $binaryResponse.Content
            }
            catch {
                Write-Warning "Gzipファイルの処理に失敗しました: $sitemapUrl"
                continue
            }
        }
        
        if ([string]::IsNullOrWhiteSpace($sitemapContent)) { 
            Write-Warning "空のコンテンツをスキップします: $sitemapUrl"
            continue 
        }

        # XMLをパースし、URLを抽出(堅牢性向上)
        try {
            [xml]$sitemapXml = $sitemapContent
            if (-not $sitemapXml -or -not $sitemapXml.urlset) {
                Write-Warning "無効なXML構造をスキップします: $sitemapUrl"
                continue
            }
        }
        catch {
            Write-Warning "XML解析エラー ($sitemapUrl): $($_.Exception.Message)"
            continue
        }

        foreach ($url in $sitemapXml.urlset.url) {
            $loc = $url.loc
            $lastmod = if ($url.lastmod) { $url.lastmod } else { "" }
            
            # URLの形式に応じてデータを整形
            $obj = $null
            switch -Regex ($loc) {
                "^https://zenn\\.dev/([^/]+)/articles/([^/]+)" { $obj = [PSCustomObject]@{ Type = \'Article\'; Creator = $matches[1]; Slug = $matches[2]; URL = $loc; LastMod = $lastmod } }
                "^https://zenn\\.dev/([^/]+)/books/([^/]+)"    { $obj = [PSCustomObject]@{ Type = \'Book\'; Creator = $matches[1]; Slug = $matches[2]; URL = $loc; LastMod = $lastmod } }
                "^https://zenn\\.dev/topics/([^/]+)"          { $obj = [PSCustomObject]@{ Type = \'Topic\'; Creator = \'\'; Slug = $matches[1]; URL = $loc; LastMod = $lastmod } }
                "^https://zenn\\.dev/p/([^/]+)"               { $obj = [PSCustomObject]@{ Type = \'Publication\'; Creator = \'\'; Slug = $matches[1]; URL = $loc; LastMod = $lastmod } }
                "^https://zenn\\.dev/([^/]+)$"                { if ($loc -notmatch \'/(articles|books)/\') { $obj = [PSCustomObject]@{ Type = \'User\'; Creator = $matches[1]; Slug = \'\'; URL = $loc; LastMod = $lastmod } } }
            }
            if ($obj) { 
                $null = $results.Add($obj) # 戻り値抑制
            }
        }
    }
    Write-Progress -Activity "サイトマップを処理中" -Completed
}
catch {
    Write-Error "致命的なエラーが発生しました: $($_.Exception.Message)"
    exit 1
}

# --- 結果の出力 ---
if ($results.Count -gt 0) {
    Write-Host "`n抽出完了! $($results.Count) 件の項目が見つかりました。" -ForegroundColor Green
    
    # 設定に応じてファイル形式を決定
    $fullOutputFile = "$OutputFile.$OutputFormat"
    
    try {
        if ($OutputFormat -eq \'csv\') {
            # BOMなしUTF-8出力
            $csvContent = $results | ConvertTo-Csv -NoTypeInformation
            [System.IO.File]::WriteAllLines($fullOutputFile, $csvContent, [System.Text.Encoding]::UTF8)
        }
        elseif ($OutputFormat -eq \'json\') {
            # BOMなしUTF-8出力
            $jsonContent = $results | ConvertTo-Json -Depth 5
            [System.IO.File]::WriteAllText($fullOutputFile, $jsonContent, [System.Text.Encoding]::UTF8)
        }
        elseif ($OutputFormat -eq \'txt\') {
            # BOMなしUTF-8出力
            $urlList = $results | Select-Object -ExpandProperty URL
            [System.IO.File]::WriteAllLines($fullOutputFile, $urlList, [System.Text.Encoding]::UTF8)
        }
        
        Write-Host "結果を $fullOutputFile に保存しました。" -ForegroundColor Green
    }
    catch {
        Write-Error "ファイル出力に失敗しました: $($_.Exception.Message)"
        exit 1
    }
}
else {
    Write-Host "`n現在の設定では項目が見つかりませんでした。" -ForegroundColor Yellow
}

Write-Host "`n処理が完了しました。" -ForegroundColor Green

🚀 使用方法

1. PowerShellスクリプトファイルの作成

  1. メモ帳を開く(Windowsキーでメモ帳と検索)
  2. 上記のスクリプトコードをコピーして、メモ帳に貼り付け
  3. ファイル > 名前を付けて保存 を選択
  4. ファイル名を ZennGetter.ps1 に設定
    • 重要:ファイルの種類を「すべてのファイル(.)」に変更
    • 拡張子 .ps1 が正しく付くように注意
    • 文字コードは「UTF-8」を選択(Windows 10以降は自動でUTF-8)
  5. デスクトップなど、わかりやすい場所に保存

2. 実行用バッチファイルの作成

  1. 再びメモ帳を開く
  2. 以下の内容を入力:
@echo off
powershell -ExecutionPolicy Bypass -File "ZennGetter.ps1"
pause
  1. ファイル > 名前を付けて保存 を選択
  2. ファイル名を run.bat に設定
    • 重要:ファイルの種類を「すべてのファイル(.)」に変更
    • 拡張子 .bat が正しく付くように注意
    • 文字コードは「UTF-8」を選択
  3. PowerShellスクリプトと同じフォルダに保存

3. 実行

  1. run.bat をダブルクリック
  2. 黒い画面(コマンドプロンプト)が開き、処理が開始されます
  3. 完了すると zenn_sitemap_data.txt ファイルが同じフォルダに作成されます

📈 実行結果

スクリプトを実行すると、以下のような統計が表示されます:

処理対象のサイトマップを 19 件見つけました。
抽出完了! 205384 件の項目が見つかりました。
結果を zenn_sitemap_data.txt に保存しました。

出力されるファイルには、1行につき1つのURLが記録されます:

https://zenn.dev/username/articles/article-slug
https://zenn.dev/username/books/book-slug
https://zenn.dev/topics/react
https://zenn.dev/username

🔍 取得データの活用例

コンテンツの分類

CSV形式で出力すれば、ExcelやPowerShellで詳細な分析ができます:

CSV形式での出力方法

  1. スクリプト内の $OutputFormat = \'txt\'$OutputFormat = \'csv\' に変更
  2. 再実行すると zenn_sitemap_data.csv が作成されます

Excelでの分析

  • CSVファイルをExcelで開く
  • ピボットテーブル機能で集計・分析

PowerShellでの分析

# CSVファイルを読み込んで分析
$data = Import-Csv "zenn_sitemap_data.csv"

# タイプ別の件数を確認
$data | Group-Object Type | Sort-Object Count -Descending

# 記事数の多いユーザーTop10
$data | Where-Object Type -eq "Article" | Group-Object Creator | Sort-Object Count -Descending | Select-Object -First 10

AI開発ツールとの連携

最近のAI開発ツールは、URLを指定して記事内容を取得・分析できます。取得したURLリストと組み合わせることで、大規模な記事分析が可能になります。

Claude Code での活用例

# URLリストから任意の10件を抽出してファイルに保存
head -n 10 zenn_sitemap_data.txt > sample_urls.txt

# Claude Codeで記事を分析
claude code "sample_urls.txtに記載されたURLから記事を取得して、内容を要約したマークダウンレポートを作成してください"

Gemini CLI での活用例

# URLリストから任意の10件を抽出
head -n 10 zenn_sitemap_data.txt > sample_urls.txt

# Gemini CLIで記事を分析・要約
gemini "sample_urls.txtに含まれる10記事を読んで、その内容をまとめてください。結果をsummary.mdに保存"

記事コンテンツの一括収集と加工

取得したURLリストを基に、記事の実際のコンテンツを収集することも可能です。例えば、PowerShellのInvoke-WebRequestコマンドレットや、curlなどのツールを使って、各URLからHTMLコンテンツを取得し、テキストとして保存できます。

# PowerShellで記事コンテンツを取得する例(抜粋)
# この処理はZennのサーバーに負荷をかける可能性があるため、慎重に実行してください。
# また、利用規約を確認し、適切な範囲で実施してください。

$urls = Get-Content "zenn_sitemap_data.txt" | Select-Object -First 10 # 例として最初の10件
$outputDir = ".\zenn_contents"
New-Item -ItemType Directory -Force -Path $outputDir

foreach ($url in $urls) {
    try {
        $fileName = ($url -replace "^https?://", "") -replace "/", "_" # URLをファイル名に変換
        Invoke-WebRequest -Uri $url -OutFile "$outputDir\$fileName.html"
        Start-Sleep -Milliseconds 1000 # サーバー負荷軽減のため1秒待機
    }
    catch {
        Write-Warning "記事のダウンロードに失敗しました: $($url) - $($_.Exception.Message)"
    }
}

このようにして収集したコンテンツは、ローカルでの高速検索や、オフラインでの閲覧、あるいは特定のキーワードやフレーズの出現頻度分析などに活用できます。

その他の活用アイデア

技術トレンドの時系列分析
記事の投稿日時とURLを組み合わせることで、特定の技術に関する記事の投稿数の推移を分析し、その技術の人気度の変化を可視化できます。

学習リソースのキュレーション
自分の学習目標に合わせて、関連性の高い記事をURLリストから抽出し、自分だけの学習パスを作成できます。

コンテンツ監視・アラート
新しい記事の投稿を定期的にチェックし、関心のある分野の更新があった場合に通知を受け取るシステムを構築できます。

Zennコミュニティ分析
ユーザーURLやトピックURLを活用し、Zennコミュニティ内での影響力のあるユーザーや、活発なトピックを特定できます。

⚠️ 注意事項

  • スクリプトにはサーバー負荷軽減のため、リクエスト間に500msの待機時間を設けています
  • 全データの取得には数分から数十分程度の時間がかかります
  • 出力ファイルは数十MBになる場合があります
  • サイトマップの構造が変更された場合、スクリプトの修正が必要になる可能性があります
  • 記事コンテンツの自動収集を行う際は、Zennの利用規約を必ず確認し、適切な範囲で実施してください。

📝 まとめ

Zennのサイトマップを活用することで、ページネーションの制限を回避し、効率的に全記事URLを取得できました。このアプローチは他のサイトマップを公開しているWebサイトにも応用可能です。

取得したデータは、技術トレンドの分析、コンテンツの分類、学習用データセットの構築など、様々な用途に活用できます。このスクリプトが、皆さんのZennデータ活用における新たな一歩となり、技術探求や情報分析の可能性を広げる一助となれば幸いです。

Discussion