KindlePaperwhite内の単語帳DBをJSONエクスポート

   · ☕ 29 min read
🏷️
  • #Kindle
  •  KindlePaperwhite内に保存されているSQLiteファイルをJSONへエクスポートするPythonスクリプトを書いた。

    TL;DL

     Kindle Paperwhiteに搭載されてい単語帳の問題点はここ、単語帳のDB内部に関してはここ、DBからJSONを書き出すスクリプトはここに書きました。

    背景

     KindlePaperwhiteに標準搭載されている単語帳は優れている。読書中に分からない単語を辞書で引くと、引いた単語が自動で単語帳に追加される。これにより、自分だけの単語帳が自動で生成されていく。

    それだけでも嬉しいのだが、この単語帳が優れているのは、単語だけでなく用例も同時に保存されることだ。

     これはどういうことかというと、例えば次の一文における"slouching” という単語を辞書で調べたとしよう。

    He was slouching forward, both hands on the steering wheel, his eyes fixed on a single point in the sky.
    “Pinball”: Haruki Murakami

    すると単語帳へは次の3つが保存される。

    1): 英単語:
        slouching
    2): 辞書で引いた結果:
        [動],(自)前かがみになる、前に垂れて
    3): 用例:
        He was slouching forward, both…

    利点として挙げたいのは、このうちの用例が文中における一文それ自体だということだ。一般的な辞書の用例が、どこからともなく降って湧いてきた一文であるのと対照的に、自分が読み進めている本の内容を元に構成された単語帳が、一冊の本を読み終わる頃には自動で作られているのだ。

    多読による語彙獲得を指向する際のジレンマ

     とはいっても、語彙の獲得を目的とした読書をしたことのない方にとっては、その利点は伝わりにくいかもしれない。ここでは多読時における単語帳作成の難しさを知っていただくために、多読実践者の頭の中を追う形で説明させていただきたい。

     まず、語彙の獲得を多読によって実現しようとする者を指して、XX語多読者と呼ぶとしよう。

    XX語多読者たちが多読を開始する理由は様々で、ここでは取り上げることはしないが、彼らが多読を続けていくためには、以下の事を(あるいは似たような事を)意識している(/したことがある)はずだ。

    1. 辞書は引かない (引かなくてもわかる本を読む)
    2. 分からないところは飛ばして前へ進む (わかっているところをつなげて読む)
    3. つまらなくなったら止める (1 2 の原則で楽しく読めない本は読まない)

    多読3原則 SSS英語学習法のご案内

     詳細についてはリンクを参照いただく方が確実に意図が伝わるため言及は避ける。「多読者は辞書を"頻繁に"引くべきでない」と、一般化するに留める。
     さて仮にいま、XX語多読者が、単語の暗記という苦しい作業を、読書という楽しい方法で代替し、維持・継続しているとしよう。彼/彼女らは一定の期間において多読を続け、少しずつ語彙の獲得に成功してきた。すると彼らのの頭の中には、次の疑問が浮かぶ。

    「辞書は引かないとはいえ、せめて苦手な単語のリストアップくらいはした方が効率的ではないだろうか?」

    辞書を引かない ではなく、 後でまとめて引く

     多読の弱点は、単語との出会いが偶然に委ねられていることにある。人間の脳は、より多く触れた物事に対してより強く記憶に残るようつくらている。特に記憶の忘却曲線を考慮した記憶法は、少ない労力での長期記憶を可能にするというデータが存在する。
     
    一概には言い切れないが、せっかく多くの単語に触れているにも関わらず、彼らが自身の脳をハックしようとしないのは、少し効率が悪いように感じる。

     そういった観点から、多読を行う際に、未知の単語のリストアップは(あくまでも辞書を引かない、その上で本来の読書を阻害しない限りにおいて)効率的だと言える。

    そこで単語のリストアップを決意した彼らだが、今度は次のような問題が浮かび上がる。

    「どのようにリストアップをし(How)、どのような単語帳にまとめるべきか(What)?」

    XX語多読者は、そもそも退屈で無味乾燥とした単語カードによる暗記方法を避けてきた。そんな彼らが単語帳を作るのであるから、可能な限り優れた単語帳としない限り、本末転倒である。また、リストアップ作業に気を取られてしまって多読量が下がることも、できる限り避けたい。

    多読を阻害せずに効率的な単語帳を作るには

     まず、どのように単語帳を作るかについて考える。
    XX語の多読においてもっとも排除すべき点は、多読時における割り込み作業である。

    想像してみてほしい。読書をしている最中に、不明な単語が表れたとしよう。その時あなたは、一度手に持っている本を置き、必要に応じて辞書をひき、そして今度は単語帳とボールペンに持ちかえて単語のメモを行い、また本に持ち替えて読書の続きに戻る。そして数行読み進めたたのち、また新たな不明な単語が表れたため、手に持っている本を置き、単語帳とボールペンに持ち替え…

    この作業を想像して問題がなさそうならば良いのだが、本来の目的である読書に本当に集中できるだろうか。また、例えば混雑した車内でもこの作業が行えるだろうか。単語帳をどこに置いたか忘れてしまい、捜索に時間を取られるようなことはないだろうか。
     
    可能であれば物理的に単語帳を用意するのは避け、自動化できる端末…電子書籍端末(あるいは読書アプリ)がもつ機能に頼るべきだ。それが難しい場合は、少なくとも読書の段階では簡単なメモ程度に収め、読書中に単語帳を作り込んだり辞書を引いたりすることは避けるべきだ。もし紙の本を使用しているのあれば、不明な単語にペンで印をつけておき、読了後に全てを見返して単語帳に落とし込むと良い。

     次に、どのような単語帳を作るかについて考える。
    これに関しては、効率的な方法は人によって異なるため、一概に言い切ることはできない。しかし、敢えて良い方法をあげるのであれば、それはエピソード記憶に頼った方法を検討することだ。

    「人のエピソード記憶力は、一般的な意味記憶と異なり、加齢によって衰退しない(要出典)」と言われている。そのため物語の一部として物事を記憶する方法は多くの一般的な学習者にとって効果的だと考えられる(DUO3.0をはじめにこの方法を採用している単語本は少なくない)。

    例えば日本人の英語学習者が英単語を学習する際、(A)にあげる英単語を全て暗記するとしよう。その際に(A)の英語と日本語を付き合わせてひたすら暗記するよりも、覚える英単語が組み込まれた文章B)自体を暗記するの方が、負荷が低く達成できると言われている。

    # A)
    freighter : n)貨物船
    abandoned : adj)廃墟と化した、捨てられた
    rusted    : adj)錆びた
    flanks    : n)横側、側面
    barnacles : n)フジツボ
    scabs     : n)かさぶた
    
    # B)
    The freighter looked abandoned; its white-painted deck was rusted red by the salt wind, and its flanks were caked with barnacles, like a sick man’s scabs.
    その貨物船は打ち捨てられたように見えた。潮風により、白く塗られていたはずのデッキには赤錆が見られ、船の側面にはフジツボがくっついており、まるで病人のかさぶたのように見えた。
    

     しかし、この例文も含めて単語帳にリストアップするのは、控えめにいって楽ではない。そのため、ここでも自動化可能な方法を検討するべきだ。

    以上の、エピソード記憶に特化した単語帳を自動で作成する方法の一つとして、KindlePaperwhiteの単語帳学習機能は優れていると言える。自分のお気に入りの文章を利用して学習するのであれば、効率もさらに上がるかもしれない。

    Kindle単語帳機能の弱点

     効果的な英語学習方法の検討と、その実現のためにKindleの利用を進めてきた。

    ここからは、もっと効率的にKindleを活用していくためにも、敢えてKindleのデメリットについても考えて、理解しておく。

    以下の3つについて述べたい。

    1: 学習のログが記録できない
    2: UI,表示速度の面で劣る
    3: 保存単語数に上限がある
    

    デメリット1: 学習のログが記録できない

     ここまで詳しく述べてこなかったが、Kindleには、単語管理機能として、“単語帳"と"フラッシュカード"の2つが提供されている。

    単語帳が、いわゆる一般的な"単語帳"であるのに対して、フラッシュカードは、その中から一問一答式でランダムに問題を出題してくれる機能だ。

    ここで述べたいのは、このフラッシュカードの難点である。ここの単語カードには"習得済み"ボタンが付与されており、ユーザーはこのボタンをクリックすることで、各単語を"習得済み"と"学習中"の2つのグループに分けて管理することができる。

    一見便利なこの機能であるが、これは後述するLingQ、Ankiと比較してしまうとどうしても見劣りする。それは、記憶の忘却曲線を意識した出題ができない、という点だ。

    LingQやAnkiは、単語別にユーザーの理解度をパラメータとして保有しており、ユーザーが回答を行うたびに逐次データを更新している。この情報をもとにサービス/アプリは、人間の状態を把握することで、現時点で学ぶべき最も学習効率の高い単語をサジェストしてくれる。
    詳細はAnkiのページを確認いただくのが良いと考える。

     大切なことは、ユーザーが本質的に求めているのは機能ではなく、その活用による根性理論に基づいた暗記法からの脱却である。シンプルで悪くはない"習得済み"ボタンだが、記憶の逓減という手間ではあるが予測可能な問題を、敢えて人間が対処する必要はない。

    デメリット2: UI,表示速度の面で劣る

     これは仕方がない。KindlePaperwhite端末の魅力と、その機能については、現在の我々の技術力ではトレードオフの関係にならざるを得ない。

    とはいえ、UI周りについては改善できそうな部分も見受けられる。例えば単語帳一覧画面にはスクロールバーが存在しないのだが(処理能力的に難しい)、この状態だと、全単語のうち、自分が今閲覧している箇所の相対的ないちが把握できない。全体のページ数や現在のページ数を表示するだけで学習者の満足感は違うものになるだろう。

     UIの改善は、時に大きなレバレッジを発揮するため、力をもう少し入れても良いと感じた。2,000件の単語を並べ替え機能なしに頭から眺めることは、自分は今後もしないだろう。

    デメリット3: 保存単語数に上限がある

     単語帳として管理できる上限は2,000件である。詳細は把握できていないが、古い順番から上書きされている可能性がある。個人的には大きな問題とは考えていないが、単語帳機能の使用を頻繁に行う方にとっては嬉しくないであろう。クラウドに同期することも可能そうだが、行われているかどうかは怪しい。

     以上、現在考えうる単語帳機能の弱点について述べた。

    量的な問題である単語数の上限についてはよいとしても、質的な問題、少なくともログ管理に関しては改善したい。

    Kindle単語帳の弱点は克服されないかもしれない

     デメリットについて上述した後に述べるのもおかしな話だが、現在Amazonが考えうる、電子書籍端末市場という戦場での最適解としてKindlePaperwhiteの強みを魅力を最大限に活かした戦略を選択した場合、これらデメリットは必然的に発生し得るものであり、そして何より、このデメリットがKindleの魅力を損なうものとは考え難い

    このような(ややマイナーな)機能が存在しようとなかろうと、そのシェアに影響はないだろう。

    要するに単語帳はおまけ機能、あるいは発展/開発途中の機能であり、そのことについて開発者側も、(おそらく多くの)ユーザー側も不満を抱いていない

     そういったことから、機能改善を近い将来に期待するのは賭けである。

     希望としては、Kindleアプリのみに搭載されている機能たちの存在だ。これらのアプリの機能が、Paperwhiteの機能改善に対して希望を残している

    Kindleアプリ(スマホ)にのみ存在する機能

     KindlePaperwhiteと、Kindleアプリでは、それぞれの機能は大きく異なっている。これまで述べてきたフラッシュカードについて話すと、同名の機能はアプリにも組み込まれているのだが、機能としてはPaperwhiteのものを大きく上回る

     アプリ版PaperwhiteのUIは優れており、機能こそそれ以外のサービスには劣るものの、直感的な操作かんやアニメーション表示については優れている。また本のタイトル別に、複数のカードデッキを作成できる点も魅力的だ。

    こちらの機能のフィードバックが、将来Paperwhiteに活かされる可能性はある

     また、WordRunnerという斬新な機能も存在する。ユーザーの速読能力を強化するための機能のようだが、公式では深く言及されておらず、“実験的な機能"として端末内で言及されているに止まっている。
     自分はこの機能の存在に対して、直接的なユーザー満足度の向上を指向しているというよりは、フィードバックを元にデバイスの価値を高めていこうという姿勢の産物だと捉えていて、Amazonのやる気が伺えるという点で興味を抱いている。

    Paperwhiteに変わる代替案

     とはいえ、本質的でない機能に注力し魅力を損なうようなリスクを取ることは考えにくく、仮にスマホ版で優れた機能が追加されたとしても、それがPaperwhiteにおりてくるかどうかは定かではない。

    ここでは、Paperwhite単語帳に変わる代替案として、前述したLingQとAnkiについて見てみる。

    LingQ

     LingQという言語学習サービス/アプリが存在する。このサービスの詳細についての言及はここでは控えるが、LingQにはepub形式の電子書籍をインポートする機能が存在し、これを利用することで、LingQのもつPaperwhiteよりも優れた単語帳生成機能や、フラッシュカード機能を使用することができる。
     インポートした書籍はいつでも閲覧できるため、Kindleと同様に電子書籍ビューアーとして活用することもできる。

    スマートフォン用としてネイティブアプリが配信されているため、外出先などでも気軽に使用できる。もしこのサービスを使用するのであれば、Kindleは手放して、全ての管理を任せてしまうやり方がストレスフリーかと。

     このサービス/アプリの難点としては、単語帳作成機能に一部制限があり、有料会員でないと満足な使用感が得られにくいということが上げられる。月額10$(だったかな?)を支払わない限り、カード作成機能や、epub本の取り込みに制限がかかる。

    さらに個人的な不満を言えば、月額費を支払っている割にはサーバーの動作の動作が不安定であり、UXの観点からはコスパは良いとは言えない。レイテンシーへの不満から、最近はあまり起動しなくなってしまった。

    ただとても高機能ではあるので、個人での単語管理がどうしても手間と感じるのであれば、検討してもよいと思う(初月無料で全機能を使用可)。

    Anki, Ankidoroid

     Ankiは"物事"を簡単に覚えるためのツールである。PC版がAnki、Android版がAnkidroid。“物事"と書いたのには意味があり、それはこのフラッシュカードシステムが、非常に柔軟性に富んでいるからだ。詳細は公式を参照いただきたいが、単語に限らず、多くの概念を暗記するために特化したデザインとなっている。

    上述の忘却曲線に基づいた管理/出題機能を有し、そして完全無料で使用できる。

    PC(Anki)とモバイル(Ankidroid)のクラウドシンクも安定している。洗練されたUIとは言いにくいが、片手でのフラッシュカード操作が問題なく使用できるだけで十分であり、またそのほかのデザインもシンプルで直感的だ。特にフラッシュカードのカスタム機能がとても豊富で素晴らしい。

    ただ、LingQと違い、あくまで単語帳アプリであるため、電子書籍のインポートは不可能だ。そのため、自動で単語帳を生成することはできない。そのため、もしAnki/Ankidoroidをしようするのであれば、Paperwhiteとの併用ということになるかもしれない。

    このAnki + Paperwhite を組み合わせた学習法は良いアイデアかもしれない。ただし単語のインプットは手入力になるため、そこだけは注意が必要だ。

    Anki/Ankidroid自体はKindleとは対立する存在ではないため、多読とは別のところで使うのも良いかもしれない。例えば自分は、TOEFLの対策で学ぶ英単語のみは、こちらで管理しようと考えている。

    Kindleから単語帳データを取り出して個人で管理

     さて、本記事の本題である。いささか手間はかかるが、思考放棄して原始的方法に回帰する前に、一度は試してみたいと考えた。

     先に述べておくと、データの取り回しについての再集計は、まだ検討中である。だが内部のデータ構造を把握しておくだけで、現状を改善できる可能性はあるはずだ。例えば、Kindle端末からデータを抽出しAnkiにエクスポートするコードを用意できれば、それだけで多くの手間を削減できるだそうr。

    そんなわけで、せっかくなのでPaperwhiteの単語帳データをみてみることとした。

    Kindle単語帳のDB

     まずはじめに、KindlePaperwhite端末内部のDBに関して、これには自分よりも詳しく調べている方がいらっしゃるはずなので、詳細は各々で調べていただくことを推奨する。またこれから記すことに対して、これを見られた方が何かしらの問題に直面したとしても、私は責任を取ることはできないので、ご了承願う。

     さて、KindlePaperwhite単語帳のDBだが、これはSQLiteで構築されており、その実データは端末内の下記の場所に存在する。

    1
    
    /Volumes/Kindle/system/vocabulary/vocab.db
    

    (ネットで調べた際に、数名の方が言及されている情報をもとにした。)

    このファイルをMacローカルに読み込んだ上で、今回はDB Browser for SQLiteというGUIツールを使用して中身を覗いてみた。

    テーブルとインデックス

     vocab.dbスキーマを確認したところ、内部にはテーブルが6つと、インデックスが3つ存在した。各オブジェクトの構造とパラメータについて確認できた内容と、そこから推測できることを記す。

    テーブル

    TABLE column1 c2 c3 c4 c5 c6 c7
    BOOK_INFO id asin guid lang title authors
    DICT_INFO id asin langin langout
    LOOKUPS id word_key book_key pos usage timestamp
    METADATA id dsname sscnt profilid
    VERSION id dsname value
    WORDS id word stem lang category timestamp profileid

    いずれのテーブルについても、idカラムが単一のプライマリキーとなっている。

    • BOOK_INFO:

      • 書籍情報。辞書を使用した際に登録されるもよう?
    • DICT_INFO:

      • 辞書情報。辞書インストール時に登録される?
    • LOOKUPS:

      • ユーザーの検索情報。辞書使用時にユニークIDが振られる。
      • 登録上限数は不明。
      • id:
        • ユニークキー。
      • word_key:
        • WORDSテーブルのIDと対応。
      • book_key:
        • BOOK_INFOテーブルのIDと対応。
      • dict_key:
        • DICT_INFOテーブルのIDと対応。
      • usage:
        • 検索時に単語を含んでいた文章一文。
    • WORDS:

      • 単語情報。
      • 登録上限数2000。
      • category:
        • 0 or 100;0は学習中、100は習得済み。

    METADATA, VERSIONテーブルについては詳細を確認していない。

    インデックス

    lookupbookkey:
        CREATE INDEX lookupbookkey ON LOOKUPS (book_key)
    lookupwordkey:
        CREATE INDEX lookupwordkey ON LOOKUPS (word_key)
    wordprofileid:
        CREATE INDEX wordprofileid ON WORDS (profileid)
    

    本の情報(book_key)と検索情報(word_key)の高速化を狙っているようだ。

    これについては後述する。

    LOOKUPSテーブルを抑える

     これらの情報から察するに、ユーザーが辞書を引いた際のDBの動作は以下のようなものだと推測できる。

    ユーザーが辞書を引く-> 引いた情報(単語名、使用した辞書キー、街灯の文章、タイムスタンプ)がLOOK_UPテーブルに保存される。-> 引いた単語に重複がなければ、WORDSテーブルに情報が追加される。

     LOOKUPSテーブルがユーザーの索引履歴を管理していることは間違いなく、その観点で言えば、このテーブルが最も重要だと言える。

    試しにLOOKUPSテーブルからいくつかのレコードを、抜き出してみる。

    id word_key book_key dict_key pos usage                                                                                                                                                     timestamp
    CR!3S15YEKVR93MNEVN0TSJ3FHFEVA5:AV0GAAABAwAA:74143:13 en:recuperate CR!3S15YEKVR93MNEVN0TSJ3FHFEVA5 B00771M5IA AV0GAAABAwAA:74143 The reason, he said, was to recuperate from illness, but he had no knowledge of what the illness was, whether she had recovered to the point she could eat salad, or why she had chosen to drop out instead of taking a leave of absence. 1576627898713
    CR!54VX74M0KX5PZ3491RZ3PKHHT21J:AfUHAAAOAAAA:255966:13 en:recuperate CR!54VX74M0KX5PZ3491RZ3PKHHT21J B00771M5IA AfUHAAAOAAAA:255966 “So I went to recuperate at my grandmother’s place on the coast in Izu. 1574732995946
    CR!0MG2F55NQH453A6F4SPKJ47V3APW:7B3051F5:629959:5 ja:賛襄 CR!0MG2F55NQH453A6F4SPKJ47V3APW:7B3051F5 B00771M8JQ 629959 羌族は夏王を賛襄したではありませんか」 「なるほど……、ふむ、憶いだした。 1445255060560

     word_keyには検索を行なった単語が格納されている。この値は主keyではないため、重複が許されている。

     例えば、上2つのレコードについてみてみると、いずれも"recuperate"という単語を調べていること、またそれぞれのbook_keyが異なっていることから、ユーザーが過去に引いたことのある単語でも、別レコードとして改めて計上されるということがわかる。

     これはユーザーが引いた単語は全て重複なく保存されている、とも言い換えられる。これはusage(単語が使用されている一文)やUNIXタイムスタンプの値が異なることからも明らかである。

     ユーザーの索引情報が全てLOOKUPSテーブルに記録されているということから、このテーブル単体のみで十分な情報が得られそうだということがわかった。

    余談: スキーマ内のインデックスの意図

     Kindleの単語帳機能を起動すると、上部に単語 | 本という選択ボタンがUIとして表示される。この時、単語を選択すると、これまでに索引を行なった単語全件が全てリストアップされ、を選択すると、本のタイトル別に単語をチェックできるように本の一覧がリストアップされる。

     おそらく上記にあげたインデックスのうち、上の二つが存在する意図は、このリストアップ機能の高速化のためであろう。処理能力の低いKindle端末でも、より良いUXを提供できるように用意した可能性がある。もしこの仮定が正しければ、こういったUI/アプリ側からの要望を実現するためにDB側が便宜を測っているという意味で、しっかりとした設計が施されたプロダクトだと感じる。

    profileidに関するインデックスについてはよくわからない。

    最適なレシピは?

     スキーマの概要がおおよそ掴めたところで、Kindle単語帳のデメリット(ログ記録、UI、保存単語数上限)を改善するために計画をたてる。

    まずはじめに、考えうる手法を2つ示す。

    1): Ankiへ単語データをインポートする仕組みを作成する
    2): Paperwhite端末外に新規アプリを作成する

    最も工数が少ない方法をとるのであれば、(1)のAnkiへインポートする仕組みを作成することだ。この方法であれば、わざわざDBスキーマの調整や画面の実装を行うことなく、I/Oに必要なスクリプトを用意するだけで済む。

    もしそれが困難であった場合は、(2)の方法をとることとなる。 

    現時点では (1)の方法が最適 と思われるため、こちらを積極的に検討したい。

     さて、どちらの方法を選択するにせよ、Paperwhite端末からデータを引き出す必要がある。
    ここから先は、データ抽出のための素振り(個人的練習)として、PythonによるSQLiteファイルのデータ操作の備忘録を記す。

    Pythonスクリプト

     これまでKindlePaperwhite端末について、それがどのように単語帳機能を実現しているかをみてきた。
     ここからは一転して、その機能改善のための個人的な下準備としてPythonスクリプトを記述したので、その解説を個人的な備忘を目的に記す
     
     少し大きめの風呂敷を広げるような書き方をしてしまったため、予め断っておくが、このコードのなかに何かしらのアイデアが詰まっているわけではない。ただ、Python初心者である私が、練習のためにテーブル情報をエクスポートしているファイルに過ぎないため、読み飛ばしていただくべきかもしれない。

     さて、Pythonを使う理由だが、これは標準でSQLite専用のAPIが提供されており、何より自分が一度も書いたことがなかったからだ。
    とりあえずのトレーニングとして、個々のテーブルをJSON書き出しすることを目的に以下のようなコードを書いた。

      1
      2
      3
      4
      5
      6
      7
      8
      9
     10
     11
     12
     13
     14
     15
     16
     17
     18
     19
     20
     21
     22
     23
     24
     25
     26
     27
     28
     29
     30
     31
     32
     33
     34
     35
     36
     37
     38
     39
     40
     41
     42
     43
     44
     45
     46
     47
     48
     49
     50
     51
     52
     53
     54
     55
     56
     57
     58
     59
     60
     61
     62
     63
     64
     65
     66
     67
     68
     69
     70
     71
     72
     73
     74
     75
     76
     77
     78
     79
     80
     81
     82
     83
     84
     85
     86
     87
     88
     89
     90
     91
     92
     93
     94
     95
     96
     97
     98
     99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    
    import json
    import sqlite3
    import subprocess
    import sys
    #
    # 1. Copy file from Kindle
    #
    #   1.1 Define copy command as a subprocess.
    COMMAND = "cp"
    FILE_NAMES = {
        "INPUT": "vocab.db",
        "OUTPUT": {
            "WORDS": "look_ups.json",
            "BOOKS": "books.json"
        }
    }
    PATH_FROM = "/Volumes/Kindle/system/vocabulary/" + FILE_NAMES["INPUT"]
    PATH_TO = "./" + FILE_NAMES["INPUT"]
    def after_func(n):
        def succ_cmd():
            return None
        def fail_cmd():
            sys.exit()
        return succ_cmd if n == 0 else fail_cmd
    def list2str(*args):
        cmd = ""
        for str in args:
            cmd += str + " "
        return cmd[:-1]
    def do_cmd(cmd):
        is_success = -1
        try:
            subprocess.run([cmd], shell=True)
            is_success = 0
        except FileNotFoundError:
            print("Err: ファイルが存在しません。", "端末のマウントを確認ください。")
            is_success = -2
        except Exception as other:
            print("Err: ", other)
            is_success = -3
        return is_success
    # 1-2. Exec command.
    after_func(do_cmd(list2str(COMMAND, PATH_FROM, PATH_TO)))()
    #
    # 2. Fetch SQLite and exort JSON.
    #
    # 2-1. Fetch SQLite.
    STTMT = {
        "DDL": {
            "CREATE_VIEW_WORDS": 
                'CREATE VIEW UNIQUE_ENGLISH_WORDS\
    	            AS SELECT word_key, book_key, usage, timestamp\
    		                FROM LOOKUPS\
    	                WHERE word_key LIKE "en:%"\
    	                GROUP BY word_key;',
            "DROP_VIEW_WORDS":
                'DROP VIEW IF EXISTS UNIQUE_ENGLISH_WORDS',
        },
        "DML": {
            "SELECT_WORDS": 'SELECT * FROM UNIQUE_ENGLISH_WORDS',
            "SELECT_BOOKS": 'SELECT id, title, authors FROM BOOK_INFO WHERE lang = "en"',
        }}
    record = {
        "look_ups": {},
        "books": {}
    }
    conn = sqlite3.connect(FILE_NAMES["INPUT"])
    conn.row_factory = sqlite3.Row
    cur = conn.cursor()
    cur.execute(STTMT["DDL"]["DROP_VIEW_WORDS"])
    cur.execute(STTMT["DDL"]["CREATE_VIEW_WORDS"])
    cur.execute(STTMT["DML"]["SELECT_WORDS"])
    rows_words = cur.fetchall()
    cur.execute(STTMT["DML"]["SELECT_BOOKS"])
    rows_books = cur.fetchall()
    # 2-2. Manupulate list for writing JSON.
    def addDictionary(accumlator):
        l = list()
        def add(row):
            for i, v in enumerate(row.keys()):
                l.append((v, row[i]))
            accumlator[l[0][1]] = dict(l)
            return None
        return add
    def write_file(list):
        def open_fille(name):
            f = open(name, "w")
            json.dump(list, f, ensure_ascii=False, indent=4, sort_keys=True, separators=(',', ': '))
            f.close()
            return None
        return open_fille
    for row in rows_words:
        addDictionary(record["look_ups"])(row)
    for row_book in rows_books:
        addDictionary(record["books"])(row_book)
    write_file(record["look_ups"])(FILE_NAMES["OUTPUT"]["WORDS"])
    write_file(record["books"])(FILE_NAMES["OUTPUT"]["BOOKS"])
    cur.close()
    conn.close()
    #
    # END
    #
    # Check the document:
    #   はじめに — pep8-ja 1.0 ドキュメント:
    #       https://pep8-ja.readthedocs.io/ja/latest/
    #   subprocess — Subprocess management — Python 3.8.1 documentation:
    #       https://docs.python.org/3/library/subprocess.html
    #   11.13. sqlite3 — DB-API 2.0 interface for SQLite databases — Python 2.7.17 documentation:
    #       https://docs.python.org/2/library/sqlite3.html
    #
    # Tips:
    #   subprocess.run():
    #       There is two way to set Args;
    #           1) List
    #           2) String
    #       - If set String, must set "check=True" to 2nd arg.
    #       - When use command with authority - like cp, rm -
    #           must need 2nd arg "check=True".
    #   SQL - Create View:
    #       We cannnot use placefolder with a create view statement.
    #
    

    このコードでは、大きく分けて以下の3つを実行している。

    • SQLiteファイルをローカルへコピーして
    • ローカルのSQLiteから必要なデータを引っ張り
    • そのデータを加工してJSONへ書き込む

     ここから先は備忘録を兼ねて、簡単な詳細を記す。なお、重ねてになるかもしれないが、まともにPythonコードを書いたのはこれが初めてであるため、ごく基礎的な内容について、つまりは文の書き方や関数の使い方といった記述を以降では取り上げるにとどまることを、ここで述べる。

    シェルの呼び出しにsubprocessを使用する

    1
    2
    3
    4
    
    def do_cmd(cmd):
        is_success = -1
        try:
            subprocess.run([cmd], shell=True)
    

     Kindle端末の内部では、単語帳の管理にSQLiteを使用している。Kindle端末をMacに接続することで、SQLiteファイルを確認することができる。
    直接操作するのは危険であるため、Python側でシェルを呼び出してローカルへとコピーする。

    これにはいくつか方法があるようだが、subprocessという標準モジュールを使用することで基本的な操作は問題ないようだ。

    The subprocess module allows you to spawn new processes, connect to their input/output/error pipes, and obtain their return codes. This module intends to replace several older modules and functions:
    os.system
    os.spawn
    subprocess — Subprocess management

    Python3.5 からはsubprocess.doc() という関数が追加されたようで、今回はこちらを使用している。run()に渡された引数がコマンドとして実行される。
    この引数がやや複雑で、1)タプルで渡す方法と、2)文字列で渡す方法の2つが存在する。またオプションについても沢山存在する。

    余談だが、Docをざっと見る限りだと、run()関数は、これまで存在した複数の関数を、使いやすくラップする形で生まれたらしく、コマンド引数の複雑化にはそのあたりの理由がありそうだ。

    cpコマンドのようにshell側で権限を要するコマンドを実行するためには、引数に"shell=true"を与えておく必要がある。

    例外処理

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    # ...
        try:
            subprocess.run([cmd], shell=True)
            is_success = 0
        except FileNotFoundError:
            print("Err: ファイルが存在しません。", "端末のマウントを確認ください。")
            is_success = -2
        except Exception as other:
            print("Err: ", other)
            is_success = -3
    # ...
    

     例外処理については、PEPのプログラミングに関する推奨事項の説明が役に立つ。仕組み、書き方については他の言語と同じだと考えられる。

    サブプロセスであるシェルが落ちた際の被害を食い止めるため、後片付け用の関数にサブプロセスの成功時と失敗時の処理を記述し、失敗時はsys.exit()を実行することで後続のメインプロセスの実行を中断するようにした。

    今回はエラーコードを返す形で実装してみた。

    残余引数

    1
    2
    3
    4
    5
    
    def list2str(*args):
        cmd = ""
        for str in args:
            cmd += str + " "
        return cmd[:-1]
    

     subprecessに渡すコマンドの連結には残余引数を使用した。

    仮引数の最後に **name の形式のものがあると、それまでの仮引数に対応したものを除くすべてのキーワード引数が入った辞書 (マッピング型 — dict を参照) を受け取ります。
    **name は *name の形式をとる、仮引数のリストを超えた位置引数の入った タプル を受け取る引数 (次の小節で述べます) と組み合わせられます。
    4.7.2. キーワード引数

    この説明は少しわかりにくい。辞書型とタプル型の同時使用が前提のように取り違えてしまう。日本語訳がよくないのかな。
    関数の仮引数に対して**name とすることで辞書型、*name とすることでタプルを実引数に渡すことができる

    文字列の末尾1文字を消去

    1
    
    return cmd[:-1]
    

     文字列の末尾の切り捨てにはインデックスを利用した。

    文字列は インデックス (添字) を指定して文字を取得できます。
    3.1.2. 文字列型 (string)

    SQLiteからレコードをFetchする

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    conn = sqlite3.connect(FILE_NAMES["INPUT"])
    conn.row_factory = sqlite3.Row
    cur = conn.cursor()
    cur.execute(STTMT["DDL"]["DROP_VIEW_WORDS"])
    cur.execute(STTMT["DDL"]["CREATE_VIEW_WORDS"])
    cur.execute(STTMT["DML"]["SELECT_WORDS"])
    rows_words = cur.fetchall()
    cur.execute(STTMT["DML"]["SELECT_BOOKS"])
    rows_books = cur.fetchall()
    

     PythonでのSQLiteファイルの読み込みは、標準モジュールのsqlite3を使用することで実現できる。

    接続するDBを指定の上でコネクションオブジェクトを作成し、これを使い回す形でクエリを実行していく。PHPで言うところのPDOオブジェクトのようなものと言える。

    To use the module, you must first create a Connection object that represents the database. Here the data will be stored in the example.db file:

    1
    2
    
      import sqlite3
      conn = sqlite3.connect('example.db')
    

     cur.fetchAll()を実行した際の返り値は使いにくいもので、データの細かい操作を行うためにRow Objectというものが用意されている。

    A Row instance serves as a highly optimized row_factory for Connection objects. It tries to mimic a tuple in most of its features.
    11.13.4. Row Objects

    Rowインスタンスというタプルのようなイテレータブルなクラスを返してくれる。

    ごちゃごちゃとしたファイル操作

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    
    def addDictionary(accumlator):
        l = list()
        def add(row):
            for i, v in enumerate(row.keys()):
                l.append((v, row[i]))
            accumlator[l[0][1]] = dict(l)
            return None
        return add
    def write_file(list):
        def open_fille(name):
            f = open(name, "w")
            json.dump(list, f, ensure_ascii=False, indent=4, sort_keys=True, separators=(',', ': '))
            f.close()
            return None
        return open_fille
    

    イテレート時のインデックス取得

     enumerateという関数を使用することで、イテレート時に要素のインデックス番号を取得できる。

    enumerate(iterable, start=0)
    enumerate オブジェクトを返します。 iterable は、シーケンスか iterator か、あるいはイテレーションをサポートするその他のオブジェクトでなければなりません。
    enumerate(iterable, start=0)

    ファイルの書き出し

     ファイルの書き出しにはopen関数を使用する。

    open() は file object を返します。大抵、 open(filename, mode) のように二つの引数を伴って呼び出されます。
    7.2. ファイルを読み書きする

    見逃していたが、with と合わせて使用することで、記述を安全側へと寄せることができる
    (フィードバックありがとうございます!)

    ファイルオブジェクトを扱うときに with キーワードを使うのは良い習慣です。 その利点は、処理中に例外が発生しても必ず最後にファイルをちゃんと閉じることです。 with を使うと、同じことを try-finally ブロックを使って書くよりずっと簡潔に書けます
    7.2. ファイルを読み書きする

    コードを書く上で意識したこと

     完成形を見越した設計に気を払いすぎると一向に開発が進まない危険性があるため、早く、極力シンプルな記述を意識した。(classは使用しない)

    その上で、Pythonらしいコードを少しだけ意識した。

    PEPは価値のある文章

     Pythonらしい文法や考え方はPEPを参考にした。特に一貫性にこだわりすぎるのは、狭い心の現れであるに表されている哲学がわかりやすかったので斜め読みした。

    特にPEPにおける以下の文面には、Pythonの哲学が濃く表現されていると感じた。

    しかし、一貫性を崩すべき場合があることも知っておいてください - つまり、このスタイルガイドが適用されない場合があります。疑問に思ったときは、あなたの判断を優先してください。他の例を調べ、一番良さそうなものを決めて下さい。そして、躊躇せずに質問して下さい!

    その他にもPEPにはレイアウトルールについては興味深いものがいくつか見られ(2項演算子の前で改行すべきか、後で改行すべきか?など)。空行の扱いについてもルールが設けられているものの、ワンライナーについては省力も問わないとされている。

    いま改めて見返しているが、importの順番については見落としていた。

    import文 は次の順番でグループ化すべきです:
    1. 標準ライブラリ
    2. サードパーティに関連するもの
    3. ローカルな アプリケーション/ライブラリ に特有のもの
    上のグループそれぞれの間には、1行空白を置くべきです。

    今回は名前順で記述をしてしまったので、元ファイルについては後ほど修正を行う。
    (言い訳を記述しておくと、私がたまに書くGo言語では、名前順での振り分けか、もしくはPythonのようなドメインによる振り分けのいずれかが推奨されている)

    Pythonらしさを目指すよりも、Pythonを使う姿勢が大切

     結局のところ、Pythonらしさとは何か、はっきりとはわかっていないのだが、言語らしさを重視するよりも、各々のドメインにおける目的やルールを重要視する意識そのものが、Pythonを書くうえで求められる姿勢なのかなと感じた。Go言語の「Goに入ってはGoに従え」という哲学とはかなり異なる。

    おわりに

     以上、Kindle端末の内部データをほじくり、改良することができないか検討を行なってきた。今回はここまで区切りとする。

     さて、自分は、ここまで述べてきたようなXX語多読者であり、この文章で述べた"多読のジレンマ"に日々悩まされてきた。

    その改善のためにもKindle単語帳の仕組みが見てきたわけだが、実を言うとKindle自体に不満はなく、Amazonのエンジニアがどうにかするのを待てば良いとも思っている。

    一言でいえば、「今よりもっと良い方法があるのではないか」と考えているだけで、つまりは好奇心を原動力とした妄想以外の何物でもない。

     ではなぜわざわざコードを用意したのかと言うと、それはただPythonを書いてみたかったからだ。Pythonの流行により書店のプログラミング本コーナーから駆逐されていった本たちを常日頃から哀れに思っており、一度この言語にしっかりと触れねばならないと思っていた。

     まだ"さわり"どころか、Hello world!レベルのことしかできていないが、なるほど、いざ気合いを入れて調べてみると、この言語は手軽な割に高機能で、色々と腑に落ちる部分があった。

    JavaScript(/TypeScript)と同じように、簡単なスクリプトから大規模なソフトウェア開発まで、様々な用途に活用できるという意味では優れている言語だと感じた。(個人的には、JSよりも改行が増えてしまう点が難点に感じた。)

    今回はGoやNodeでの記述も検討したのだが、せっかく興味本位からPythonで書き始めることと認め、このコードの続きを行う際は、またPythonで書いていきたいと思う。

    Share on

    whasse
    WRITTEN BY
    whasse
    Web Developer