読者です 読者をやめる 読者になる 読者になる

べにやまぶろぐ

技術寄りの話を書くつもり

Treasure Data でクエリを書く時に真っ先に頭に浮かべたい UDF、それが TD_TIME_RANGE()

Treasure Data SQL

Treasure Data での時間の範囲指定にはとにかく TD_TIME_RANGE() を使おう

この記事で言いたいことはこれにつきます。

Treasure Data での唯一のパーティションキーは time だけ

Treasure Data ではユーザがインデックスを作成したりパーティションキーを指定することはできず、あるのは time カラムによるパーティションだけです。

例えば、

SELECT 
  time, 
  td_client_id
FROM pageviews
ORDER BY time ASC

はもちろんフルスキャンですが、ここで 2017-03-01 以降のログが欲しい!というとき

SELECT 
time, 
td_client_id
FROM pageviews 
WHERE '2017-03-01' < to_iso8601(from_unixtime(time))
ORDER BY time ASC

とかすれば OK… ではありません。

このクエリを打つと TD の実行ログのところに

started at 2017-03-13T17:04:38Z
executing query: SELECT 
time, 
td_client_id
FROM pageviews 
WHERE '2017-03-01' < to_iso8601(from_unixtime(time))
ORDER BY time ASC
**
** WARNING: time index filtering is not set on pageviews
** This query could be very slow as a result.
** Please see https://docs.treasuredata.com/articles/presto-performance-tuning#leveraging-time-based-partitioning
**
Query plan:
- Stage-0
    Partitioning: SINGLE
    PartitionFunction: SINGLE
    -> Output[7]
        Columns: time = time:bigint, td_client_id = td_client_id:varchar
        -> Sort[3]
            OrderBy: time ASC NULLS LAST
            -> RemoteSource[13]
                Sources: Stage-1
- Stage-1
    Partitioning: SOURCE
    PartitionFunction: UNKNOWN
    -> Project[12]
        Assignments: 
        -> Filter[11]
            Condition: ('2017-03-01' < "to_iso8601"("from_unixtime"(CAST("time" AS DOUBLE))))
            -> TableScan[0]
                Table: pageviews
                Columns: td_client_id:varchar = td_client_id:"td_client_id",

などと出ると思います。

注目すべきは

WARNING: time index filtering is not set on pageviews This query could be very slow as a result. ** Please see https://docs.treasuredata.com/articles/presto-performance-tuning#leveraging-time-based-partitioning

のところで、お前のクエリ(フルスキャンで)くそ重くなるからこのURL見て出直してこいって親切にURLまで書いてあります。

ここでこのクエリを TD_TIME_RANGE() を使って

SELECT 
time, 
td_client_id
FROM pageviews 
WHERE TD_TIME_RANGE(time, '2017-03-01', NULL, 'JST')
ORDER BY time ASC

などと書くと

started at 2017-03-13T17:07:08Z
executing query: SELECT 
time, 
td_client_id
FROM pageviews 
WHERE TD_TIME_RANGE(time, '2017-03-01', NULL, 'JST')
ORDER BY time ASC
Query plan:
- Stage-0
    Partitioning: SINGLE
    PartitionFunction: SINGLE
    -> Output[7]
        Columns: time = time:bigint, td_client_id = td_client_id:varchar
        -> Sort[3]
            OrderBy: time ASC NULLS LAST
            -> RemoteSource[13]
                Sources: Stage-1
- Stage-1
    Partitioning: SOURCE
    PartitionFunction: UNKNOWN
    -> Project[12]
        Assignments: 
        -> Filter[11]
            Condition: ("time" BETWEEN BIGINT '1488294000' AND 9223372036854775806)
            -> TableScan[0]
                Table: pageviews
                Columns: td_client_id:varchar = td_client_id:"td_client_id", time:bigint = time:"time"
                ** Time indexes:
                    Time index: [2017-02-28 15:00:00 UTC, 9999-12-31 23:59:59 UTC]

となって警告が消え、

** Time indexes:
   Time index: [2017-02-28 15:00:00 UTC, 9999-12-31 23:59:59 UTC]

