自分が過去に書いたモノを一箇所にまとめたいの話を実際に実現しよう、と決心する。

システムの名前は unimemo とする

とりあえず名前をつける。unified memoという事でunimemoに決定。

レポジトリ: https://github.com/karino2/unimemo

基本的な考え

まずGoogle Drive上に置く事にする。 対象としてはlivejournalとかmixiとかtweetとかGoogle Keepとかいろいろあるので、 それぞれのデータからmarkdownへのコンバーターをそれぞれ書く、というスタンスにする。

unimemoのルートを/で表すと、まずexportした生のデータを/original下に置く。

  • /original/Keep
  • /original/mixi
  • /original/twitter
  • /original/...

など。このデータはコンバートせず、何らかのexportしたものをそのまま置く事にする。 新しくデータをexportしたらここにそのまま置くだけで良い。

で、これをmarkdownにコンバートする。それはmdフォルタとする。 フォルダとしては、月までを掘って、中をどうまとめるかはコンバーターに任せる。 例えばtweetは一日単位を1ファイルにまとめる。 ブログは1記事を1ファイル、など。

  • /original
  • /md/2020/01
  • /md/2020/02
  • /md/...

ファイル名は、export元_日付_任意の文字.mdとする。任意の文字はoptional(ブログのタイトルなどを入れる)。 例えばtwitter_2020_01_07.mdとか、mixi_2020_01_07_153732.mdとか。

どういうマークダウンを作るかはコンバーター任せ。

画像は現時点ではoriginalの下にそのまま相対リンクで貼ってしまえばいいかなぁ。 数百MBというオーダーなのでmd下にコピーしてしまってもいいんだけど、とりあえずはやめておこう。

grepはmdフォルダ下で行う。

ビュワー(は後回し)

また、マークダウンのビュワーはそのうち何か作りたい。ローカルにhttp立ち上げて月ごとにまとめるビューを、 適当なファイル数ごとにページネーションつける感じのモノと、export元ごとに見れるようなビューが欲しいかなぁ。 ただこれは後回しで良かろう。とりあえずgrep出来て、VSCodeのmarkdown previewerとかで1ファイルは見れるだろうし。

追記: 試してみたところ、相対パスで親を見るのはVSCodeのmarkdown previewerでは出来ないっぽい。 Chromeのextentionでは出来たので、とりあえず1ファイルだけならChromeのextentionで見れば見れる。

インクリメンタルに作業出来る事を重視する

一気に作ろうとするとやる気が出ないので、まず最低限の所から始めたい。 とりあえずサービスが消えてしまう前にexportするのが第一歩。 これも全部exportしようとかいうとやる気が出ないので、とりあえず必要なtwitterから始めて徐々に進めたい。

とりあえずoriginal下に置くだけなら害は無いので、コンバーターを書くのは後回しに出来る。 気力が湧いた時にexportしていって、気力が湧いた時にコンバーターを書きたい。 ビュワーとか作る前にとりあえずexportだけ出来るように決めてしまうのが大切(たぶん)。

作業の進め方

とりあえずunimemoというフォルダにプログラム関連を置く。 unimemo.fsprojを作り、Scratch.fsxというファイルで適当にデータをつつきつつ、 いいところまで行ったら.fsファイルに切り出していこう。 まぁコンバートなんて一回やればしばらくはいいのだから、fsx上でゆるゆるやって切り出しとか考えなくてもいいかもしれない。

twitter

一番のモチベーションは過去のtweetを適当に検索して、それがいつだったかを知りたい、なので、まずはtwitterから作業する。

exrpot方法

twitterの設定とプライバシーから、アカウント>データのアーカイブをダウンロード でパスワードを入れてリンクを押すと、3日後くらいにアプリやサイトに通知が来る。 リンクを押した段階ではなんか終わったふうに見えるのに何もリンクが無いので混乱するが、待てば良いだけ。

ダウンロードした結果はjsonだけが入ったtweet.jsと画像。

FSharp.Dataのセットアップ

jsonのパースはFSharp.Dataを使う事にする。

まずはunimemo.fsprojのファイルにFSharp.Dataのエントリを足してビルド。

  <ItemGroup>
    <PackageReference Include="FSharp.Data" Version="3.3.3" />
    <Compile Include="Program.fs" />
  </ItemGroup>

次にScratch.fsxの先頭に以下のように書いてみる。

#r "bin/Debug/net5.0/FSharp.Data.dll"

すると、なんかFSharp.Data.DesignTime.dllが無い的な事を言われる。何これ?

