yikegaya’s blog

仕事関連(Webエンジニア)と資産運用について書いてます

「Go言語で作るインタプリタ」を読んだので内容整理

「Go言語で作るインタプリタ」を読んだ。親切な内容の本ではあるけどやっぱりインタプリタを作る。ってテーマ自体が難しく、読むのに苦戦したので内容書きながら整理してみる。

O'Reilly Japan - Go言語でつくるインタプリタ

ざっくり全体の流れ

  • プログラムを標準入力に渡す
  • 入力したプログラムを字句解析器(lexer)でトークンに分割する
  • 分割したトークンから抽象構文木(ast)をつくる
  • 抽象構文器を評価(eval)して結果を出力する

もう少し噛み砕いてみる

プログラムを構成するパッケージ

repl

「Read(読み込み)、Eval(評価)、Print(出力)、Loop(繰り返し)」の略。main関数からまず呼ばれるところ

token

ソースコードを分解して意味ごとにまとめたようなもの。「let a = 5」だったら「let」は変数宣言のletというキーワードで「a」は識別子で「=」はイコール分で「5」は整数。といった風に空白区切りで文字列を作ってそれぞれに意味を持たせる

lexer

字句解析器。ソースコードを受け取ってソースコードを表現するトークンを返す

parser

構文解析器。構文解析器とは入力データを受け取ってデータ構造を返すもの。今回の場合はプログラムを受け取って木構造のastを返却する。

ast

replで受け取った文字列を木構造にする。例えば「1 + 2」だったら「+」の左下に「1」がぶら下がっていて右下に「2」がぶら下がっているようなイメージ。ほとんどのコンパイラインタプリタの内部表現がこのデータ構造らしい。