などと確かに time index が効いていそうな感じが醸し出されています。

手元の環境でこの実行速度を比べると 45sec だった実行時間が 28sec (およそ1.6倍の性能向上)になりました

TD_TIME_RANGE() を使うべし。BETWEEN も要注意!

Treasure Data を利用する上で最も簡単でかつ有効なアドバイスは、「時間を扱うときは必ず UDF を使え」というものです。

上の警告メッセージにもありますが https://docs.treasuredata.com/articles/performance-tuning を読むといろんな注意点があることがわかります。

例えば先頭にある 1) WHERE time <=> Integer ですが

1) WHERE time <=> Integer

When the ‘time’ field within the WHERE clause is specified, the query parser will automatically detect which partition(s) should be processed.

とあり、time をWHERE句に入れると自動的にパーティションを絞ってくれるように書いてありますが

Please note that this auto detection will not work if you specify the time with float instead of int.

とあるように int 以外の型で指定すると無効になる、というのがわかります。

例えば

SELECT field1, field2, field3 FROM tbl WHERE time > 1349393020
SELECT field1, field2, field3 FROM tbl WHERE time > 1349393020 + 3600
SELECT field1, field2, field3 FROM tbl WHERE time > 1349393020 - 3600

は GOOD ですが

SELECT field1, field2, field3 FROM tbl WHERE time > 13493930200 / 10 
SELECT field1, field2, field3 FROM tbl WHERE time > 1349393020.00

は BAD です。直接 float で比較するケースは余りない気がしますが除算をしてしまうとダメだということです。当然前述の文字列比較もダメなわけです。

さらには BETWEEN、これも BAD でした。

SELECT field1, field2, field3 FROM tbl WHERE time BETWEEN 1349392000 AND 1349394000

普通にSQL書く感覚だとつい書いてしまうと思います。

これに対して TD_TIME_RANGE() は int/long/string を受け付けるので

TD_TIME_RANGE(time, '2017-03-01', '2017-03-10', 'JST')

みたいな書き方が可能です(BETWEEN とは異なり終端期間を含まないことに注意)。しかもタイムゾーンも簡単に指定できます。

つまり、何も考えずに TD_TIME_RANGE() を使うのが最も間違い無いのです。

TD には便利な時間系の UDF が揃っていますのでDATE系の標準SQLの関数を使う前にまず

docs.treasuredata.com

をご覧になることをお勧めします。

補足:TD_TIME_RANGE() と TD_TIME_FORMAT() を併用するとダメなのか?

上のドキュメントによると TD_TIME_RANGE()TD_TIME_FORMAT() を突っ込むと time index が無効化されると書いてあります。

However, if you use TD_TIME_FORMAT UDF or division in TD_TIME_RANGE, time partition opimization doesn’t work. For instance, the following conditions disable optimization.

また、

blog-jp.treasuredata.com

にも

ここで注意すべきは「月初から現在まで」といった start_time に 2014-09-01 などの文字列として設定したい場合です。TD_TIME_FORMAT については後述しますが,以下は time index pushdown が利用できない例です:

– 月初から現時点までのレコードを抽出する(time index pushdown が効かない例)

SELECT … WHERE TD_TIME_RANGE(time, TD_TIME_FORMAT(TD_SCHEDULED_TIME(), ‘yyyy-MM-01’), TD_SCHEDULED_TIME())

現時点では TD_TIME_FORMAT で動的に文字列を生成すると pushdown が効かなくなる仕様になっています。

と書いてあります。

しかしこれを実行してみると

started at 2017-03-11T18:02:34Z
executing query: SELECT TD_TIME_FORMAT(time, 'yyyy-MM-dd', 'JST')
FROM pageviews 
WHERE TD_TIME_RANGE(time, TD_TIME_FORMAT(TD_SCHEDULED_TIME(), 'yyyy-MM-01'), TD_SCHEDULED_TIME(), 'JST')
ORDER BY time ASC
Query plan:
- Stage-0
    Partitioning: SINGLE
    PartitionFunction: SINGLE
    -> Output[7]
        Columns: _col0 = td_time_format:varchar
        -> Project[14]
            Assignments: 
            -> Sort[3]
                OrderBy: time ASC NULLS LAST
                -> RemoteSource[13]
                    Sources: Stage-1