以下のissueっぽいが、解決策がよくわからないな。 Referencing Fsharp.Data.dll from the script produces design time-related error (#647)

分からないのでfsharp.dataのnugetパッケージをとってきて該当dllを抜き出そう。

https://www.nuget.org/packages/FSharp.Data/から、Version 3.3.3を選んで右側のDownload Packageを選ぶ。 拡張子をzipにして開いてみる。dllの大きさを比較すると、lib/netstandard2.0/FSharp.Data.dllが我々の参照しているdllっぽい? なんでnet5.0じゃないのかは良く分からないが、まぁいい。 lib/netstandard2.0/FSharp.Data.DesignTime.dllを、プロジェクトのbin/Debug/net5.0/にコピーする。

fsxからロードしたら動いたヽ(´ー`)ノ 

==追記==

fsprojへのNuGetパッケージの追加は以下のようにdotnetコマンドでやるのが正しいやり方っぽい。

dotnet add unimemo.fsproj package FSharp.Data

簡単だ。すばら。

さらにfsxからのロードもnugetパッケージを指定してやる方が良さそう。

#r "nuget: FSharp.Data"

これだとvar下に別のバイナリをダウンロードしてロードしちゃってるように見えるが、大したサイズじゃないし変なトラブルでハマるよりはいいでしょう。

jsonのロード関連のメモ

tweet.jsは先頭の行を細工すればjsonになりそうなので細工するコードを書く。

json自体は70MBくらい。

スキーマを定義するのはかったるいなぁ、と思っていた所、FSharp.DataはTypeProviderという仕組みで型を動的に生成して、 さらにそれを活かしてデータからスキーマをguess出来るらしい。(・∀・)イイネ!!

という事でtweet.jsの一行目を直したものをtemp_tweet.jsonという名前で吐く事にし、これを

type TweetParser = JsonProvider<"temp_tweet.json">
let data = TweetParser.GetSamples()

これでロード出来ているっぽい。まぁまぁ一瞬。2秒くらいか?ちょっと待つがそのくらい。70MBくらいのデータがこれだけ早くパース出来るなら十分やね。

型はTweetParser下に作られるっぽい。だいたい良さそうだが、MediaとMedia2というのが出来ている。どう違うんだろう? こういう生成された型をダンプする方法って無いのかしら?

良くわからんので、

let hoge2 x:TweetParser.Media =
  x.

という感じでインテリセンスしたものを目視で比べると(ゆとり)、どうもMedia2の方にだけAdditionalMediaInfoというのがあるらしい。 どういう事だ?とこれを含んでるっぽいjsonを一つだけ取り出してロードしていろいろ試した所、 このMedia2はdata.[0].Tweet.ExtendedEntitiesの下のMediaの型っぽいな。

コンバーター関連のメモ。

とりあえずリンクと画像はちゃんと扱おう。 リンクはshortじゃない元の奴に戻す。画像はmarkdownのインラインのにする。

あとは1日単位で一つのmarkdownとして、中身は以下でいいか?

### 2007-05-15のツイート

*2007-05-15 15:55:04*  
ここにツイート

*2007-05-15 17:23:04*  
ここに別のツイート

日付は鬱陶しいが、仕方ないか。

ハッシュに突っ込みたいがどういうハッシュにするかなぁ。月か日だが、どっちがいいか。日でいいか。 空の日の影響はどのくらいあるかな? 日付でfor文回すと、今年てtwitterはじめて14年くらいか?なので、365x14か。まぁこの位なら一瞬だな。じゃあ日でハッシュに突っ込んで、 月ごとにDateTimeを31個生成して引いてみる感じでいいか。

livejournal

livejournal時代のブログをgrepしたくなったので作業する。

export

exporterとかがいろいろあるが、いまいち動かなかった。公式はweb UIで一月ごとに指定。かったるい。 公式のweb UIで一月分のxmlをダウンロードしてみて、developer toolsでcURLでコピーしてyearとmonthを指定できるシェルクリプトを作り(ljget.shとする)、 それをfsxから叩く。

これだと画像が入らないな。 https://ironymaiden.dreamwidth.org/1072255.htmlがローテクでいい感じなので真似してみるか。 アルバムから全選択してADD TO POSTでオリジナルサイズにして、ソースビューからコピペしてurl一覧を取り出し、wget -i urls.txtする。

これではブログ中からはサイズ指定されていると、画像のurlとファイル名は違う物になっちゃうが、 頑張れば対応は作れるはずなので、とりあえずローカルに落としておいただけで満足しておこう。

このままでも見づらいけれどgrepはできるので、

mixi

2021年現在、mixiのexportは良い方法が無さそう(!)。 という事でまずは自分でexportを書く所からかぁ。

mixiのexport事情とexportの方針

昔exportした時はmixi_exportを使ったのだが、最近はログイン周りが難しくなっているようで、自分試した範囲ではログイン出来なかった。 そもそもにreCAPTCHAの対策にプロキシで一旦ログインするようにしたっぽいが、それは筋が悪そう(実際やがてログイン出来なくなるはず、と書かれていた)。 他のものも幾つか試したがどれも動かなかった。

canopyでログインしてみようかと思ったが、webdriver越しのchromeでは手動でもログイン出来なかった。ふむ。 頑張れば突破もできるのだろうが、突破させないようにしているものを突破するのもなぁ、という事でその路線はやめる。 自分の書いたものを取り出す方法を(pdfで出す以外)提供しないのに邪魔するのはどうなんだ、という気もするが。

こういうのはログイン周りを頑張って突破するよりは、ログインしたあとにChromeのdev toolsでcurlとしてリクエストをコピーする方が良いだろう、という事で、 コピペしたものの引数だけ変数にしたシェルスクリプトを毎回作り、それをキックして必要なファイルを取得して、 fsxとしてはスクリプトのキックとローカルファイルの処理を順番にやっていく感じでいいかなぁ、と思う。

全自動を目指すよりは、半自動くらいで、リクエストの間隔が短すぎる、とか怒られたらそこでちょっとまって続きをevalする、くらいの方が良かろう。 これなら手動と自動の境目くらいなので、そう仁義にもとる事でも無いだろう、という事で。 幸い、一度exportすれば以後は追加分だけで良いので、半手動でかったるいのも一回だけだろう。

HTMLのパーサーはHtmlAgilityPackを使う事に

最初はFSharpDataを使ったのだが、どうも引っかかるはずのcss selectorが引っかからない。 結構複雑なhtmlの時は挙動が怪しいっぽい、と結論づけて、他のを探す。JsonProviderは複雑なのも頑張ってくれたのになぁ。

最初はAngleSharpが今風だ、というのを見かけて試そうとしたが、自分の目的には不必要にasyncで不便なのでやめる。 HtmlAgilityPackに、HtmlAgilityPack.CssSelectors.NetCoreを加えて使うという方針に。

#r "nuget: HtmlAgilityPack"
#r "nuget: HtmlAgilityPack.CssSelectors.NetCore"

open HtmlAgilityPack
open HtmlAgilityPack.CssSelectors.NetCore


let doc = HtmlDocument()
doc.Load("test.html")

doc.QuerySelectorAll(".pageNavigation01")

まぁ良さそう。

QuerySelectorがFirstOrDefault、つまり最初の一致を返し、QuerySelectorAllは全一致を返すのか。 ドキュメントはなくてもいいがdoc commentくらい書いてほしいなぁ。

HtmlAgilityPackでEUCを扱う (.NET Coreでの方法)

mixiは結果をEUCで返してくるので、この辺でloadにエンコーディングを指定しよう。

お、.NET CoreはEncoding.GetEncoding("EUC-JP")すると

System.ArgumentException: 'EUC-JP' is not a supported encoding name. For information on defining a custom encoding, see the documentation for the Encoding.Reg

だって。CodePagesというNuGetパッケージがいるらしい。

#r "nuget: System.Text.Encoding.CodePages"

Encoding.RegisterProvider(CodePagesEncodingProvider.Instance)
Encoding.GetEncoding("EUC-JP")

取れた。loadしてみよう。

let doc = HtmlDocument()
doc.Load("test.html", Encoding.GetEncoding("EUC-JP"))

ちゃんとできてるね。よしよし。

ダウンロード方針

まずはlist_diary.plをすべてhtmlとしてダウンロードしていく。>DONE

次にlist_diaryのedit_diaryのクエリからidを取り出してview_diaryを取得していく。>DONE

画像はどうしようかなぁ。取り出してもいいんだが、ブログと違ってmixiの画像はそんなに重要じゃないんだよねぇ。

だいたいgrepしていつの事だったか知りたい、がやりたい事のほとんどなので、 先にボイスを落とすか。

htmlはEUCでしかも余計な物がたくさんついているのでこのままでは見づらい。 やはりmdにコンバートする必要はありそうかなぁ。

ボイスのダウンロード。 まずlist_voice.plを順番に保存していく。

この後はどうするかなぁ。 日記と同じやり方なら各ボイスのview_voice.plをダウンロードしていく訳だし、 それをやるのもそんな大変でも無いんだが、ボイスは一つずつの量が少ないのでだいたいlist_voice.plに含まれるんだよなぁ。

「もっとコメントを見る」のリンクがある奴だけ別途保存しようかしら。

現状はボイスは一気に取ってしまうのであとから追加分だけ取る機能が無いが、これはひつようになった時に適当に書こう。

コンバート

mixiのhtmlはなかなか読みづらいのとEUCなのでgrepしづらいので、mdへのコンバートもやってしまう事に。

diaryは特に難しい事も無く変換できるが一部Bad Gatewayのファイルに気づいていなっくてそこでexceptionが出た。 そのファイルを取り直して事なきを得る。

ボイスはlistの方だけでほとんど十分だからexpandはしてないでいいかな。 1ボイス1ファイルだとちょっと見づらいので、1リスト1ファイルとする。 リストの中には複数の月のボイスが入りうる訳だが、 面倒なのでlistのbaseの日付をそのボイスの年月とみなして、

2017/10/mixivi_20171025152430.md

みたいなファイル名として中につなげたmdを吐く事にする。 ボイスの中のhtmlの構造は良く分からないので、 最後の日付のspanだけ特別扱いして、後はInnerTextを空白でconcatした。 まぁなんとなく見えている。

これでgrepできるようになった。 mixiとtwitterをgrepできるようになると、 だいぶ「あれ、いつだったかな〜」の検索が実用的になってきて良いな。