tag:crieit.net,2005:https://crieit.net/users/en30/feed en30の投稿 - Crieit Crieitでユーザーen30による最近の投稿 2018-10-31T12:04:12+09:00 https://crieit.net/users/en30/feed tag:crieit.net,2005:PublicArticle/14585 2018-10-25T18:31:17+09:00 2018-10-31T12:04:12+09:00 https://crieit.net/posts/LINE-Messaging-API LINE Messaging APIの使い始めで混乱した概念色々 <p>LINEのMessaging APIを使い始めるにあたって、プログラミング以前の管理関係の概念で混乱したのでまとめます。</p> <h2 id="ボットを作る前に"><a href="#%E3%83%9C%E3%83%83%E3%83%88%E3%82%92%E4%BD%9C%E3%82%8B%E5%89%8D%E3%81%AB">ボットを作る前に</a></h2> <h3 id="概要"><a href="#%E6%A6%82%E8%A6%81">概要</a></h3> <p>主な登場人物は</p> <ul> <li><strong>プロバイダ</strong></li> <li><strong>チャネル</strong></li> <li><strong>LINE@アカウント</strong></li> </ul> <p>で、それぞれの役割と関係は</p> <ul> <li>開発者は複数のプロバイダを持つことができる</li> <li>1プロバイダは複数ボットを管理するチームや会社のような単位。複数チャネルを持つことができる。</li> <li>1ボットは1チャネルと1LINE@アカウントのセット <ul> <li>Messaging API等技術的な部分に関わる情報はチャネル</li> <li>アカウント周りやMessaging APIを利用しない手作業でのメッセージ送信等に関わる情報はLINE@アカウント</li> </ul></li> </ul> <p>となっているようです。</p> <p><a href="https://crieit.now.sh/upload_images/255c01718747621c9982c08194999ae05bd18c8951be6.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/255c01718747621c9982c08194999ae05bd18c8951be6.png?mw=700" alt="LINE Messaging API" /></a></p> <p>僕はMessaging APIを使う上でLINE@アカウントという概念の必要性がわからず混乱したのですが、元々店舗用等手動で管理するLINEアカウントとしてLINE@アカウントがあり、そこにMessaging APIが建て増しされたのかな?と想定すると理解し易くなった気がします。</p> <h3 id="管理"><a href="#%E7%AE%A1%E7%90%86">管理</a></h3> <ul> <li>プロバイダとチャネルの管理は<a target="_blank" rel="nofollow noopener" href="https://developers.line.me/console/">LINE developersのコンソール</a></li> <li>LINE@アカウントの管理は<a target="_blank" rel="nofollow noopener" href="https://admin-official.line.me/">LINE@ MANAGER</a></li> </ul> <p>で管理します。</p> <p>複数管理者がいる場合の権限周りも、プロバイダ、チャネル、LINE@アカウントそれぞれ独立しています。例えば、プロバイダの権限を与えても属するチャネルの権限は与えられらない、チャネルの権限を与えてもLINE@アカウントの権限は与えられない、という形です。</p> <h2 id="実際にボットを作るまで"><a href="#%E5%AE%9F%E9%9A%9B%E3%81%AB%E3%83%9C%E3%83%83%E3%83%88%E3%82%92%E4%BD%9C%E3%82%8B%E3%81%BE%E3%81%A7">実際にボットを作るまで</a></h2> <p>Messaging APIを利用したボットを作成するには2ルートあるようです。</p> <h3 id="ルート1"><a href="#%E3%83%AB%E3%83%BC%E3%83%881">ルート1</a></h3> <ol> <li><a target="_blank" rel="nofollow noopener" href="https://entry-at.line.me/">アカウントの作成フォーム</a>からLINE@アカウントを作る</li> <li><code>https://admin-official.line.me/{id}/bot-api/setting</code> からAPI利用の設定をする <ul> <li>プロバイダの設定/作成</li> <li>チャネルの作成</li> </ul></li> </ol> <p>こちらは既にLINE@アカウントを持っていた人用に作られた道なのではないかなと想定しています。なので、新たに作る場合は以下のルート2が良いと思います。</p> <h3 id="ルート2(オススメ)"><a href="#%E3%83%AB%E3%83%BC%E3%83%882%EF%BC%88%E3%82%AA%E3%82%B9%E3%82%B9%E3%83%A1%EF%BC%89">ルート2(オススメ)</a></h3> <p><a target="_blank" rel="nofollow noopener" href="https://developers.line.me/console/">LINE developersのコンソール</a>上で</p> <ol> <li>プロバイダを作る</li> <li>チャネルを作る</li> </ol> <p>チャネルを作ると<strong>自動でLINE@アカウントができます</strong>。</p> <p>僕は<a target="_blank" rel="nofollow noopener" href="https://developers.line.me/ja/docs/messaging-api/overview/">ドキュメント</a>の</p> <blockquote> <p>Messaging APIを利用するには、LINE@アカウントが必要です。LINE@アカウントとは、LINEボットのためのLINEアカウントです。</p> </blockquote> <p>というのを読んで勘違いしてしまったのですが、こちらの場合<strong>LINE@アカウントを事前に作成する必要はありません</strong>。</p> <h2 id="その他注意点"><a href="#%E3%81%9D%E3%81%AE%E4%BB%96%E6%B3%A8%E6%84%8F%E7%82%B9">その他注意点</a></h2> <h3 id="一度Developer Trialプランで作ると他のプランへは変更できない"><a href="#%E4%B8%80%E5%BA%A6Developer+Trial%E3%83%97%E3%83%A9%E3%83%B3%E3%81%A7%E4%BD%9C%E3%82%8B%E3%81%A8%E4%BB%96%E3%81%AE%E3%83%97%E3%83%A9%E3%83%B3%E3%81%B8%E3%81%AF%E5%A4%89%E6%9B%B4%E3%81%A7%E3%81%8D%E3%81%AA%E3%81%84">一度Developer Trialプランで作ると他のプランへは変更できない</a></h3> <p>よく見るとプランの説明にも書いてありました。</p> <p>LINE developersのチャネル管理ページ上にはプランの「変更はこちら」というリンクがあるのですが、LINE@ MANAGERに飛ばされて「ログイン中のLINEアカウントでは購入できません。」と表示されてどうしたらいいのかわかりませんでした。</p> <p>チャネル作成時にはDeveloper Trialsかフリーしか選べませんが、フリーを選んだ場合には作成後に<a target="_blank" rel="nofollow noopener" href="https://admin-official.line.me/">LINE@ MANAGER</a>でプロ(API)プラン等に変更することができます。</p> <p>名前は7日間変更できないのでDeveloper Trialsでテスト用のチャネルを作る場合には名前に"(テスト)"などを入れておくと、本番用のチャネルを作ったときに一覧で区別しやすいと思います。</p> <h3 id="認証済みアカウントの申請はLINE@ MANAGERからでもできる"><a href="#%E8%AA%8D%E8%A8%BC%E6%B8%88%E3%81%BF%E3%82%A2%E3%82%AB%E3%82%A6%E3%83%B3%E3%83%88%E3%81%AE%E7%94%B3%E8%AB%8B%E3%81%AFLINE%40+MANAGER%E3%81%8B%E3%82%89%E3%81%A7%E3%82%82%E3%81%A7%E3%81%8D%E3%82%8B">認証済みアカウントの申請はLINE@ MANAGERからでもできる</a></h3> <p>Messaging APIで<strong>グループやトークルーム関係のAPIを使うには認証済みアカウントである必要がある</strong>ので、そういった機能を使いたい場合には認証済みアカウントの申請をしたくなるでしょう。</p> <p><a target="_blank" rel="nofollow noopener" href="https://help2.line.me/line_at_application/web/pc?lang=ja">よくある質問</a>では</p> <blockquote> <p>※すでにご利用中の一般アカウントを認証済みアカウントへ申請する場合は、LINE@アプリからのみ可能です</p> </blockquote> <p>とあるのでLINE@アプリを入れてみたのですが、チャネル作成時にできたLINE@アカウントを管理することができませんでした。作り直しかと思ったのですが、<a target="_blank" rel="nofollow noopener" href="https://admin-official.line.me/">LINE@ MANAGER</a>のアカウント設定から申請できました。</p> <h2 id="おわりに"><a href="#%E3%81%8A%E3%82%8F%E3%82%8A%E3%81%AB">おわりに</a></h2> <p>以上です。<br /> LINE Messaging APIはコストが低く開発できながら、面白い使い方が色々できそうです。<br /> この記事がスムーズに開発に入る助けになったら幸いです。</p> en30 tag:crieit.net,2005:PublicArticle/14553 2018-09-27T15:16:50+09:00 2018-10-23T09:24:26+09:00 https://crieit.net/posts/HTML-5bac75d259324 HTMLの文字コード決定プロセス <p>スクレイピングしていたら文字化けしているものがあったので、HTTPでやりとりされるHTMLの文字コード判定が、どのようなプロセスを経て行われているのか調べてみました。</p> <h2 id="HTTPでやりとりするHTMLでの文字コード"><a href="#HTTP%E3%81%A7%E3%82%84%E3%82%8A%E3%81%A8%E3%82%8A%E3%81%99%E3%82%8BHTML%E3%81%A7%E3%81%AE%E6%96%87%E5%AD%97%E3%82%B3%E3%83%BC%E3%83%89">HTTPでやりとりするHTMLでの文字コード</a></h2> <p>基本的には以下の情報を見ていくようです。</p> <ol> <li>BOM</li> <li>HTTPのContent-Typeヘッダ</li> <li>HTMLのmetaタグ <ul> <li>charset属性</li> <li><code>http-equiv="Content-Type"</code>なもののcontent属性</li> </ul></li> </ol> <p>参考: <a target="_blank" rel="nofollow noopener" href="https://www.w3.org/International/questions/qa-html-encoding-declarations.ja">https://www.w3.org/International/questions/qa-html-encoding-declarations.ja</a></p> <h2 id="axiosの場合"><a href="#axios%E3%81%AE%E5%A0%B4%E5%90%88">axiosの場合</a></h2> <p>元々はnodeでaxiosを使っていて困った部分だったので、axiosで文字コードを考慮してどう処理するかをTypeScriptで書いていきます。</p> <p>axiosはデフォルトでは上の情報はどれも利用されずにutf-8決め打ちでデコードされてしまいます。なのでoptionに<code>responseType: 'arraybuffer'</code>を渡し<code>response.data</code>を<code>Buffer</code>として受け取って処理していきます。</p> <p>最終目標は</p> <pre><code class="typescript">import axios from 'axios'; import iconv = require('iconv-lite'); import * as charset from './charset'; (async () => { const response = await axios.get(url, { responseType: 'arraybuffer' }); const body = iconv.decode(response.data, charset.detect(response)); })() </code></pre> <p>のように使える<code>charset.detect</code>を実装することです。</p> <p>各判定処理ごとに関数にして、決定できなかった場合にはutf-8にフォールバックするようにします。</p> <pre><code class="typescript">import { AxiosResponse } from 'axios'; type Charset = string; type IntermediateResult = Charset | null; export const detect = (res: AxiosResponse): Charset => fromBOM(res.data) || fromHeader(res.headers["content-type"]) || fromMetaTag(res.data) || Charset.UTF8; </code></pre> <p><code>Charset</code>はきちんとやるなら <a target="_blank" rel="nofollow noopener" href="https://www.iana.org/assignments/character-sets/character-sets.xhtml">https://www.iana.org/assignments/character-sets/character-sets.xhtml</a> にあるもののunion typeとかstring enumsとかの方が良いのかもしれません。</p> <h3 id="BOM"><a href="#BOM">BOM</a></h3> <p>BOMはByte Order Markで先頭数バイトを特定のパターンにすることで、ユニコードであることとそのエンコーディング、エンディアンを示すものです。<br /> <a target="_blank" rel="nofollow noopener" href="https://en.wikipedia.org/wiki/Byte_order_mark#Byte_order_marks_by_encoding">https://en.wikipedia.org/wiki/Byte_order_mark#Byte_order_marks_by_encoding</a> から持ってきています。</p> <pre><code class="typescript">// to assert elements as tuple (inferred Array<string | Buffer>) const bomify = ([c, bytes]) => ([c, Buffer.from(bytes)] as [Charset, Buffer]); const BOMS: ReadonlyMap<Charset, Buffer> = new Map([ ['utf-8', [0xEF, 0xBB, 0xBF]], ['utf-16be', [0xFE, 0xFF]], ['utf-16le', [0xFF, 0xFE]], ['utf-7', [0x2B, 0x2F, 0x76, 0x38]], ['utf-7', [0x2B, 0x2F, 0x76, 0x39]], ['utf-7', [0x2B, 0x2F, 0x76, 0x2B]], ['utf-7', [0x2B, 0x2F, 0x76, 0x3F]], ['utf-7', [0x2B, 0x2F, 0x76, 0x38, 0x2D]], ['utf-1', [0xF7, 0x64, 0x4C]], ['utf-ebcdic', [0xDD, 0x73, 0x66, 0x73]], ['scsu', [0x0E, 0xFE, 0xFF]], ['bocu-1', [0xFB, 0xEE, 0x28]], ['gb-18030', [0x84, 0x31, 0x95, 0x33]], ].map(bomify)); export const fromBOM = (buf): IntermediateResult => { const startsWith = (bom) => buf.slice(0, bom.length).equals(bom) for (let [charset, bom] of BOMS) { if (startsWith(bom)) return charset; } return null; } </code></pre> <h3 id="Content-Type Header"><a href="#Content-Type+Header">Content-Type Header</a></h3> <p>Content-Typeヘッダのフォーマットは<a target="_blank" rel="nofollow noopener" href="https://tools.ietf.org/html/rfc7231#section-3.1.1.5">RFC 7231のSection 3.1.1.5</a>で決められています。<br /> それに基づいて実装されている<a target="_blank" rel="nofollow noopener" href="https://github.com/jshttp/content-type">jshttp/content-type</a>を利用します。</p> <pre><code class="typescript">import contentType = require('content-type'); export const fromHeader = (ctype): IntermediateResult => { const res = contentType.parse(ctype); return res.parameters.charset || null; } </code></pre> <h3 id="metaタグ"><a href="#meta%E3%82%BF%E3%82%B0">metaタグ</a></h3> <p>Bufferをasciiにデコードして</p> <ul> <li>metaタグのcharset属性</li> <li><code>http-equiv="Content-Type"</code>なmetaタグのcontent属性</li> </ul> <p>を<a target="_blank" rel="nofollow noopener" href="https://github.com/cheeriojs/cheerio">cheerio</a>を使って探します。</p> <pre><code class="typescript">import cheerio = require('cheerio'); export const fromMetaTag = (buf): DetectionResult => { const $ = cheerio.load(buf.toString('ascii')); let res = $('meta[charset]').attr('charset'); if (res) return res; res = $('meta[http-equiv="Content-Type"]').attr('content'); if (res) return fromHeader(res); return null; } </code></pre> <h3 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h3> <p>以上です。全体像を貼っておきます。</p> <pre><code class="typescript">import { AxiosResponse } from 'axios'; import contentType = require('content-type'); import cheerio = require('cheerio'); type Charset = string; type IntermediateResult = Charset | null; // to assert elements as tuple (inferred Array<string | Buffer>) const bomify = ([c, bytes]) => ([c, Buffer.from(bytes)] as [Charset, Buffer]); const BOMS: ReadonlyMap<Charset, Buffer> = new Map([ ['utf-8', [0xEF, 0xBB, 0xBF]], ['utf-16be', [0xFE, 0xFF]], ['utf-16le', [0xFF, 0xFE]], ['utf-7', [0x2B, 0x2F, 0x76, 0x38]], ['utf-7', [0x2B, 0x2F, 0x76, 0x39]], ['utf-7', [0x2B, 0x2F, 0x76, 0x2B]], ['utf-7', [0x2B, 0x2F, 0x76, 0x3F]], ['utf-7', [0x2B, 0x2F, 0x76, 0x38, 0x2D]], ['utf-1', [0xF7, 0x64, 0x4C]], ['utf-ebcdic', [0xDD, 0x73, 0x66, 0x73]], ['scsu', [0x0E, 0xFE, 0xFF]], ['bocu-1', [0xFB, 0xEE, 0x28]], ['gb-18030', [0x84, 0x31, 0x95, 0x33]], ].map(bomify)); export const fromBOM = (buf): IntermediateResult => { const startsWith = (bom) => buf.slice(0, bom.length).equals(bom) for (let [charset, bom] of BOMS) { if (startsWith(bom)) return charset; } return null; } export const fromHeader = (ctype): IntermediateResult => { const res = contentType.parse(ctype); return res.parameters.charset || null; } export const fromMetaTag = (buf): IntermediateResult => { const $ = cheerio.load(buf.toString('ascii')); let res = $('meta[charset]').attr('charset'); if (res) return res; res = $('meta[http-equiv="Content-Type"]').attr('content'); if (res) return fromHeader(res); return null; } export const detect = (res: AxiosResponse): Charset => fromBOM(res.data) || fromHeader(res.headers["content-type"]) || fromMetaTag(res.data) || 'utf-8'; </code></pre> <h2 id="最後に"><a href="#%E6%9C%80%E5%BE%8C%E3%81%AB">最後に</a></h2> <p>僕は普段主にRubyを使っているので、Rubyの場合どうなのかも気になって少し調べてみたのですが、標準ライブラリの<code>Net::HTTP</code>で<code>Content-Type</code>をハンドルすべきかについての<a target="_blank" rel="nofollow noopener" href="https://bugs.ruby-lang.org/issues/2567">Issueがありました</a>。</p> <p>現実的には複数の方法で違う文字コードとして指定されていたり、実際使われているものと違ったりということもあるようで、絶対に信用できるメタデータというわけではないようです。</p> <p>頻度から分類するアプローチもあるようで、現実的にはこちらの方がうまく動くかもしれません。<br /> <a target="_blank" rel="nofollow noopener" href="https://github.com/runk/node-chardet">runk/node-chardet</a><br /> データがあれば機械学習の実験課題としてちょうど良さそうですね。</p> <p>最初は雑な正規表現で書いていたのですが、記事を書いているうちに正しいフォーマットはどうなのか気になってRFCを見にいったりして結構勉強になりました。全部utf-8だと嬉しいですね。</p> en30