tag:crieit.net,2005:https://crieit.net/tags/HTTP/feed 「HTTP」の記事 - Crieit Crieitでタグ「HTTP」に投稿された最近の記事 2021-05-27T22:53:38+09:00 https://crieit.net/tags/HTTP/feed tag:crieit.net,2005:PublicArticle/17301 2021-05-27T22:50:56+09:00 2021-05-27T22:53:38+09:00 https://crieit.net/posts/Web-60afa3c0c702c Webアプリケーションに関する勉強記録① <p>最近Webアプリケーション作成に興味を持ちましたが、知識が足りないと痛感しました…</p> <p>そのため、この<br /> <a target="_blank" rel="nofollow noopener" href="https://zenn.dev/bigen1925/books/introduction-to-web-application-with-python">伸び悩んでいる3年目Webエンジニアのための、Python Webアプリケーション自作入門</a><br /> を読み、Webアプリケーションとはどういうものなのかを理解していけたらと思います。<br /> 私は3年目でもなければWebエンジニアでもないのですが、とてもわかりやすく楽しい内容だと思うので、興味のある方はこの本を読んでみてください。</p> <p>本記事は、Chapter1~Chapter8の内容について、自分用のまとめとして作成しています。</p> <h2><a target="_blank" rel="nofollow noopener" href="https://zenn.dev/bigen1925/books/introduction-to-web-application-with-python/viewer/what-is-web-application">Webアプリケーションとはなにか</a></h2> <h3 id="Webアプリケーションについて"><a href="#Web%E3%82%A2%E3%83%97%E3%83%AA%E3%82%B1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6">Webアプリケーションについて</a></h3> <p><code>Webアプリケーションとは、Webサービスを提供するプログラムである</code></p> <p>乱暴にかみ砕くとこのように言えます。Webサービスというのはブラウザを介して利用するサービスのことで、GmailやGithubなど普段私たちがブラウザ上で利用しているもののことです。このWebサービスは、プログラミンした文字列ではなく、それらを解釈して実行した結果実現される機能群です。なので、このWebアプリケーションのソースコードを書き、あるプログラムで実行することで、Webサービスが私たちに提供されています。</p> <p>以下の画像がWebアプリケーションの概要図になります。ブラウザがWebサーバーへ何かリクエストを送信すると、Webサーバーは、Webフレームワークを介してサービスごとに固有のプログラムをブラウザに返してくれます。この<em>Webサーバー+Webフレームワーク+サービスごとに固有のライブラリ</em>をまとめてWebアプリケーションと呼ぶことができます。</p> <h3 id="Webサーバとはなにか"><a href="#Web%E3%82%B5%E3%83%BC%E3%83%90%E3%81%A8%E3%81%AF%E3%81%AA%E3%81%AB%E3%81%8B">Webサーバとはなにか</a></h3> <p>Webサーバーとは、Webサービスを提供する<strong>プログラム</strong>と考えていいです。ここで、Webアプリケーションもそうだったじゃないかと思いますよね。実は広義の意味ではこの二つは同じ意味なのですが、多くの場合、WebサーバーはWebアプリケーションの中でも特にWebサービスを提供する窓口部分のプログラムのことだけを指します。</p> <p>Webサービスはかつては単一のプログラムで提供されており、それをWebサーバーと呼んでいました。しかし、提供するサービスがとても多くなり、よりリッチなサービスとなったため、どんなWebサービスでも使用する共通部分は共通プログラムとして、サービスごとのプログラムと分けて作成するようになりました。その結果、この共通プログラム部分がWebサーバーと呼ばれるものになってきました。</p> <p>Webサーバーの代表例として、apacheやnginxがあります。</p> <h3 id="Webフレームワークについて"><a href="#Web%E3%83%95%E3%83%AC%E3%83%BC%E3%83%A0%E3%83%AF%E3%83%BC%E3%82%AF%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6">Webフレームワークについて</a></h3> <p>先ほどのサービスごとのプログラムの中にも、定番の共通機能がよく見られ、それらがライブラリとして世の中に出回っていきました。それが<em>Webフレームワーク</em>と呼ばれるものです。<br /> Webサーバー上で追加で動かすような構成になっており、PythonのDjangoや、Laravelなどがあります。</p> <h3 id="Webサーバ以外のサーバ"><a href="#Web%E3%82%B5%E3%83%BC%E3%83%90%E4%BB%A5%E5%A4%96%E3%81%AE%E3%82%B5%E3%83%BC%E3%83%90">Webサーバ以外のサーバ</a></h3> <p>Webサーバは別名HTTPサーバと呼ばれ、クライアントとHTTPというプロトコルで通信します。他にも様々なサーバが存在し、それぞれプロトコルの違いにより区別されています。</p> <ul> <li>DBサーバ:RDBMSなど、データを格納し走査するためのサーバ</li> <li>ファイルサーバ:主に企業等で用いられる、メンバーがLANの中で自由にファイルを保存し、共有できる仕組みに使用されるサーバコンピュータ</li> <li>メールサーバ:メールの送信・受信の役割を持つサーバ</li> </ul> <h3 id="通信プロトコルについて"><a href="#%E9%80%9A%E4%BF%A1%E3%83%97%E3%83%AD%E3%83%88%E3%82%B3%E3%83%AB%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6">通信プロトコルについて</a></h3> <p>通信プロトコルとは、通信する際の規格のことです。この取り決めを守ることで、メーカー、機種などに左右されずに、全てのパソコンやサーバ間で通信を行うことができます。データの通信ではパケット交換方式が用いられています。これは、通信データをパケットと言われる単位に分割し送ることで、多くのユーザが同時にデータを送受信することを可能にしています。パケットは、ヘッダという通信先や通信元の情報と分割されたデータによって構成されています。</p> <ul> <li>HTTP:ブラウザとWebサーバが通信する際に使われるプロトコル</li> <li>SMTP/POP/IMAP:メールの送受信委関するプロトコル。SMTPはメールの送信、POPはメールサーバから電子メールを受信する際に、IMAPはメールをサーバ上で保持し続ける際に使用するプロトコル。</li> <li>FTP:クライアントとサーバ間のファイル転送の際に使用されるプロトコル</li> </ul> <h2><a target="_blank" rel="nofollow noopener" href="https://zenn.dev/bigen1925/books/introduction-to-web-application-with-python/viewer/making-silly-web-server">簡易Webサーバーを作る手順</a></h2> <p>実際にWebサーバとブラウザが通信しているところを確認し、真似することで簡易的なWebサーバを作ります。</p> <h3><a target="_blank" rel="nofollow noopener" href="https://zenn.dev/bigen1925/books/introduction-to-web-application-with-python/viewer/making-silly-web-server-step1">STEP1:ChromeとApacheで通信してみる</a></h3> <h4 id="ブラウザとWebサーバーが「通信する」とは"><a href="#%E3%83%96%E3%83%A9%E3%82%A6%E3%82%B6%E3%81%A8Web%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC%E3%81%8C%E3%80%8C%E9%80%9A%E4%BF%A1%E3%81%99%E3%82%8B%E3%80%8D%E3%81%A8%E3%81%AF">ブラウザとWebサーバーが「通信する」とは</a></h4> <p>ChromeとApacheの通信、つまり、ブラウザとWebサーバの通信というのは、ブラウザからWebサーバに向かってリクエスト(Webサービスを提供してほしい)を送り、Webサーバがそれに対してレスポンス(Webサービス)を返すということです。</p> <h4 id="ブラウザからWebサーバーへリクエストを送るためには"><a href="#%E3%83%96%E3%83%A9%E3%82%A6%E3%82%B6%E3%81%8B%E3%82%89Web%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC%E3%81%B8%E3%83%AA%E3%82%AF%E3%82%A8%E3%82%B9%E3%83%88%E3%82%92%E9%80%81%E3%82%8B%E3%81%9F%E3%82%81%E3%81%AB%E3%81%AF">ブラウザからWebサーバーへリクエストを送るためには</a></h4> <p>インターネット上で、あるプログラムが別のプログラムへ何かを送るとき、必要となるのは<strong>宛先</strong>と<strong>内容</strong>です。<br /> このうち、Chromeでは、<strong>宛先</strong>だけをURLバーに入力すれば、<strong>内容</strong>は自動で生成されます。</p> <p>ここで、Webサービスにおける<strong>宛先</strong>のことを、<strong>URL</strong>と呼び、以下のような形式で表現されます。</p> <p><span style="font-size: 150%;"> URL = ** <code><protocol></code> : // <code><host></code> : <code><port></code> / <code><path></code> ?** <code><query></code> </span></p> <h4 id="protocol"><a href="#protocol">protocol</a></h4> <p>リクエストの送り方を指定する。Webサービスでは<code>http</code>や<code>https</code>がよく用いられる。郵便に例えると、「普通郵便」や「本人限定受取」といった郵便の送り方のようなものです。このように、宛先や内容には関係ないですが、送る手順や受け取り方がprotocolごとに変わります。</p> <p><code>http</code>ではいわば普通郵便で、送りたい内容をそのまま送ります。<br /> <code>https</code>は暗号化通信で、<code>http</code>よりセキュアな通信が可能となります。</p> <h4 id="host"><a href="#host">host</a></h4> <p>送り先のWebサーバープログラムが動いているマシン(=コンピュータ)のインターネット上の住所を示します。基本的には、インターネット上のマシンを特定するために<code>IPアドレス</code>を使用する。</p> <p>IPアドレス以外に<code>host</code>に使用できるものとして以下のものがあります。</p> <ul> <li><p>DNSに登録されたドメイン:<code>zenn.dev</code> <code>google.com</code>など</p></li> <li><p>localhost:IPアドレス<code>127.0.0.1</code>とみなされ、自分のPCを指す。</p></li> </ul> <p>ただ、自分のPC上には様々なプログラムが動いており、<code>host</code>だけでは宛先として不十分となります。例えば、マンションで、住所だけがわかっても部屋の番号がわからないような状態といえます。そこで、必要になるのが<code>port</code>です。</p> <h4 id="port"><a href="#port">port</a></h4> <p><code>port</code>は、インターネット通信の際に特定のマシンの中で動いている複数のプログラムから、目的のプログラムを特定するための番号。通常、PCやサーバー上では複数のプログラムが動いているため、ポート番号を指定することで、同一IPのプログラムの中から、任意のプログラムへリクエストを送ることができる。</p> <p>port番号は、プログラム起動時にプログラムが0~65535番の中から自分で設定することができる。</p> <ul> <li>Well-Known ports : 複数のプログラムが同じportを番号を使うことはできず、後からそのport番号を割り振ろうとしたほうがエラーとなってしまいます。そのため、どのマシンでもよく使うようなプログラムには予めport番号が予約されており、このport番号のことを<code>well-known ports</code>という。0~1023番まではすでに決まっている。例えば、<code>HTTP</code>は80番、<code>HTTPS</code>は443番、<code>FTP</code>は20・21番など。ただし、必ずしもwell-known portsでサービスを動作させる必要はなく、ソフトウェアによって設定できるようになっていることが多いです。</li> </ul> <p>多くのブラウザでは、http通信する際は、デフォルトで80番ポートへ向けて通信すると決まっているため、httpでポートが80番の場合に限り、port番号を省略し、<code>http://localhost/~</code>と書いてもいいです。</p> <h4 id="path"><a href="#path">path</a></h4> <p>上記の、<code>protocol</code>,<code>host</code>,<code>port</code>がそろえば、ブラウザは相手のWebサーバプログラムの場所を特定して、通信を始めることができます。しかし、一般的に1つのWebサービスに対する要求は1種類ではありません。そのため、どのようなサービスを要求したいかを伝える目的で、pathにその情報を追加します。</p> <p>pathは、<code>/</code>区切りで、サーバ内部のアクセスする対象のファイルの場所を指定する。初期のウェブでは、ウェブサーバー上の物理的なファイルの場所を指定していたが、現在は物理的なものではなく、ウェブサーバーによって処理される、抽象的なものを指定することが多いです。</p> <h4 id="query"><a href="#query">query</a></h4> <p><code>query</code>は、pathに加えて、何か情報を追加で送りたいときに使います。送る情報は、名前と値を<code>=</code>で区切り、パラメータ同士を<code>&</code>で区切ります。ウェブサーバーに与える引数のようなものです。</p> <h3><a target="_blank" rel="nofollow noopener" href="https://zenn.dev/bigen1925/books/introduction-to-web-application-with-python/viewer/making-silly-web-server-step2">STEP2:ブラウザと自作サーバーで通信してみる</a></h3> <h4 id="TCPサーバー"><a href="#TCP%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC">TCPサーバー</a></h4> <p>TCPサーバーとは、TCP(Transmission Control Protocol)で通信を行うサーバーのことです。TCPは、IPの上位プロトコルで、トランスポート層で動作します。ネットワーク層のIPとセッション層以上のプロトコル(HTTP,FTP,telnet)の橋渡しをする形で動作しています。トランスポート層では、UDPというプロトコルも存在しています。</p> <ul> <li>UDP : コネクションレス型通信(通信開始前に相手との間で事前のやり取りをしない)。通信の信頼性(データの漏れや、順序が守られているかどうか)は高くないが、高速でリアルタイムの通信を行えます。</li> <li>TCP : コネクション型通信(3 wayハンドシェイク)。通信の信頼性が高いです</li> </ul> <p>TCPのコネクションは、<code>3wayハンドシェイク</code>と呼ばれる方法で結ぶ、通信相手との仮想の通信路のことをいいます。</p> <h3><a target="_blank" rel="nofollow noopener" href="https://zenn.dev/bigen1925/books/introduction-to-web-application-with-python/viewer/making-silly-web-server-step2">STEP3:自作クライアントとApacheで通信してみる</a></h3> <h3><a target="_blank" rel="nofollow noopener" href="https://zenn.dev/bigen1925/books/introduction-to-web-application-with-python/viewer/making-silly-web-server-step2">STEP4:自作サーバーを進化させる</a></h3> k-s-p tag:crieit.net,2005:PublicArticle/15868 2020-04-25T02:52:35+09:00 2020-04-26T16:32:05+09:00 https://crieit.net/posts/VPS-HTTP-80-HTTPS-443 VPSを引っ越したらHTTP(80)は繋がるがHTTPS(443)は繋がらなくなった原因 <h1 id="原因"><a href="#%E5%8E%9F%E5%9B%A0">原因</a></h1> <p>sslhの設定のIPアドレスが古いVPNのもののままだったせいでした。<br /> 新しいVPSのIPアドレスに修正したら直りました。</p> <h1 id="現象"><a href="#%E7%8F%BE%E8%B1%A1">現象</a></h1> <p>VPSを引っ越したらHTTP(80)は繋がるがHTTPS(443)は繋がらなくなりました。<br /> IPアドレスを指定しても、HTTPは繋がるのにHTTPSは<code>connection refused</code>になってしまいました。<br /> Webサーバーにsshでログインして、<code>curl https://localhost</code>を実行しても<code>connection refused</code>になるので、ufwなどを疑い無効にしてみたりしましたが、現象は変わらずHTTPSだけが問題で、他は問題なしでした。<br /> 結局、<code>grep -nr 443 /etc</code>で設定をさらったら、<code>/etc/default/sslh</code>が引っかかって、sslhのインストールしたことを思い出したのでした。</p> <h1 id="sslhとは?"><a href="#sslh%E3%81%A8%E3%81%AF%EF%BC%9F">sslhとは?</a></h1> <p>sslポート(443)などに、sshなどのプロトコルでも接続できるようにしてくれる、プロトコル多重化サーバーです。<br /> <a target="_blank" rel="nofollow noopener" href="https://www.rutschle.net/tech/sslh/README.html">sslh – A ssl/ssh multiplexer</a></p> 真田 保 tag:crieit.net,2005:PublicArticle/15241 2019-07-15T02:50:01+09:00 2019-07-16T01:48:10+09:00 https://crieit.net/posts/PHP-HTTP-date PHPでHTTP-date形式の時刻を生成する <p>中々触れる機会は無いと思いますが、HTTP汎用ヘッダーに使われる日付形式がやや特殊なためメモ。<br /> 最初のほうは仕様について書いています。コードが欲しい方は飛ばしてください。</p> <hr /> <h3 id="仕様について"><a href="#%E4%BB%95%E6%A7%98%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6">仕様について</a></h3> <p><a target="_blank" rel="nofollow noopener" href="https://triple-underscore.github.io/RFC7231-ja.html#p.HTTP-date">RFC7231</a>では、HTTPレスポンスヘッダで日付と時刻を表現するための書式が決められています。</p> <p><em>実際のレスポンスの例</em></p> <blockquote> <p><strong>Date:</strong> Sun, 14 Jul 2019 15:00:00 GMT<br /> <strong>Expires:</strong> Tue, 13 Aug 2019 15:00:00 GMT<br /> <strong>Last-Modified:</strong> Mon, 03 Oct 2016 15:12:06 GMT</p> </blockquote> <p>この構文は「<strong>IMF-fixdate</strong>」と呼ばれ、次のように決められています。</p> <blockquote> <p><曜日>, <日> <月> <年> <時>:<分>:<秒> GMT</p> </blockquote> <p>IMF-fixdateは「RFC1123-date」とも呼ばれていますが、RFC1123から派生した書式であり、<strong>RFC1123とは異なります</strong>。</p> <p>具体的には次のような違いがあります。</p> <div style="height:15px"></div> <p><em><a target="_blank" rel="nofollow noopener" href="https://wiki.suikawiki.org/n/RFC%201123%E3%81%AE%E6%97%A5%E4%BB%98%E5%BD%A2%E5%BC%8F">RFC1123の日付形式</a> の例</em></p> <blockquote> <p>Sun, 8 Jul 2019 15:00:00 +0000</p> </blockquote> <ul> <li>タイムゾーンは地方時を指定でき、GMTからの時差「+0900」形式で表します。</li> <li>曜日、月は大文字小文字を区別しません。</li> <li>曜日、秒は省略できます。</li> <li>曜日の「,」は省略できます。</li> <li>日は1桁か2桁で表します。</li> <li>連続した空白が許容されています。</li> </ul> <div style="height:15px"></div> <p>RFC1123は全体的にゆるめな印象です。<br /> 対照的なのがIMF-fixdateです。IMF-fixdateは固定長での表現を重視したため、複数の制限があります。</p> <div style="height:15px"></div> <p><em><a target="_blank" rel="nofollow noopener" href="https://wiki.suikawiki.org/n/IMF-fixdate">HTTPの日付形式</a>(IMF-fixdate) の例</em></p> <blockquote> <p>Sun, 08 Jul 2019 15:00:00 GMT</p> </blockquote> <ul> <li>タイムゾーンは必ず「GMT」と書きます。<strong>GMT以外の指定はできません</strong>。</li> <li>曜日、月は<strong>大文字小文字を区別します</strong>。</li> <li>曜日、秒、「,」いずれも<strong>必須です</strong>。</li> <li>日は<strong>2桁でなければなりません</strong>。</li> <li>空白は<code>U+0020</code><strong>1文字だけを入れなければなりません</strong>。</li> </ul> <div style="height:15px"></div> <hr /> <h3 id="本題"><a href="#%E6%9C%AC%E9%A1%8C">本題</a></h3> <p>なぜRFC1123の話をしたのかと言うと、PHPのDateTimeクラスには書式の<a target="_blank" rel="nofollow noopener" href="https://www.php.net/manual/ja/class.datetime.php#datetime.constants.types">定義済み変数が用意されている</a>のですが、その中の「DATE_RFC1123」の書式がHTTPの時間表現と異なるため、利用することができないからです。</p> <p>とは言え、基本構文は同じですので、最後の「+0000」形式を「GMT」に戻せばいいことになります。</p> <p>PHPでは、date()をグリニッジ標準時として利用できる <strong><a target="_blank" rel="nofollow noopener" href="https://www.php.net/manual/ja/function.gmdate.php">gmdate()</a></strong> という関数があります。<br /> これを利用すると、次のようなコードでHTTPの日付形式を表現できます。</p> <pre><code class="php">gmdate('D, d M Y H:i:s T') </code></pre> <p>出力は完全なHTTPの日付形式となります。<br /> gmdate()は第二引数にUnixタイムスタンプを指定することができます。</p> <h3 id="結論"><a href="#%E7%B5%90%E8%AB%96">結論</a></h3> <p>header()とかで表記したいときはこのコードを使ってください。</p> <div style="border:solid 1px #aaa;padding:10px;color:#555;border-radius:4px"> 補足になりますが、HTTPレスポンスヘッダでは歴史的理由によりIMF-fixdateの他にも2種類の書式を使うことができます。 RFC 850の日付形式 「<a target="_blank" rel="nofollow noopener" href="https://wiki.suikawiki.org/n/IMF-fixdate#section-RFC-850-の日時形式" target="_blank">rfc850-date</a>」と、asctime形式「<a target="_blank" rel="nofollow noopener" href="https://wiki.suikawiki.org/n/IMF-fixdate#section-asctime-の日時形式" target="_blank">asctime-date</a>」です。 しかしながら、これらは現在<a target="_blank" rel="nofollow noopener" href="https://wiki.suikawiki.org/n/IMF-fixdate#anchor-116" target="_blank">RFCで禁止</a>されています。「ブラウザは必ず解釈できなければならない」とされているため全てのブラウザが解釈できますが、廃用形式とも言われていますし、使わないほうがいいでしょう。 </div> <p><strong>引用・参考</strong></p> <p>Date - HTTP | MDN<br /> <a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Date">https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Date</a></p> <p>RFC 7231 — HTTP/1.1: Semantics and Content (日本語訳)<br /> <a target="_blank" rel="nofollow noopener" href="https://triple-underscore.github.io/RFC7231-ja.html#http.date">https://triple-underscore.github.io/RFC7231-ja.html#http.date</a></p> <p>rfc1123-date - suikawiki<br /> <a target="_blank" rel="nofollow noopener" href="https://wiki.suikawiki.org/n/rfc1123-date">https://wiki.suikawiki.org/n/rfc1123-date</a></p> <p>PHP: DateTime - Manual<br /> <a target="_blank" rel="nofollow noopener" href="https://www.php.net/manual/ja/class.datetime.php#datetime.constants.types">https://www.php.net/manual/ja/class.datetime.php#datetime.constants.types</a></p> <p>PHP: gmdate - Manual<br /> <a target="_blank" rel="nofollow noopener" href="https://www.php.net/manual/ja/function.gmdate.php">https://www.php.net/manual/ja/function.gmdate.php</a></p> ウラル 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