備忘ぶ録-新犬小屋

ココログ「備忘ぶ録(https://kotatuinu.cocolog-nifty.com/blog/)」のコピー場所です。

Powershellでココログの記事をExportするプログラム

現状、ココログでは、記事の内容をExportすることができません。
というわけで、一身上の都合(※)で、ココログに投稿されている全記事の内容(タイトル、本文・追記、カテゴリ)を出力するPowershellプログラムを作ってみました。

※:Amazonから”アソシエイトプログラムで1年間だれも買わなかったから垢バンするのでリンクを全て消せ”という横暴なことを通知されたので、このブログの内容を一旦保存して、それからリンクを消す方法を模索してみようかと思っている次第であります。

■仕様

  • ココログの記事編集画面にログイン→記事一覧を取得→記事一覧のリンクから記事投降画面に遷移して内容(タイトル、本文・追記、カテゴリ)を取得しJSONファイルに出力する。
    なので、ココログに投稿できる自分のブログしか、取得できません。
  • 出力するファイル名は、記事一覧のリンクにあるパスの最後尾=記事IDとタイトルを組み合わせて、"<記事ID>_<タイトル>.json"というフォーマットになります。ファイル名に使用できない文字("<>|:*?\/\r\n[])は、アンダーバー"_"に置き換えます。
  • タイトル、本文、追記、カテゴリのキーは、その画面のHTML中の該当要素にあるid属性の値を使っています。例えば以下のようになります。(記事IDが先頭にあるので、記事ごとにキーが変わります)
    • タイトル:126006900__entry_title
    • 本文:126006900__entry_text
    • 追記:126006900__entry_text_more
    • カテゴリ:126006900__category_ids
  • 記事一覧100ページ、2,985記事を取得するのに40分くらいかかります。ウェイトは入れていません。

■技術的なこと

  • Webページを取得するのに、Invoke-WebRequestコマンドを使っています。Cookieを使ってトップ画面から画面遷移が必要。
  • 基本的には、リンクを踏んで移動。ログインだけがPOST。
  • 記事の内容を取得するのに、記事投降画面のHTMLから正規表現を使用。ParsedHTMLからタグを指定して要素を取得したほうが、確実化と思われましたが、要素取得getElementsByTagName()が同じ画面でも取得するたびに不定期にNotSupportExceptionとなる現象が発生。正常に取得できた時とエラーになるときとHTMLを比較してみると属性の順番が異なっているのは確認できたが、内容がおかしいようには見えない・・・。しょうがないので文字列として扱う、つまり正規表現で検索する方が安定。

 

# cocolog start page
write-host "** cocolog start page"
$resp = Invoke-WebRequest "http://www.cocolog-nifty.com/" -SessionVariable "session"
if($resp.StatusCode -ne 200) { exit; }

# cocolog start page→login page
write-host "** cocolog start page->login page"
$resp = $null
$resp = Invoke-WebRequest "http://www.cocolog-nifty.com/t/sso/start" -WebSession $session
if($resp.StatusCode -ne 200) { exit; }

do {
	# login実行→ブログトップページ
	#input UID/PDW
	$cr = Get-Credential
	$link = "https://sso.nifty.com/pub/" + $resp.Forms[0].Action
	$body = $resp.Forms[0].Fields
	$body.item("username") = $cr.username
	$body.item("password") = $cr.GetNetworkCredential().password
	$body.item("input_selector") = "0"
	$body.item("remember") = "1"
	$body.Remove("saveLogin")
	write-host "** login -> cocolog management top page"
	$resp = $null
	$resp = Invoke-WebRequest $link -WebSession $session -Body $body -Method "POST"
	if($resp.StatusCode -ne 200) { exit; }

	# cocolog management top pageでなかったらログイン失敗→UID/PWD入力しなおし
} while($resp.ParsedHtml.title -ne "ココログ管理ページ:@nifty")

# 記事一覧のリンク取得
$blogTopLink = "http://app.f.cocolog-nifty.com" + ($resp.Links | ?{$_.innerText -ne $null -and $_.outerHTML.indexof("<a class=`"blog-menu__item -list box-sub-01`"") -ge 0 }).href
# 記事一覧ページに移動
write-host "** contents list:" $blogTopLink
$currentPageLink = $blogTopLink
$resp = $null
$resp = Invoke-WebRequest $currentPageLink -WebSession $session
if($resp.StatusCode -ne 200) { exit; }

#全ページを舐める
$nextPage = ""
do {
	if($resp.Links.count -eq 0) {
		exit;
	}

	# 記事一覧を取得
	$resp.Links | ? { $_.outerHTML.indexof("title") -ge 0 } | %{
		$title = $_.title
		$href = $_.href
		 "title:`"{0}`"`thref:`"{1}`"" -F $title, $href

		$contentLink = "http://app.f.cocolog-nifty.com" + $href
		write-host "** contents page:" $contentLink
		# 各記事に遷移
		$respContent = $null
		$respContent = Invoke-WebRequest $contentLink -WebSession $session
		if($respContent.StatusCode -ne 200) { exit; }

		# 記事の情報を取得→JSONファイル出力
		#  ファイル名:<エントリ番号>_<記事title>.json
		$entryNo = $href.Substring($href.lastIndexOf("/")+1)
		$fileName = ("{0}_{1}.json" -F $entryNo, $title)
		# ファイル名使用禁止文字をアンダーバーに置き換える
		$fileName = $fileName -replace "[`"<>\|:\*\?\\/`r`n\[\]]", "_"

		# 内容の改行文字を\nに置き換える(JSONでは"\\n"になる)
		$content = @{}

		# 記事のtitle,text,text_moreを取得
		$text = $respContent.Content
		"${entryNo}__entry_title", "${entryNo}__entry_text", "${entryNo}__entry_text_more" | %{
			$value = ""
			$rgx = [regex]::matches($text, "(?s:<textarea.*?( (id|name)=""${_}"")+.*?>(.*?)</textarea>)")
			if($rgx.Success) {
				$value = $rgx.Groups[3].value.replace("`n", "\n")
			}
			$content[$_] = $value
		}

		# カテゴリ(選択されているもの)
		$elmName = "${entryNo}__category_ids"
		$categories = @()
		$rgxRslt = [regex]::Matches($text, "(?s:<select.*?name=""${elmName}"".*?>(.+?)</select>)")
		if($rgxRslt.Success) {
			$options = $rgxRslt.Groups[1]
			$options -split "</option>" | %{
				$rgxOpt = [regex]::Matches($_, "<option.*selected=""selected"".*>.+")
				if($rgxOpt.Success) {
					$rgxOpt = [regex]::Matches($_, "<option.*value=""(\d+)"".*>(.+)")
					if($rgxOpt.Success) {
						$category = @{}
						for($i=0; $i -lt $rgxOpt.Groups.count/3; $i++) {
							$category["id"] = $rgxOpt.Groups[$i*3+1].value
							$category["text"] = $rgxOpt.Groups[$i*3+2].value
							$categories += $category
						}
					}
				}
			}
		}
		$content[$elmName] = $categories

		# ファイル出力
$content | ConvertTo-Json -Compress
		$content | ConvertTo-Json -Compress | Out-File $fileName -Encoding UTF8

		# 記事一覧に戻る
		#write-host "** return to contents list page:" $currentPageLink
		#$respContent = $null
		#$respContent = Invoke-WebRequest $currentPageLink -WebSession $session
		#if($respContent.StatusCode -ne 200) { exit; }
	}

	# ここでは記事一覧ページにいる
	# 次ページの取得
	$nextPage = ($resp.Links | ? {$_.class -eq "next" } | %{$_.href})
	$nextPageLink =  $blogTopLink + $nextPage 

	if($nextPage.length -gt 0) {
		$currentPageLink = $nextPageLink

		# 次ページ移動
		write-host "** next contents list page:" $currentPageLink
		$resp = $null
		$resp = Invoke-WebRequest $currentPageLink -WebSession $session
		if($resp.StatusCode -ne 200) { exit; }
	}
} while($nextPage.length -gt 0)

write-host "** finish!"