ローカルにバックアップしておきたいしgrepもしたい。100記事くらいある。なんとか自動化できないものか。挑戦した結果、みごとに玉砕。ログインの壁に阻まれた。

結論

JavaScriptのfetchで取得しようとしたが、ログインできず取得できなかった。

動機

  • 学習結果を記事にしてきたのでそれをバックアップしておきたい
  • ローカルでgrep検索できるようにしたい
  • 100記事に達しそう。手作業でやるには多すぎる

自分の学習記録から「あれ何だっけ?」と検索したくなることが稀によくある。検索ならモナレッジでもGoogleでも可能だが、マシンのメモリ不足をカバーするためブラウザを閉じる必要があるときもある。また、モナレッジ以外の資料も含めて一括検索したいときもある。バックアップもしておきたい。諸々を考えるとローカルに自分の記事を保存して煮るなり焼くなりすればいいという結論になる。ただ、モナレッジで書いた自分の記事が100に達しそう。手作業でやるのは億劫。さりとてダウンロード機能もない。というわけで自分でやってみようと思い立つ。

データはHTMLでなくマークダウンがいい。そのほうが編集、grep検索ともに好都合。

方法

  • https://monaledge.com/mypageにアクセスする
    • ログインする
    • 自分の記事一覧から記事番号を取得する
      • <a href="/article/435" >要素
    • ページネーションして全ページ分取得する
  • https://monaledge.com/edit/記事番号にアクセスする
    • <pre class="auto-textarea-block" ...>要素のテキストノードにマークダウンが含まれている
    • どうにかしてそれを取得し、ファイル保存する

モナレッジのページを解析した結果、こんな感じのフローでいけそうな気がする。問題はこれが実現できるかどうか。

懸念

先に問題となりそうな所をピックアップしておく。

ログイン

これは自動化できなさそう。これができないと自分の記事番号を取得できないと思う。さりとて、ここを手作業でやるとほぼ無意味。すべてを手作業でやるのと変わらない。ログイン処理をどう解決するかがキモになりそう。

サーバ負荷

できるだけサーバ負荷をかけずに行うにはどうしたらいいか。

  • 一回のリクエストあたり最低1秒のウェイトをかける
  • すでに保存した記事番号まで達したら終了する

昔の記事を変更したときはどうするか。個別に取得するしかない。自動化はできなさそう。そこは諦める。

著作権

自分の記事だけを対象にしていれば問題ないはず。もとよりそれが目的だし。

ただ、https://monaledge.com/edit/記事番号とすれば自分以外の記事のマークダウンまで見れるみたい。

もしログインできず自分の記事だけを取得するのが面倒だからと1から順にすべての記事番号でダウンロードすると他人の記事まで取得することになる。その場合、著作権の侵害になったりしないのかな? そもそもスクレイピングしてサイトから情報を取得すること自体グレー。そもそも法律うんぬん関係なく運営や記事を書いた人の同意を得ず勝手に取得するという時点でどうかと思う。あと、サーバ負荷もムダに増える。もとより他人の記事はいらない。あらゆる意味で全ダウンロードはやめるべき。横着してやらかす前に考えだけはまとめておく。

自分の記事だけを取得するなら著作権に関する問題はないはず。やはりどうにかして自分の記事番号だけを取得することが必要。

記事番号を取得する

  1. ブラウザでhttps://monaledge.com/mypageにアクセスする
  2. 以下コマンドをデベロッパツールのコンソールで実行する

自分の記事へのリンクを定義したa要素をすべて取得する。

document.querySelectorAll(`a[href^='/article/']`)

自分の記事へのリンクを定義した全a要素からURLを抽出する。

Array.from(document.querySelectorAll(`a[href^='/article/']`)).map(e=>e.getAttribute('href'))

URLから末尾の記事番号だけを取得する。

Array.from(document.querySelectorAll(`a[href^='/article/']`)).map(e=>e.getAttribute('href').replace(/.*[\/\\]/, ''))

記事番号は整数値なので数値化する。

Array.from(document.querySelectorAll(`a[href^='/article/']`)).map(e=>parseInt(e.getAttribute('href').replace(/.*[\/\\]/, '')))

たとえば以下のように取得できる。間の455はらいうさんの記事なので私のmypageには出てこない。ここまでは思い通り。

(10) [457, 456, 454, 453, 452, 451, 450, 449, 448, 447]

ページネーション

上記を全ページ分だけ実行したい。

マイページには自分の記事が新しい順に1ページあたり10件まで表示されている模様。それより古い記事を取得するには2ページ以降にアクセスする必要がある。そのURLはhttps://monaledge.com/mypage/ページ数らしい。そこで、まずは最大ページ数を取得する。

以下がページ遷移する要素らしい。ここから10を取得するのが狙い。

<button type="button" aria-label="Goto Page 10" class="v-pagination__item">10</button>

最大ページ数

  1. https://monaledge.com/mypageにアクセスする
  2. 以下コマンドをコンソールで実行する

ページ遷移ボタンからページ数リストを取得する。

Array.from(document.querySelectorAll(`button.v-pagination__item`)).map(e=>parseInt(e.textContent))

ページ数リストから最大値を取得する。

Math.max(...Array.from(document.querySelectorAll(`button.v-pagination__item`)).map(e=>parseInt(e.textContent)))

あとは1〜最大値までの値をhttps://monaledge.com/mypage/ページ数でリクエストすればページにアクセスできるはず。

ページネーション

これでいけるかと思いきや、すべて空だった。

