はじめに
Zennの記事データを分析したり、技術トレンドを調査したりする際に、全記事のURLを取得したいケースがあります。
最初は https://zenn.dev/articles?page=1
のようにページを順番に辿る方法を試しましたが、100ページまでしか表示されないため、全記事を取得することができませんでした。
そこで別の方法を探したところ、Zennがサイトマップを公開していることがわかりました。サイトマップとは、Webサイトの全ページを検索エンジンに知らせるためのファイルです。このサイトマップを解析することで、20万件以上の記事URLを効率的に取得できることがわかりました。
対象読者
- PowerShellの基本的な使い方を知っている方
- Webスクレイピングやデータ収集に興味がある方
- Zennの記事データをプログラムやAIで活用したい方
環境
- Windows 10/11
- PowerShell 5.1以降
サイトマップを利用したアプローチ
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スクリプト(クリックで展開)
# 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スクリプトファイルの作成
- メモ帳を開く(Windowsキーでメモ帳と検索)
- 上記のスクリプトコードをコピーして、メモ帳に貼り付け
- ファイル > 名前を付けて保存 を選択
- ファイル名を
ZennGetter.ps1
に設定- 重要:ファイルの種類を「すべてのファイル(.)」に変更
- 拡張子
.ps1
が正しく付くように注意 - 文字コードは「UTF-8」を選択(Windows 10以降は自動でUTF-8)
- デスクトップなど、わかりやすい場所に保存
2. 実行用バッチファイルの作成
- 再びメモ帳を開く
- 以下の内容を入力:
@echo off
powershell -ExecutionPolicy Bypass -File "ZennGetter.ps1"
pause
- ファイル > 名前を付けて保存 を選択
- ファイル名を
run.bat
に設定- 重要:ファイルの種類を「すべてのファイル(.)」に変更
- 拡張子
.bat
が正しく付くように注意 - 文字コードは「UTF-8」を選択
- PowerShellスクリプトと同じフォルダに保存
3. 実行
-
run.bat
をダブルクリック - コマンドプロンプトが開き、処理が開始されます
- 完了すると
zenn_sitemap_data.txt
ファイルが同じフォルダに作成されます
初回実行時のエラーについて
「実行ポリシーが制限されています」のようなエラーが出た場合:
- Windowsキー + R を押す
-
powershell
と入力してEnter - 以下のコマンドを入力してEnter:
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
-
Y
を入力してEnter - PowerShellを閉じて、再度バッチファイルを実行
実行結果
スクリプトを実行すると、以下のような統計が表示されます:
処理対象のサイトマップを 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
取得データの活用例
基本的な分析
Windowsのコマンドプロンプトで実行:
# 特定技術の記事数を調べる
findstr /c:"react" zenn_sitemap_data.txt | find /c /v ""
findstr /c:"vue" zenn_sitemap_data.txt | find /c /v ""
PowerShellで実行:
# より詳細な分析
(Get-Content "zenn_sitemap_data.txt" | Select-String "react").Count
(Get-Content "zenn_sitemap_data.txt" | Select-String "vue").Count
コンテンツの分類
CSV形式で出力すれば、ExcelやPowerShellで詳細な分析ができます:
CSV形式での出力方法:
- スクリプト内の
$OutputFormat = 'txt'
を$OutputFormat = 'csv'
に変更 - 再実行すると
zenn_sitemap_data.csv
が作成されます
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
記事コンテンツの一括収集と加工
取得したURLリストを基に、記事の実際のコンテンツを収集することも可能です。PowerShellのInvoke-WebRequest
コマンドレットを使って、各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コミュニティ内での影響力のあるユーザーや、活発なトピックを特定
技術的なポイント
エラーハンドリング
スクリプトでは以下のエラーハンドリングを実装しています:
- ネットワークエラー時の自動リトライ機能
- 404や403エラーの適切な処理
- XML解析エラーの処理
- リソースの適切な解放
パフォーマンス最適化
-
System.Collections.Generic.List
を使用してメモリ効率を向上 - Gzip圧縮ファイルの適切な展開処理
- プログレスバーによる進捗表示
サーバー負荷への配慮
- リクエスト間の待機時間設定
- User-Agent の適切な設定
- タイムアウト値の設定
注意事項
- スクリプトにはサーバー負荷軽減のため、リクエスト間に500msの待機時間を設けています
- 全データの取得には数分から数十分程度の時間がかかります
- 出力ファイルは数十MBになる場合があります
- サイトマップの構造が変更された場合、スクリプトの修正が必要になる可能性があります
- 記事コンテンツの自動収集を行う際は、Zennの利用規約を必ず確認し、適切な範囲で実施してください
まとめ
Zennのサイトマップを活用することで、ページネーションの制限を回避し、効率的に全記事URLを取得できました。このアプローチは他のサイトマップを公開しているWebサイトにも応用可能です。
取得したデータは、技術トレンドの分析、コンテンツの分類、学習用データセットの構築など、様々な用途に活用できます。PowerShellの強力なテキスト処理機能と組み合わせることで、さらに高度な分析も可能になります。