- Stage-1
    Partitioning: SOURCE
    PartitionFunction: UNKNOWN
    -> Project[12]
        Assignments: td_time_format:varchar = "td_time_format"("time", CAST('yyyy-MM-dd' AS VARCHAR), CAST('JST' AS VARCHAR))
        -> Filter[11]
            Condition: ("time" BETWEEN BIGINT '1488294000' AND BIGINT '1489287599')
            -> TableScan[0]
                Table: pageviews
                Columns: time:bigint = time:"time"
                ** Time indexes:
                    Time index: [2017-02-28 15:00:00 UTC, 2017-03-12 02:59:59 UTC]

となりちゃんと指定されたパーティションを見に行っているように見えます。

Presto だからかな?と思って Hive で投げても

Hive history file=/mnt/hive/tmp/5397/hive_job_log_c1869776-51fb-4138-8a6f-141dfb872efd_955348095.txt
**
** Time indices:
**    Time index: [2017-02-28 15:00:00 +0000, 2017-03-12 02:59:59 +0000]
**

となっていてドキュメントが書かれた時分よりクエリ最適化が進化して施されている様子でした。

気になって Treasure Data に問い合わせたところ、TD_ 系の UDF はすでに time index pushdown に対応していて、ドキュメントも近く更新されるということでした。これで安心して TD_TIME/DATE_* が使えますね。

デブサミ 2017 での講演の感想など #devsumi #devsumiB

EdTech デブサミ2017 教育 x IT 発表資料・スライド Digdag/Treasure Workflow Embulk

f:id:beniyama:20170220223816j:plain

大変ありがたいことに 2/17(金)にデブサミにて登壇する機会をいただいたので、目黒の雅叙園に行ってまいりました。

event.shoeisha.jp

思えば去年はまさにこのスタディサプリ移管の真っ最中で公募枠に応募することすらできず、今回は溜まりに溜まったネタを丸ごとぶつけました。

そのせいもあってスライドもモリモリの84ページ。

こんな感じになりました。

まだまだデータの組織としては小さいですし、データの文化が根付いているかと言われるとそんなことないですし、機械学習みたいな先端のデータ活用ブンブンしてるかと言われると全くこれからなんですが、データの組織を立ち上げたばかり、あるいはこれから立ち上げたい人や企業にとって等身大の現場のお話はできたのではないかなと思っています。

実際、Ask the speaker でもデータの組織について悩みを共感していただける方に複数お声がけいただき、やはりみなさん色々悪戦苦闘しているのだなと再認識しました。データの組織といってもサイエンティスト、アナリスト、エンジニア…と違う人格が入り混じってますし、もっと大きな組織であればそのロールごとに部署があったりするので一概には言えませんが、だいたい10~20名程度の組織規模に関して言えば現状の延長線で考えられる気がしています。

ちなみに講演中は全然気づかなかったのですが人生初のグラフィックレコーディングもしていただいていました!感激すぎる。

こうやって画像になっているとスライド探さなくても Twitter だけでどんな感じのセッションだったか感じがわかって良いですね。参加できなかったセッションのとかとてもありがたい。

そしてリハの最中に気付いたのですが今回登壇したB会場は2年前と同じ会場でした。前回は残席わずかとなっていたところ、今回は立ち見の方もちらほら見える感じでお越しいただいて感謝の気持ちでいっぱいです(Ask the speaker もぼっちにならなくて嬉しかった)

beniyama.hatenablog.jp

ちなみに本番前にこの記事を読み返していて

特に竹迫さんの『ドットコムバブルの再来~アセンブラ短歌を一句~』は素晴らしいの一言でした

と言及されている竹迫さんが今や自分の直属の上司になっているということに気づいたのでした。面白い縁だな〜と思うと同時に、何でもブログに書いておくもんだなと(当時面識なかったので書いておかなかったら気づくこともなかった)。