const sleep = s => new Promise(resolve => setTimeout(resolve, s*1000));
const myPages = []
const lastPage = Math.max(...Array.from(document.querySelectorAll(`button.v-pagination__item`)).map(e=>parseInt(e.textContent)))
for (let page=1; page<=lastPage; page++) {
    const res = await fetch(`https://monaledge.com/mypage/${page}`)
    const txt = await res.text()
    const body = document.createElement('body')
    body.innerHTML = txt
    myPages.push(Array.from(body.querySelectorAll(`a[href^='/article/']`)).map(e=>parseInt(e.getAttribute('href').replace(/.*[\/\\]/, ''))))
    console.debug(myPages)
    await sleep(1)
}
myPages.flat()

原因はログインできなかったこと。https://monaledge.com/mypage/${page}で返されるHTMLがログインしていないときのものだと思われる。そのせいで記事へのリンク要素が存在せず結果ゼロ件となった模様。

const res = await fetch(`https://monaledge.com/mypage/1`)
const txt = await res.text()
console.debug(txt)

つまりログイン自動化できないせいでページネーションも自動化できないということだ。

ならどこまで自動化できるか予想してみる。

  1. https://monaledge.com/mypageにアクセスする
  2. ログインする
  3. 1ページずつクリックする
  4. そのたびにページ内から記事番号リストを取得するコードを実行する

1ページあたり10記事表示されることから、上記手順を全記事÷10回実行することになる。面倒くさいが仕方ない。

現在ページの記事番号をクリップボードに保存する

せめて記事番号を取得する手間を少しでも減らすためクリップボードへコピーしたい。

await navigator.clipboard.writeText(Array.from(document.querySelectorAll(`a[href^='/article/']`)).map(e=>parseInt(e.getAttribute('href').replace(/.*[\/\\]/, ''))).join('\n')).catch(e=>console.error(e))

以下のようなエラーが出た。

DOMException: Document is not focused.

どうやら文書にフォーカスが当たってないとダメらしい。デベロッパツールを開いているとそっちにフォーカスが当たってしまい使えない。そこでブックマークレットにする。

javascript:(async()=>{await navigator.clipboard.writeText(Array.from(document.querySelectorAll(`a[href^='/article/']`)).map(e=>parseInt(e.getAttribute('href').replace(/.*[\/\\]/, ''))).join('\n')).catch(e=>console.error(e))})();

ブックマークレットはブラウザでブックマークを編集すれば作れる。あるいはどこか適当なページで以下HTMLを表示させ、ブラウザでブックマークすれば登録できる。

<a href="javascript:(async()=>{await navigator.clipboard.writeText(Array.from(document.querySelectorAll(`a[href^='/article/']`)).map(e=>parseInt(e.getAttribute('href').replace(/.*[\/\\]/, ''))).join('\n')).catch(e=>console.error(e))})();">モナレッジのmypageから記事番号を取得し、クリップボードにコピーするブックマークレット</a>

モナレッジのmypageから記事番号を取得し、クリップボードにコピーするブックマークレット

あとはモナレッジのmypageにアクセスし、ブラウザのブックマーク一覧からさきほどのブックマークレットをクリックすれば実行される。以下のように。

457
456
454
453
452
451
450
449
448
447

これを全ページで実行すれば、すべての記事番号を取得できる。あー面倒くさい。一発でやりたかったなぁ。しかもまだ記事番号を取得しただけ。肝心のマークダウンを取得できていないし。

マークダウンを取得する

試しに以下URLでページを解析してみる。

タイトル

document.querySelector(`#input-32`).value

マークダウン本文

document.querySelector(`pre.auto-textarea-block`).textContent

カテゴリ

document.querySelector(`div.v-select__selection`).textContent

指定ページのマークダウンを取得する

const sleep = s => new Promise(resolve => setTimeout(resolve, s*1000));
const pages = [457,456,454,453,452,451,450,449,448,447]
for (let page of pages) {
    const res = await fetch(`https://monaledge.com/edit/${page}`)
    const txt = await res.text()
    const body = document.createElement('body')
    console.debug(txt)
    body.innerHTML = txt
    const title = document.querySelector(`#input-32`)?.value
    const content = document.querySelector(`pre.auto-textarea-block`)?.textContent
    const category = document.querySelector(`div.v-select__selection`)?.textContent
    console.debug(page, category, title, content)
    await sleep(1)
    break
}

すべてundefinedだった。

嫌な予感がする。単体ページを取得してみる。

const res = await fetch(`https://monaledge.com/mypage/457`)
const txt = await res.text()
console.debug(txt)

結果、ログインしていないときのHTMLが返された模様。

あ、ダメだこれ。ログインしないと本文も取得できないらしい。ブラウザで閲覧しているときは見えたのに。たぶんクッキーを使ってログインを維持していたのだろう。それをJavaScriptでも再現したいのだが。

どうにかしてログインしないと解決できない。

後悔

最初からローカルでファイル作成しておき、それをモナレッジに投稿すればよかった。

でも、まさか100記事も書くと思ってなかったしなぁ。

手作業で取得できないこともないけれど絶対ミスする。そして負けた気がする。どうにかして自動化できないものか。こういう作業を自動化できるのがプログラミングの醍醐味なのに。まだまだ修行が足りないか。

モナレッジにログインできなくなった件

この後、seleniumを使ってログインを手動でやりつつ、ページから記事番号を取得できないか試みました。結果からいえば取得できませんでした。ログイン前と思しきHTMLしか取得できませんでした。何度かブラウザの再起動とログインを繰り返しているうち、あるときログインできなくなりました。何度もログインを繰り返したのでBANされたかと思い、らいう様に連絡した次第です。

seleniumのくだりは長すぎるのでさらに記事を分けます。関係あるとすればログインを繰り返したことだと思いますが。