GitLab CI で GitLab Container Registry に push した Docker イメージを使う
GitLab には Docker コンテナレジストリである GitLab Container Registry が統合されています。 GitLab CI は Docker イメージを使って CI を行うので、そこで GitLab Container Registry に push した Docker イメージを使う方法を調べました。
以下の 2 つのリポジトリを使うと仮定します。
- Docker イメージ自体のリポジトリ: gitlab.com/{user}/dockerfile
- Docker イメージを CI で使うリポジトリ: gitlab.com/{user}/dockerfile-user
Docker イメージ自体も CI するのが都合が良いと思うので、別のリポジトリとしています。 もちろん同じリポジトリであっても構いません。
なおこの記事中ではホスティングサービスである gitlab.com を想定しています。
Docker イメージのリポジトリで deploy token を作成する
2018-09-27 追記
同じユーザーのプライベートリポジトリ間、もしくは同じグループのプライベートリポジトリ間では、deploy token なしでも .gitlab-ci.yml
で image: registry.gitlab.com/{user}/dockerfile:latest
とするだけで Container Registry に push した Docker イメージを利用することができました。
GitLab のリポジトリ間の読み取り権限をそのまま利用しているのではないかと思われます。
追記ここまで
Docker イメージ自体のリポジトリ、すなわち Container Registry を有効にしているリポジトリにて、Settings > Repository > Deploy Tokens から deploy token を作成します。
このとき read_registry
にチェックを入れて、Container Registry に対する読み取りアクセスを有効にします1。
作成された認証情報である username と password を後ほど使います。
Docker イメージを使うリポジトリで変数 DOCKER_AUTH_CONFIG
を設定する
GitLab CI でプライベートコンテナレジストリにログインするには、認証情報を変数 DOCKER_AUTH_CONFIG
に設定する必要があります2。
設定する値は、作成した deploy token の username と password を :
(コロン)で連結し、Base64 エンコードしたものです。
base64
コマンドを使うと、次のようなコマンドで Base64 エンコードすることができます。echo
で改行を出力させないために -n
オプションを付けます。
$ echo -n "{username}:{password}" | base64
そして Settings > CI / CD > Variables から DOCKER_AUTH_CONFIG
を設定します。
後は .gitlab-ci.yml
の image
に Container Registry に push した Docker イメージを指定すれば完了です。
image: registry.gitlab.com/{user}/dockerfile:latest
まとめ
GitLab CI で GitLab Container Registry に push した Docker イメージを使う方法を説明しました。
Container Registry のリポジトリで read_registry
権限のある deploy token を作成し、CI を行うリポジトリで deploy token を Base64 エンコードしたものを変数 DOCKER_AUTH_CONFIG
に設定することで利用できます。
2018-09-27 追記
同じユーザーまたは同じグループのリポジトリでは、deploy token は必要なく、registry.gitlab.com
から始まる Docker イメージをそのまま利用できるようです。
SVG の path 要素のみを用いたくり抜き図形について
react-share を LINE に対応させるプルリクエストを作成する際に SVG アイコンを用意する必要があり、いろいろと慣れない作業をしました。 SVG をエディタで開いて手作業で編集する機会はなかなかないと思うので、半分作業メモのような形で記録を残しておきます。
react-share のアイコンについて
react-share は Facebook や Twitter などのソーシャルシェアボタンの React コンポーネントライブラリです。 LINE には対応していなかったので、対応させるプルリクエストを送りました。
react-share ではアイコンに SVG を使っています。
色付きの正方形 rect
または円 circle
の上に、白色の塗りつぶしパス path
を描写することでアイコンを形成しています。
LINE のアイコンを用意するときに悩んだのは、path
要素が 1 つしか使われていないことです1。
1 つの path
要素の d
属性 と fill
属性のみを使って、文字の部分をくり抜いたような図形(以下、くり抜き図形と呼ぶ)を作るのに、どうすればいいのか当初はさっぱりわかりませんでした。
デモを見ると、Reddit のアイコンは明らかにくり抜き図形なので、何らかの方法でできるはずだと思っていろいろと調査しました。
path
要素を使って塗りつぶし図形を作るには
まず SVG のパス(path
要素)を用いて塗りつぶし図形をつくるのに簡単に解説をしておきます。
path
要素の図形は d
属性によって定義されて、d
要素には一連のコマンドによって定義されるパスを複数設定することができます。簡単のため、ここでは直線のみのパスを考えることにします。一連のコマンドとは、以下のようなコマンドを並べたものです。
M x y
: パスの始点を座標(x, y)
に設定するH y
: 現在の位置から Y 座標y
への垂直な直線を描画するV x
: 現在の位置から X 座標x
への水平な直線を描画するZ
: 現在の位置からパスの始点へ戻る直線を描画する
そして fill
属性に色を指定することでパスで囲まれた部分が塗りつぶされます。例えば正方形を描画する path
は次のようになります。わかりやすさのためにパス自体にも stroke
属性を利用して色を付けています。
<svg viewBox="0 0 100 100" width="100" height="100"> <path d="M 10 10 H 90 V 90 H 10 Z" fill="rgb(147,200,254)" stroke-width="5" stroke="rgb(38,145,252)" /> </svg>
コマンドの詳細は Paths - SVG: Scalable Vector Graphics | MDN などを見てください。
path
要素の fill-rule
属性について
単純な図形の塗りつぶしなら問題はありませんが、くり抜き図形を塗りつぶそうとするとうまくいかない場合があります。そこで用いられるのが fill-rule
属性です。
fill-rule
属性は複数のパスで囲まれる部分がパスの内側かどうかを判断して塗りつぶしを制御するための属性で、値としては主に nonzero
(省略した場合のデフォルト値) と evenodd
があります。
fill-rule="nonzero"
(省略時のデフォルト値)
ある視点を図形の外から内へ向けて進めていくと仮定します。カウント 0 から始めて、右向きのパスを横切るときにカウントに 1 を加え、左向きのパスを横切るときにカウントから 1 を引きます。そしてカウントが 0 でない部分を塗りつぶします。
これを利用してくり抜き図形を作るときは、外側のパスと内側のパスを逆向きにする必要があります。
<!-- 左: 外側パスが反時計回り、内側パスが半時計回り。くり抜けない。 --> <svg viewBox="0 0 100 100" width="100" height="100"> <path d="M 10 10 H 90 V 90 H 10 Z M 30 30 H 70 V 70 H 30 Z" fill-rule="nonzero" fill="rgb(147,200,254)" stroke-width="5" stroke="rgb(38,145,252)" /> </svg> <!-- 右: 外側パスが反時計回り、内側パスが時計周り --> <svg viewBox="0 0 100 100" width="100" height="100"> <path d="M 10 10 H 90 V 90 H 10 Z M 30 30 V 70 H 70 V 30 Z" fill-rule="nonzero" fill="rgb(147,200,254)" stroke-width="5" stroke="rgb(38,145,252)" /> </svg>
fill-rule="evenodd"
同じくある視点を図形の外から内へ向けて進めていくと仮定します。カウント 0 から始めて、パスの向きは考慮せずパスを横切るときにカウントに 1 を加えます。そしてカウントが奇数の部分を塗りつぶします。
これを利用してくり抜き図形を作るときは、外側のパスと内側のパスの向きが合っているかどうかは無関係になります。
<!-- 左: 外側パスが反時計回り、内側パスが反時計回り --> <svg viewBox="0 0 100 100" width="100" height="100"> <path d="M 10 10 H 90 V 90 H 10 Z M 30 30 H 70 V 70 H 30 Z" fill-rule="evenodd" fill="rgb(147,200,254)" stroke-width="5" stroke="rgb(38,145,252)" /> </svg> <!-- 右: 外側パスが反時計回り、内側パスが時計周り --> <svg viewBox="0 0 100 100" width="100" height="100"> <path d="M 10 10 H 90 V 90 H 10 Z M 30 30 V 70 H 70 V 30 Z" fill-rule="evenodd" fill="rgb(147,200,254)" stroke-width="5" stroke="rgb(38,145,252)" /> </svg>
fill-rule="evenodd"
な path
要素を fill-rule="nonzero"
に変換する
こちらが作業メモになります。
ベクタ画像の編集に Gravit Designer を使っているのですが、これでエクスポートした SVG には fill-rule="evenodd"
属性が付いていました。
まず fill-rule
属性を削除して、図形の様子を確認しました。
案の定どの文字もくり抜かれていなかったので、パスの向きを逆にする必要がありました。
何か良いツールはないかと探したら、svg-path-reverse を発見し、これを使ってパスの向きを逆にすることができました。 使い方はこのような感じです。
const { reverse } = require('svg-path-reverse'); const path = 'ここに d 要素の中身'; console.log(reverse(path, 0));
reverse
関数の第 2 引数にサブパスのインデックス(0 始まり)を指定することで、指定したサブパスだけを逆向きにできます。
今回は外側のパスが最初のサブパスであり、外側のパスだけ逆向きにすればよかったので、第 2 引数に 0
を指定しています。
パスの向き変更とは関係が薄いですが、svg-path-reverse が出力したパスは冗長になりがちだったので、svgo という SVG を最適化するツールも併用しておきました。
まとめ
内部がくり抜かれた塗りつぶし図形を SVG の path
要素のみを用いて描画する方法を調査し、簡単にまとめました。
fill-rule="nonzero"
(もしくは省略時)の場合に外側のパスの向きと内側のパスの向きを逆にする方法と、fill-rule="evenodd"
を使う方法の 2 通りを紹介しました。
参考
LINE で共有するリンク・ディープリンクについて調べた
テキストや URL を LINE で共有するためのリンク・ディープリンクについて調べました。
LINEで送るボタン と LINE URLスキームを使う の 2 通りの方法があります。
LINEで送るボタン
LINEで送るボタンのカスタムアイコンのリンクを使うことで、URL を LINE で送るリンクを作成できます。
URL を送ることを目的としていて、リンク URL 中の url
パラメータに共有する URL を設定します。
以下 URL 中のパラメータとして、共有する URL を {URL}
、テキストを {TEXT}
として表します。
- リンク URL:
https://social-plugins.line.me/lineit/share?url={URL}
- パラメータ
url
: 共有する URL - パラメータ
text
: 共有するテキスト(ドキュメントに記載なし)
例: https://social-plugins.line.me/lineit/share?url=http://example.com
ドキュメントには記載されていませんが、ブラウザで開いた場合のみ text
パラメータに設定したテキストがテキストボックスに入力されます。
ディープリンク(アプリで開く)の場合は text
パラメータは無視されて、url
パラメータに設定された URL がテキストボックスに入力されます。
例: https://social-plugins.line.me/lineit/share?url=http://example.com&text=foobar
LINE URLスキームを使う
LINE URLスキームを使うのテキストメッセージを送る機能を使うことで、テキストを LINE で送るリンクを作成できます。
- リンク URL:
https://line.me/R/msg/text/?{TEXT}
- クエリ文字列: 共有するテキストを URL エンコードしたもの
例: https://line.me/R/msg/text/?foobar
テキストメッセージを送ることを目的としています。 ただしブラウザで開いた場合のみ、HTTP Header の Referer に設定された URL も共有されるようになっているようです。
react-share を使って LINE で共有するボタンを作る
React のソーシャルメディアボタンライブラリである react-share が LINE に対応していなかったので、LINE で共有するボタンを作成してみました。 アイコンの扱いについてガイドラインを守っているか微妙なので、プルリクエストは送っていません。
line-share-icon.js
LINE APP ICON GUIDELINE から Circle type のアイコンを AI 形式ダウンロードして SVG 形式でエクスポート後、svgr を使って React コンポーネントに変換しています。 react-share では動的に SVG アイコンが丸型かどうかを変えられますが、今回は丸型で固定しています。
import React from 'react'; import PropTypes from 'prop-types'; const LineShareIcon = props => ( <svg style={{ isolation: 'isolate' }} viewBox="0 0 160 160" width={props.size} height={props.size} > <defs> <clipPath id="a"> <path d="M0 0h160v160H0z" /> </clipPath> </defs> <g clipPath="url(#a)"> <path d="M160 80c0 44.183-35.817 80-80 80S0 124.183 0 80 35.817 0 80 0s80 35.817 80 80z" fill="#00B900" /> <path d="M133.213 75.196c0-23.811-23.87-43.183-53.213-43.183-29.339 0-53.212 19.372-53.212 43.183 0 21.347 18.93 39.224 44.502 42.604 1.732.373 4.091 1.143 4.688 2.624.538 1.345.351 3.453.172 4.812 0 0-.624 3.755-.76 4.555-.232 1.345-1.068 5.262 4.61 2.869 5.68-2.393 30.645-18.045 41.809-30.895h-.002c7.712-8.457 11.406-17.04 11.406-26.569z" fill="#FFF" /> <path d="M69.188 63.69h-3.733c-.572 0-1.036.464-1.036 1.035v23.186c0 .571.464 1.034 1.036 1.034h3.733c.572 0 1.036-.463 1.036-1.034V64.725c0-.571-.464-1.035-1.036-1.035zM94.88 63.69h-3.732c-.574 0-1.038.464-1.038 1.035v13.774L79.485 64.15a1.024 1.024 0 0 0-.087-.112c-.02-.023-.042-.043-.062-.064l-.02-.017c-.018-.016-.036-.032-.055-.046l-.027-.021-.054-.037-.031-.019a1.561 1.561 0 0 0-.088-.047l-.06-.025-.033-.012-.062-.02-.036-.009-.06-.014a.682.682 0 0 0-.042-.005c-.019-.004-.038-.005-.055-.007l-.055-.004-.036-.001H74.89c-.572 0-1.037.464-1.037 1.035v23.186c0 .571.465 1.034 1.037 1.034h3.732c.574 0 1.038-.463 1.038-1.034V74.139l10.638 14.37a1.073 1.073 0 0 0 .338.301c.01.007.02.011.029.016l.049.025.051.02c.011.004.02.01.031.012.025.01.048.016.07.024.006 0 .011.003.015.003.084.023.173.035.267.035h3.732c.572 0 1.036-.463 1.036-1.034V64.725c0-.571-.464-1.035-1.036-1.035zM60.191 83.14H50.05V64.725c0-.572-.464-1.036-1.036-1.036H45.28c-.572 0-1.036.464-1.036 1.036v23.186c0 .277.111.53.29.714a.11.11 0 0 0 .014.018l.015.014c.187.178.437.288.716.288H60.191c.572 0 1.035-.464 1.035-1.036v-3.733c0-.572-.463-1.036-1.035-1.036zM115.492 69.496c.572 0 1.035-.464 1.035-1.037v-3.732c0-.572-.463-1.036-1.035-1.036H100.58c-.28 0-.532.11-.72.292l-.011.01a1.024 1.024 0 0 0-.304.732V87.911c0 .278.111.529.289.716l.015.016a1.025 1.025 0 0 0 .731.302H115.492c.572 0 1.035-.464 1.035-1.036v-3.733c0-.571-.463-1.036-1.035-1.036h-10.141v-3.92h10.141c.572 0 1.035-.464 1.035-1.036v-3.732c0-.573-.463-1.039-1.035-1.039h-10.141v-3.917h10.141z" fill="#00B900" /> </g> </svg> ); LineShareIcon.propTypes = { size: PropTypes.number }; LineShareIcon.defaultProps = { size: 160 }; export default LineShareIcon;
line-share-button.js
LINEで送るボタンの共有方法を使っています。
import PropTypes from 'prop-types'; import createShareButton from 'react-share/es/utils/createShareButton'; import objectToGetParams from 'react-share/es/utils/objectToGetParams'; function lineLink(url, { title }) { return ( 'https://social-plugins.line.me/lineit/share' + objectToGetParams({ url, text: title }) ); } const LineShareButton = createShareButton( 'line', lineLink, props => ({ title: props.title, url: props.url }), { title: PropTypes.string, url: PropTypes.string }, { windowWidth: 500, windowHeight: 500 } ); export default LineShareButton;
使い方
<LineShareButton url={'http://example.com'} title={'foobar'}> <LineShareIcon size={32} /> </LineShareButton>
まとめ
LINE で共有するためのリンク・ディープリンクについて調べました。
URL を共有するための LINEで送るボタン、テキストを送信するための LINE URLスキーム の 2 通りの方法について紹介しました。
react-share を使って LINE で共有するための React コンポーネントを作成しました。
Twitter API を使って特定ツイートをリツイートしたユーザーを取得する方法を調べた
特定のツイートをリツイートしたユーザー一覧を取得する必要があったので、その方法を調べました。
結論から言うと、事前準備をしなかった場合では、リツイートしたユーザーを完全に取得するのは難しいことがわかりました。
Twitter API を使ってリツイートを取得する方法
詳しくは [Twitter API] リツイートを100件より多く取得する方法 | プログラミング生放送 にまとまっています。
Retweets API を使う
Retweets API には特定ツイートをリツイートしたユーザーを取得するための 2 つのエンドポイントがあります。
GET statuses/retweets/:id
は特定ツイートに対する最新のリツイートをユーザー情報付きで最大 100 件まで取得できます。
GET statuses/retweeters/ids
も同じようなエンドポイントですが、リツイート自体ではなくリツイートしたユーザーの ID のみを最大 100 件まで取得できます。
これらの API は最大でも 100 件しか取得できない制限があります。 そのためリツイートしたユーザーを 100 人以上取得したい場合は、定期的に実行して結果を保存しておくなどする必要があります。
例として Node 環境で twitter
モジュールを使ったときのソースコードを載せておきます。
GET statuses/retweets/:id
からユーザー情報を取得して、CSV 形式で標準出力に出力します。
const Twitter = require('twitter'); const client = new Twitter({ consumer_key: process.env.CONSUMER_KEY, consumer_secret: process.env.CONSUMER_SECRET, access_token_key: process.env.ACCESS_TOKEN, access_token_secret: process.env.ACCESS_TOKEN_SECRET }); const params = { id: 'TARGET_TWEET_ID', count: 100 }; client.get('statuses/retweets', params, (err, tweets) => { if (err) { console.error(err); return; } console.log('id_str,created_at,user.id_str,user.name,user.screen_name'); for (const status of tweets) { const columns = [ status.id_str, status.created_at, status.user.id_str, status.user.name, status.user.screen_name ]; console.log(columns.join(',')); } });
Standard Search API を使う
Standard Search API は 7 日以内のツイートを検索できるエンドポイントであり、これを利用して対象ツイートに含まれる文字列で検索を行うことで、リツイートを取得することができます。 Retweets API における 100 件の制限を超えてリツイートを取得することができますが、Standard Search API にも制限があり、検索対象となる期間は 7 日以内であり、更に 7 日以内であっても全てのツイートが検索対象となるわけではありません1。
以下に twitter
モジュールを使ったときのソースコード例を載せておきます。
同様にリツイートとそのユーザー情報を CSV 形式で標準出力に出力します。
const querystring = require('querystring'); const Twitter = require('twitter'); const client = new Twitter({ consumer_key: process.env.CONSUMER_KEY, consumer_secret: process.env.CONSUMER_SECRET, access_token_key: process.env.ACCESS_TOKEN, access_token_secret: process.env.ACCESS_TOKEN_SECRET }); (async () => { console.log('id_str,created_at,user.id_str,user.name,user.screen_name,text'); const params = { q: 'TARGET_TWEET_TEXT', since_id: 'TARGET_TWEET_ID', count: 100 }; while (true) { const response = await client.get('search/tweets', params).catch(err => { console.error(err); process.exit(1); }); for (const status of response.statuses) { const escapedText = status.text.replace(/\n+/g, '\\n'); const columns = [ status.id_str, status.created_at, status.user.id_str, status.user.name, status.user.screen_name, escapedText ]; console.log(columns.join(',')); } if (response.search_metadata.next_results) { const next_results = querystring.parse( response.search_metadata.next_results.replace(/^\?/, '') ); params.max_id = next_results.max_id; } else { break; } } })();
リクエストパラメータの q
には対象ツイートに含まれるテキストなどを、since_id
には対象ツイートの ID を設定します。
100 件以上取得するにはパラメータ max_id
を設定して結果のページングを行う必要があり、設定すべき max_id
の値は、直前のレスポンスの search_metadata.next_results
プロパティにクエリ文字列として含まれているので、それをパースして設定しています。
q
パラメータによっては思ったような結果が得られない場合もあり、何度か試行錯誤をしたり、結果のツイートのテキストを元にフィルタリングしたりする必要がありました。
番外編:メール通知を使う
注意:アイデアでしかなく、実現可能かどうかはわかりません。
自分のリツイートがリツイートされたことをメールで通知されるように設定しておき、後から受信したメールを検索すればリツイートしたユーザーを取得できるのではないでしょうか?
試していないのと、メール通知にそれほど詳しくないので、実現可能かどうかはわかりません。
まとめ
Twitter API を使って特定ツイートをリツイートしたユーザーを取得する方法を調べました。 Retweets API では直近 100 件までのリツイートを、Standard Search API では直近 7 日間のおおよそのリツイートを取得できることがわかりました。
7 日間以上に渡ってリツイートを取得したい場合はこれらの API を定期的に実行するか、Premium Search API を利用する必要があると思われます。
.npm-init.js を使って package.json の初期値を設定する
これまでは npm init
実行時に作成される package.json
の初期値の設定に .npmrc
を使っていましたが、.npm-init.js
を使ってより多くのプロパティを設定できるようにしました。
.npmrc
を使って package.json
の初期値を設定する
npm は設定に .npmrc
ファイルを使います1。
ユーザー単位の設定ファイルは ~/.npmrc
になります。
npm v6 時点では npm init
時に作成される package.json
のプロパティに関係するコンフィグは以下の通りです。
package.json のプロパティ |
.npmrc のコンフィグ |
---|---|
author.name |
init-author-name |
author.email |
init-author-email |
author.url |
init-author-url |
license |
init-license |
version |
init-version |
.npmrc
を使って設定する例は以下のようになります。
init-author-name=John Due init-author-email=john@example.com init-license=MIT init-version=0.1.0
~/.npmrc
の問題点
npm でパッケージを公開するために npm adduser
(npm login
) すると、認証トークンが ~/.npmrc
に書き込まれます。
~/.npmrc
を dotfiles リポジトリで管理していたりすると、この認証トークンをリポジトリにコミットする訳にはいけません。
git で特定の行だけ無視するという方法2もありますし、.npmrc
を使わずに NPM_CONFIG_INIT_AUTHOR_NAME
などの環境変数に設定する3ことでも初期値を設定できます。
ただ、どうせならより柔軟にプロパティを設定できる .npm-init.js
を使うことにしました。
.npm-init.js
を使って package.json
の初期値を設定する
npm init
実行時にはコンフィグ init-module
(デフォルト値は ~/.npm-init.js
)に設定されたモジュールが呼び出されます。
ファイルが存在しない場合は、init-package-json
パッケージの default-input.js
が呼び出されます。
このモジュールの仕組みをざっくりと言うと、module.exports
でエクスポートした Object
がそのまま package.json
の初期値に設定されるようになっています。
私は基本的にソースコードのディレクトリを ~/src
以下に Go のお作法に習って配置する4ようにしているので、カレントディレクトリがその作法に則っていれば、リポジトリの URL なども初期値に設定するようにしました。
設定した package.json
のプロパティ
fixpack
を使って package.json
をフォーマットすることが多いので、プロパティの順番をそれっぽくしています。
プロパティの詳細は https://docs.npmjs.com/files/package.json にあります。
author
author.name
, author.email
, author.url
は、まとめて author
プロパティにショートハンド文字列として設定できます。
例: "John Due <john@example.com> (https://example.com)"
repository
ホスティング先が GitHub, GitLab, Bitbucket の場合は、repository.type
と repository.url
をまとめて repository
プロパティにショートハンド文字列として設定できます。
例: "github:user/repo"
private
意図しない npm publish
を防止するために、初期値では true
に設定しました。
作成した ~/.npm-init.js
のソースコードは以下のようになりました。
// ~/.npm-init.js function repositoryMeta(provider, user, repo) { if (provider === 'github.com' || provider === 'gitlab.com') { const homepage = `https://${provider}/${user}/${repo}`; return { bugs: { url: `${homepage}/issues` }, homepage, repository: `${provider.replace(/\.com$/, '')}:${user}/${repo}` }; } return null; } const cwdTree = process.cwd().split('/'); const repo = cwdTree.pop(); const user = cwdTree.pop(); const provider = cwdTree.pop(); const meta = repositoryMeta(provider, user, repo); // Sort properties as `fixpack` does by default const json = { name: repo, description: '', version: '0.1.0', author: 'Yuichi Tanikawa <kojole.jp@gmail.com> (https://kojole.jp)' }; if (meta) { json.bugs = meta.bugs; json.homepage = meta.homepage; } json.keywords = []; json.license = 'MIT'; json.main = ''; json.private = true; if (meta) { json.repository = meta.repository; } json.scripts = { test: 'exit 1' }; module.exports = json;
まとめ
npm init
実行時に作成される package.json
の初期値の設定に、 ~/.npmrc
ではなく ~/.npm-init.js
を使うようにしました。
これにより、.npmrc
を使うとき以上に柔軟にプロパティの初期値を設定できるようになりました。
西友 5%OFF 開催日カレンダーをスクレイピングして Google カレンダーとして公開しました
元ネタ:スーパーの5%OFF開催日をiCal形式で配信してGoogleカレンダーに表示してみた
はじめに
私は毎日に西友に行くほどの西友ヘビーユーザーなので、元ネタに触発されて、西友 5%OFF 開催日の Google カレンダーを作って公開しました。
- URL: https://calendar.google.com/calendar/embed?src=0s95g7m3irrcrabge49tlk6so0%40group.calendar.google.com&ctz=Asia%2FTokyo
- Calendar ID:
0s95g7m3irrcrabge49tlk6so0@group.calendar.google.com
自分の Google カレンダーに追加するには、上記カレンダーの URL を開いて右下のボタンを押すか、Calendar ID を [友だちのカレンダーを追加] に入力してエンターを押すとできます。
元ネタでは AWS Lambda 上でスクレイピングを実行した結果を iCal 形式で配信し、それを Google カレンダーに追加するという形をとっています。
AWS Lambda は無料枠があるため個人で使う程度であれば無料ですが、カレンダーを一般公開しようとすると課金される可能性があります。 そこで、完全無料で定期的に動作させることを目指し、結果としては Google カレンダー、Google Apps Script、Travis CI cron job を使うことで完全無料を実現しました。
スクレイピングのプログラムは GitHub で公開しています。
https://github.com/kojole/seiyu-5off
実装について
スクレイピング
HTTP クライアントは node-fetch
、スクレイピングには jsdom
を使いました。
スクレイピングのライブラリに特に詳しくはなかったので、ブラウザ環境で使うことに慣れている fetch
と DOM
と同様に扱えることを理由に選びました。
スクレイピングした開催日のデータは、リポジトリ中に JSON ファイルとして保存しています。 Travis CI の cron Job を使って定期的にスクレイピングを行い、自動的に JSON ファイルを更新するようにしました。 Travis CI からリポジトリにコミットする方法については、Travis CIのcron jobsを使ってGitHubに定期的にcommitする方法 が非常に参考になりました。
Google カレンダーのイベント作成
Google API クライアントである google-api-nodejs-client
を使おうとすると OAuth 周りが少し面倒なので、Google Apps Script を使ってカレンダーにイベントを作成するようにしました。
リポジトリから開催日データの JSON を取得してカレンダーにイベントを作成するだけの割と単純な機能だったので、今回はモジュールバンドラを使用せず生の JavaScript (ES5) で書きました。
モジュールを使わないので単体テストを実行するのに少し工夫が必要です。例えばテスト対象のファイルを index.js
とすると、テストファイルの先頭でテスト対象のファイルを読み込んで、vm.runInContext
で対象ファイルを実行する必要があります。なお、テストには jest
を使っています。
// index.test.js const fs = require('fs'); const path = require('path'); const vm = require('vm'); const code = fs.readFileSync(path.join(__dirname, 'index.js')); const globalContext = vm.createContext(global); vm.runInContext(code, globalContext, { filename: 'index.js' }); describe('', () => { // ... });
まとめ
西友 5%OFF 開催日をスクレイピングで取得し、Google カレンダーとして公開しました(リンク)。 Travis CI cron job と Google Apps Script を使って定期的に開催日データが更新される仕組みを作りました。
全然関係ないですが、西友のセルフレジの重さ判定で時々エラーになって店員さんに対応してもらうとき、少し居心地が悪く感じてしまいます。