まだまだスタディサプリのデータ周りは色々やりたいこと、面白いこと、アイデア諸々たくさんあるのでまたどこかの機会でお話しできると良いな、と思います。

Digdag / Treasure Workflow でプラグインを使わずに `http:>` オペレータで Slack 通知を行う

Digdag/Treasure Workflow Slack

ワークフローエンジンの Digdag を本格的に使い始めたのですが、バージョン 0.9.3 の現在、通知系のオペレータは mail:> くらいしかありません。

プラグインを使うことで Slack 通知が可能になりますが、Digdag のホスティングサービスである Treasure Workflow で同じことをしようとしてややハマったのでメモしておきます。

Digdag で Slack プラグインを使う

Digdag には digdag-slack という素敵プラグインがあるのですが、残念ながら最新の 0.9.3 では動作しないということで @bwtakacy さんが qiita.com という記事を書いてくれました。

その結果、

_export:
  plugin:
    repositories:
      - file://path/to/workspace/digdag-slack
    dependencies:
      - jp.techium.blog:digdag-slack:0.1.2

+step1:
  slack>: message.txt
  webhook: https://hooks.slack.com/services/XXXXXXXX  # <-- Slack Incoming WebHooks url
  channel: general
  username: webhookbot
  icon_emoji: ghost

という感じで動かすことができるようになったのですが、これを Treasure Workflow で動かそうとすると

2017-01-31 21:04:53.077 +0000 [INFO] (0589@+my_project+step1) io.digdag.core.agent.OperatorManager: slack>: message.txt
2017-01-31 21:04:53.077 +0000 [ERROR] (0589@+my_project+step1) io.digdag.core.agent.OperatorManager: Configuration error at task +my_project+step1: Unknown task type: slack (config)

などと怒られてしまいます。

@myui さんの

github.com

を参考に Jitpack に登録して

_export:
  plugin:
    repositories:
      - https://jitpack.io
    dependencies:
      - com.github.myui:digdag-plugin-example:v0.1.2

などとしても結果は同じでした。

http:> オペレータで Webhook を叩くのが楽

結局、

+step1:
  http>: https://hooks.slack.com/services/XXXXXXXXXX  # <-- Slack Incoming WebHooks url
  method: POST
  content:
    text: "タスク1の通知です :tada:"
  content_format: json

などとして標準梱包の http:> オペレータを使うようにしてプラグインが使えない問題を回避しています。Digdag も Treasure Workflow も同じコードでいけるはずなので、これが現状一番手軽かもしれません。

Incoming Webhooks | Slack に記載されているような他のオプションも投げられるはずなので

+step1:
  http>: https://hooks.slack.com/services/XXXXXXXXXX  # <-- Slack Incoming WebHooks url
  method: POST
  content:
    username: "ghost-bot"
    icon_emoji: ":ghost:"
    channel: "#other-channel"
    text: "タスク1の通知です :tada:"
  content_format: json

というようにもできるはずです(未検証)。

(余談)0.9.3 で digdag push から -p パラメータがなくなった

digdag-slack プラグインの README では

_export:
  plugin:
    repositories:
      - file://${repository_path}
    dependencies:
      - jp.techium.blog:digdag-slack:0.1.0

として

$ digdag r hello_world.dig -p repository_path=/path/to/workspace/digdag-slack

というように repository のパスを -p で指定するようになっていますが、これを push しようとした時 digdag 0.9.3 では -p オプションが無くなっているためうまくいきません。