まず木構造っていうのはこういうの👇(wikipediaja.wikipedia.org

evaluator

astを評価する。つまりここでastのデータ構造を元にプログラムとしての実行結果を導き出す

object

evalの評価結果。この本ではeval構造体中の評価関数に使われるinterfaceとして実装される

その他用語整理

プログラム中に出てくる英単語の意味を忘れがちで混乱したので書き出して整理

Node

astで出力した木構造の要素。式(Statement)か文(Expression)のどちらかで構成される。式は値を生成し、文はしない。

Identifier

識別子のこと。識別子は式の1種でもある

prefix

5 + 5とか、5 * 5*みたいな中置演算子

infix

-5-とか!foobarの!みたいな前置演算子

enviroment

変数に値を保存するために使う環境と呼ばれるもの。 プログラム内では文字列とobjectを関連付けるハッシュマップとして実装されていて、objectパッケージの中に定義されている構造体でlet文を評価する際に更新される。

上記の用語を踏まえてプログラムの流れを追ってみる

  • 標準入力を受け取る
  • 入力した文字列を使ってlexer構造体を初期化する
  • 初期化したlexer構造体を元にparser構造体を初期化する。
  • parse構造体のParseProgramに標準入力を読み込ませる
  • parse構造体の初期化メソッド(New)の中でlexer構造体のnextToken関数を呼び出してtoken構造体を初期化していく
  • ParseProgram内でparseStatement関数を呼び出しastの構造体を作成する。
  • parseStatement関数内で呼び出されるastを作成する。parseStatement関数はトークンのタイプをcase文で判定してlet式、return式、それ以外のいずれかが呼ばれる
  • ParseProgramを実行するとastの配列が返却される
  • 返却されたast(抽象構文木)の構造体をEvalメソッドで評価していく
  • Evalメソッドの中ではnodeの種別によって実行プログラムが分岐する。nodeの種別がLetstatementの場合はenviromentを更新して変数の値を保持する
  • evalでの評価が終わったら結果を標準出力に書き出して実行終了

よくわからないところ掘り下げてみる

抽象構文木の構造について

最終的にEvalメソッドに抽象構文木(ast構造体)を渡して結果を受け取るところでこのプログラムの処理は完結するんだけど、該当のプログラムを読むと以下のように書かれている

program := p.ParseProgram()
if len(p.Errors()) != 0 {
    printParserErrors(out, p.Errors())
    continue
}

evaluated := evaluator.Eval(program, env)
if evaluated != nil {
    io.WriteString(out, evaluated.Inspect())
    io.WriteString(out, "\n")
}

parsePrigramメソッドでast構造体(program)を作ってEvalメソッドに渡している。

Evalメソッドはastとenviromentを受け取ってオブジェクトを返す

func Eval(node ast.Node, env *object.Environment) object.Object

で、そのastの構造は例えば以下のようになっている(この本の内容に含まれているastのテストコードから抜粋)

これはlet myVar = anotherVar;という文字列を

   program := &Program{
        Statements: []Statement{
            &LetStatement{
                Token: token.Token{Type: token.LET, Literal: "let"},
                Name: &Identifier{
                    Token: token.Token{Type: token.IDENT, Literal: "myVar"},
                    Value: "myVar",
                },
                Value: &Identifier{
                    Token: token.Token{Type: token.IDENT, Literal: "anotherVar"},
                    Value: "anotherVar",
                },
            },
        },
    }

つまり頂点にStatements(式を複数保持する配列)があってその下にその式の種別が定義されている。式は種別ごとに異なった値を保持する構造体。上記の場合はlet式の構造体でトークンと変数名、変数の値を定義している。

Statementsというnodeが頂点にあり、そのNodeがLetStatementというnodeを持っていてそのLetStatementがToken、Name、Valueというnodeを持っている。という感じ。

enviromentについて

例えば以下のように入力を評価していく場合、前回の受付で更新した変数の状態を参照するのに必要となる。この場合、変数「a」、「b」、「c」、「d」それぞれの名前に何が保存されているかenviromentに記録される

let a = 5;

let b = a > 3;

let c = a * 99;

if (b) { 10 } else { 1 };

let d = if (c > a) { 99 } else { 100 };

d * c * a

enviromentがないと複数回入力した場合に以前の変数の格納結果を参照できない。

その他

計算の際のトークンの優先度はどう決める?

例えば1 + 2 * 2という式だと2 * 2を先に計算してから1を足す必要がある。これをどうするのか?

→優先度を定義した連想配列をparser構造体の中に定義していてそれを参照してEvalメソッドが適切な順序で評価できるようにastを出力する。

var precedences = map[token.TokenType]int{
    token.EQ:       EQUALS,
    token.NOT_EQ:   EQUALS,
    token.LT:       LESSGREATER,
    token.GT:       LESSGREATER,
    token.PLUS:     SUM,
    token.MINUS:    SUM,
    token.SLASH:    PRODUCT,
    token.ASTERISK: PRODUCT,
    token.LPAREN:   CALL,
    token.LBRACKET: INDEX,
}

それぞれの意味はtoken構造体に定義されている

const (
    ILLEGAL = "ILLEGAL"
    EOF     = "EOF"

    // Identifiers + literals
    IDENT  = "IDENT"  // add, foobar, x, y, ...
    INT    = "INT"    // 1343456
    STRING = "STRING" // "foobar"

    // Operators
    ASSIGN   = "="
    PLUS     = "+"
    MINUS    = "-"
    BANG     = "!"
    ASTERISK = "*"
    SLASH    = "/"

    LT = "<"
    GT = ">"

    EQ     = "=="
    NOT_EQ = "!="

    // Delimiters
    COMMA     = ","
    SEMICOLON = ";"
    COLON     = ":"

    LPAREN   = "("
    RPAREN   = ")"
    LBRACE   = "{"
    RBRACE   = "}"
    LBRACKET = "["
    RBRACKET = "]"

    // Keywords
    FUNCTION = "FUNCTION"
    LET      = "LET"
    TRUE     = "TRUE"
    FALSE    = "FALSE"
    IF       = "IF"
    ELSE     = "ELSE"
    RETURN   = "RETURN"
)

precedencesは下に行くほど優先度が高い。抽象構文木の中で優先度が高いものを評価の上位に割り当てることでEVal関数が適切な順序で処理できる。

Evalメソッドでやってること

Evalメソッドの中ではNodeのタイプごとに対応する関数を作ってそれぞれnodeを評価している。この1つ1つの分岐の中身を実装していく必要がある。

func Eval(node ast.Node, env *object.Environment) object.Object {
    switch node := node.(type) {

    // Statements
    case *ast.Program:
        return evalProgram(node, env)

    case *ast.BlockStatement:
        return evalBlockStatement(node, env)

    case *ast.ExpressionStatement:
        return Eval(node.Expression, env)

流石に1つ1つこの記事に書くのは辛いので割愛するが、例えば中置演算子の式計算は以下のような感じ。

func evalIntegerInfixExpression(
    operator string,
    left, right object.Object,
) object.Object {
    leftVal := left.(*object.Integer).Value
    rightVal := right.(*object.Integer).Value

    switch operator {
    case "+":
        return &object.Integer{Value: leftVal + rightVal}
    case "-":
        return &object.Integer{Value: leftVal - rightVal}
    case "*":
        return &object.Integer{Value: leftVal * rightVal}
    case "/":
        return &object.Integer{Value: leftVal / rightVal}
    case "<":
        return nativeBoolToBooleanObject(leftVal < rightVal)
    case ">":
        return nativeBoolToBooleanObject(leftVal > rightVal)
    case "==":
        return nativeBoolToBooleanObject(leftVal == rightVal)
    case "!=":
        return nativeBoolToBooleanObject(leftVal != rightVal)
    default:
        return newError("unknown operator: %s %s %s",
            left.Type(), operator, right.Type())
    }
}

左下と右下の値を引数に渡して演算していく。こういった評価関数を「式」と「文」のそれぞれの種別ごとに用意する必要がある。

以上

NginxでCSS、JSが反映されない時の対応メモ

NginxでWebページを公開する際にCSS、JSが反映されず対応した時の備忘録です。

キャッシュをoffにする

古いページがキャッシュされている場合があるのでブラウザのキャッシュをoffにするのと、設定ファイル(nginx.conf)に以下の設定を追加してnginxのキャッシュをoffにすると直る場合があるらしい。が、今回は試しても解決せず。

# serverディレクティブ内に追記
server {
      sendfile off;
}

nginx.confにrootを指定する。

app/public/assets内に対象のCSSがある場合

HTMLのリンクが以下の形だとして

<link rel="stylesheet" href="/assets/sample.css">

この場合nginxに以下追記すると読み込まれるらしいが、パスは問題なさそうでこれでも解決せず。

root /app/public;

# もしくは

location ~ ^/assets/ {
   root   /app/public;
}

nginxのサーバには以下のように配置

/app/public/assets/sample.css

mime.types、octet-streamの設定をnginx.confに追加

以下の設定追加しないとMIME Typeを拡張子から判定できずエラーになるみたいです。結局これで今回直りました

  include /etc/nginx/mime.types; 
  default_type application/octet-stream;

参考

qiita.com qiita.com qiita.com

RailsのECS環境を構築した時のエラー対応振り返り

Rails製のサービスを動かすECS環境をterraformで構築したんだけど結構苦労したんでハマったところ振り返ってみる。

一度Goで作ったAPIをECSで動かしたことはあったんだけどその時には踏まなかった地雷も結構踏んでしんどかった。

前提

  • 現状EC2で稼働しているサービス
  • Nginx +pumaで稼働している
  • 開発はdocker-compose上で進めている

苦労したところ、解決に時間かかったところ

  • タスクがPENDINGになったときの対応
  • nginxをpumaで動かすためのvolumeの設定
  • Railsのassets:precomileがエラーになる
  • 改行込みの環境変数の扱い
  • ECSからS3へのアクセス
  • 固定IPアドレスの設定
  • CodeDeployとの連携

以下それぞれの詳細

具体的なコードや設定ファイルの内容は抜きにざっくりそれぞれの内容と解決策を書き残しています。

タスクがPENDINGになったときの対応

ECSのサービスに設定しているネットワークに問題があるとPENDINGになるらしい。この場合ECSの管理画面やCloudWatchにはっきりエラーが出ず原因特定が辛い。

VPC、NAT、サブネット、ロードバランサなどの状態を地道に確認して直した。

nginxをpumaで動かすためのvolumeの設定

socketファイルを通じてpumaとnginxが通信しているのでvolumeを持たせないといけない。 これは同じタスク内にRaisとnginxのコンテナを動かすことでタスク内でvolumeを共有させて解決できた。

Railsのassets:precomile

ECS タスク内のcommand実行でpumaコマンドの前にassets:precompileさせてたんだけど何もエラーが出ずに落ちていた。

で、実行時間が長かったんでタスクに割り当てるCPUとメモリ増やしたら解決。これがはっきりエラーメッセージが出ないんでしばらく気づかなかった。

改行込みの環境変数の扱い

コンテナの環境変数の指定でS3に置いたenvファイルを使うことができて今回そうしたんだけど複数行の環境変数が含まれているせいで想定通りに動作しないRailsの動作があった。

aws ecs execute-commandで確認したところ2行目以降が切り捨てられて読み込まれているらしい。

ちなみにdocker runコマンドやdocker-composeでもenvファイルから環境変数は読めるんだけどそれぞれ挙動が違い、docker runの場合は実行時にエラーになって、docker-composeの場合は正常に読み込める。

しょうがないんで改行のところを改行コード\nにして1行にまとめたがRailsの仕様でアプリケーション側で確認すると\n\\nに勝手にエスケープ される。

これはenvの書き方いろいろ書き換えてみたりもしたけど解決できないんで愚直にRaisアプリ内でgsubメソッドで\\n\nに置き換えて動かした。

固定IPアドレスを使う方法

接続先サービスにIPアドレスの制限があったのでアウトバウンドのIPを固定する必要があった。

ECSのserviceにサブネットが割り当てられていてそのサブネットのNATゲートウェイにElastic IPを使うことでそのIPを固定IPにできる。 また、ECSのタスクはパブリックサブネットに置くこともプライベートサブネットに置くこともできるけど固定IPにするにはプライベートサブネットに置かないといけない。

パブリックサブネットでタスク動かしたんでこれもしばらく気づかず。

ECSからS3へのアクセス

タスクにS3へのGetとListを許可するIAM権限を付与しないといけない。最初Getだけでうまくいくと思っていてListも必要なことにしばらく気づかず。

CodeDeployとの連携

最初ロードバランサのターゲットグループを1つだけ作って動かしたんだけど一通り動いた後にCodeDeployでデプロイしようとしたらエラーが出た。どうやらBlue/Greenデプロイモードだとターゲットグループが2つないとダメだったらしい。ロードバランサ本体やリスナーな1つで大丈夫。

あと管理画面からGUIでデプロイするときはCodeDeploy側ではなくECSのサービス→「更新」ボタンを押下して実行するんだけどCodeDeploy側の画面でデプロイする方法を探してしばらく迷子になってしまった。

ECSのデバッグ

こんな感じでいろいろ問題は起きたけど以下の手段でエラー原因特定して解決してました。

  • タスクにCloudWatchのロググループを設定して確認する
  • AWS管理画面上でECS→クラスタ→サービス内のイベントを確認する
  • タスクの詳細画面を確認する
  • aws ecs execute-commandコマンドを使ってコンテナの状態を確認する

効率よくaws ecs execute-commandを実行する

aws ecs execute-commandを使うとECSで動いているコンテナのシェルを操作できるんだけど実行にはタスクIDを取得する必要がある。

そのタスクIDはAWSの管理画面かaws ecs describe-tasksで確認できるんだけどいちいち確認してターミナルにコピペするのが面倒なんでこうやって👇コマンドを繋げて確認すると割と楽でした。

TASKARN=`aws ecs list-tasks \
    --cluster {対象クラスタの名前を指定} \
    --service-name {対象サービスの名前を指定}\
    | jq  -r '.taskArns[0]'

TASKID=`aws ecs describe-tasks \
    --cluster {対象クラスタの名前を指定} \
    --tasks $TASKARN \
    | jq -r '.tasks[0].containers[0].runtimeId' \
    | awk -F '-' '{print $1}'`

aws ecs execute-command \
    --cluster {対象クラスタの名前を指定} \
    --task $TASKID \
    --container  {対象サービスの名前を指定} \
    --interactive \
    --command "/bin/sh"

やってること

  1. aws ecs list-tasksでタスクのARNを取得。結果がJSONで返ってくるのでjqで取得。
  2. aws ecs describe-tasksでタスクのIDを取得してawkで必要な部分のみ切り出し
  3. 取得したタスクIDを使ってECSのコンテナ操作

マークダウンをHTMLに変換するツールをいくつか試してみた

Gitのレポジトリにコミットされているマークダウンで書かれたドキュメントがあって、それをCI/CDパイプラインの中でHTMLに変換してBASIC認証かけて公開したい。。

というタスクを業務で振られたんでマークダウン をHTMLに変換するツールをいくつか試した。その時の内容を書いてみる。

要件

  • マークダウン ファイルをCLIでHTMLに変換したい
  • 所定のフォルダの中にHTMLとして吐き出してNginxでBASIC認証をかけて公開したい
  • マークダウンの中のリンク先のファイル拡張子がマークダウン用の「.md」になっているのを「.html」に変換したい
  • 見た目もいい感じにしたい

試したもの

  • pandoc
  • redcarpet
  • vuepress
  • gatsby.js

以下それぞれのツールについて

pandoc

  • ツール自体の実装はHaskell
  • マークダウン→HTMLだけじゃなくPDFとかOfficeファイルとかいろんなフォーマットに対応している
  • Haskellの実行環境作らなくても公式のDockerイメージがあるのでpullしてすぐ使える。
  • ファイル内のリンク先の拡張子を「.md」から「.html」に変換するのがちょっと面倒。フィルタ用のスクリプトを書いてコマンド実行時にオプションで渡さないといけないんだけどその場合その言語の実行環境も作らないといけなかったりちょっと面倒。
  • 個人的にRubyが好きなのでRubyスクリプト書く手段を調べた結果、rubyでのラッパーだったりrubyでフィルターを書くgemがあったんでその辺りは良さそう
  • 見た目をいい感じにするにはCSS書いて読み込ませる必要あり

redcarpet

  • マークダウンをHTMLに変換するRubyのgem
  • Railsの中のhelper関数の中で使ってWebアプリケーション上でマークダウン をHTMLにして表示する。みたいな使われ方が多そう
  • 今回のような使い方で見た目をいい感じにするにはCSS書いて読み込ませる必要あり。Railsアプリ内で使うならRailsCSSを適用すればいいんだろうけど、今回の要件だとちょっと面倒
  • カスタマイズするにはgem内で定義されているクラスを再オープンしてメソッドをオーバライドしていく↓

こんな感じ

class CustomHTMLRender < Redcarpet::Render::HTML
  def table(header, body)
    "<table class='table'>" \
      "<thead>#{header}</thead>" \
      "<tbody>#{body}</tbody>" \
    "</table>"
  end
end

  options ||= {
    tables: true,
  }
  markdown = Redcarpet::Markdown.new(CustomHTMLRender, options)

vuepress

  • 見た目がデフォルトでもかっこいい。カスタマイズも楽
  • カスタマイズはスタイルシート追加、config追加で基本対応可能
  • 名前にvueとついてるけど普通に使う分にはVueのコードを書く必要ない。凝ったカスタマイズをする場合はVueでプログラムを書く必要あり
  • 開発用のホットリロードのコマンドと本番配布用にdistフォルダにbundleしたコードを吐き出すコマンドが用意されていてwebpackを書く必要がない
  • 特に設定したりプログラム書かなくてもリンク先を「.html」に自動で変換してくれる

gatsby

  • 有名で情報量が多い
  • vuepressは名前の通りvueだけどこっちはReactっぽく書ける
  • マークダウンに対応するにはjsライブラリを追加する必要あり
  • GraphQLで記事を管理できる。ワードプレスみたいなCMS感覚で使いたいならvuepressよりこっちのがいいのかもしれない。。けど今回のCI/CDでコマンド叩いてHTML作ってNginxで公開すればいいのでvuepressのがいいかな?

結果

要件に合致しているvuepressにしました。

Vue +GoでPocketの記事にまとめてタグ付ができるサービスを作った

Pocketに記事をどんどん放り込んでいて整理できてない。。というモヤモヤがありPocketのAPIを使ってうまいこと整理できないかと考えてた。

https://ikeyu0806.hatenablog.com/entry/2021/05/09/113856

で、整理するならとりあえず記事にタグ付けできる機能があるんでそれを使うと良さそうなんだけど現状あんまり使えていない。

  • 公式のWebページからだと記事1つ1つ選択してタグ付しないといけないので面倒な気がする
  • 記事をクリップした時にタグ付けできるけどつける習慣がないので現状ほとんどついてない

なのでまとめてチェックボックスとかで選択してタグ付できる自分用Webサービスを作ってみたのでここまでの振り返りと感想を書いてみる。

画面こんな感じ

ソースコード

github.com

使ってる技術

バックエンド: Go言語

選んだ理由

  • 仕事ではバックエンドはずっとRubyしか書いてないんでプライベートは静的型付け言語で書きたい。
  • 最近マイクロサービスの現場でよく使われてるんでその内触る機会があるかも
  • プライベートでちょっとしたもの作るのに良さそう
  • 構造体とか並行処理とかポインタとかスクリプト言語にはないいろいろな気づきが得られる。。気がする

フロントエンド: Vue

選んだ理由

  • 現状Reactとほぼ二択だけど仕事がReactなんで違う方触りたい

あとCSSフレームワークはなんとなくbuefyを使ってみた。

buefy.org

あと勉強のため以下の方針で作ってみる

  • 以前GoでHTTPサーバを作ったときはライブラリ使ったけど今回は標準ライブラリのみで書いてみる
  • Nuxtは使わずスクラッチでwebpackを書いてbundleする

開発環境

  • ホスト側での作業だとGOPATHの管理が面倒だったり将来的にサーバにあげようと思った時に楽なんでDocker上で作業した。
  • GoのサーバでWebpackでbundleしたjsを読み込んだHTMLをホストしてくれればいいしMySQLとかRedisみたいなストレージも今回は不要。なのでコンテナ一つで事足りるのでDocker Composeは使わない。
  • webpackはwatchコマンド付きでbundleしてbundle先のvolumeはDockerコンテナと共有する。これでいちいち再起動しなくても開発できる。

実装

やってることざっくり

クライアントのボタンを押すとバックエンドからPocketのAPIを叩いてリクエストークン取得→リクエストークン認証→アクセスキー取得→取得したアクセスキーを使って記事取得やタグ付をする。

よくあるWebのシステムで特に特殊なところはないかな。

面倒だったところ

Goでjsonを扱う場合、同じキー値とデータ型を持った構造体を定義してその構造体を基にjsonをパースしていくんだけどネストが深いとちょっと戸惑う

{
    "consumer_key": "コンシューマキー",
    "access_token": "アクセストークン",
    "actions": [
        {
            "action": "tags_add",
            "item_id": "528698205",
            "tags": [
                "プログラミング", "javascript"
            ]
        }
    ]
}

Goの構造体

type ModifyRequestParam struct {
    ConsumerKey string          `json:"consumer_key"`
    AccessToken string          `json:"access_token"`
    Actions     []*ModifyAction `json:"actions"`
}

type ModifyAction struct {
    Action string   `json:"action"`
    ItemID string   `json:"item_id"`
    Tags   []string `json:"tags"`
}

リクエス

# request_paramはVueからのリクエスト。Vueのフォームから入力したItemIDやTagが渡される。
modify_action := new(entity.ModifyAction)
modify_action.Action = "tags_add"
modify_action.ItemID = request_param.ItemIDs
modify_action.Tags = request_param.Tags

使ってみてどうなのか

  • タグ付は実際本家より楽にできるようになって快適になった気がする。
  • 記事一覧も本家だと画像付きで表示されて1画面で確認できる記事本数が少なかったり、クリックしたときに記事の元のリンクに飛んでくれず、Pocket内のサイトで文章が表示されるんで今回作ったサイトで見た方が快適な気がしている。

雑に作った割には結構使えるんじゃないかなこれ。 多分Pocket本家のサイトも有料プランがあるんで課金すれば便利に使えると思うんだけど無料枠だと流石に使いにくいところはあるので。

今後やりたいこと、課題

  • バックエンドの処理を並行処理にしていきたい
  • とりあえずローカルでしか動かない前提なのでアクセストークンの扱いはゆるい(有効期限なしでlocalStorageに入れてある)
  • いい感じに記事ソートしたいとかアーカイブもまとめてできるようにしたいとかUIもっといい感じに作れそう。。

とかやりたいこと諸々あるけど、とりあえずタグなしの記事にまとめてタグ付けして記事を整理しやすくなったので一旦当初の目的は果たせたかな。

curlでPocketのアクセストークンを取得する

あとあと読みたいネットの記事のURLをPocketにどんどん放り込んでたんだけど、未読がたまっていく一方だったりして少し整理したりいい感じに管理できるプログラム作れないかな。。と思ってとりあえずPocketのAPI叩いてクリップした記事を取得してみた。

Pocket

Pocket

apps.apple.com

Pocketの認証手順

他のFacebookとかTwitterOAuth認証と同じような流れ。割と面倒。

  1. Pocketにログインする。
  2. Developer向けのページにアクセスしてApplicationを登録する。 https://getpocket.com/developer/
  3. アプリケーションを登録するとコンシューマキーが発行されるので抑えておく
  4. コンシューマキーをbodyに含めてPocketのAPIにhttpリクエストを送ってリクエストークンを取得する
  5. リクエストークンを含んだURLをwebで開いて認証する。この時にリダイレクト先のWebページを用意しておく
  6. 認証されたリクエストークンを使って再びPocketのAPIにhttpリクエストを送ってアクセストークンを取得
  7. アクセストークンが取得できたらそのトークンを元にhttpリクエスト送って目的の記事検索やその他諸々ができるようになる。

curlでの取得例

リクエストークン取得

curl -X POST -H "Content-Type: application/json" -H "X-Accept: application/json"
-d '{"consumer_key":"{発行したコンシューマキー}", "redirect_uri":"http://localhost:8080"}'
https://getpocket.com/v3/oauth/request

リクエストークン認証

この時にリダイレクト先のURLが必要なんだけど今回はCLI上で試してみたいだけなんで適当にlocalhostphpのサーバ実行してそこを指定する

# リダイレクト先のサーバ
php -S localhost:8080

# openコマンドでchromeからwebページを開く
open -a '/Applications/Google Chrome.app' https://getpocket.com/auth/authorize?request_token={リクエストトークン}&redirect_uri=http://localhost:8080

うまくいくとこんな感じのサイトが出るので認証

アクセストークン取得

curl -X POST -H "Content-Type: application/json" -H "X-Accept: application/json"
-d '{"consumer_key":"{コンシューマキー}", "code":"{リクエストトークン}"}'
https://getpocket.com/v3/oauth/authorize

試しに記事1件取得してみる

curl -X POST -H "Content-Type: application/json" -H "X-Accept: application/json"
-d '{"consumer_key":"{コンシューマキー}", "code":"{リクエストトークン}"}'
https://getpocket.com/v3/oauth/authorize

結果

{
    "status": 1,
    "complete": 1,
    "list": {
        "3325624398": {
            "item_id": "3325624398",
            "resolved_id": "3325624398",
            "given_url": "https://togetter.com/li/1710737",
            "given_title": "【朗報】mRNAワクチンは、変異株にも相当効果が高い~「圧倒的すぎて専門家も引き気味」「ガラケー時代のiPhone」 - Togetter",
            "favorite": "0",
            "status": "0",
            "time_added": "1620527156",
            "time_updated": "1620527156",
            "time_read": "0",
            "time_favorited": "0",
            "sort_id": 0,
            "resolved_title": "【朗報】mRNAワクチンは、変異株にも相当効果が高い~「圧倒的すぎて専門家も引き気味」「ガラケー時代のiPhone」",
            "resolved_url": "https://togetter.com/li/1710737",
            "excerpt": "よく、この時代に間に合ったものだな…/※なお「こういう人もいる」的な感じで、リプ欄でみた、ちょっと賛同し難い意見も収録しています",
            "is_article": "0",
            "is_index": "0",
            "has_video": "0",
            "has_image": "0",
            "word_count": "0",
            "lang": "ja",
            "top_image_url": "https://s.togetter.com/ogp/9eb2843312e0379332e5717d0bb6165e-1200x630.png",
            "domain_metadata": {
                "name": "トゥギャッター公式",
                "logo": "https://logo.clearbit.com/togetter.com?size=800",
                "greyscale_logo": "https://logo.clearbit.com/togetter.com?size=800&greyscale=true"
            },
            "listen_duration_estimate": 0
        }
    },
    "error": null,
    "search_meta": {
        "search_type": "normal"
    },
    "since": 1620527161
}

とりあえず取得できたけどどう使おうかな。。