統計学の本をKindleで買った。Claude Codeと一緒に読み進めたかったからだ。ページを開いて「読んで」と言えば要点をまとめてくれて、わからないところは噛み砕いてくれる。そういう読書がしたかった。
ブラウザ版Kindle(read.amazon.co.jp)で開ける本なら話は簡単で、ブラウザを操作できるAIならそのまま読んでくれる。ところが、買いたい本を無料サンプルで試したら「Kindleアプリが必要です」と断られた。本によってはブラウザ版で開けないのだ。
そこで、Macで動いているKindleアプリから表示中のページの本文を取り出す方法を探した。
結論から
Kindleアプリの本文は、アクセシビリティAPIから読める。 そこを読むSwiftを書いたら、表示中ページの本文がそのまま取れた。コード全文は後半に載せる。
なぜアクセシビリティAPIなのか。macOSにはVoiceOverという画面読み上げ機能があって、目の不自由な人はKindleの本を音声で読んでいる。読み上げができるということは、本文のテキストはアクセシビリティAPIを通って外に出ているということだ。同じ道を通れば、こちらからも読める。
動作確認した環境:macOS Ventura 13.6 + Amazon Kindle.app(Mac App Store版)
新しいmacOS(Sonoma・Sequoia)では試していないので、動くかどうかは分からない。ここで使うアクセシビリティAPIはVoiceOverの土台として長く同じ形で提供されているが、動作を保証できるのは上の環境だけだ。
先にやっておく準備
3つ済ませておくと詰まらない。
- Kindleアプリで本を開いておく。ライブラリ画面ではなく、本文が表示されている状態にする(ここで挙げるコードは、いちばん手前のウィンドウを読む)
-
Xcodeのコマンドラインツールを入れる。
xcode-select --installを実行するとダイアログが出るので、完了するまで待つ(数分かかる。Xcode本体は要らない)。xcrun swift --versionがバージョンを返せば入っている - アクセシビリティの許可を出す。これは次の節で説明する
アクセシビリティの許可を出す
この記事のコードは、どれもアクセシビリティの許可がないと動かない(許可がないとエラーになるか、空の結果が返る)。
「システム設定 → プライバシーとセキュリティ → アクセシビリティ」を開いて、コマンドを実行するアプリをオンにする。ターミナルから直接実行するならTerminalやiTerm2、AIコーディングツールに実行させるならそのツールを動かしているターミナルアプリが対象だ。一覧に出てこないときは「+」ボタンで /Applications/ユーティリティ/ から手動で追加できる。許可を与えたらそのアプリを一度終了して開き直す。
まずAppleScriptで試す
Kindleウィンドウの中にある全要素の value をかき集めてみる。ターミナルに貼り付けて、最後の EOF の行まで入れてEnterを押すと動く。
osascript <<'EOF'
tell application "System Events"
tell process "Kindle"
set out to ""
repeat with e in (entire contents of window 1)
try
set v to value of e
if v is not missing value and v is not "" then set out to out & (v as string) & linefeed
end try
end repeat
return out
end tell
end tell
EOF
出た。表示中の見開きの本文が、まるごと文字列で取れた。
⚠ ここで tell process "Kindle" としているアプリ名は、環境によって違うことがある。うまくいかないときは、アクティビティモニタでKindleのプロセス名を確認して置き換えてほしい。
ただし、この方法には弱点が2つあった。
- 遅い。手元の実測で1.6秒、ページによっては数秒かかる
- 不安定。要素の多いページだと、空の結果が返ることがある
Swiftで書き直す
AppleScriptが遅いのは、要素を1個たどるたびにSystem Eventsとのプロセス間通信が発生するからだ。同じことをアクセシビリティAPI(AXUIElement)で直接やれば、この行き来が消える。
次のコードを kindle_read.swift という名前で保存する。
import ApplicationServices
import AppKit
func axValue(_ el: AXUIElement, _ attr: String) -> AnyObject? {
var v: AnyObject?
AXUIElementCopyAttributeValue(el, attr as CFString, &v)
return v
}
func walk(_ el: AXUIElement, _ out: inout [String]) {
if let v = axValue(el, kAXValueAttribute as String) as? String, !v.isEmpty {
out.append(v)
}
if let children = axValue(el, kAXChildrenAttribute as String) as? [AXUIElement] {
for c in children { walk(c, &out) }
}
}
guard let app = NSWorkspace.shared.runningApplications.first(where: { $0.localizedName == "Kindle" }) else {
print("ERROR: Kindleアプリが起動していない"); exit(1)
}
let axApp = AXUIElementCreateApplication(app.processIdentifier)
guard let windows = axValue(axApp, kAXWindowsAttribute as String) as? [AXUIElement], let win = windows.first else {
print("ERROR: Kindleのウィンドウが見つからない"); exit(1)
}
var out: [String] = []
walk(win, &out)
print(out.joined(separator: "\n"))
やっていることは3つだけ。
- 起動中のアプリ一覧からKindleを探し、そのプロセスIDを
AXUIElementCreateApplicationに渡してAXUIElementを作る - いちばん手前のウィンドウから子要素を再帰的にたどる
-
AXValueに文字列が入っていたら集めて、最後に改行区切りで出力する
毎回使うものなので、swiftc でビルドしておく。-O は最適化の指定で、外しても動くが起動が少し遅くなる。
xcrun swiftc -O -o kindle_read kindle_read.swift
./kindle_read
待ち時間はほとんど感じない。AppleScript版で起きていた「空の結果が返る」も、いまのところ起きていない。
出力はこうなる(『完全独習 統計学入門』のあるページ。長いので途中で省略した)。
0-6 リンク リンク 数学記号も数学公式もほとんど使わない リンク 本書では、大胆に確率の部門をカットしたので、高校以上の数学を使う必要がなくなりました。他の統計学の教科書は、…
Kindle
たった2行しか出ない。見開き2ページぶんの本文が、まるごと1つの要素に入っているからだ。ページの中の見出しも段落も、Kindleは1個の大きなテキスト要素としてまとめて持っている。文中に混ざる「リンク」「画像」という単語は、目次へのリンクや挿絵があった位置に入っているように見えるが、Kindleが何を基準に入れているかまでは確かめていない。読むぶんには邪魔にならない。
2行目の Kindle は、ウィンドウ上部に出ている本のタイトル表示の要素で、本文ではない。
なお、ページによっては 位置No. 102 , 25% のような読書位置の行も一緒に出てくる。これも本文ではないが、あとで役に立つ。
ハマりどころ3つ
実際に使い込んで見つかった落とし穴を挙げておく。
- 取れるのは表示中のページだけ。続きが読みたければページをめくってもう一度実行する
- 図・イラスト・画像として埋め込まれた数式は取れない。テキストとして流れているものだけが対象だ。図が主役の本には向かない
- ページをめくった直後や表示を変えた直後は、前のページの内容が返ってくることがある。画面は進んでいるのに、取れる本文が1つ前のままになる。一緒に出てくる位置Noが画面下の表示とズレていたらこの状態だ。少し待ってもう一度実行するか、ページをめくり直すと直る
Claude Codeとつなぐ
ビルドしたバイナリを、消えない置き場所に移す。
mkdir -p ~/Library/Scripts
mv kindle_read ~/Library/Scripts/
あとは、この道具の存在をClaude Codeに覚えさせる。私はメモリに1行残したが、CLAUDE.md(Claude Codeが起動時に読む設定ファイル。~/.claude/CLAUDE.md に置けば全プロジェクトで、作業フォルダの直下に置けばそのフォルダでだけ効く)に書いても同じことができる。
Kindleアプリで表示中のページを読むときは ~/Library/Scripts/kindle_read を実行する
これで、本のページを開いて「このページ読んで」と言うだけになる(初回はコマンド実行の許可を1度聞かれる)。Claude Codeがコマンドを打ち、本文を読んで、要約や解説を返してくる。ページをめくって「次」と言えば、そのまま読み進められる。
他のアプリにも使えるか
他のアプリでは試していないので、同じ手が通るかは分からない。相手のアプリがどんな属性に何を入れているかは、Accessibility Inspector で覗ける。ウィンドウの上にカーソルを重ねると、その要素のRoleとValueがその場で表示されるので、kAXValueAttribute 以外に当たりがあるかを探すときに使える。
⚠ これはXcode本体に付属するツールなので、準備の節で入れたコマンドラインツールだけでは入らない。App StoreからXcodeを入れる必要がある。
おことわり
この方法で取れるのは、自分のアカウントで買った本の、いま画面に表示している1ページぶんのテキストだけだ。私の用途は「買った本をAIと一緒に読む」という読書補助で、この記事もその範囲を想定している。
取り出したテキストを保存して溜めること、配布すること、本をまるごと別の形にすることはしない。著作権の問題になるし、Kindleストア利用規約にも目を通したうえで、自分の読書の範囲で使ってほしい。
まとめ
- Kindleアプリの本文は、VoiceOverが読み上げるのと同じアクセシビリティAPIから取れる
-
AXUIElementを直接たどるSwiftを1本書いておけば、表示中ページの本文がコマンド1回で取れる(AppleScriptでも取れるが1.6秒かかり、空振りすることがある) - 取れないもの(図・画像の数式)と、表示が固まったときの直し方(ページを1回めくる)だけ覚えておけば、日常の読書ツールとして使える
- 自分が買った本を自分で読むための道具として使うこと