$ digdag push --help
2017-02-01 02:31:38 +0900: Digdag v0.9.3
Usage: digdag push <project> -r <revision>
  Options:
        --project DIR                use this directory as the project directory (default: current directory)
    -r, --revision REVISION          specific revision name instead of auto-generated UUID
        --schedule-from "yyyy-MM-dd HH:mm:ss Z"  start schedules from this time instead of current time
    -e, --endpoint HOST[:PORT]       HTTP endpoint (default: http://127.0.0.1:65432)
    -L, --log PATH                   output log messages to a file (default: -)
    -l, --log-level LEVEL            log level (error, warn, info, debug or trace)
    -X KEY=VALUE                     add a performance system config
    -c, --config PATH.properties     Configuration file (default: /Users/yamabe/.config/digdag/config)

結局上述したように file://path/to/workspace/digdag-slack というようなパスを埋め込むことにしたのですが、他に良いやり方がある気がします。

Embulk で任意のカラムをマスクする embulk-filter-mask プラグインを公開しました

Embulk OSS

久しぶりの投稿ですが生きております。

ふと Embulk のプラグインを見よう見まねで作ってみました。

github.com

Embulk で転送かけるデータのカラムを指定して * で置換を行うフィルタープラグインです。

センシティブな情報なのでマスクしたい、がカラムごと削ってしまうと入力有無がわからなくて困る…あるいは JSON の一部分だけ不要なんだけど他は連携する必要がある…などの時に使えるかなと思い作りました。

README にもある通り、例えば

first_name last_name gender age contact
Benjamin Bell male 30 bell.benjamin_dummy@example.com
Lucas Duncan male 20 lucas.duncan_dummy@example.com
Elizabeth May female 25 elizabeth.may_dummy@example.com
Christian Reid male 15 christian.reid_dummy@example.com
Amy Avery female 40 amy.avercy_dummy@example.com

というデータを input に食わせた時、

filters:
  - type: mask
    columns:
      - { name: last_name}
      - { name: age}
      - { name: contact, pattern: email, length: 5}

という設定を embulk の config に記述しておくと

first_name last_name gender age contact
Benjamin **** male ** *****@example.com
Lucas ****** male ** *****@example.com
Elizabeth *** female ** *****@example.com
Christian **** male ** *****@example.com
Amy ***** female ** *****@example.com

このようなデータが output に渡されます。

カラム名を指定しただけの状態では単純に文字数分の * に置換されますが、例えばlength オプションで5文字と指定することで一律 ***** とすることも可能です。文字長に意味を持たせたくないときなどによろしいかと思います。

あとは、pattern: email とするとメールアドレスのドメイン部分(@ 以降)は残したままでマスクを行います(デフォルトは pattern: all の全文字置換)。

JSON 型のサポートもしており、

{
  "full_name": {
    "first_name": "Benjamin",
    "last_name": "Bell"
  },
  "gender": "male",
  "age": 30,
  "email": "test_mail@example.com"
}

という構造のデータが入っている user というカラムがあった時、

filters:
  - type: mask
    columns:
      - { name: user, paths: [{key: $.full_name.first_name}, {key: $.email, pattern: email}]}    

などとすると $.full_name.first_name$.email の二つの JSONPath に合致するノードをマスクします。

またこの時、前述の patternlength オプションも個別に指定が可能なので、上記の例の出力は

{
  "full_name": {
    "first_name": "********",
    "last_name": "Bell"
  },
  "gender": "male",
  "age": 30,
  "email": "*********@example.com"
}

のようになります。

指定されたカラムの value が文字列以外の型だった場合は一回文字列に変換したものを置換します。例えば [0, 1, 2, 3] のような配列は ********* になります。

あるいは上の例で $.full_name などと指定すると

  {
    "first_name": "Benjamin",
    "last_name": "Bell"
  }

の部分が単なる文字列として解釈されてまるっと * になります。

だいぶ乱暴というか投げやりなのでこの辺は今後直して各ノードの value 部分だけをマスクして

  {
    "first_name": "********",
    "last_name": "****"
  }

などとできるようにしたいところです。

JSONPath については

github.com

を内部で使っていますので、そこで解釈できるパスであれば基本的に動作すると思います。

また今回プラグインの作り方、特にテストの書き方や JSON の扱い方などは

qiita.com

や、その中でも題材にされている embulk-filter-expand_json プラグイン

github.com

を参考にさせていただきました。ありがとうございました!

Facebook Messenger アプリに隠されたバスケットボールゲームのやり方

小ネタ

Facebook Messenger に隠れバスケットボールゲームがあるという記事 を読んだのですがいかんせんゲームの起動方法がわからない。英語圏のサイトでは basketball emoji をメッセージで送るだけ!って書いてあるんですが絵文字を開いても

f:id:beniyama:20160329000341p:plain

💩 はある癖にバスケットボールの絵文字が見つからない!

日本語のせいなのか自分のせいなのか悩みつつ、他のサイトで見つけた下のバスケットボールの絵文字

🏀

テキスト選択してからコピペしてメッセージで送り、送った🏀をタップしたらちゃんと起動しました。

なお、文章中に混ぜちゃうとダメで、絵文字単体でメッセージとして送る必要があるみたいです。

f:id:beniyama:20160329001747p:plain

ゲーム自体はすでにいろんなところでスクショ貼られていますが成功すると

f:id:beniyama:20160329002145p:plain

になったり失敗すると

f:id:beniyama:20160329002154p:plain

になったりします。

結果もちゃんと

f:id:beniyama:20160329002507p:plain

と日本語で表示されてました。

ちなみに

apps.timwhitlock.info

こちらのサイトによると🏀の名称は BASKETBALL AND HOOP で UTF8 で表すと F0 9F 8F 80 だそうです。Hoop どこいった…

また

emojipedia.org

こちらを見ると Hoop もあったりと同じ絵文字でも端末でだいぶ表現が違うようですね。あんまり絵文字についてこれまで気にしたことなかったので勉強になりました。

BOOKSCAN のプレミアム会員が、月に50冊以上の電子化を試みると何が起きるのか

体験談

二年ぶりの BOOKSCAN

およそ二年前に利用した 本・蔵書の電子書籍化サービス BOOKSCAN に再度お世話になりました。今回は実家の本を全て電子化するという目的があったのでプレミアム会員の月50冊上限に合わせて調整した前回より大がかりです。

二年前に初めて電子化した時の記事はこちら。

beniyama.hatenablog.jp

上記の記事でも触れているように、電子化終了後にプレミアム会員を解約して再び今回プレミアム化をしたのですが、途中無料会員の期間を挟んでも以前プレミアム会員の状態で電子化した書籍はマイ本棚に残っていました。通常会員の PDF 保存期間は3ヶ月と書かれていたので、ちょっと嬉しい誤算です。プレミアム会員として電子化したファイルが永続的に残るということなのかまでは確認できていません。

今回電子化したい冊数は304冊。月の無料枠50冊をゆうに超えています。

マイページを見ると

50冊/月スキャン無料 毎月50冊分(冊数の計算方法はこちら)まで無料でスキャン致します。(51冊分以上は、別途対応方法についてご相談させていただきます。)

とあるのですが、肝心の別途の対応方法が知りたいところ。 BOOKSCAN プレミアム会員で月50以上電子書籍化してほしいときの料金プラン - わからん に問い合わせ結果が書かれてあり、大体の方向性がわかったのでその上で箱詰めして送ってしまいました。

「冊数オーバーについてのご連絡」が届く

本の到着が確認されると、サポートセンターに冊数超過のお知らせがきます。ちょっと長いですが具体的な金額が書かれているので引用します。

ブックスキャンです。

書籍の到着を確認致しました。
ありがとうございます。

届きました梱包を開封致しまして
弊社規定の方法にて冊数の確認をさせていただきましたところ、
293冊分で確認致しました。
今月分は既に11冊分ご注文いただいており、計304冊分になります。

よって、304冊分 - 50冊分で254冊分が超過している状態です。

書籍のページ数の冊数カウント方法については
以下URLをご参考ください。
URL: http://www.bookscan.co.jp/price.php

今後の処理方法につきまして
以下の方法をご提案させていただきます。
ご都合の良い方法をお選びいただければと思います。
━━━━━━━━━━━━━━━━━━━
1.届いた書籍全てをお客様へ一旦ご返送する
━━━━━━━━━━━━━━━━━━━
お客様へ今回届いた書籍全てをご返却させていただきます。
そのままのご返送になりますので、返送作業料はかかりません。
ただし、送料は着払いになります。
お手元に書籍が届きましたら
冊数を調整のうえ再度ブックスキャンへ
お送りいただければと思います。

【これまで1のみでしたが、お客様のご要望にお応えし他の案もご用意致しました。】

━━━━━━━━━━━━━━━━━━━
2.通常会員分として処理する
━━━━━━━━━━━━━━━━━━━
超過冊数分を通常の納期にて対応させていただきます。
この場合、現在ですとおよそ2ヶ月待ちになります。
お客様ご自身で超過冊数分の通常申し込みを行っていただき、
その後、発行された管理番号をこのメールへ返信にてお伝えください。

また、通常料金に加えまして変更作業料として税込¥1,080.-、
さらに保管期間が発生するため保管料として¥30×254冊分で¥7,620、
税込で¥8,230.-となり、税込合計金額と致しまして¥9,310.-を頂戴致します。

■内訳
変更作業料(¥1,000/件)
保管料(¥30/冊)
【別途通常のご注文の確定が必要です】
━━━━━━━━━━━━━━━━━━━
3.超過している書籍をお客様へ一旦ご返送する
━━━━━━━━━━━━━━━━━━━
お客様へ超過分の書籍をご返却させていただきます。

返送作業料が¥500で 税込¥540.-、
送料が¥1,000×6箱で¥6,000で 税込¥6,480.-となるため
合計で 税込¥7,020.-が必要になります。
※申し訳ございませんが、トラブル防止のため書籍の選択は出来ません
━━━━━━━━━━━━━━━━━━━
4.翌月以降のプレミアム分として保管し、順次処理する
━━━━━━━━━━━━━━━━━━━
翌月以降もプレミアム会員をご継続いただく前提で、
超過している書籍を50冊分を上限に月別に移し替え管理番号を新たに発行し
翌月分以降のお客様のプレミアム会員枠にて順次ご対応させていただきます。
※プレミアム会員費は毎月の自動引き落としによりお支払いいただく形になり、
振り分け登録しました超過分はプレミアム会員更新後の作業開始となります。

小分け作業料・変更手数料と致しまして、
¥100×254冊分=¥25,400で 税込¥27,432.-を頂戴することになります。
※申し訳ございませんが、トラブル防止のため書籍の選択は出来ません

■内訳
小分け作業料・変更手数料 (¥100/冊分)
━━━━━━━━━━━━━━━━━━━
5.このままお受けして、プレミアム枠分と一緒にスキャンする
━━━━━━━━━━━━━━━━━━━
1冊分あたり¥350をお支払いいただくことで、
プレミアム枠の書籍としてお送りいただいた分と合わせて作業し、
1週間を目安に納品させていただく事が可能です。

追加料金は¥350 × 254冊分=¥88,900となり 税込¥96,012.-になります。

■内訳
スキャン・お急ぎスキャン (OCR・名前変更無料)(¥350/冊分)
--------------------------------------

上記から、ご希望の方法をお選びください。

1をご希望の際はその旨お申し付けください。
早急にご返送の手続きを致します。
その他のプランをご希望の際は、以下の口座へお振込みいただけましたら
お手数ですが再度サポートセンターまでご連絡をいただければと思います。

▼振込先
XXXXXXXX

その他の案をご希望でしたら、
可能かどうか検討させていただきますのでご返信いただければと思います。

ご不明点等ございましたらお問い合わせください。
何とぞよろしくお願い致します。

実際いくらかかりそうなのか?

注意しないといけないのは会費が含まれていないので、書籍を処理する期間に応じてトータルの支払いが変わるであろうという点です。 なのでそれぞれのパターンについて自分なりに計算してみました(正式な見積もりではないので間違っている可能性があります)。

プレミアム会員状態でのスキャンと比較するためにOCRと名前変更付きオプションをつけて計算しています。 また、返却の選択肢 1.届いた書籍全てをお客様へ一旦ご返送する3.超過している書籍をお客様へ一旦ご返送する は除外しています。

2.通常会員分として処理する

プレミアム会員を初月で解約したとして、

  • プレミアム会員費1ヶ月分 : 9,505 x 1.08 = 10,266 円
  • 通常会員としてのスキャン(OCR・名前変更付き) : (100 + 100 + 50) x 254 x 1.08 = 68,580 円
  • 変更作業料 : 1,080 円
  • 保管料 : 30 x 254 x 1.08 = 8,230 円

合計(多分) 88,156 円(ただし超過分のスキャンは早くても2ヶ月後に開始 & PDFの保持期間はおそらく3ヶ月)

4.翌月以降のプレミアム分として保管し、順次処理する

304冊 / 月50冊 で全部で7ヶ月間プレミアム会員を継続する必要があります。

  • プレミアム会員費7ヶ月分 : 9,505 x 7 x 1.08 = 71,858 円
  • 小分け作業料・変更手数料 : 100×254 x 1.08 = 27,432 円

合計(多分) 99,290 円(ただし全ての書籍のスキャンが終わるのは7ヶ月後)

5.このままお受けして、プレミアム枠分と一緒にスキャンする

プレミアム会員初月以内に全ての処理が終わる前提です。

  • プレミアム会員費1ヶ月分 : 9,505 x 1.08 = 10,266 円
  • スキャン : 350 × 254 x 1.08 = 96,012 円

合計(多分) 106,278 円(最も高いが初月中に全て完了・PDF保存期間無制限)

どのプランも高いですが、あとは総作業期間がどれだけ気になるかでしょうか。

プランが決まったらメール中に指定された口座に振り込みを行い、サポートセンターへの返信を行った後に着金が確認されると処理が開始されます。

あくまで個人的に計算してみた結果ですので、実際にご利用なされる際はサポートセンターに問い合わせをお願いいたします。

ちなみにスキャンした場合に何冊分になるかや電子化を拒否されていないかなど手軽に確認できるアプリが公開されているので本の選別に活用できます。

BOOKSCAN Checker

BOOKSCAN Checker

  • BOOKSCAN Inc.
  • ユーティリティ
  • 無料

StaticPress で静的化した WordPress を S3 に置いてデザインが崩れた時は css の Content-Type を確認しよう

HTML/CSS AWS S3 WordPress

大学時代の研究をまとめたサイトを WordPress で構築して EC2 でホストしてたのですが、

  • ラトビアあたりからの不正ログイン試行のアラートが Wordfence から頻繁に送られてくるようになった
  • そもそも更新もしないし PV も少ないし micro インスタンスでも運用するコストが見合わない

という理由で静的化プラグインの StaticPress で静的化して S3 から配信することにしました。

今回作業したのはこちらのサイト。

beniyama.com

具体的な作業内容はまたどこかで書ければと思うのですが、諸々の設定が終わった後はまったのがデザイン崩れ。崩れというより css が当たっていない。

f:id:beniyama:20160104234324p:plain

テーマが対応していないとダメだ、という記事も見かけたのですが結論としては css のファイルが S3 アップロード時に Content-Type:text/plain になってしまっていたのが原因。Content-Type:text/css に変更することで問題なくデザインが適用されました。

/tmp/static 以下に StaticPress で静的 html を吐いたとして、

s3cmd put -r --acl-public /tmp/static/* s3://<バケット名>

などとするとバケットにファイルが転送されますが、そのままだと css ファイルのメタデータは下記のようになってしまいます。

f:id:beniyama:20160104235300p:plain

s3cmd でバケット内のファイルのメタデータを一括変更することが可能ですので、続けて

s3cmd modify --add-header='Content-Type':'text/css' s3://<バケット名>/*/*.css

などとすればバケット内の全 css の Content-Type が書き換わります。

f:id:beniyama:20160105002652p:plain

これで WordPress と同じデザインが再現されました。

ちなみに余談ですが、WordPress のプラグイン StaticPress でハマった話 - わりと技術的な話 に書かれている

記事のタイトルは 〜.html というリンクになっているがアクセスするとディレクトリになってしまうという事だった。

という問題にもはまり(ページ名.html というディレクトリができてしまう)、試行錯誤した結果、パーマリンク設定のカスタム構造は

http://beniyama.com/%category%/%postname%/

で落ち着きました(%postname%.html などとしなくて良かった)。

参考)