tag:crieit.net,2005:https://crieit.net/users/barley_ural/feed ウラルの投稿 - Crieit Crieitでユーザーウラルによる最近の投稿 2022-08-31T19:41:17+09:00 https://crieit.net/users/barley_ural/feed tag:crieit.net,2005:PublicArticle/18289 2022-08-31T18:48:03+09:00 2022-08-31T19:41:17+09:00 https://crieit.net/posts/NestJS-Prisma-PlanetScale-REST-API-GraphQL NestJSとPrismaとPlanetScaleでREST APIとGraphQLサーバを作る <h1 id="はじめに"><a href="#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB">はじめに</a></h1> <p>Node.jsをベースとしたAPIを作ろうとしたときに、REST APIとGraphQLを同時に生やしたいと思ったので、その流れについて記事にしようと思います。</p> <h2 id="この記事の目標"><a href="#%E3%81%93%E3%81%AE%E8%A8%98%E4%BA%8B%E3%81%AE%E7%9B%AE%E6%A8%99">この記事の目標</a></h2> <p>NestJSでREST APIとGraphQLが同時に動くサーバを作成する</p> <h2 id="構成"><a href="#%E6%A7%8B%E6%88%90">構成</a></h2> <ul> <li>データベース:PlanetScale</li> <li>ORM:Prisma</li> <li>フレームワーク:NestJS</li> </ul> <h1 id="REST APIの実装"><a href="#REST+API%E3%81%AE%E5%AE%9F%E8%A3%85">REST APIの実装</a></h1> <p>最初にREST APIを作成し、その次にGraphQLを作成します。</p> <h2 id="NestJSでプロジェクトを作成"><a href="#NestJS%E3%81%A7%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%82%92%E4%BD%9C%E6%88%90">NestJSでプロジェクトを作成</a></h2> <p>Prismaの利用を前提としたNestJSの公式チュートリアルがあるので、やっておくと理解しやすいですが、</p> <p><a target="_blank" rel="nofollow noopener" href="https://docs.nestjs.com/recipes/prisma">https://docs.nestjs.com/recipes/prisma</a></p> <p>やや説明不足&実装不足でこのままだと動かないので、以下の記事も参考にすると良いでしょう。</p> <p><a target="_blank" rel="nofollow noopener" href="https://zenn.dev/tossy_yukky/articles/0075f9f0054b39d4ef59">https://zenn.dev/tossy_yukky/articles/0075f9f0054b39d4ef59</a></p> <p>まず、NestJS CLIを使ってプロジェクトを作成します。<br /> わざわざグローバルインストールする必要はないのでnpxで作ります。</p> <pre><code class="sh">npx @nestjs/cli new プロジェクト名 </code></pre> <p>するとプロジェクト名に設定した名前のフォルダが生成されます。<br /> シェル上で、使用したいパケージマネージャを選択するプロンプトが表示されるので、好きなものを選択してください。今回はyarnを選びます。</p> <pre><code class="sh">? Which package manager would you ❤️ to use? (Use arrow keys) npm > yarn pnpm </code></pre> <p>選ぶとインストールがいずれ完了します。<br /> 完了したら、</p> <pre><code class="sh">cd プロジェクト名 yarn start </code></pre> <p>でNestJSが起動することを確認してみましょう。<br /> <a target="_blank" rel="nofollow noopener" href="http://localhost:3000">http://localhost:3000</a> にアクセスすると、<code>Hello World!</code>と表示されているはずです。</p> <p>ちなみに、</p> <pre><code class="sh">yarn start:dev </code></pre> <p>とすると変更監視モードで起動できます。コードの変更をすぐに確認したいときは、こちらで起動し続けると便利です。</p> <h2 id="GitHubにリポジトリを作成"><a href="#GitHub%E3%81%AB%E3%83%AA%E3%83%9D%E3%82%B8%E3%83%88%E3%83%AA%E3%82%92%E4%BD%9C%E6%88%90">GitHubにリポジトリを作成</a></h2> <p>ここまで来たら、GitHubに接続しましょう。<br /> package.jsonの内容を自分に合わせて書き換えて、GitHubにPublishしました。</p> <h2 id="Prismaを導入する"><a href="#Prisma%E3%82%92%E5%B0%8E%E5%85%A5%E3%81%99%E3%82%8B">Prismaを導入する</a></h2> <p>Prisma CLIを開発環境にインストールします。</p> <pre><code class="sh">yarn add -D prisma </code></pre> <p>Prisma Clientをインストールします。</p> <pre><code class="sh">yarn add @prisma/client </code></pre> <p>Prismaを初期化します。</p> <pre><code class="sh">yarn prisma init </code></pre> <p>.envファイルが作られますが、現時点で.gitignoreに<code>.env</code>が指定されていません。<br /> セキュリティ上問題があるので、<code>.env</code>を追加してください。</p> <pre><code class="gitignore">.env </code></pre> <p>ここまで終わったら、データベースを接続していきます。</p> <h2 id="PlanetScaleを用意"><a href="#PlanetScale%E3%82%92%E7%94%A8%E6%84%8F">PlanetScaleを用意</a></h2> <p><a target="_blank" rel="nofollow noopener" href="https://qiita.com/tak001/items/cfbaa9dcb542929ff235">https://qiita.com/tak001/items/cfbaa9dcb542929ff235</a></p> <p>この辺の記事を参考にして、PlanetScaleプロジェクトを作成してください。<br /> 作成できたら、Branchesタブから<code>main</code>ブランチを選択し、Connectボタンを押して、接続情報を表示してください。</p> <p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/218499/682f9e8b-6459-2283-10d1-73f1308f0a68.png" alt="image.png" /></p> <p>「Connect with」でPrismaを選択すると、<code>.env</code>と<code>schema.prisma</code>が表示されます。<br /> <code>.env</code>はプロジェクトルートに、<code>schema.prisma</code>はprismaディレクトリに既に作成されているので、表示された内容でファイルを書き換えてください。</p> <h2 id="Prismaでデータベーステーブルを作成する"><a href="#Prisma%E3%81%A7%E3%83%87%E3%83%BC%E3%82%BF%E3%83%99%E3%83%BC%E3%82%B9%E3%83%86%E3%83%BC%E3%83%96%E3%83%AB%E3%82%92%E4%BD%9C%E6%88%90%E3%81%99%E3%82%8B">Prismaでデータベーステーブルを作成する</a></h2> <p>Prisma Migrateでデータベースのテーブルを作っていきます。<br /> Prismaの公式チュートリアルも参考にしてください。</p> <p><a target="_blank" rel="nofollow noopener" href="https://www.prisma.io/docs/getting-started/setup-prisma/start-from-scratch/relational-databases/using-prisma-migrate-typescript-planetscale">https://www.prisma.io/docs/getting-started/setup-prisma/start-from-scratch/relational-databases/using-prisma-migrate-typescript-planetscale</a></p> <p>先ほど書き換えた<code>schema.prisma</code>に、データベースのデータモデルを追加します。</p> <p>prisma/schema.prisma</p> <pre><code>generator client { provider = "prisma-client-js" previewFeatures = ["referentialIntegrity"] } datasource db { provider = "mysql" url = env("DATABASE_URL") referentialIntegrity = "prisma" } model User { id Int @default(autoincrement()) @id email String @unique name String? posts Post[] } model Post { id Int @default(autoincrement()) @id title String content String? published Boolean? @default(false) author User? @relation(fields: [authorId], references: [id]) authorId Int? } </code></pre> <p>Prismaのスキーマは独自記法なので、意味不明だと思います。以下の公式リファレンスを適宜参照してください。</p> <p><a target="_blank" rel="nofollow noopener" href="https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference">https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference</a></p> <p>簡単な説明だけすると、modelの後に書いたもの(Post、Profile、User)がそれぞれテーブルになります。<br /> 波括弧の中で定義しているのは雑に言えばカラムです。<br /> @〇〇(<code>@id</code>など)となっているものはカラムに対する設定です。後ろに括弧を付けると、引数のようにオプションの値を受け取ることができます。<br /> @@〇〇(<code>@@index</code>など)となっているものはテーブルに対する設定です。<br /> IntやStringとなっている部分はカラムの型です。デフォルトはNOT NULLです。<code>?</code>を付けるとNULLABLEになります。<br /> リレーションを張る場合は、モデル自体を示すもの(例えばauthor)と、それに紐づくidを示すもの(例えばauthorId)が必要です。今回はauthorがUserモデルを指し、設定で<code>@relation(fields: [authorId], references: [id])</code>とすることで、authorIdとUser.idが紐づいていることを表しています。詳しくは以下を参照してください。</p> <p><a target="_blank" rel="nofollow noopener" href="https://www.prisma.io/docs/concepts/components/prisma-schema/relations">https://www.prisma.io/docs/concepts/components/prisma-schema/relations</a></p> <p><code>posts Post[]</code>のような感じで、一対多を表すこともできます。<br /> 多対多については以下を参照してください。</p> <p><a target="_blank" rel="nofollow noopener" href="https://www.prisma.io/docs/concepts/components/prisma-schema/relations/many-to-many-relations">https://www.prisma.io/docs/concepts/components/prisma-schema/relations/many-to-many-relations</a></p> <p>ここまでできたら早速PlanetScaleにテーブル定義を反映させてみましょう。</p> <pre><code class="sh">yarn prisma db push </code></pre> <p>PlanetScaleで<code>main</code>ブランチを選択し、Schemaタブを開くと、先ほど定義したスキーマがSQLに変換されて表示されているはずです。</p> <h2 id="NestJSのサービスを構成"><a href="#NestJS%E3%81%AE%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9%E3%82%92%E6%A7%8B%E6%88%90">NestJSのサービスを構成</a></h2> <p>PrismaとNestJSを繋げるために、<code>src</code>ディレクトリ内に<code>prisma.service.ts</code>を作ってください。</p> <p>src/prisma.service.ts</p> <pre><code>import { INestApplication, Injectable, OnModuleInit } from '@nestjs/common'; import { PrismaClient } from '@prisma/client'; @Injectable() export class PrismaService extends PrismaClient implements OnModuleInit { async onModuleInit() { await this.$connect(); } async enableShutdownHooks(app: INestApplication) { this.$on('beforeExit', async () => { await app.close(); }); } } </code></pre> <p><code>@Injectable()</code>という見慣れない書き方が出てきたと思いますが、これは<strong>デコレータ</strong>と言います。Pythonとかだと馴染みある機能だと思いますが、JavaScriptではまだ実験的な機能のようです。詳しく知りたい方は以下を参照してください。</p> <p><a target="_blank" rel="nofollow noopener" href="https://zenn.dev/miruoon_892/articles/365675fa5343ed">https://zenn.dev/miruoon_892/articles/365675fa5343ed</a></p> <p>NestJSではデコレータを利用した書き方がたくさん出てきます。<br /> デコレータは、後ろに続くメソッドやプロパティをラップできます。そのおかげで、シンプルなコードで強力な機能が使えるようになるわけです。</p> <p>さて、次にモデルごとに便利な操作関数を作りましょう。<br /> <code>src/user.service.ts</code>と<code>src/post.service.ts</code>を作ります。</p> <p>src/user.service.ts</p> <pre><code>import { Injectable } from '@nestjs/common'; import { PrismaService } from './prisma.service'; import { User, Prisma } from '@prisma/client'; @Injectable() export class UserService { constructor(private prisma: PrismaService) {} async user( userWhereUniqueInput: Prisma.UserWhereUniqueInput, ): Promise<User | null> { return this.prisma.user.findUnique({ where: userWhereUniqueInput, include: { posts: true, }, }); } async users(params: { skip?: number; take?: number; cursor?: Prisma.UserWhereUniqueInput; where?: Prisma.UserWhereInput; orderBy?: Prisma.UserOrderByWithRelationInput; }): Promise<User[]> { const { skip, take, cursor, where, orderBy } = params; return this.prisma.user.findMany({ skip, take, cursor, where, orderBy, include: { posts: true, }, }); } async createUser(data: Prisma.UserCreateInput): Promise<User> { return this.prisma.user.create({ data, include: { posts: true, }, }); } async updateUser(params: { where: Prisma.UserWhereUniqueInput; data: Prisma.UserUpdateInput; }): Promise<User> { const { where, data } = params; return this.prisma.user.update({ data, where, }); } async deleteUser(where: Prisma.UserWhereUniqueInput): Promise<User> { return this.prisma.user.delete({ where, }); } } </code></pre> <p>src/post.service.ts</p> <pre><code>import { Injectable } from '@nestjs/common'; import { PrismaService } from './prisma.service'; import { Post, Prisma } from '@prisma/client'; @Injectable() export class PostService { constructor(private prisma: PrismaService) {} async post( postWhereUniqueInput: Prisma.PostWhereUniqueInput, ): Promise<Post | null> { return this.prisma.post.findUnique({ where: postWhereUniqueInput, include: { author: true, }, }); } async posts(params: { skip?: number; take?: number; cursor?: Prisma.PostWhereUniqueInput; where?: Prisma.PostWhereInput; orderBy?: Prisma.PostOrderByWithRelationInput; }): Promise<Post[]> { const { skip, take, cursor, where, orderBy } = params; return this.prisma.post.findMany({ skip, take, cursor, where, orderBy, include: { author: true, }, }); } async createPost(data: Prisma.PostCreateInput): Promise<Post> { return this.prisma.post.create({ data, include: { author: true, }, }); } async updatePost(params: { where: Prisma.PostWhereUniqueInput; data: Prisma.PostUpdateInput; }): Promise<Post> { const { data, where } = params; return this.prisma.post.update({ data, where, include: { author: true, }, }); } async deletePost(where: Prisma.PostWhereUniqueInput): Promise<Post> { return this.prisma.post.delete({ where, }); } } </code></pre> <p><code>include: { posts: true }</code>や<code>include: { author: true }</code>というのは、リレーションクエリの設定です。デフォルトではリレーションするデータを取得することはできないので、明示的に指定する必要があります。<br /> 後述するGraphQLでもこのサービスを再利用するので重要です。</p> <p><a target="_blank" rel="nofollow noopener" href="https://www.prisma.io/docs/concepts/components/prisma-client/relation-queries">https://www.prisma.io/docs/concepts/components/prisma-client/relation-queries</a></p> <p>具体的には、リレーションクエリを指定しないと以下のようなレスポンスになります。(後述する動作確認の段階まで進むと実行できるようになります。)</p> <pre><code class="sh">curl http://localhost:3000/post/1 {"id":1,"title":"titleTest","content":"contentTest","published":true,"authorId":1} </code></pre> <p>リレーションクエリを指定すると以下のようになります。</p> <pre><code class="sh">curl http://localhost:3000/post/1 {"id":1,"title":"titleTest","content":"contentTest","published":true,"authorId":1,"author":{"id":1,"email":"test.jp","name":"namosuke"<span>}</span><span>}</span> </code></pre> <h2 id="NestJSのコントローラを構成"><a href="#NestJS%E3%81%AE%E3%82%B3%E3%83%B3%E3%83%88%E3%83%AD%E3%83%BC%E3%83%A9%E3%82%92%E6%A7%8B%E6%88%90">NestJSのコントローラを構成</a></h2> <p>最後に、コントローラを書いて、APIのエンドポイントと便利関数を繋げます。</p> <p>src/app.controller.ts</p> <pre><code>import { Controller, Get, Param, Post, Body, Put, Delete, } from '@nestjs/common'; import { UserService } from './user.service'; import { PostService } from './post.service'; import { User as UserModel, Post as PostModel } from '@prisma/client'; @Controller() export class AppController { constructor( private readonly userService: UserService, private readonly postService: PostService, ) {} @Get('post/:id') async getPostById(@Param('id') id: string): Promise<PostModel> { return this.postService.post({ id: Number(id) }); } @Get('feed') async getPublishedPosts(): Promise<PostModel[]> { return this.postService.posts({ where: { published: true }, }); } @Get('filtered-posts/:searchString') async getFilteredPosts( @Param('searchString') searchString: string, ): Promise<PostModel[]> { return this.postService.posts({ where: { OR: [ { title: { contains: searchString }, }, { content: { contains: searchString }, }, ], }, }); } @Post('post') async createDraft( @Body() postData: { title: string; content?: string; authorEmail: string }, ): Promise<PostModel> { const { title, content, authorEmail } = postData; return this.postService.createPost({ title, content, author: { connect: { email: authorEmail }, }, }); } @Post('user') async signupUser( @Body() userData: { name?: string; email: string }, ): Promise<UserModel> { return this.userService.createUser(userData); } @Put('publish/:id') async publishPost(@Param('id') id: string): Promise<PostModel> { return this.postService.updatePost({ where: { id: Number(id) }, data: { published: true }, }); } @Delete('post/:id') async deletePost(@Param('id') id: string): Promise<PostModel> { return this.postService.deletePost({ id: Number(id) }); } } </code></pre> <p>GETリクエストの場合は<code>@Get('post/:id')</code>みたいなデコレータでエンドポイントから受け取るパラメータを指定して、<code>@Param('id')</code>に流すみたいなそんな感じですね。<br /> POSTリクエストの場合は<code>@Post</code>から<code>@Body() postData: { title: string; content?: string; authorEmail: string }</code>みたいにして受け取ります。<br /> ちなみに、<code>@Param</code>も<code>@Body</code>も中身はString型になります。</p> <p>公式チュートリアルはここで終わりですが、このままだとエラーが出るので以下のように<code>src/app.module.ts</code>を書き換えて修正します。<br /> <code>providers</code>にサービスを追加しています。</p> <p>src/app.module.ts</p> <pre><code>import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { PrismaService } from './prisma.service'; import { UserService } from './user.service'; import { PostService } from './post.service'; @Module({ imports: [], controllers: [AppController], providers: [AppService, PrismaService, UserService, PostService], }) export class AppModule {} </code></pre> <p>ここまで書けばREST APIの実装は終わりです。</p> <h2 id="REST APIの動作確認"><a href="#REST+API%E3%81%AE%E5%8B%95%E4%BD%9C%E7%A2%BA%E8%AA%8D">REST APIの動作確認</a></h2> <p>動作確認方法は以下に詳しく書かれています。</p> <p><a target="_blank" rel="nofollow noopener" href="https://zenn.dev/tossy_yukky/articles/0075f9f0054b39d4ef59#%E8%B5%B7%E5%8B%95%E3%81%A8%E7%A2%BA%E8%AA%8D">https://zenn.dev/tossy_yukky/articles/0075f9f0054b39d4ef59#%E8%B5%B7%E5%8B%95%E3%81%A8%E7%A2%BA%E8%AA%8D</a></p> <p><code>yarn start</code>したら http://localhost:3000 にサーバが立つので、curlとか使って動作するか試してみてください。</p> <h3 id="実行例"><a href="#%E5%AE%9F%E8%A1%8C%E4%BE%8B">実行例</a></h3> <pre><code class="sh">curl -XPOST -d 'name=test3&[email protected]' http://localhost:3000/user {"id":4,"email":"[email protected]","name":"test3","posts":[]} curl -XPOST -d 'title=niceTitle&content=niceContent&[email protected]' http://localhost:3000/post {"id":3,"title":"niceTitle","content":"niceContent","published":false,"authorId":4,"author":{"id":4,"email":"[email protected]","name":"test3"<span>}</span><span>}</span> curl -XPUT http://localhost:3000/publish/3 {"id":3,"title":"niceTitle","content":"niceContent","published":true,"authorId":4,"author":{"id":4,"email":"[email protected]","name":"test3"<span>}</span><span>}</span> curl http://localhost:3000/feed [{"id":1,"title":"titleTest","content":"contentTest","published":true,"authorId":1,"author":{"id":1,"email":"test.jp","name":"namosuke"<span>}</span><span>}</span>,{"id":2,"title":"はろー","content":"コンテンツ","published":true,"authorId":3,"author":{"id":3,"email":"[email protected]","name":"test"<span>}</span><span>}</span>,{"id":3,"title":"niceTitle","content":"niceContent","published":true,"authorId":4,"author":{"id":4,"email":"[email protected]","name":"test3"<span>}</span><span>}</span>] </code></pre> <h2 id="Prisma Studioの利用"><a href="#Prisma+Studio%E3%81%AE%E5%88%A9%E7%94%A8">Prisma Studioの利用</a></h2> <p>データベースの中身をいじりたいときはPrisma Studioを使うと楽ちんです。機能がシンプルになったphpMyAdminみたいなイメージです。</p> <pre><code class="sh">yarn prisma studio </code></pre> <p>と入力すると、 http://localhost:5555 でPrisma Studioが立ち上がります。</p> <p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/218499/73364eb6-d89c-236b-9c51-5a163923cebd.png" alt="image.png" /></p> <p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/218499/21f5f09e-9b53-4da8-9fae-9cea6a023b46.png" alt="image.png" /></p> <p>データの簡単な追加、修正はここでやれば良さそうです。</p> <h2 id="シードの利用"><a href="#%E3%82%B7%E3%83%BC%E3%83%89%E3%81%AE%E5%88%A9%E7%94%A8">シードの利用</a></h2> <p>開発していると、初期データとして同じレコードを一度に投入したくなることがあります。<br /> 特にPlanetScaleでは<code>main</code>ブランチを<code>production</code>ブランチに統合する際にすべてのレコードが失われるので、必要性が高いでしょう。<br /> そんなときは、Prismaを利用してシードスクリプトを作成すると便利です。</p> <p><a target="_blank" rel="nofollow noopener" href="https://www.prisma.io/docs/guides/database/seed-database">https://www.prisma.io/docs/guides/database/seed-database</a></p> <h2 id="APIドキュメントを自動生成"><a href="#API%E3%83%89%E3%82%AD%E3%83%A5%E3%83%A1%E3%83%B3%E3%83%88%E3%82%92%E8%87%AA%E5%8B%95%E7%94%9F%E6%88%90">APIドキュメントを自動生成</a></h2> <p>NestJSにはOpenAPI形式のドキュメントを扱うフレームワークSwaggerを利用して、APIドキュメントを自動生成してくれる機能があります。<br /> まずは必要なパッケージをインストールしましょう。</p> <pre><code class="sh">yarn add @nestjs/swagger </code></pre> <p>次に、<code>src/main.ts</code>でSwaggerを初期化します。</p> <p>src/main.ts</p> <pre><code>import { NestFactory } from '@nestjs/core'; import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; import { AppModule } from './app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); const config = new DocumentBuilder() .setTitle('ユーザ投稿API') .setDescription('ユーザが投稿できるAPIです') .setVersion('1.0') .build(); const document = SwaggerModule.createDocument(app, config); SwaggerModule.setup('api', app, document); await app.listen(3000); } bootstrap(); </code></pre> <p>あとは<code>yarn start</code>するだけで、 http://localhost:3000/api に自動的にSwaggerのドキュメントページが立ち上がります。<br /> <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/218499/5bed8412-ec44-03f2-0d3c-36a77aec3fc3.png" alt="image.png" /></p> <p>ドキュメント内で実際にAPIを実行してみることもできます。<br /> <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/218499/aa857f9d-0d13-df1c-19ba-ca5aec109d84.png" alt="image.png" /></p> <p>ドメインルートにドキュメントを設置したい場合は</p> <pre><code class="ts">SwaggerModule.setup('api', app, document); </code></pre> <p>となっている部分を</p> <pre><code class="ts">SwaggerModule.setup('', app, document); </code></pre> <p>に変えることで、 http://localhost:3000 で表示できるようになります。</p> <p>ちなみに、OpenAPIのJSONでの定義ファイルは http://localhost:3000/api-json からダウンロードできます。<br /> 同様にYAMLでの定義ファイルは http://localhost:3000/api-yaml からダウンロードできます。<br /> (ドキュメントをドメインルートに設置している場合はそれぞれ http://localhost:3000/-json 、 http://localhost:3000/-yaml からダウンロードできます。)</p> <h1 id="GraphQLの実装"><a href="#GraphQL%E3%81%AE%E5%AE%9F%E8%A3%85">GraphQLの実装</a></h1> <p>GraphQLはREST APIの進化版のようなものです。一つの処理のために何度もAPIを呼んだり、実際にAPIを呼ぶまでレスポンスの形式がわからなかったりといった苦痛を解消してくれます。詳しくは以下を参照してください。</p> <p><a target="_blank" rel="nofollow noopener" href="https://www.apollographql.com/blog/graphql/basics/graphql-vs-rest/">https://www.apollographql.com/blog/graphql/basics/graphql-vs-rest/</a></p> <p>ここからは以下を参考にしていきます。</p> <p><a target="_blank" rel="nofollow noopener" href="https://docs.nestjs.com/graphql/quick-start">https://docs.nestjs.com/graphql/quick-start</a></p> <p><a target="_blank" rel="nofollow noopener" href="https://zenn.dev/rince/articles/50a66241d04f0b">https://zenn.dev/rince/articles/50a66241d04f0b</a></p> <p>必要なパッケージをインストールしていきます。</p> <pre><code class="sh">yarn add @nestjs/graphql @nestjs/apollo graphql apollo-server-express </code></pre> <p>GraphQLの開発では、コードからスキーマを生成する<strong>コードファースト</strong>と、スキーマからコードを生成する<strong>スキーマファースト</strong>という2つのアプローチがあります。<br /> どちらにせよ処理に必要なコードを書かないといけないので、コードファーストのほうが良いと思います。コードファーストで進めます。</p> <h2 id="NestJSのモジュールを構成"><a href="#NestJS%E3%81%AE%E3%83%A2%E3%82%B8%E3%83%A5%E3%83%BC%E3%83%AB%E3%82%92%E6%A7%8B%E6%88%90">NestJSのモジュールを構成</a></h2> <p>AppModuleに色々追加していきます。</p> <p>src/app.module.ts</p> <pre><code>import { Module } from '@nestjs/common'; import { GraphQLModule } from '@nestjs/graphql'; import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { PrismaService } from './prisma.service'; import { UserService } from './user.service'; import { UserResolver } from './user.resolver'; import { PostService } from './post.service'; import { PostResolver } from './post.resolver'; import { join } from 'path'; @Module({ imports: [ GraphQLModule.forRoot<ApolloDriverConfig>({ driver: ApolloDriver, autoSchemaFile: join(process.cwd(), 'src/schema.gql'), }), ], controllers: [AppController], providers: [ AppService, PrismaService, UserService, UserResolver, PostService, PostResolver, ], }) export class AppModule {} </code></pre> <h2 id="NestJSのモデルを構成"><a href="#NestJS%E3%81%AE%E3%83%A2%E3%83%87%E3%83%AB%E3%82%92%E6%A7%8B%E6%88%90">NestJSのモデルを構成</a></h2> <p>続いて、GraphQLのスキーマとして必要な型を設定していきます。<br /> <code>src/user.model.ts</code>と<code>src/post.model.ts</code>を作ります。</p> <p>src/user.model.ts</p> <pre><code>import { Field, ID, ObjectType } from '@nestjs/graphql'; import { Post } from 'src/post.model'; @ObjectType() export class User { @Field((type) => ID) id: number; @Field() email: string; @Field({ nullable: true }) name: string | null; @Field((type) => [Post], { nullable: true }) posts: Post[] | null; } </code></pre> <p>src/post.model.ts</p> <pre><code>import { Field, ID, ObjectType } from '@nestjs/graphql'; import { User } from 'src/user.model'; @ObjectType() export class Post { @Field((type) => ID) id: number; @Field() title: string; @Field({ nullable: true }) content?: string; @Field() published: boolean; @Field((type) => User, { nullable: true }) author?: User; } </code></pre> <p>型を使うために相互に参照し合っているのが面白いですね。<br /> <code>@Field</code>には、曖昧さを無くすためにGraphQLの型を指定できます。例えばTypeScriptの型<code>: number</code>では<code>Int</code>なのか<code>Float</code>なのか<code>ID</code>なのかわからないので、明示的に指定してあげましょう。</p> <p>記述する際には<code>prisma/schema.prisma</code>を見ながら書くと楽です。<code>prisma/schema.prisma</code>からモデルを自動生成してくれる非公式パッケージ(<a target="_blank" rel="nofollow noopener" href="https://www.npmjs.com/package/prisma-nestjs-graphql">prisma-nestjs-graphql</a>)もあるようですが、動作に不安があるので手書きのほうが安心だと思います。</p> <h2 id="NestJSのリゾルバを構成"><a href="#NestJS%E3%81%AE%E3%83%AA%E3%82%BE%E3%83%AB%E3%83%90%E3%82%92%E6%A7%8B%E6%88%90">NestJSのリゾルバを構成</a></h2> <p>いよいよGraphQL版のコントローラみたいなやつ、リゾルバを書いていきます。<br /> ここに書かれたメソッドが、そのままGraphQLから呼び出せるようになります。<br /> <code>src/user.resolver.ts</code>と<code>src/post.resolver.ts</code>を作成します。</p> <p>src/user.resolver.ts</p> <pre><code>import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; import { UserService } from 'src/user.service'; import { User } from './user.model'; @Resolver(() => User) export class UserResolver { constructor(private userService: UserService) {} @Query(() => [User]) async users() { return this.userService.users({}); } @Query(() => User) async user(@Args('id') id: number) { return this.userService.user({ id }); } @Mutation(() => User) async createPost(@Args('email') email: string, @Args('name') name: string) { return this.userService.createUser({ email, name }); } } </code></pre> <p>src/post.resolver.ts</p> <pre><code>import { Args, Mutation, Parent, Query, ResolveField, Resolver, } from '@nestjs/graphql'; import { PostService } from 'src/post.service'; import { UserService } from './user.service'; import { Post } from './post.model'; @Resolver(() => Post) export class PostResolver { constructor( private postService: PostService, private userService: UserService, ) {} @Query(() => [Post]) async posts() { return this.postService.posts({}); } @Query(() => Post) async post(@Args('id') id: number) { return this.postService.post({ id }); } @Mutation(() => Post) async createPost( @Args('title') title: string, @Args('content') content: string, ) { return this.postService.createPost({ title, content }); } @ResolveField() async author(@Parent() post: Post) { return this.userService.user({ id: post.author.id }); } } </code></pre> <p>REST APIを作成するときに作ったサービスをそのまま使っています。<br /> サービスに作った便利関数をREST APIでもGraphQLでも使えるわけですね。</p> <p>ちなみに、<code>@ResolveField()</code>という部分では、入れ子にしてデータを深掘って取得できるフィールドを指定しています。<br /> これが無いと、REST APIで取得できる以上のデータが取得できず、せっかくのGraphQLの強みが活かせません。<br /> 例えば今回は<code>post</code>のリゾルバに<code>author</code>を指定しているので、特定の投稿から著者を取得し、さらに著者の持つ全ての投稿を同時に取得できるようになります。</p> <p><code>@ResolveField()</code>を設定しなかった場合:<br /> <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/218499/428011f8-90ab-d9c5-900a-6e9ab4c5e905.png" alt="image.png" /></p> <p><code>@ResolveField()</code>を設定した場合:<br /> <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/218499/ddca20a3-3043-8354-336a-5095554a8654.png" alt="image.png" /></p> <h2 id="GraphQLの動作確認"><a href="#GraphQL%E3%81%AE%E5%8B%95%E4%BD%9C%E7%A2%BA%E8%AA%8D">GraphQLの動作確認</a></h2> <p>これでおしまい!<br /> <code>yarn start:dev</code>したあとに http://localhost:3000/graphql を開いてplaygroundを確認してみましょう。</p> <p>スキーマを確認したり…<br /> <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/218499/cf3f1c57-b90e-940b-772b-e2cfda55cf65.png" alt="image.png" /></p> <p>自動生成されたdocsを確認したり…<br /> <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/218499/9dc6fee9-adf5-cc85-aefd-daca010e6188.png" alt="image.png" /></p> <p>クエリを投げてみたり…<br /> <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/218499/2cca6608-5ec3-af51-4bad-6f93db7683ce.png" alt="image.png" /></p> <p>大丈夫そうですね!ばっちりです!</p> ウラル tag:crieit.net,2005:PublicArticle/18188 2022-05-12T23:28:44+09:00 2022-05-17T11:34:30+09:00 https://crieit.net/posts/RFC3492-Punycode-IDNA-Unicode-Bootstring [RFC3492日本語訳] Punycode: アプリケーションにおいてドメイン名国際化(IDNA)を行うためのUnicodeのBootstringエンコーディング <h1 id="はじめに"><a href="#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB">はじめに</a></h1> <p>この記事は、RFC3492の日本語訳を転載したものです。Punycodeの仕様を参照する上で、JDNA翻訳版と思われる日本語訳ドキュメントが大いに参考になっていたのですが、2020年末から<a target="_blank" rel="nofollow noopener" href="http://jdna.jp/">http://jdna.jp/</a>へのアクセスができないようで、本ドキュメントも参照できない状態となっていますので、その中身をこちらに転載しています。転載にあたって、一部内容を改変・省略しています。</p> <p>RFC3492原文:<a target="_blank" rel="nofollow noopener" href="https://datatracker.ietf.org/doc/html/rfc3492">https://datatracker.ietf.org/doc/html/rfc3492</a><br /> 日本語訳(転載元Webアーカイブ):<a target="_blank" rel="nofollow noopener" href="https://web.archive.org/web/20201019220316/http://jdna.jp/survey/rfc/rfc3492j.html">https://web.archive.org/web/20201019220316/http://jdna.jp/survey/rfc/rfc3492j.html</a></p> <h1 id="Punycode: アプリケーションにおいてドメイン名国際化(IDNA)を行うためのUnicodeのBootstringエンコーディング"><a href="#Punycode%3A+%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%8A%E3%81%84%E3%81%A6%E3%83%89%E3%83%A1%E3%82%A4%E3%83%B3%E5%90%8D%E5%9B%BD%E9%9A%9B%E5%8C%96%28IDNA%29%E3%82%92%E8%A1%8C%E3%81%86%E3%81%9F%E3%82%81%E3%81%AEUnicode%E3%81%AEBootstring%E3%82%A8%E3%83%B3%E3%82%B3%E3%83%BC%E3%83%87%E3%82%A3%E3%83%B3%E3%82%B0">Punycode: アプリケーションにおいてドメイン名国際化(IDNA)を行うためのUnicodeのBootstringエンコーディング</a></h1> <p>ネットワークワーキンググループ A. Costello<br /> Request for Comments: 3492 Univ. of California, Berkeley<br /> 分類: 標準化過程<em>(Standards Track)</em> 2003年3月</p> <h2 id="本文書の位置づけ"><a href="#%E6%9C%AC%E6%96%87%E6%9B%B8%E3%81%AE%E4%BD%8D%E7%BD%AE%E3%81%A5%E3%81%91">本文書の位置づけ</a></h2> <p>本文書はインターネットコミュニティーのためにインターネット標準化過程にあるプロトコルを規定し、その向上のために議論と提案を求めるものである。このプロトコルの標準化の状況については"Internet Official Protocol Standards"(STD 1)の最新版を参照のこと。本文書の配布は制限されない。</p> <h2 id="著作権表示"><a href="#%E8%91%97%E4%BD%9C%E6%A8%A9%E8%A1%A8%E7%A4%BA">著作権表示</a></h2> <p>Copyright (C) The Internet Society (2003). All Rights Reserved.</p> <h2 id="要旨"><a href="#%E8%A6%81%E6%97%A8">要旨</a></h2> <p>Punycodeは、単純で効率的な転送用エンコーディング<em>(transfer encoding)</em>記法であり、IDNA (Internationalized Domain Names in Applications)と共に使用されるように設計されている。PunycodeはUnicode文字列を、一意かつ可逆的にASCII文字列に変換する。ASCII文字は、そのまま文字として表現され、非ASCII文字はホスト名ラベルで使用可能なASCII文字(文字、数字、ハイフン)で表現される。本文書は、Bootstringと呼ぶ一般的アルゴリズムを定義する。本アルゴリズムは、より大きな文字集合に属するコードポイントの並びを、基本コードポイントの並びによって一意に表現することを可能にする。Punycodeは、IDNA向けに本文書で規定したパラメーター値を使用する、Bootstringの適用事例<em>(instance)</em>である。</p> <h2 id="1. はじめに"><a href="#1.+%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB">1. はじめに</a></h2> <p>IDNAは国際化ドメイン名をサポートするためのアーキテクチャーを記述している。これは非ASCII文字を含むラベルを、特別なACEプレフィックスで始まるASCII文字だけで構成されたACEラベルによって表現可能にするものである。プレフィックスの後に続けられたラベルの残りは、ある制約条件を満たすようにPunycodeでエンコードされたUnicode文字列である。プレフィックスと制約条件の詳細については、IDNAとNAMEPREPを参照のこと。</p> <p>Punycodeは、Bootstringと呼ばれる、より一般的なアルゴリズムの適用事例<em>(instance)</em>である。このアルゴリズムは、より大きな文字集合に属するコードポイントの並びを、小さな"基本<em>(basic)</em>"コードポイントの集合に含まれるコードポイントの並びによって一意に表現可能にするものである。Punycodeは、Bootstringの特定のパラメーター値をIDNA向けに適切に設定したものである。</p> <h2 id="1.1 機能的特徴"><a href="#1.1+%E6%A9%9F%E8%83%BD%E7%9A%84%E7%89%B9%E5%BE%B4">1.1 機能的特徴</a></h2> <p>Bootstringは、以下の機能的特徴を持つように設計された。</p> <ul> <li><p>完全性: 拡張文字列(任意のコードポイントの並び)は、基本文字列(基本コードポイントの並び)によって表現できる。許容される文字列、文字列の長さ等の制約は、上位層で導入可能である。</p></li> <li><p>一意性: 与えられた拡張文字列を表現する基本文字列は一つしか存在しない。</p></li> <li><p>可逆性: 任意の拡張文字列を基本文字列に変換した場合、その基本文字列から変換前の拡張文字列を再生可能である。</p></li> <li><p>効率的エンコーディング: 拡張文字列の長さと、それを表現する基本文字列の長さの比率は小さい。処理対象がドメイン名であることを考慮すると、これは重要な意味を持つ。なぜなら、RFC1034はドメインラベルを63文字に制限しているからである。</p></li> <li><p>単純性: エンコーディングとデコーディングのアルゴリズムは、実装には充分に単純なものである。効率性と単純性のゴールは二律背反するものだが、Bootstringはその間で優れたバランスを取ることを目標としている。</p></li> <li><p>読みやすさ: 拡張文字列に含まれる基本コードポイントは、変換後の基本文字列においてもそのまま表現される。(主たる意図は効率を改善することであり、読みやすさを改善することではないにしても)。</p></li> </ul> <p>PunycodeはIDNAのToASCII処理とToUnicode処理で使用されない付加的機能もサポート可能である。エンコーディング前に拡張文字列の大文字、小文字の文字種が統一されている<em>(case-folded)</em>場合、文字種が統一された文字列をどのように文字種混在の文字列に変換したかを示すために、基本文字列は大文字と小文字が混在した文字列を使用することができる。<a href="#A.%20文字種注釈">付録A "文字種注釈<em>(Mixed-case annotation)</em>"</a>を参照のこと。</p> <h2 id="1.2 プロトコルとの関係"><a href="#1.2+%E3%83%97%E3%83%AD%E3%83%88%E3%82%B3%E3%83%AB%E3%81%A8%E3%81%AE%E9%96%A2%E4%BF%82">1.2 プロトコルとの関係</a></h2> <p>PunycodeはドメインラベルをASCIIに変換するために、IDNAプロトコルで使用されるものであり、他の目的のためには設計されていない。任意の文章を処理するために設計されたものではないことをここに明記する。</p> <h2 id="2. 用語"><a href="#2.+%E7%94%A8%E8%AA%9E">2. 用語</a></h2> <p>本文書におけるキーワード"しなければならない<em>(MUST)</em>"、"してはならない<em>(MUST NOT)</em>"、"要求される<em>(REQUIRED)</em>"、"するものとする<em>(SHALL)</em>"、"しないものとする<em>(SHALL NOT)</em>"、"すべきである<em>(SHOULD)</em>"、"すべきでない<em>(SHOULD NOT)</em>"、"推奨される<em>(RECOMMENDED)</em>"、"してもよい<em>(MAY)</em>"、"任意である<em>(OPTIONAL)</em>"は、BCP14、RFC2119に記述されているとおりに解釈される。</p> <p>コードポイントは符号化文字集合<em>(coded character set)</em>において、文字に関連付けられる整数値<em>(integral value)</em>である。</p> <p>Unicode標準に記述されているとおり、Unicodeコードポイントは、"U+"に4桁の16進数を続けることによって表現される。また、コードポイントの範囲は、プレフィックス無しで2つの16進数を".."で分割した表現形式を使用する。</p> <p>演算子divとmodは整数の割り算を実行する。<code>(x div y)</code>はxをyで割った商で、余りは捨てられる。また、<code>(x mod y)</code>はxをyで割った余りである。したがって、<code>(x div y) * y + (x mod y) == x</code> となる。Bootstringはこれらの演算子を負でない演算数に対してだけ使用する。したがって、商と余りは常に負でない値を持つ。</p> <p>break構文は、(C言語のように)一番深い(内側の)ループから脱出するものである。</p> <p>オーバーフローは、整数を保存する変数の最大値を超える値を計算しようとする試みである。</p> <h2 id="3. Bootstringの説明"><a href="#3.+Bootstring%E3%81%AE%E8%AA%AC%E6%98%8E">3. Bootstringの説明</a></h2> <p>Bootstringは、任意のコードポイントの並び("拡張文字列")を、基本コードポイントの並び("基本文字列")として表現する。本セクションは、この表現形式について記述する。<a href="#6.%20Bootstringアルゴリズム">セクション6"Bootstringアルゴリズム"</a>では、このアルゴリズムを擬似的なコードとして提示する。<a href="#7.2%20デコーディング処理の検証">セクション7.2"デコーディング処理の検証"</a>と<a target="_blank" rel="nofollow noopener" href="7.3%20エンコーディング処理の検証">7.3"エンコーディング処理の検証"</a>では、入力例に対するアルゴリズムの処理を検証する。</p> <p>以下のセクションでは、Bootstringが使用する4つの手法を記述する。"基本コードポイントの分離<em>(segregation)</em>"は、拡張文字列に含まれる基本コードポイントを対象とする、非常に単純で効果的なエンコーディングである。これらは、単純に全てが直ちにコピーされる。"挿入による整列復元コーディング<em>(insertion unsort coding)</em>"は、非基本コード<em>(non-basic code point)</em>をdelta(*1)としてエンコードする。その際に、コードポイントの処理は、入力されるコードポイントの並びの順番ではなく、コードポイントの値が数値的に小さいものから順に処理される。この結果、一般的にdeltaはより小さくなる。deltaは、"一般化可変長整数<em>(generalized variable-length integer)</em>"として表現される。これは負でない整数を表現するために、基本コードポイントを使用する。この整数表現のパラメーターは、続くdeltaが同様な大きさ<em>(magnitude)</em>を持つ場合には、効率を改善するために、"bias補正<em>(bias adaptation)</em>"を使用して動的に調整される。</p> <blockquote> <p>《脚注》<br /> *1: delta<br /> 原文に含まれる幾つかの語、delta, bias, tmin, tmax, damp は、後に擬似コード内でそのまま扱われるため、原文のままとした。</p> </blockquote> <h2 id="3.1 基本コードポイントの分離"><a href="#3.1+%E5%9F%BA%E6%9C%AC%E3%82%B3%E3%83%BC%E3%83%89%E3%83%9D%E3%82%A4%E3%83%B3%E3%83%88%E3%81%AE%E5%88%86%E9%9B%A2">3.1 基本コードポイントの分離</a></h2> <p>拡張文字列に含まれる基本コードポイントは、全てそのままの文字として原文どおりの順序で基本文字列の先頭にコピーされる。基本コードポイントの数がゼロでない場合(に限っては)、区切り文字<em>(delimiter)</em>が続けられる。区切り文字は、残りの基本文字列には決して現れない特定の基本コードポイントである。したがって、デコーダーは、区切り文字が存在する場合、最後の区切り文字を探すことにより、そのまま文字として表現されている部分の終端部を見つけることができる。</p> <h2 id="3.2 挿入による整列復元コーディング"><a href="#3.2+%E6%8C%BF%E5%85%A5%E3%81%AB%E3%82%88%E3%82%8B%E6%95%B4%E5%88%97%E5%BE%A9%E5%85%83%E3%82%B3%E3%83%BC%E3%83%87%E3%82%A3%E3%83%B3%E3%82%B0">3.2 挿入による整列復元コーディング</a></h2> <p>基本文字列の残り部分(区切り文字がある場合には、最後の区切り文字よりも後ろの部分)は、<a href="#3.3%20一般化可変長整数">セクション3.3</a>に記述されるとおり、一般化可変長整数として表現された、負でない整数のdeltaの並びである。deltaの意味は、デコーダーの観点から考えるのが最も理解しやすい。</p> <p>デコーダーは拡張文字列を徐々に構築する。初めは、拡張文字列は基本文字列のうち、そのまま文字として使用されている部分をコピーしたものである(ただし、最後の区切り文字は除かれる)。デコーダーは非基本コードポイントを各delta毎に拡張文字列に挿入していき、最終的にはデコードされた文字列に到達する。</p> <p>この処理の主要な部分は、2つの状態変数インデックスiとカウンターnを持つ状態マシン<em>(state machine)</em>である。インデックスiは拡張文字列における位置を示し、0(最初の位置)から拡張文字列の現在の長さ(現在の終端を越えた位置に存在する仮想的な位置)の範囲を採る。現在の状態が<code><n,i></code>である場合、iが拡張文字列長よりも短かければ次の状態は<code><n,i+1></code>となり、iが拡張文字列長と等しければ、次の状態は<code><n+1,0></code>となる。言い換えると、各状態の変化はiを増加<em>(increment)</em>させ、必要に応じてゼロに周回する。nは周回した回数をカウントするものである。</p> <p>状態は常に単調に進むことに注意してもらいたい(デコーダーには現在よりも前の状態に戻す方法は存在しない)。各状態において、挿入処理は行われるか行われないかのどちらかである。任意の状態において行われる挿入処理は、多くとも1回である。挿入処理は、拡張文字列のiの位置に、値nを挿入する。deltaはこの一連のイベントの連続長<em>(run-length)</em>エンコーディングである。つまり、deltaは挿入処理後の状態に先立つ挿入処理前の状態の連続の長さである。したがって、各delta毎に、デコーダーはdelta状態の変更を実行し、挿入処理を行った後に、もう1度状態の変更を行う。(実装は各状態の変更を個別に実行する必要はなく、代わりに割り算を使用し、余りの計算を行い、次の挿入状態を直接演算することができる)。挿入されたコードポイントが基本コードポイントの場合、エラーとなる。(<a href="#3.1%20基本コードポイントの分離">セクション3.1</a>に記述されるとおり、基本コードポイントは分離されているはずだからである)。</p> <p>エンコーダーの主な仕事は、期待する文字列をデコーダーに構築させるdeltaの並びを導出することである。この目標は、拡張文字列を走査してデコーダーが挿入しなければならない次のコードポイントを順次探していき、デコーダーが実行しなければならない状態変更の回数をカウントすることによって達成される。デコーダーから出力される拡張文字列は、挿入処理がされたコードポイントだけしか含まないという事実を心に留めておいてもらいたい。<a href="#6.3%20エンコーディング処理">セクション6.3 "エンコーディング処理"</a>で詳細なアルゴリズムを示す。</p> <h2 id="3.3 一般化可変長整数"><a href="#3.3+%E4%B8%80%E8%88%AC%E5%8C%96%E5%8F%AF%E5%A4%89%E9%95%B7%E6%95%B4%E6%95%B0">3.3 一般化可変長整数</a></h2> <p>従来の整数表現では、base(基数)は数字を識別するために使用できる記号の数であり、記号に対応する値は0からbase-1までである。ここで、digit_0が最も低い桁の値を示し、digit_1が次に低い桁の値、以下同様であるとする。すると、表現される値は、<code>digit_j * w(j)</code> の和となる。ただし、<code>w(j) = base^j</code>は、jの桁の位置に対する重み(倍率)である。例えば、基数8の整数437の場合、桁の値はそれぞれ7, 3, 4であり、重みはそれぞれ1, 8, 64である。したがって、表現される値は<code>7 + 3*8 + 4*64 = 287</code> となる。この表現方法には2つの欠点がある。1つめの欠点は、各値に対して複数のエンコーディングが存在することである。(なぜなら、最上位の桁に余分なゼロを追加できるからである)。これは一意なエンコーディングが必要とされる場合には不便である。2つめの欠点は、整数はそれ自身で区切り文字を持たないことである。このため、複数の整数を続けて記述すると、2つの整数の境界はわからなくなる。</p> <p>一般化可変長表現は、この2つの問題を解決する。桁の値は依然として0からbase-1の範囲だが、この整数はしきい値t(j)によって境界が設定される。各桁それぞれについて、しきい値t(j)は0からbase-1の範囲で設定される。そして、最上位の一桁だけが<code>digit_j < t(j)</code>を必ず満たす。したがって、幾つかの整数が続けて記述されている場合でも、それらを分離することは容易である。リトルエンディアン(最下位桁が先頭にくる)の場合は1桁目から、ビッグエンディアン(最上位桁が先頭に来る)の場合は最後の桁から分離処理を開始すればよい。 既に述べたとおり、値は<code>digit_j * w(j)</code> の和であるが、以下に示すとおり重みが異なる。</p> <pre><code>w(0) = 1 w(j) = w(j-1) * (base - t(j-1)) j > 0の場合 </code></pre> <p>例として、リトルエンディアンのbase 8の数の並び734251...を考える。ここで、しきい値はそれぞれ2, 3, 5, 5, 5, 5, ...であるとする。重みは、それぞれ<code>1</code>, <code>1*(8-2) = 6</code>, <code>6*(8-3) = 30,</code> <code>30*(8-5) = 90</code>, <code>90*(8-5) = 270</code>, 以下同様である。ここで、7は2よりも大きく、3は3より小さくはない。しかし4は5よりも小さいことから、4が最後の数字であることがわかる。734が表現する値は、<code>7*1 + 3*6 + 4*30 = 145</code>である。次の整数は251で、その値は<code>2*1 + 5*6 + 1*30 = 62</code>である。この表現のデコーディングは、従来の整数のデコーディングと極めて似ている。現在の値N = 0、重み w = 1から開始する。次の桁の数dを取り込み、Nを <code>d * w</code> だけ増加させる。dが現在のしきい値(t)よりも小さければ処理を終え、そうでない場合はwを<code>(base - t)</code>の係数で増加させ、次の桁の処理に向けてtを更新する。以後、それを繰り返す。</p> <p>この表現のエンコーディングは従来の整数のエンコーディングと同様である。N < tの場合、Nに対する1桁の数を出力し、処理を終える。そうでない場合、<code>t + ((N - t) mod (base - t))</code>に対する桁の数を出力し、Nを<code>(N - t) div (base - t)</code>に置き換え、次の桁の計算に向けてtを更新するといった処理を繰り返す。</p> <p>t(j)の値が採る範囲がどのようなものであるとしても、負でない整数値それぞれに対して、一般化可変長表現が1つだけ存在する。</p> <p>Bootstringは、deltaを先頭から分割できるように、リトルエンディアンの順序づけを使用する。t(j)の値は、定数base, tmin, tmaxと、biasと呼ばれる状態変数を使用して、以下のように定義される。</p> <pre><code>t(j) = base * (j + 1) - bias 値はtminからtmaxの範囲に制限される。 </code></pre> <p>値を制限するとは、与式がtminより小さい値またはtmaxよりも大きい値を導出した場合、それぞれt(j) = tminまたはtmaxとなることを意味する。(<a href="#6.%20Bootstringアルゴリズム">セクション6 "Bootstringアルゴリズム"</a>で提示する擬似コードでは、パフォーマンス向上のために数式 <code>base * (j + 1)</code>をkと記述している)。このようなt(j)の値は、整数の表現をbiasによって決定される特定の範囲内に納める際に有効に作用する。</p> <h2 id="3.4 bias補正"><a href="#3.4+bias%E8%A3%9C%E6%AD%A3">3.4 bias補正</a></h2> <p>各deltaがエンコードまたはデコードされた後に、次のdeltaに対するbiasを以下のように設定する。</p> <ol> <li><p>次の処理段階におけるオーバーフローを回避するために、deltaを縮小させる。</p> <pre><code>let delta = delta div 2 </code></pre> <p>deltaが最初のdeltaである場合には、除数は2ではなく、代わりにdampと呼ばれる定数を使用する。これにより、2番目に生成されるdeltaは、通常最初のdeltaよりも極めて小さくなるという事実を補正する。</p></li> <li><p>次のdeltaは、より長い文字列に挿入されるという事実にあわせて、deltaを増加させる。</p> <pre><code>let delta = delta + (delta div numpoints) </code></pre> <p>numpointsとは、ここまでの段階でエンコード/デコードされたコードポイントの数である。(このdeltaに対応するものと、基本コードポイントを含む)。</p></li> <li><p>deltaは、次のdeltaを表現するために必要な最小桁数を予測するために、しきい値内に収まるまで繰り返し除算される。</p> <pre><code>while delta > ((base - tmin) * tmax) div 2 do let delta = delta div (base - tmin) </code></pre></li> <li><p>biasの設定</p> <pre><code>let bias = (base * 処理段階3で実行された割り算の回数) + (((base - tmin + 1) * delta) div (delta + skew)) </code></pre> <p>この処理の目指すものは、現在のdeltaが次のdeltaの大まかなサイズのヒントを提供するため、最上位桁である可能性がより高い桁の数値に対するt(j)をtmaxに設定し、最下位桁から上位3桁目である可能性が高い桁まではt(j)をtminに設定する。最後に最上位桁から2番目であると期待される桁に対してはt(j)をtminとtmaxの間の値に設定することである。(これは最上位桁であると予想される桁が不要になるという希望と、その桁が最上位桁ではないという危険をバランスさせるということである)。</p></li> </ol> <h2 id="4. Bootstringのパラメーター"><a href="#4.+Bootstring%E3%81%AE%E3%83%91%E3%83%A9%E3%83%A1%E3%83%BC%E3%82%BF%E3%83%BC">4. Bootstringのパラメーター</a></h2> <p>任意の基本コードポイントの集合に対して、区切り文字が1つ指定されなければならない。baseは残りの識別可能な基本コードポイント数よりも大きくてはいけない。0からbase-1の範囲で桁に表示される値は、それぞれ個別に、区切り文字ではない基本コードポイントに関連付けられなければならない。幾つかの場合においては、複数のコードポイントが同じ桁表示値を持つ必要がある。例えば、基本文字列が大文字小文字を区別しない<em>(case-insensitive)</em>場合、同じ文字の大文字と小文字は等しいものとして扱われる必要がある。</p> <p>nの初期値は、拡張文字列内に現れ得る非基本コードポイントの最小値よりも大きくてはいけない。</p> <p>残りの5つのパラメーター(tmin, tmax, skew, damp, biasの初期値)は以下の制約を満たさなければならない。</p> <pre><code>0 <= tmin <= tmax <= base-1 skew >= 1 damp >= 2 initial_bias mod base <= base - tmin </code></pre> <p>提示された制約が満たされていれば、これら5つのパラメーターは効率に影響を与えるが、正確さには影響を与えない。これらは経験的に最適な値が定められる。</p> <p>文字種注釈のサポートが求められる場合(<a href="#A.%20文字種注釈">付録A</a>参照)、0からtmax-1までに対応するコードポイントが全て大文字と小文字の形式を持っていることを確認すること。</p> <h2 id="5. Punycode用のパラメーター値"><a href="#5.+Punycode%E7%94%A8%E3%81%AE%E3%83%91%E3%83%A9%E3%83%A1%E3%83%BC%E3%82%BF%E3%83%BC%E5%80%A4">5. Punycode用のパラメーター値</a></h2> <p>Punycodeは以下のBootstringパラメーター値を使用する。</p> <pre><code>base = 36 tmin = 1 tmax = 26 skew = 38 damp = 700 initial_bias = 72 initial_n = 128 = 0x80 </code></pre> <p>Punycodeに入力する整数は負でないものでなければならないという制約が付加されてはいるが、これらのパラメーターは0..10FFFFの範囲の整数を採るUnicodeコードポイントに対して特にうまく動作するように設計されている。(しかし、UnicodeのUTF-16エンコーディングが使用するために予約されているD800..DFFFは対象ではない)。</p> <p>基本コードポイントはASCIIコードポイント(0..7F)であり、そのうちU+002D(-)は区切り文字である。また、他のコードポイントの幾つかは以下に示すように桁表示値を持つ。</p> <pre><code>コードポイント 桁表示値 ------------ --------------------------- 41..5A (A-Z) = それぞれ0から25に対応 61..7A (a-z) = それぞれ0から25に対応 30..39 (0-9) = それぞれ26から35に対応 </code></pre> <p>ハイフン(マイナス)を区切り文字として使用するということは、Unicode文字列が全て基本コードポイントで構成されている場合にはエンコードされた文字列はハイフン(マイナス)で終わっても良いことを意味する。しかし、IDNAはそのような文字列にエンコードされることを禁止している。エンコードされた文字列はハイフン(マイナス)で始まってもよいが、IDNAは先頭にプレフィックスを追加する。以上の結果、Punycodeを使用するIDNAは、ホスト名ラベルがハイフン(マイナス)で開始、終了しないというRFC952のルールに準拠する。</p> <p>デコーダーは、ある文字について大文字と小文字の形態を(大文字と小文字が混在している状態も含めて)認識できなければならない<em>(MUST)</em>。エンコーダーは、文字種注釈(<a href="#A.%20文字種注釈">付録A</a>参照)を使用していない限り、出力を大文字または小文字だけの形態にすべきである<em>(SHOULD)</em>。</p> <p>おそらくは、ほとんどのユーザーはエンコードされた文字列を直接書いたりタイプしたりはしないだろう(むしろカット&ペーストをするだろうから)。しかし、直接書いたりタイプしたりするユーザーに対しては、以下に示すように視覚的にどちらとも取れる可能性がある文字の集合があることを警告する必要がある。</p> <pre><code>G 6 I l 1 O 0 S 5 U V Z 2 </code></pre> <p>このような不明瞭さは通常文脈から判断して解決される。しかし、Punycodeでエンコードされた文字列の場合、そのような文脈は人間に対して何も示されない。</p> <h2 id="6. Bootstringアルゴリズム"><a href="#6.+Bootstring%E3%82%A2%E3%83%AB%E3%82%B4%E3%83%AA%E3%82%BA%E3%83%A0">6. Bootstringアルゴリズム</a></h2> <p>擬似コードの幾つかの部分は、パラメーターが(Punycodeに適した)ある条件を満たす場合には省略できる。省略可能な部分は{括弧}でくくられており、省略可能な条件の説明が、擬似コードのすぐ後ろに記載される。</p> <p>形式上、コードポイントは整数であるため、擬似コードはコードポイントに対して直接算術的処理が実行可能であると想定している。幾つかのプログラミング言語では、コードポイントと整数を明示的に変換する必要があるかもしれない。</p> <h2 id="6.1 bias補正関数"><a href="#6.1+bias%E8%A3%9C%E6%AD%A3%E9%96%A2%E6%95%B0">6.1 bias補正関数</a></h2> <pre><code>function adapt(delta,numpoints,firsttime): if firsttime then let delta = delta div damp else let delta = delta div 2 let delta = delta + (delta div numpoints) let k = 0 while delta > ((base - tmin) * tmax) div 2 do begin let delta = delta div (base - tmin) let k = k + base end return k + (((base - tmin + 1) * delta) div (delta + skew)) </code></pre> <p>adapt()内部におけるdeltaとkの値変更が、エンコーディング/デコーディング処理の内部で使用されている同名の変数に影響を与えるかについては気にしなくてよい。adapt()を呼び出した後に、呼び出した処理<em>(caller)</em>はそれらの変数の値を読み出すことなく、上書きするからである。</p> <h2 id="6.2 デコーディング処理"><a href="#6.2+%E3%83%87%E3%82%B3%E3%83%BC%E3%83%87%E3%82%A3%E3%83%B3%E3%82%B0%E5%87%A6%E7%90%86">6.2 デコーディング処理</a></h2> <pre><code>let n = initial_n let i = 0 let bias = initial_bias let output = an empty string indexed from 0 consume all code points before the last delimiter (if there is one) and copy them to output, fail on any non-basic code point if more than zero code points were consumed then consume one more (which will be the last delimiter) while the input is not exhausted do begin let oldi = i let w = 1 for k = base to infinity in steps of base do begin consume a code point, or fail if there was none to consume let digit = the code point's digit-value, fail if it has none let i = i + digit * w, fail on overflow let t = tmin if k <= bias {+ tmin}, or tmax if k >= bias + tmax, or k - bias otherwise if digit < t then break let w = w * (base - t), fail on overflow end let bias = adapt(i - oldi, length(output) + 1, test oldi is 0?) let n = n + i div (length(output) + 1), fail on overflow let i = i mod (length(output) + 1) {if n is a basic code point then fail} insert n into output at position i increment i end </code></pre> <p>(nが基本コードポイントであるか検査している){}括弧に囲まれた記述は、initial_nが全ての基本コードポイントよりも大きい場合(Punycodeではこれは正しい)省略可能である。なぜなら、nは決してinitial_nより小さくはならないからである。</p> <p>tの値がtminからtmaxの範囲に制限される条件下では、tの割り当ての際に{+tmin}部分は常に省略可能である。tの値を制限することにより、bias < k < bias + tmin の場合に制約計算が正しくなくなるが、bias計算方法とパラメーターの制約により、そのような事態は発生しない。</p> <p>デコーダーの状態は単調にしか進展しないため、任意のdeltaに対して1つだけしか表現が存在しない。したがって、与えられた整数の並びを表現可能なエンコードされた文字列は1つしか存在しない。エラー条件は、コードポイントが無効な場合、予期しない入力終了の発生、オーバーフロー発生、基本コードポイントをそのまま文字として扱わずにdeltaを使用してエンコードした場合等である。これらのエラーによってデコーダーの処理が失敗するため、異なる2つの入力から同じ出力を生成することはできない。</p> <p>この性質が無ければ、エンコーディングの一意性を保証するために、出力を再度エンコードし、入力と一致するかを検証する必要があっただろう。</p> <h2 id="6.3 エンコーディング処理"><a href="#6.3+%E3%82%A8%E3%83%B3%E3%82%B3%E3%83%BC%E3%83%87%E3%82%A3%E3%83%B3%E3%82%B0%E5%87%A6%E7%90%86">6.3 エンコーディング処理</a></h2> <pre><code>let n = initial_n let delta = 0 let bias = initial_bias let h = b = the number of basic code points in the input copy them to the output in order, followed by a delimiter if b > 0 {if the input contains a non-basic code point < n then fail} while h < length(input) do begin let m = the minimum {non-basic} code point >= n in the input let delta = delta + (m - n) * (h + 1), fail on overflow let n = m for each code point c in the input (in order) do begin if c < n {or c is basic} then increment delta, fail on overflow if c == n then begin let q = delta for k = base to infinity in steps of base do begin let t = tmin if k <= bias {+ tmin}, or tmax if k >= bias + tmax, or k - bias otherwise if q < t then break output the code point for digit t + ((q - t) mod (base - t)) let q = (q - t) div (base - t) end output the code point for digit q let bias = adapt(delta, h + 1, test h equals b?) let delta = 0 increment h end end increment delta and n end </code></pre> <p>(入力がnより小さい非基本コードポイントを含むか検査している){}括弧に囲まれた記述は、nより小さい全てのコードポイントが基本コードポイントである場合(Punycodeでは、コードポイントが割り当てられていなければこれは正しい)には省略可能である。</p> <p>{}括弧に囲まれている条件 "non-basic" と "or c is basic" 部分は、intial_nが全ての基本コードポイントより大きい場合(Punycodeではこれは正しい)省略可能である。なぜなら、検査されるコードポイントは、決してinitial_nよりも小さくはならないからである。</p> <p>tの値がtminからtmaxの範囲に制限される条件下では、tの割り当ての際に{+tmin}部分は常に省略可能である。tの値を制限することにより、bias < k < bias + tmin の場合に制約計算が正しくなくなるが、bias計算方法とパラメーターの制約により、そのような事態は発生しない。</p> <p>入力に非常に大きな値が含まれている場合または入力が非常に長い場合に、無効な出力の生成を回避するために、オーバーフローの検査が必要である。</p> <p>外側のループの最後にあるdeltaの増加(increment)処理はオーバーフローしない。なぜなら、deltaを増加させるまでは <code>delta < length(input)</code> であり、length(input)は常に表現可能(representable)であると想定されるからである。nの増加処理はオーバーフローする可能性があるが、それは <code>h == length(input)</code>の場合だけである。nがオーバーフローした場合、処理は終了となる。</p> <h2 id="6.4 オーバーフローの処理"><a href="#6.4+%E3%82%AA%E3%83%BC%E3%83%90%E3%83%BC%E3%83%95%E3%83%AD%E3%83%BC%E3%81%AE%E5%87%A6%E7%90%86">6.4 オーバーフローの処理</a></h2> <p>IDNAにおいて、有効なIDNAラベル全てをオーバーフロー無しで処理するには26ビットの符号無し<em>(unsigned)</em>整数があれば充分である。27ビットのdeltaを必要とする文字列は、コードポイントの限界(0..10FFFF)を越えるか、ラベル長の限界(63文字)を越えるかのどちらかとなるからである。しかし、入力は必ずしも有効なIDNAラベルではないため、オーバーフローの処理は必要である。</p> <p>プログラミング言語がオーバーフロー検知を提供しない場合、以下に述べる手法を使用することができる。A、B、Cは表現可能な負でない整数で、Cはゼロでないとする。その場合、<code>B > maxint - A</code>の場合にだけA+Bはオーバーフローする。また、<code>B > (maxint - A) div C</code>の場合にだけ<code>A + (B * C)</code>はオーバーフローする。ここで、maxintはmaxint + 1が表現不可能な整数の最大値である。この手法をC言語で実証したものについては<a href="#C.%20Punycodeの実装例">付録C "Punycodeの実装例"</a>を参照のこと。</p> <p><a href="#6.2%20デコーディング処理">セクション6.2</a>と<a href="#6.3%20エンコーディング処理">6.3</a>で示したデコーディングアルゴリズム、エンコーディングアルゴリズムは、オーバーフローが発生した時点でそれを検知し、オーバーフロー処理を行う。別の方法として、オーバーフロー発生を抑制するために、入力に制限を課すことが挙げられる。例えば、エンコーダーが入力されたコードポイントがMを越えていないこと、および入力長がLを越えていないことを確認済みであれば、deltaは<code>(M - initial_n) * (L + 1)</code>を越えることはないため、整数を保存する変数がこの大きな値を表現可能であれば、オーバーフローは発生しない。このオーバーフロー抑制という方法は、オーバーフロー検知を使用する方法に比べて、より多くの制限を入力に課すことになるが、幾つかのプログラミング言語ではより簡便であると見なされるかもしれない。</p> <p>理論的には、デコーダーの場合にも、可変長整数における数字の数を制限することにより(これは、最も内側のループで繰り返しの回数を制限することである)、類似した方法を採ることができる。しかし、与えられたdeltaを表現するために充分な桁数で、(補正処理のために)はるかに大きなdeltaを表現できることが時折ある。したがって、この方法はおそらく、32ビットよりも大きな整数を必要とするだろう。</p> <p>デコーダーが使用できるもう一つの方法として、オーバーフロー発生を許容しておき、最終的に出力される文字を再度エンコーディングし、それをデコーダーの入力と比較することによってオーバーフロー発生を検査するというものが挙げられる。(大文字小文字を区別しないASCII比較を使用して)両者が一致しない場合は、オーバーフローが発生している。この後処理型オーバーフロー検知方式<em>(delayed-detection)</em>は、即時型オーバーフロー検知方式<em>(immediate-detection)</em>に比べて入力に課す制約はないため、幾つかのプログラミング言語ではより簡便であると見なされるかもしれない。</p> <p>実際、デコーダーがINDAのToUnicode処理内部だけでしか使用されないのであれば、オーバーフローを検査する必要は全くない。ToUnicode処理はより上位レベルで再度エンコーディングと比較を実行し、そこで一致しなければPunycodeのデコーダーが失敗したのと同じ結果となるからである。</p> <h2 id="7. Punycode例"><a href="#7.+Punycode%E4%BE%8B">7. Punycode例</a></h2> <h2 id="7.1 文字列の例"><a href="#7.1+%E6%96%87%E5%AD%97%E5%88%97%E3%81%AE%E4%BE%8B">7.1 文字列の例</a></h2> <p>以下に示すPunycodeエンコーディングでは、ACEプレフィックスは表示されない。1行で表示するには長すぎる文字列については、改行が挿入される場所にバックスラッシュが示される。</p> <p>初めに示す幾つかの例は全て"Why can't they just speak in (language)?"という文章を変換したものである。(Michael Kaplanの"地方"ページから無償提供された)。単語の分割点<em>(word break)</em>と句読点については、ドメイン名でしばしばそのように処理されることにあわせて除去した。</p> <p>(A) アラビア語 (エジプト語): <code>ليهمابتكلموشعابي؟</code></p> <pre><code class="txt">u+0644 u+064A u+0647 u+0645 u+0627 u+0628 u+062A u+0643 u+0644 u+0645 u+0648 u+0634 u+0639 u+0631 u+0628 u+064A u+061F Punycode: egbpdaj6bu4bxfgehfvwxn </code></pre> <p>(B) 中国語 (簡体字): <code>他们为什么不说中文</code></p> <pre><code class="txt">u+4ED6 u+4EEC u+4E3A u+4EC0 u+4E48 u+4E0D u+8BF4 u+4E2D u+6587 Punycode: ihqwcrb4cv8a8dqg056pqjye </code></pre> <p>(C) 中国語 (繁体字): <code>他們爲什麽不說中文</code></p> <pre><code class="txt">u+4ED6 u+5011 u+7232 u+4EC0 u+9EBD u+4E0D u+8AAA u+4E2D u+6587 Punycode: ihqwctvzc91f659drss3x8bo0yb </code></pre> <p>(D) チェコ語: <code>Pročprostěnemluvíčesky</code></p> <pre><code class="txt">U+0050 u+0072 u+006F u+010D u+0070 u+0072 u+006F u+0073 u+0074 u+011B u+006E u+0065 u+006D u+006C u+0075 u+0076 u+00ED u+010D u+0065 u+0073 u+006B u+0079 Punycode: Proprostnemluvesky-uyb24dma41a </code></pre> <p>(E) ヘブライ語: <code>למההםפשוטלאמדבריםעברית</code></p> <pre><code class="txt">u+05DC u+05DE u+05D4 u+05D4 u+05DD u+05E4 u+05E9 u+05D5 u+05D8 u+05DC u+05D0 u+05DE u+05D3 u+05D1 u+05E8 u+05D9 u+05DD u+05E2 u+05D1 u+05E8 u+05D9 u+05EA Punycode: 4dbcagdahymbxekheh6e0a7fei0b </code></pre> <p>(F) ヒンズー語 (デバナーガリ文字): <code>यहलोगहिन्दऺक्योंनहऺंबोलसकतेहैं</code></p> <pre><code class="txt">u+092F u+0939 u+0932 u+094B u+0917 u+0939 u+093F u+0928 u+094D u+0926 u+0940 u+0915 u+094D u+092F u+094B u+0902 u+0928 u+0939 u+0940 u+0902 u+092C u+094B u+0932 u+0938 u+0915 u+0924 u+0947 u+0939 u+0948 u+0902 Punycode: i1baa7eci9glrd9b2ae1bj0hfcgg6iyaf8o0a1dig0cd </code></pre> <p>(G) 日本語 (漢字と平仮名): <code>なぜみんな日本語を話してくれないのか</code></p> <pre><code class="txt">u+306A u+305C u+307F u+3093 u+306A u+65E5 u+672C u+8A9E u+3092 u+8A71 u+3057 u+3066 u+304F u+308C u+306A u+3044 u+306E u+304B Punycode: n8jok5ay5dzabd5bym9f0cm5685rrjetr6pdxa </code></pre> <p>(H) 韓国語 (ハングル音節): <code>세계의모든사람들이한국어를이해한다면얼마나좋을까</code></p> <pre><code class="txt">u+C138 u+ACC4 u+C758 u+BAA8 u+B4E0 u+C0AC u+B78C u+B4E4 u+C774 u+D55C u+AD6D u+C5B4 u+B97C u+C774 u+D574 u+D55C u+B2E4 u+BA74 u+C5BC u+B9C8 u+B098 u+C88B u+C744 u+AE4C Punycode: 989aomsvi5e83db1d2a355cv1e0vak1dwrv93d5xbh15a0dt30a5j\ psd879ccm6fea98c </code></pre> <p>(I) ロシア語 (キリル文字): <code>почемужеонинеговорятпорусски</code></p> <pre><code class="txt">U+043F u+043E u+0447 u+0435 u+043C u+0443 u+0436 u+0435 u+043E u+043D u+0438 u+043D u+0435 u+0433 u+043E u+0432 u+043E u+0440 u+044F u+0442 u+043F u+043E u+0440 u+0443 u+0441 u+0441 u+043A u+0438 Punycode: b1abfaaepdrnnbgefbaDotcwatmq2g4l </code></pre> <p>(J) スペイン語: <code>PorquénopuedensimplementehablarenEspañol</code></p> <pre><code class="txt">U+0050 u+006F u+0072 u+0071 u+0075 u+00E9 u+006E u+006F u+0070 u+0075 u+0065 u+0064 u+0065 u+006E u+0073 u+0069 u+006D u+0070 u+006C u+0065 u+006D u+0065 u+006E u+0074 u+0065 u+0068 u+0061 u+0062 u+006C u+0061 u+0072 u+0065 u+006E U+0045 u+0073 u+0070 u+0061 u+00F1 u+006F u+006C Punycode: PorqunopuedensimplementehablarenEspaol-fmd56a </code></pre> <p>(K) ベトナム語: <code>TạisaohọkhôngthểchỉnóitiếngViệt</code></p> <pre><code class="txt">U+0054 u+1EA1 u+0069 u+0073 u+0061 u+006F u+0068 u+1ECD u+006B u+0068 u+00F4 u+006E u+0067 u+0074 u+0068 u+1EC3 u+0063 u+0068 u+1EC9 u+006E u+00F3 u+0069 u+0074 u+0069 u+1EBF u+006E u+0067 U+0056 u+0069 u+1EC7 u+0074 Punycode: TisaohkhngthchnitingVit-kjcr8268qyxafd2f1b9g </code></pre> <p>次に示す幾つかの例は、全て日本の音楽アーティスト名、歌の名前、テレビ番組名である。これらを例として挙げた理由は、単に著者がたまたま手近にこれらの情報を持っていたからである。(しかし、日本語は英数字、仮名、漢字、それらが多様に混在した文字列の例を提供するのに便利である)。</p> <p>(L) <code>3年B組金八先生</code></p> <pre><code class="txt">u+0033 u+5E74 U+0042 u+7D44 u+91D1 u+516B u+5148 u+751F Punycode: 3B-ww4c5e180e575a65lsy2b </code></pre> <p>(M) <code>安室奈美恵-with-SUPER-MONKEYS</code></p> <pre><code class="txt">u+5B89 u+5BA4 u+5948 u+7F8E u+6075 u+002D u+0077 u+0069 u+0074 u+0068 u+002D U+0053 U+0055 U+0050 U+0045 U+0052 u+002D U+004D U+004F U+004E U+004B U+0045 U+0059 U+0053 Punycode: -with-SUPER-MONKEYS-pc58ag80a8qai00g7n9n </code></pre> <p>(N) <code>Hello-Another-Way-それぞれの場所</code></p> <pre><code class="txt">U+0048 u+0065 u+006C u+006C u+006F u+002D U+0041 u+006E u+006F u+0074 u+0068 u+0065 u+0072 u+002D U+0057 u+0061 u+0079 u+002D u+305D u+308C u+305E u+308C u+306E u+5834 u+6240 Punycode: Hello-Another-Way--fc4qua05auwb3674vfr0b </code></pre> <p>(O) <code>ひとつ屋根の下2</code></p> <pre><code class="txt">u+3072 u+3068 u+3064 u+5C4B u+6839 u+306E u+4E0B u+0032 Punycode: 2-u9tlzr9756bt3uc0v </code></pre> <p>(P) <code>MajiでKoiする5秒前</code></p> <pre><code class="txt">U+004D u+0061 u+006A u+0069 u+3067 U+004B u+006F u+0069 u+3059 u+308B u+0035 u+79D2 u+524D Punycode: MajiKoi5-783gue6qz075azm5e </code></pre> <p>(Q) <code>パフィーdeルンバ</code></p> <pre><code class="txt">u+30D1 u+30D5 u+30A3 u+30FC u+0064 u+0065 u+30EB u+30F3 u+30D0 Punycode: de-jg4avhby1noc0d </code></pre> <p>(R) <code>そのスピードで</code></p> <pre><code class="txt">u+305D u+306E u+30B9 u+30D4 u+30FC u+30C9 u+3067 Punycode: d9juau41awczczp </code></pre> <p>最後に示す例は、ホスト名ラベルに関する既存のルールに反するASCII文字列である。(これはIDNA用の例としては妥当なものではない。なぜなら、IDNAは決して何も制約が無い<em>(pure)</em>ASCIIラベルをエンコードしないからである)。</p> <p>(S) <code>-> $1.00 <-</code></p> <pre><code class="txt">u+002D u+003E u+0020 u+0024 u+0031 u+002E u+0030 u+0030 u+0020 u+003C u+002D Punycode: -> $1.00 <-- </code></pre> <h2 id="7.2 デコーディング処理の検証"><a href="#7.2+%E3%83%87%E3%82%B3%E3%83%BC%E3%83%87%E3%82%A3%E3%83%B3%E3%82%B0%E5%87%A6%E7%90%86%E3%81%AE%E6%A4%9C%E8%A8%BC">7.2 デコーディング処理の検証</a></h2> <p>以下の検証において、処理を進めるデコーダーの状態は、拡張文字列に含まれるコードポイントを表現する16進数の値の並びで示される。アスタリスクは、一番最後に挿入されたコードポイントの直後に現れ、n(アスタリスク直前の値)とi(アスタリスク直後の値)の両方を示す。他の数値は10進数で表現される。</p> <p><a href="#7.1%20文字列の例">セクション7.1</a>の例(B)をデコーディング処理する場合の検証は以下のとおりである。</p> <p>nは128、iは0、biasは72である。<br /> 入力は"ihqwcrb4cv8a8dqg056pqjye"である。<br /> 区切り文字は存在しないので、拡張文字列は空<em>(empty)</em>で処理が開始される。<br /> delta "ihq"は19853にデコードされる。<br /> biasは21になる。</p> <pre><code>4E0D * </code></pre> <p>delta "wc"は64にデコードされる。<br /> biasは20になる。</p> <pre><code>4E0D 4E2D * </code></pre> <p>delta "rb"は37にデコードされる。<br /> biasは13になる。</p> <pre><code>4E3A * 4E0D 4E2D </code></pre> <p>delta "4c"は56にデコードされる。<br /> biasは17になる。</p> <pre><code>4E3A 4E48 * 4E0D 4E2D </code></pre> <p>delta "v8a"は599にデコードされる。<br /> biasは32になる。</p> <pre><code>4E3A 4EC0 * 4E48 4E0D 4E2D </code></pre> <p>delta "8d"は130にデコードされる。<br /> biasは23になる。</p> <pre><code>4ED6 * 4E3A 4EC0 4E48 4E0D 4E2D </code></pre> <p>delta "qg"は154にデコードされる。<br /> biasは25になる。</p> <pre><code>4ED6 4EEC * 4E3A 4EC0 4E48 4E0D 4E2D </code></pre> <p>delta "056p"は46301にデコードされる。<br /> biasは84になる。</p> <pre><code>4ED6 4EEC 4E3A 4EC0 4E48 4E0D 4E2D 6587 * </code></pre> <p>delta "qjye"は88531にデコードされる。<br /> biasは90になる。</p> <pre><code>4ED6 4EEC 4E3A 4EC0 4E48 4E0D 8BF4 * 4E2D 6587 </code></pre> <p><a href="#7.1%20文字列の例">セクション7.1</a>の例(L)をデコーディング処理する場合の検証は以下のとおりである。</p> <p>nは128、iは0、biasは72である。<br /> 入力は"3B-ww4c5e180e575a65lsy2b"である。<br /> そのまま文字として表現されている部分は"3B-"であるから、以下の拡張文字列<br /> から処理が開始される。</p> <pre><code>0033 0042 </code></pre> <p>delta "ww4c"は62042にデコードされる。<br /> biasは27になる。</p> <pre><code>0033 0042 5148 * </code></pre> <p>delta "5e"は139にデコードされる。<br /> biasは24になる。</p> <pre><code>0033 0042 516B * 5148 </code></pre> <p>delta "180e"は16683にデコードされる。<br /> biasは67になる。</p> <pre><code>0033 5E74 * 0042 516B 5148 </code></pre> <p>delta "575a"は34821にデコードされる。<br /> biasは82になる。</p> <pre><code>0033 5E74 0042 516B 5148 751F * </code></pre> <p>delta "65l"は14592にデコードされる。<br /> biasは67になる。</p> <pre><code>0033 5E74 0042 7D44 * 516B 5148 751F </code></pre> <p>delta "sy2b"は42088にデコードされる。<br /> biasは84になる。</p> <pre><code>0033 5E74 0042 7D44 91D1 * 516B 5148 751F </code></pre> <h2 id="7.3 エンコーディング処理の検証"><a href="#7.3+%E3%82%A8%E3%83%B3%E3%82%B3%E3%83%BC%E3%83%87%E3%82%A3%E3%83%B3%E3%82%B0%E5%87%A6%E7%90%86%E3%81%AE%E6%A4%9C%E8%A8%BC">7.3 エンコーディング処理の検証</a></h2> <p>以下の検証において、コードポイント値は16進数で表記され、他の数値は10進数で表記される。</p> <p><a href="#7.1%20文字列の例">セクション7.1</a>の例(B)をエンコーディング処理する場合の検証は以下のとおりである。</p> <p>biasは72である。<br /> 入力は以下のとおりであり、基本コードポイントは存在しないので、そのまま文字として表現されている部分は存在しない。</p> <pre><code>4ED6 4EEC 4E3A 4EC0 4E48 4E0D 8BF4 4E2D 6587 </code></pre> <p>挿入されるべき次のコードポイントは4E0Dである。<br /> deltaは19853になる必要があるので、"ihq"とエンコードされる。<br /> biasは21になる。<br /> 挿入されるべき次のコードポイントは4E2Dである。<br /> deltaは64になる必要があるので、"wc"とエンコードされる。<br /> biasは20になる。<br /> 挿入されるべき次のコードポイントは4E3Aである。<br /> deltaは64になる必要があるので、"rb"とエンコードされる。<br /> biasは13になる。<br /> 挿入されるべき次のコードポイントは4E48である。<br /> deltaは64になる必要があるので、"4c"とエンコードされる。<br /> biasは17になる。<br /> 挿入されるべき次のコードポイントは4EC0である。<br /> deltaは599になる必要があるので、"v8a"とエンコードされる。<br /> biasは32になる。<br /> 挿入されるべき次のコードポイントは4ED6である。<br /> deltaは599になる必要があるので、"8d"とエンコードされる。<br /> biasは23になる。<br /> 挿入されるべき次のコードポイントは4EECである。<br /> deltaは154になる必要があるので、"qg"とエンコードされる。<br /> biasは25になる。<br /> 挿入されるべき次のコードポイントは6587である。<br /> deltaは154になる必要があるので、"056p"とエンコードされる。<br /> biasは84になる。<br /> 挿入されるべき次のコードポイントは8BF4である。<br /> deltaは154になる必要があるので、"qjye"とエンコードされる。<br /> biasは90になる。<br /> 出力は"ihqwcrb4cv8a8dqg056pqjye"になる。</p> <p><a href="#7.1%20文字列の例">セクション7.1</a>の例(L)をエンコーディング処理する場合の検証は以下のとおりである。</p> <p>biasは72である。<br /> 入力は以下のとおりである。</p> <pre><code>0033 5E74 0042 7D44 91D1 516B 5148 751F </code></pre> <p>基本コードポイント(0033, 0042)はそのまま文字としてコピーされるので、"3B-"となる。<br /> 挿入されるべき次のコードポイントは5148である。<br /> deltaは62042になる必要があるので、"ww4c"とエンコードされる。<br /> biasは27になる。<br /> 挿入されるべき次のコードポイントは516Bである。<br /> deltaは139になる必要があるので、"5e"とエンコードされる(*2)。<br /> biasは24になる。</p> <blockquote> <p>《脚注》<br /> *2: "5e"とエンコードされる。<br /> 原文では"encodes as 516B"と書かれているが、ここは文脈から原文の記述が誤っていることが明らかであるため、訳文では修正している</p> </blockquote> <p>挿入されるべき次のコードポイントは5E74である。<br /> deltaは16683になる必要があるので、"180e"とエンコードされる。<br /> biasは67になる。<br /> 挿入されるべき次のコードポイントは751Fである。<br /> deltaは34821になる必要があるので、"575a"とエンコードされる。<br /> biasは82になる。<br /> 挿入されるべき次のコードポイントは7D44である。<br /> deltaは14592になる必要があるので、"65l"とエンコードされる。<br /> biasは67になる。<br /> 挿入されるべき次のコードポイントは91D1である。<br /> deltaは42088になる必要があるので、"sy2b"とエンコードされる。<br /> biasは84になる。<br /> 出力は "3B-ww4c5e180e575a65lsy2b"となる。</p> <h2 id="8. セキュリティーに関する考察"><a href="#8.+%E3%82%BB%E3%82%AD%E3%83%A5%E3%83%AA%E3%83%86%E3%82%A3%E3%83%BC%E3%81%AB%E9%96%A2%E3%81%99%E3%82%8B%E8%80%83%E5%AF%9F">8. セキュリティーに関する考察</a></h2> <p>ユーザーは、DNSにおいて各ドメイン名がそれぞれ単一の権威<em>(authority)</em>によって制御されることを期待する。ドメインラベルとして使用されるUnicode文字列が複数のACEラベルに変換される可能性がある場合、1つの国際化ドメイン名が複数のASCIIドメイン名に変換される可能性がある。複数のASCIIドメイン名はそれぞれ異なる権威によって制御されるかもしれないため、その中の幾つかは、本来他に送られるべきサービスリクエストをハイジャックするような偽装をする可能性がある。これらの可能性を排除するため、PunycodeはUnicode文字列それぞれが単一のエンコーディングを持つように設計されている。</p> <p>しかし、依然として"同じ"文章に対して複数のUnicode表現が存在する可能性が残される。これは"同じ"という様々な定義に由来するものである。この問題は正規化という話題の下で、Unicode標準に対する幾つかの拡張を行う方向で努力がなされている。この作業をドメイン名に対して適用したものがNameprepである。</p> <h2 id="9. 参考文献"><a href="#9.+%E5%8F%82%E8%80%83%E6%96%87%E7%8C%AE">9. 参考文献</a></h2> <h2 id="9.1 必須の参考文献"><a href="#9.1+%E5%BF%85%E9%A0%88%E3%81%AE%E5%8F%82%E8%80%83%E6%96%87%E7%8C%AE">9.1 必須の参考文献</a></h2> <p>[RFC2119] Bradner, S., "Key words for use in RFCs to Indicate Requirement Levels", BCP 14, RFC 2119, March 1997.</p> <h2 id="9.2 有用な参考文献"><a href="#9.2+%E6%9C%89%E7%94%A8%E3%81%AA%E5%8F%82%E8%80%83%E6%96%87%E7%8C%AE">9.2 有用な参考文献</a></h2> <p>[RFC952] Harrenstien, K., Stahl, M. and E. Feinler, "DOD Internet Host Table Specification", RFC 952, October 1985.</p> <p>[RFC1034] Mockapetris, P., "Domain Names - Concepts and Facilities", STD 13, RFC 1034, November 1987.</p> <p>[IDNA] Faltstrom, P., Hoffman, P. and A. Costello, "Internationalizing Domain Names in Applications (IDNA)", RFC 3490, March 2003.</p> <p>[NAMEPREP] Hoffman, P. and M. Blanchet, "Nameprep: A Stringprep Profile for Internationalized Domain Names (IDN)", RFC 3491, March 2003.</p> <p>[ASCII] Cerf, V., "ASCII format for Network Interchange", RFC 20, October 1969.</p> <p>[PROVINCIAL] Kaplan, M., "The 'anyone can be provincial!' page", <a target="_blank" rel="nofollow noopener" href="http://www.trigeminal.com/samples/provincial.html">http://www.trigeminal.com/samples/provincial.html</a>.</p> <p>[UNICODE] The Unicode Consortium, "The Unicode Standard", <a target="_blank" rel="nofollow noopener" href="http://www.unicode.org/unicode/standard/standard.html">http://www.unicode.org/unicode/standard/standard.html</a>.</p> <h2 id="A. 文字種注釈"><a href="#A.+%E6%96%87%E5%AD%97%E7%A8%AE%E6%B3%A8%E9%87%88">A. 文字種注釈</a></h2> <p>大文字小文字を区別しない文字列に対してPunycodeを使用するために、上位層では、Punycodeエンコーディングに先立ち、文字列の文字種統一を行う必要がある。エンコードされた文字列では、大文字と小文字を混在して使用可能であり、エンコードされた文字列を表示する際に、文字種が統一された文字列をどのように文字種混在の文字列に変換したかを示す注釈として使用される。しかし、文字種注釈はIDNAで規定されるToASCII処理とToUnicode処理では使用されないので、IDNAの実装者はこの付録を無視できることに注意してもらいたい。</p> <p>基本コードポイントは、混在した文字種を直接使用することができる。デコーダーはこれらのコードポイントを文字通りコピーするので、小文字のコードポイントは小文字のまま残され、大文字のコードポイントは大文字のまま残される。非基本コードポイントは、それぞれdeltaで表現される。deltaは基本コードポイントの並びで表現され、その最後で注釈を提供する。大文字であった場合には、(可能であれば)非基本コードポイントを大文字に変換し、小文字であった場合には、(可能であれば)非基本コードポイントを小文字に変換することを示す。</p> <p>これらの注釈は、デコーダーから返されるコードポイントを変更しない。注釈は独立して返され、その処理を呼び出したもの<em>(caller)</em>はその値を使用するか無視するかを選択する。エンコーダーはコードポイントに加えて、注釈を受理できる。しかし注釈はASCII文字の大文字/小文字の形態に影響を与える以外には、出力を変更しない。</p> <p>Punycodeエンコーダーとデコーダーはこれらの注釈をサポートする必要はなく、また上位層もこれらを使用する必要はない。</p> <h2 id="B. 免責条項と使用許諾"><a href="#B.+%E5%85%8D%E8%B2%AC%E6%9D%A1%E9%A0%85%E3%81%A8%E4%BD%BF%E7%94%A8%E8%A8%B1%E8%AB%BE">B. 免責条項と使用許諾</a></h2> <p>本文書全体またはその一部(擬似コードとCコードも含む)について、著者は一切の保証を行わないし、その使用の結果生じたあらゆる損害について責任を負わない。著者はこれらを任意の方法で使用、修正、配布を確定的に許可する<em>(irrevocable permission)</em>。ただし、他者が自由にこれらを使用、修正、配布する権限を損なわないこと、またこれらから派生したものを再配布する際には、著者やバージョン情報に関して誤解を招く情報を含めないことが条件である。また、派生物の使用許諾条件がこれらのものと同様である必要はない。</p> <h2 id="C. Punycodeの実装例"><a href="#C.+Punycode%E3%81%AE%E5%AE%9F%E8%A3%85%E4%BE%8B">C. Punycodeの実装例</a></h2> <pre><code class="c">/* punycode.c from RFC 3492 [http://www.nicemice.net/idn/](http://www.nicemice.net/idn/) Adam M. Costello [http://www.nicemice.net/amc/](http://www.nicemice.net/amc/) This is ANSI C code (C89) implementing Punycode (RFC 3492). */ /************************************************************/ /* Public interface (would normally go in its own .h file): */ enum punycode_status { punycode_success, punycode_bad_input, /* Input is invalid. */ punycode_big_output, /* Output would exceed the space provided. */ punycode_overflow /* Input needs wider integers to process. */ }; typedef unsigned int punycode_uint; typedef unsigned long punycode_uint; enum punycode_status punycode_encode( punycode_uint input_length, const punycode_uint input[], const unsigned char case_flags[], punycode_uint *output_length, char output[] ); /* punycode_encode() converts Unicode to Punycode. The input */ /* is represented as an array of Unicode code points (not code */ /* units; surrogate pairs are not allowed), and the output */ /* will be represented as an array of ASCII code points. The */ /* output string is *not* null-terminated; it will contain */ /* zeros if and only if the input contains zeros. (Of course */ /* the caller can leave room for a terminator and add one if */ /* needed.) The input_length is the number of code points in */ /* the input. The output_length is an in/out argument: the */ /* caller passes in the maximum number of code points that it */ /* can receive, and on successful return it will contain the */ /* number of code points actually output. The case_flags array */ /* holds input_length boolean values, where nonzero suggests that */ /* the corresponding Unicode character be forced to uppercase */ /* after being decoded (if possible), and zero suggests that */ /* it be forced to lowercase (if possible). ASCII code points */ /* are encoded literally, except that ASCII letters are forced */ /* to uppercase or lowercase according to the corresponding */ /* uppercase flags. If case_flags is a null pointer then ASCII */ /* letters are left as they are, and other code points are */ /* treated as if their uppercase flags were zero. The return */ /* value can be any of the punycode_status values defined above */ /* except punycode_bad_input; if not punycode_success, then */ /* output_size and output might contain garbage. */ enum punycode_status punycode_decode( punycode_uint input_length, const char input[], punycode_uint *output_length, punycode_uint output[], unsigned char case_flags[] ); /* punycode_decode() converts Punycode to Unicode. The input is */ /* represented as an array of ASCII code points, and the output */ /* will be represented as an array of Unicode code points. The */ /* input_length is the number of code points in the input. The */ /* output_length is an in/out argument: the caller passes in */ /* the maximum number of code points that it can receive, and */ /* on successful return it will contain the actual number of */ /* code points output. The case_flags array needs room for at */ /* least output_length values, or it can be a null pointer if the */ /* case information is not needed. A nonzero flag suggests that */ /* the corresponding Unicode character be forced to uppercase */ /* by the caller (if possible), while zero suggests that it be */ /* forced to lowercase (if possible). ASCII code points are */ /* output already in the proper case, but their flags will be set */ /* appropriately so that applying the flags would be harmless. */ /* The return value can be any of the punycode_status values */ /* defined above; if not punycode_success, then output_length, */ /* output, and case_flags might contain garbage. On success, the */ /* decoder will never need to write an output_length greater than */ /* input_length, because of how the encoding is defined. */ /**********************************************************/ /* Implementation (would normally go in its own .c file): */ /*** Bootstring parameters for Punycode ***/ enum { base = 36, tmin = 1, tmax = 26, skew = 38, damp = 700, initial_bias = 72, initial_n = 0x80, delimiter = 0x2D }; /* basic(cp) tests whether cp is a basic code point: */ /* delim(cp) tests whether cp is a delimiter: */ /* decode_digit(cp) returns the numeric value of a basic code */ /* point (for use in representing integers) in the range 0 to */ /* base-1, or base if cp is does not represent a value. */ static punycode_uint decode_digit(punycode_uint cp) { return cp - 48 < 10 ? cp - 22 : cp - 65 < 26 ? cp - 65 : cp - 97 < 26 ? cp - 97 : base; } /* encode_digit(d,flag) returns the basic code point whose value */ /* (when used for representing integers) is d, which needs to be in */ /* the range 0 to base-1. The lowercase form is used unless flag is */ /* nonzero, in which case the uppercase form is used. The behavior */ /* is undefined if flag is nonzero and digit d has no uppercase form. */ static char encode_digit(punycode_uint d, int flag) { return d + 22 + 75 * (d < 26) - ((flag != 0) << 5); /* 0..25 map to ASCII a..z or A..Z */ /* 26..35 map to ASCII 0..9 */ } /* flagged(bcp) tests whether a basic code point is flagged */ /* (uppercase). The behavior is undefined if bcp is not a */ /* basic code point. */ /* encode_basic(bcp,flag) forces a basic code point to lowercase */ /* if flag is zero, uppercase if flag is nonzero, and returns */ /* the resulting code point. The code point is unchanged if it */ /* is caseless. The behavior is undefined if bcp is not a basic */ /* code point. */ static char encode_basic(punycode_uint bcp, int flag) { bcp -= (bcp - 97 < 26) << 5; return bcp + ((!flag && (bcp - 65 < 26)) << 5); } /*** Platform-specific constants ***/ /* maxint is the maximum value of a punycode_uint variable: */ static const punycode_uint maxint = -1; /* Because maxint is unsigned, -1 becomes the maximum value. */ /*** Bias adaptation function ***/ static punycode_uint adapt( punycode_uint delta, punycode_uint numpoints, int firsttime ) { punycode_uint k; delta = firsttime ? delta / damp : delta >> 1; /* delta >> 1 is a faster way of doing delta / 2 */ delta += delta / numpoints; for (k = 0; delta > ((base - tmin) * tmax) / 2; k += base) { delta /= base - tmin; } return k + (base - tmin + 1) * delta / (delta + skew); } /*** Main encode function ***/ enum punycode_status punycode_encode( punycode_uint input_length, const punycode_uint input[], const unsigned char case_flags[], punycode_uint *output_length, char output[] ) { punycode_uint n, delta, h, b, out, max_out, bias, j, m, q, k, t; /* Initialize the state: */ n = initial_n; delta = out = 0; max_out = *output_length; bias = initial_bias; /* Handle the basic code points: */ for (j = 0; j < input_length; ++j) { if (basic(input[j])) { if (max_out - out < 2) return punycode_big_output; output[out++] = case_flags ? encode_basic(input[j], case_flags[j]) : input[j]; } /* else if (input[j] < n) return punycode_bad_input; */ /* (not needed for Punycode with unsigned code points) */ } h = b = out; /* h is the number of code points that have been handled, b is the */ /* number of basic code points, and out is the number of characters */ /* that have been output. */ if (b > 0) output[out++] = delimiter; /* Main encoding loop: */ while (h < input_length) { /* All non-basic code points < n have been */ /* handled already. Find the next larger one: */ for (m = maxint, j = 0; j < input_length; ++j) { /* if (basic(input[j])) continue; */ /* (not needed for Punycode) */ if (input[j] >= n && input[j] < m) m = input[j]; } /* Increase delta enough to advance the decoder's */ /* <n,i> state to <m,0>, but guard against overflow: */ if (m - n > (maxint - delta) / (h + 1)) return punycode_overflow; delta += (m - n) * (h + 1); n = m; for (j = 0; j < input_length; ++j) { /* Punycode does not need to check whether input[j] is basic: */ if (input[j] < n /* || basic(input[j]) */ ) { if (++delta == 0) return punycode_overflow; } if (input[j] == n) { /* Represent delta as a generalized variable-length integer: */ for (q = delta, k = base; ; k += base) { if (out >= max_out) return punycode_big_output; t = k <= bias /* + tmin */ ? tmin : /* +tmin not needed */ k >= bias + tmax ? tmax : k - bias; if (q < t) break; output[out++] = encode_digit(t + (q - t) % (base - t), 0); q = (q - t) / (base - t); } output[out++] = encode_digit(q, case_flags && case_flags[j]); bias = adapt(delta, h + 1, h == b); delta = 0; ++h; } } ++delta, ++n; } *output_length = out; return punycode_success; } /*** Main decode function ***/ enum punycode_status punycode_decode( punycode_uint input_length, const char input[], punycode_uint *output_length, punycode_uint output[], unsigned char case_flags[] ) { punycode_uint n, out, i, max_out, bias, b, j, in, oldi, w, k, digit, t; /* Initialize the state: */ n = initial_n; out = i = 0; max_out = *output_length; bias = initial_bias; /* Handle the basic code points: Let b be the number of input code */ /* points before the last delimiter, or 0 if there is none, then */ /* copy the first b code points to the output. */ for (b = j = 0; j < input_length; ++j) if (delim(input[j])) b = j; if (b > max_out) return punycode_big_output; for (j = 0; j < b; ++j) { if (case_flags) case_flags[out] = flagged(input[j]); if (!basic(input[j])) return punycode_bad_input; output[out++] = input[j]; } /* Main decoding loop: Start just after the last delimiter if any */ /* basic code points were copied; start at the beginning otherwise. */ for (in = b > 0 ? b + 1 : 0; in < input_length; ++out) { /* in is the index of the next character to be consumed, and */ /* out is the number of code points in the output array. */ /* Decode a generalized variable-length integer into delta, */ /* which gets added to i. The overflow checking is easier */ /* if we increase i as we go, then subtract off its starting */ /* value at the end to obtain delta. */ for (oldi = i, w = 1, k = base; ; k += base) { if (in >= input_length) return punycode_bad_input; digit = decode_digit(input[in++]); if (digit >= base) return punycode_bad_input; if (digit > (maxint - i) / w) return punycode_overflow; i += digit * w; t = k <= bias /* + tmin */ ? tmin : /* +tmin not needed */ k >= bias + tmax ? tmax : k - bias; if (digit < t) break; if (w > maxint / (base - t)) return punycode_overflow; w *= (base - t); } bias = adapt(i - oldi, out + 1, oldi == 0); /* i was supposed to wrap around from out+1 to 0, */ /* incrementing n each time, so we'll fix that now: */ if (i / (out + 1) > maxint - n) return punycode_overflow; n += i / (out + 1); i %= (out + 1); /* Insert n at position i of the output: */ /* not needed for Punycode: */ /* if (decode_digit(n) <= base) return punycode_invalid_input; */ if (out >= max_out) return punycode_big_output; if (case_flags) { memmove(case_flags + i + 1, case_flags + i, out - i); /* Case of last character determines uppercase flag: */ case_flags[i] = flagged(input[in - 1]); } memmove(output + i + 1, output + i, (out - i) * sizeof *output); output[i++] = n; } *output_length = out; return punycode_success; } /******************************************************************/ /* Wrapper for testing (would normally go in a separate .c file): */ /* For testing, we'll just set some compile-time limits rather than */ /* use malloc(), and set a compile-time option rather than using a */ /* command-line option. */ enum { unicode_max_length = 256, ace_max_length = 256 }; static void usage(char **argv) { fprintf(stderr, "\n" "%s -e reads code points and writes a Punycode string.\n" "%s -d reads a Punycode string and writes code points.\n" "\n" "Input and output are plain text in the native character set.\n" "Code points are in the form u+hex separated by whitespace.\n" "Although the specification allows Punycode strings to contain\n" "any characters from the ASCII repertoire, this test code\n" "supports only the printable characters, and needs the Punycode\n" "string to be followed by a newline.\n" "The case of the u in u+hex is the force-to-uppercase flag.\n" , argv[0], argv[0]); exit(EXIT_FAILURE); } static void fail(const char *msg) { fputs(msg,stderr); exit(EXIT_FAILURE); } static const char too_big[] = "input or output is too large, recompile with larger limits\n"; static const char invalid_input[] = "invalid input\n"; static const char overflow[] = "arithmetic overflow\n"; static const char io_error[] = "I/O error\n"; /* The following string is used to convert printable */ /* characters between ASCII and the native charset: */ static const char print_ascii[] = "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n" "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n" " !\"#$%&'()*+,-./" "0123456789:;<=>?" "@ABCDEFGHIJKLMNO" "PQRSTUVWXYZ[\\]^_" "`abcdefghijklmno" "pqrstuvwxyz{|}~\n"; int main(int argc, char **argv) { enum punycode_status status; int r; unsigned int input_length, output_length, j; unsigned char case_flags[unicode_max_length]; if (argc != 2) usage(argv); if (argv[1][0] != '-') usage(argv); if (argv[1][2] != 0) usage(argv); if (argv[1][1] == 'e') { punycode_uint input[unicode_max_length]; unsigned long codept; char output[ace_max_length+1], uplus[3]; int c; /* Read the input code points: */ input_length = 0; for (;;) { r = scanf("%2s%lx", uplus, &codept;); if (ferror(stdin)) fail(io_error); if (r == EOF || r == 0) break; if (r != 2 || uplus[1] != '+' || codept > (punycode_uint)-1) { fail(invalid_input); } if (input_length == unicode_max_length) fail(too_big); if (uplus[0] == 'u') case_flags[input_length] = 0; else if (uplus[0] == 'U') case_flags[input_length] = 1; else fail(invalid_input); input[input_length++] = codept; } /* Encode: */ output_length = ace_max_length; status = punycode_encode(input_length, input, case_flags, &output;_length, output); if (status == punycode_bad_input) fail(invalid_input); if (status == punycode_big_output) fail(too_big); if (status == punycode_overflow) fail(overflow); assert(status == punycode_success); /* Convert to native charset and output: */ for (j = 0; j < output_length; ++j) { c = output[j]; assert(c >= 0 && c <= 127); if (print_ascii[c] == 0) fail(invalid_input); output[j] = print_ascii[c]; } output[j] = 0; r = puts(output); if (r == EOF) fail(io_error); return EXIT_SUCCESS; } if (argv[1][1] == 'd') { char input[ace_max_length+2], *p, *pp; punycode_uint output[unicode_max_length]; /* Read the Punycode input string and convert to ASCII: */ fgets(input, ace_max_length+2, stdin); if (ferror(stdin)) fail(io_error); if (feof(stdin)) fail(invalid_input); input_length = strlen(input) - 1; if (input[input_length] != '\n') fail(too_big); input[input_length] = 0; for (p = input; *p != 0; ++p) { pp = strchr(print_ascii, *p); if (pp == 0) fail(invalid_input); *p = pp - print_ascii; } /* Decode: */ output_length = unicode_max_length; status = punycode_decode(input_length, input, &output;_length, output, case_flags); if (status == punycode_bad_input) fail(invalid_input); if (status == punycode_big_output) fail(too_big); if (status == punycode_overflow) fail(overflow); assert(status == punycode_success); /* Output the result: */ for (j = 0; j < output_length; ++j) { r = printf("%s+%04lX\n", case_flags[j] ? "U" : "u", (unsigned long) output[j] ); if (r < 0) fail(io_error); } return EXIT_SUCCESS; } usage(argv); return EXIT_SUCCESS; /* not reached, but quiets compiler warning */ } </code></pre> <h2 id="著者の連絡先"><a href="#%E8%91%97%E8%80%85%E3%81%AE%E9%80%A3%E7%B5%A1%E5%85%88">著者の連絡先</a></h2> <p>Adam M. Costello<br /> University of California, Berkeley<br /> <a target="_blank" rel="nofollow noopener" href="http://www.nicemice.net/amc/">http://www.nicemice.net/amc/</a></p> <h2 id="完全な著作権表示"><a href="#%E5%AE%8C%E5%85%A8%E3%81%AA%E8%91%97%E4%BD%9C%E6%A8%A9%E8%A1%A8%E7%A4%BA">完全な著作権表示</a></h2> <p>Copyright (C) The Internet Society (2003). All Rights Reserved.</p> <p>This document and translations of it may be copied and furnished to<br /> others, and derivative works that comment on or otherwise explain it<br /> or assist in its implementation may be prepared, copied, published<br /> and distributed, in whole or in part, without restriction of any<br /> kind, provided that the above copyright notice and this paragraph are<br /> included on all such copies and derivative works. However, this<br /> document itself may not be modified in any way, such as by removing<br /> the copyright notice or references to the Internet Society or other<br /> Internet organizations, except as needed for the purpose of<br /> developing Internet standards in which case the procedures for<br /> copyrights defined in the Internet Standards process must be<br /> followed, or as required to translate it into languages other than<br /> English.</p> <p>The limited permissions granted above are perpetual and will not be<br /> revoked by the Internet Society or its successors or assigns.</p> <p>This document and the information contained herein is provided on an<br /> "AS IS" basis and THE INTERNET SOCIETY AND THE INTERNET ENGINEERING<br /> TASK FORCE DISCLAIMS ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING<br /> BUT NOT LIMITED TO ANY WARRANTY THAT THE USE OF THE INFORMATION<br /> HEREIN WILL NOT INFRINGE ANY RIGHTS OR ANY IMPLIED WARRANTIES OF<br /> MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.</p> <h2 id="謝辞"><a href="#%E8%AC%9D%E8%BE%9E">謝辞</a></h2> <p>RFCエディターの活動に対する資金は現在Internet Societyによって提供されている。</p> ウラル tag:crieit.net,2005:PublicArticle/16037 2020-08-24T18:47:06+09:00 2020-08-24T19:08:15+09:00 https://crieit.net/posts/JavaScript-postMessage-DOM-Xpath JavaScriptのpostMessageでDOMツリーのノード参照を渡す方法[Xpath] <p>ポップアップしたウィンドウに要素の参照(DOMノード)を送りたかったので、この記事を書いた。</p> <h1 id="Web MessagingはDOMノードを送れない"><a href="#Web+Messaging%E3%81%AFDOM%E3%83%8E%E3%83%BC%E3%83%89%E3%82%92%E9%80%81%E3%82%8C%E3%81%AA%E3%81%84">Web MessagingはDOMノードを送れない</a></h1> <p>JavaScriptでは、<a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/ja/docs/Web/API/Window/postMessage">window.postMessage</a>を使うことで、ポップアップやiframeなどの別ウィンドウとWeb Messaging(HTML5)を介して通信することができる。</p> <p>子ウィンドウ、親ウィンドウへの参照はそれぞれ<code>window.open()</code>と<code>window.opener</code>で持つことができるから、<code>window.postMessage</code>と併せてあらゆるデータのやり取りが自由にできそうなものである。</p> <p>しかし、<code>window.postMessage</code>では送ることができないデータがある。</p> <p>MDN web docsには、<code>message</code>について</p> <blockquote> <p>他のウィンドウに送られるデータ。データは <a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm">the structured clone algorithm</a> に従ってシリアル化されます。つまり、手動でシリアル化することなく様々なデータオブジェクトを渡すことができます。<br /> <a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/ja/docs/Web/API/Window/postMessage">(window.postMessage - Web API | MDN</a>)</p> </blockquote> <p>と書かれている。</p> <p>この「<a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm">the structured clone algorithm</a>」(日本語「<a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/ja/docs/Web/API/Web_Workers_API/Structured_clone_algorithm">構造化複製アルゴリズム</a>」)という部分が重要で、この中で「<a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/ja/docs/Web/API/Web_Workers_API/Structured_clone_algorithm#%E6%A7%8B%E9%80%A0%E5%8C%96%E8%A4%87%E8%A3%BD%E3%81%A7%E5%8B%95%E4%BD%9C%E3%81%97%E3%81%AA%E3%81%84%E3%82%82%E3%81%AE">構造化複製で動作しないもの</a>」というのが示されいる。</p> <blockquote> <ul> <li>Function オブジェクトは構造化複製アルゴリズムでは複製されません。複製しようとすると DATA_CLONE_ERR 例外が送出されます。</li> <li>DOM ノードを複製しようとしても同様に DATA_CLONE_ERR 例外が送出されます。</li> </ul> </blockquote> <p>つまり、例えば<code>document.querySelector()</code>などを使えば要素の参照を取得できるが、このような参照はWeb Messagingで送ることができない。<br /> 実際に<code>window.postMessage</code>でDOMノードを送ろうとすると、</p> <blockquote> <p>Uncaught DOMException: Failed to execute 'postMessage' on 'Window': HTMLButtonElement object could not be cloned.</p> </blockquote> <p>のようなエラーが発生する。</p> <p>ウィンドウ間で互いのDOMの参照はできるのだから、DOMノードも送れるべきである。<br /> そこで、住所のように、テキストでDOMツリーにおけるノードの位置を表現できる方法を探していると、「Xpath」という言語構文を見つけた。</p> <h1 id="Xpathとは"><a href="#Xpath%E3%81%A8%E3%81%AF">Xpathとは</a></h1> <p>Xpathとは、XMLやHTMLのようなツリー状の階層構造を持つ文書で、様々なノードの位置や情報を表すことができる記法のことである。URLのようなパス表記ができることが特徴。</p> <p><a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/ja/docs/Introduction_to_using_XPath_in_JavaScript">Introduction to using XPath in JavaScript | MDN</a></p> <p>記法については<a target="_blank" rel="nofollow noopener" href="https://qiita.com/rllllho/items/cb1187cec0fb17fc650a">こちらの記事</a>が詳しいが、ざっくり言うと、例えばbody直下の<code><h1></code>にアクセスするためのXpathは</p> <pre><code>/html/body/h1 </code></pre> <p>となる。</p> <p>また、2番目の<code><div></code>の3番目の<code><span></code>にアクセスするためのXpathは</p> <pre><code>/html/body/div[2]/span[3] </code></pre> <p>といったように表すことができる。</p> <h1 id="要素のXpathを取得して送信する"><a href="#%E8%A6%81%E7%B4%A0%E3%81%AEXpath%E3%82%92%E5%8F%96%E5%BE%97%E3%81%97%E3%81%A6%E9%80%81%E4%BF%A1%E3%81%99%E3%82%8B">要素のXpathを取得して送信する</a></h1> <p>まず、送るためにはDOMノードのXpathを取得する必要がある。<br /> 以下の記事のコードを使用して送信側のスクリプトを書いた。</p> <p><a target="_blank" rel="nofollow noopener" href="https://qiita.com/narikei/items/fb62b543ca386fcee211">ブラウザ上のクリックした要素のXpathを取得する - Qiita</a></p> <p><strong>parent.htmlのjs</strong></p> <pre><code class="JavaScript"><br />let childWindow = window.open('child.html', 'child', 'width=300,height=400,scrollbars'); // クリックされたらその要素のXpathを子ウィンドウにpostMessageする window.addEventListener('click', (e) => { childWindow.postMessage(getXpath(e.target), 'http://localhost'); }); /* https://qiita.com/narikei/items/fb62b543ca386fcee211 */ function getXpath(element) { if(element && element.parentNode) { var xpath = getXpath(element.parentNode) + '/' + element.tagName; var s = []; for(var i = 0; i < element.parentNode.childNodes.length; i++) { var e = element.parentNode.childNodes[i]; if(e.tagName == element.tagName) { s.push(e); } } if(1 < s.length) { for(var i = 0; i < s.length; i++) { if(s[i] === element) { xpath += '[' + (i+1) + ']'; break; } } } return xpath.toLowerCase(); } else { return ''; } } </code></pre> <h1 id="要素のXpathを受け取って参照する"><a href="#%E8%A6%81%E7%B4%A0%E3%81%AEXpath%E3%82%92%E5%8F%97%E3%81%91%E5%8F%96%E3%81%A3%E3%81%A6%E5%8F%82%E7%85%A7%E3%81%99%E3%82%8B">要素のXpathを受け取って参照する</a></h1> <p>受信側は以下のようになる。</p> <p><strong>child.htmlのjs</strong></p> <pre><code class="JavaScript">let parent = window.opener.document; function receiveMessage(e) { if (e.origin !== "http://localhost") {return} parent.evaluate(e.data, parent, null, XPathResult.FIRST_ORDERED_NODE_TYPE).singleNodeValue.innerHTML = 'ok!'; } window.addEventListener("message", receiveMessage); </code></pre> <p><a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/ja/docs/Introduction_to_using_XPath_in_JavaScript">Introduction to using XPath in JavaScript | MDN</a></p> <p>成功すると、親ウィンドウでクリックした要素に「ok!」と表示されるはず。<br /> これで、親ウィンドウから子ウィンドウに送られたXpathを使って、子ウィンドウが親ウィンドウのDOMを参照し、当該要素にアクセスすることが可能になった。もちろんその逆も可能である。</p> ウラル tag:crieit.net,2005:PublicArticle/16036 2020-08-21T16:13:43+09:00 2020-08-21T16:13:43+09:00 https://crieit.net/posts/m3u8-mp4-mp3-m4a-Windows-m3u8ToMP4 m3u8を簡単にmp4・mp3・m4aへ変換できるWindows用ソフト「m3u8ToMP4」 <p>友人に頼んで、URLをコピーするだけでM3Uファイル(拡張子.m3u8)をmp4やm4a、mp3に変換できるWindows用のツールを作ってもらったので、ご紹介します。</p> <p>記事執筆時点のバージョンは1.1.2です。</p> <p><strong><a target="_blank" rel="nofollow noopener" href="https://github.com/ku-now/m3u8ToMP4">m3u8ToMP4 - Kunow</a></strong><br /> <a target="_blank" rel="nofollow noopener" href="https://github.com/ku-now/m3u8ToMP4">https://github.com/ku-now/m3u8ToMP4</a></p> <h1 id="ダウンロード方法"><a href="#%E3%83%80%E3%82%A6%E3%83%B3%E3%83%AD%E3%83%BC%E3%83%89%E6%96%B9%E6%B3%95">ダウンロード方法</a></h1> <p>GitHubの <strong><a target="_blank" rel="nofollow noopener" href="https://github.com/ku-now/m3u8ToMP4/releases">リリースページ</a></strong> を開き、最新リリースの一番上にあるzipファイルを保存します。</p> <p><a href="https://crieit.now.sh/upload_images/f37dd84b36bcbac2749ed7d9d5d7127f5f3f592d9243d.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/f37dd84b36bcbac2749ed7d9d5d7127f5f3f592d9243d.png?mw=700" alt="image.png" /></a></p> <p>保存したzipファイルを展開すると「m3u8ToMP4」という名前のフォルダが出てきました。</p> <p><a href="https://crieit.now.sh/upload_images/f37dd84b36bcbac2749ed7d9d5d7127f5f3f5a5759435.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/f37dd84b36bcbac2749ed7d9d5d7127f5f3f5a5759435.png?mw=700" alt="image.png" /></a></p> <p>フォルダの中にある「m3u8ToMP4.exe」を開くと、「WindowsによってPCが保護されました」という警告が出てきました。</p> <p><a href="https://crieit.now.sh/upload_images/f37dd84b36bcbac2749ed7d9d5d7127f5f3f5b40c5e02.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/f37dd84b36bcbac2749ed7d9d5d7127f5f3f5b40c5e02.png?mw=700" alt="image.png" /></a></p> <p>このソフトはGitHub上でソースコードを公開しており、安全なので、「詳細情報」から「実行」をクリックします。</p> <p>この時点ではまだm3u8ファイルのURLをコピーしていませんので、「クリップボードにコピーされたURLがm3u8ファイルのものではないか、URLではありません。m3u8ファイルを含んだURLをコピーしてから、もう一度やり直してください。」と書かれたメッセージが表示されました。</p> <p><a href="https://crieit.now.sh/upload_images/f37dd84b36bcbac2749ed7d9d5d7127f5f3f5c005a177.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/f37dd84b36bcbac2749ed7d9d5d7127f5f3f5c005a177.png?mw=700" alt="image.png" /></a></p> <h1 id="実際に使ってみた"><a href="#%E5%AE%9F%E9%9A%9B%E3%81%AB%E4%BD%BF%E3%81%A3%E3%81%A6%E3%81%BF%E3%81%9F">実際に使ってみた</a></h1> <p>それでは実際にm3u8形式のファイルを、mp4やmp3、m4aに変換して保存してみたいと思います。<br /> URLさえ手に入れば、変換はとても簡単です。</p> <h2 id="NHKラジオ「らじる★らじる」を保存する"><a href="#NHK%E3%83%A9%E3%82%B8%E3%82%AA%E3%80%8C%E3%82%89%E3%81%98%E3%82%8B%E2%98%85%E3%82%89%E3%81%98%E3%82%8B%E3%80%8D%E3%82%92%E4%BF%9D%E5%AD%98%E3%81%99%E3%82%8B">NHKラジオ「らじる★らじる」を保存する</a></h2> <p>「らじる★らじる」の「聴き逃し」から、試しに「基礎英語1」をmp3ファイルで保存してみます。</p> <p>まずは「基礎英語1」の聴き逃しページを開きます。<br /> <a target="_blank" rel="nofollow noopener" href="https://www.nhk.or.jp/radio/ondemand/detail.html?p=0677_01">https://www.nhk.or.jp/radio/ondemand/detail.html?p=0677_01</a></p> <p><a href="https://crieit.now.sh/upload_images/f37dd84b36bcbac2749ed7d9d5d7127f5f3f60280d065.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/f37dd84b36bcbac2749ed7d9d5d7127f5f3f60280d065.png?mw=700" alt="image.png" /></a></p> <p>一番上にあったので2020年8月10日放送分を保存してみます。クリックすると「らじる★らじる」のプレーヤーが開きます。</p> <p><a href="https://crieit.now.sh/upload_images/f37dd84b36bcbac2749ed7d9d5d7127f5f3f609e4209a.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/f37dd84b36bcbac2749ed7d9d5d7127f5f3f609e4209a.png?mw=700" alt="image.png" /></a></p> <p>ここでキーボードの「F12」を押して、「開発者ツール」を開きます。それから「Network」タブを開くのですが、この時点だと既に読み込まれたファイルが見れないので、キーボードの「F5」を押すなどしてページを再読み込みします。</p> <p><a href="https://crieit.now.sh/upload_images/f37dd84b36bcbac2749ed7d9d5d7127f5f3f61cd78768.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/f37dd84b36bcbac2749ed7d9d5d7127f5f3f61cd78768.png?mw=700" alt="image.png" /></a></p> <p>「master.m3u8」(https://nhks-vh.akamaihd.net/i/radioondemand/r/677/s/stream_677_f9befe06e685a9b6613ae84dd363fcfe.mp4/master.m3u8)という名前のファイルを見つけました。</p> <p>これが今回保存するURLになるので、ファイル名の上で右クリックして、Copy > Copy link address からリンクをコピーしておきました。</p> <p>コピーしたら、いよいよ「m3u8ToMP4.exe」を起動します。</p> <p><a href="https://crieit.now.sh/upload_images/f37dd84b36bcbac2749ed7d9d5d7127f5f3f63bbd6e8c.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/f37dd84b36bcbac2749ed7d9d5d7127f5f3f63bbd6e8c.png?mw=700" alt="image.png" /></a></p> <p>URLをコピーしていなかった時と異なり、「名前を付けて保存」のページが出てきました。<br /> 今回はMP3ファイルとして保存したいので、「ファイルの種類」から「MP3ファイル(*.mp3)」を選びます。</p> <p><a href="https://crieit.now.sh/upload_images/f37dd84b36bcbac2749ed7d9d5d7127f5f3f642a85f18.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/f37dd84b36bcbac2749ed7d9d5d7127f5f3f642a85f18.png?mw=700" alt="image.png" /></a></p> <p>「保存」を押すと、何やら真っ黒な画面が出てきました。</p> <p><a href="https://crieit.now.sh/upload_images/4565f66cb5268387fd9d4b68cdae17dd5f3f6590ae588.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/4565f66cb5268387fd9d4b68cdae17dd5f3f6590ae588.png?mw=700" alt="image" /></a></p> <p>文字が動いていれば、保存中なので終わるまで待ちます。<br /> 無事に終わると黒い画面が消えます。</p> <p><a href="https://crieit.now.sh/upload_images/f37dd84b36bcbac2749ed7d9d5d7127f5f3f65fc53186.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/f37dd84b36bcbac2749ed7d9d5d7127f5f3f65fc53186.png?mw=700" alt="image.png" /></a></p> <p>終わってから保存先に指定したフォルダを確認すると、しっかり保存できていることが確認できました。</p> <h2 id="無料動画GYAO!を保存する"><a href="#%E7%84%A1%E6%96%99%E5%8B%95%E7%94%BBGYAO%21%E3%82%92%E4%BF%9D%E5%AD%98%E3%81%99%E3%82%8B">無料動画GYAO!を保存する</a></h2> <p>Yahoo!のサービス「GYAO!」から、試しに「<a target="_blank" rel="nofollow noopener" href="https://gyao.yahoo.co.jp/p/00837/v09784/">むかしばなし ももたろう</a>」をmp4ファイルで保存してみます。</p> <p>まずは「らじる★らじる」の場合と同様に動画再生ページを開き、ブラウザの開発者ツールを開いてから、ページを再読み込みします。</p> <p><a href="https://crieit.now.sh/upload_images/f37dd84b36bcbac2749ed7d9d5d7127f5f3f701901c4d.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/f37dd84b36bcbac2749ed7d9d5d7127f5f3f701901c4d.png?mw=700" alt="image.png" /></a></p> <p>m3u8ファイルを見つけるのが面倒なので、「m3u8」で検索すると、一番上に出てきたファイルがそれっぽいのでURLをコピーします。</p> <p>「m3u8ToMP4.exe」を起動すると、無事に保存することができました。</p> <p><a href="https://crieit.now.sh/upload_images/f37dd84b36bcbac2749ed7d9d5d7127f5f3f718b8c84c.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/f37dd84b36bcbac2749ed7d9d5d7127f5f3f718b8c84c.png?mw=700" alt="image.png" /></a></p> <h1 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h1> <p>このように、「<a target="_blank" rel="nofollow noopener" href="https://github.com/ku-now/m3u8ToMP4">m3u8ToMP4</a>」ではHLS(HTTP Live Streaming)で配信されている動画を簡単に汎用的な形式に変換して保存することができます。</p> <p>VOD(オンデマンド配信)サービスやネットラジオなど、m3u8を用いた動画・音声配信サービスは近年非常に増えているので、もし動画や音声の保存・変換が必要な機会があればぜひ使ってみてください。</p> ウラル tag:crieit.net,2005:PublicArticle/16021 2020-08-01T03:02:13+09:00 2020-08-01T03:02:13+09:00 https://crieit.net/posts/PHP-5f245ca597498 各種小説投稿サイトのルビ記法をPHPで実現する <p>「<a target="_blank" rel="nofollow noopener" href="https://qiita.com/8amjp/items/d7c46d9dee0da4d530ef">各種小説投稿サイトのルビ記法をJavaScriptで実現する</a>」<br /> こちらの記事を参考にPHPで関数として作ってみました。</p> <pre><code class="php">function ruby(string $str) { $str = preg_replace('/[||](.+?)(?|((.+?))|\((.+?)\)|《(.+?)》)/u', '<ruby>$1<rt>$2</rt></ruby>', $str); $str = preg_replace('/(\p{Han}+)(?|((.+?))|\((.+?)\)|《(.+?)》)/u', '<ruby>$1<rt>$2</rt></ruby>', $str); return $str; } </code></pre> <p>短歌にルビを振る目的で作ったため、かなりルビが反映されやすくなっています。<br /> 普通の文章にルビを振る場合は、括弧のパターンを減らしたり、括弧がそのまま残る置換を追加したり、括弧内が平仮名カタカナだけになるようにカスタムするといいでしょう。<br /> 気が向いたら更新します。</p> ウラル tag:crieit.net,2005:PublicArticle/15705 2020-02-03T05:23:58+09:00 2020-02-03T19:19:44+09:00 https://crieit.net/posts/phpMyAdmin5-0-1-Fatal-Error phpMyAdmin5.0.1の「状態」タブがFatal Errorで表示されないバグ <p>どこにもエラー報告が見当たらなかったので。デフォルトでも発生する、大きいバグと思われる。問題詳細から解決方法まで記録用に。</p> <h1 id="事前に行なっていたこと"><a href="#%E4%BA%8B%E5%89%8D%E3%81%AB%E8%A1%8C%E3%81%AA%E3%81%A3%E3%81%A6%E3%81%84%E3%81%9F%E3%81%93%E3%81%A8">事前に行なっていたこと</a></h1> <p>環境:Windows 10</p> <p>64bit Windows版 XAMPP7.4.1(PHP 7.4.1, Apache 2.4.41, MariaDB 10.4.11, PHP 7.4.1, phpMyAdmin 5.0.1, OpenSSL 1.1.1, XAMPP Control Panel 3.2.4, Webalizer 2.23-04, Mercury Mail Transport System 4.63, FileZilla FTP Server 0.9.41, Tomcat 7.0.99 (with mod_proxy_ajp as connector), Strawberry Perl 5.16.3.1 Portable)をインストールした。</p> <p>起動前に若干設定ファイルを変更した。タイムアウトの延長や、前バージョンの設定の引継ぎなど。今回の問題に関わるような変更はしていない。</p> <h1 id="問題"><a href="#%E5%95%8F%E9%A1%8C">問題</a></h1> <p>「外観の設定」で「言語 - Language」を「日本語 - Japanese」にし、phpMyAdminの「状態」(http://localhost/phpmyadmin/server_status.php)を開いたときに、</p> <p><a href="https://crieit.now.sh/upload_images/f37dd84b36bcbac2749ed7d9d5d7127f5e371d0148fdd.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/f37dd84b36bcbac2749ed7d9d5d7127f5e371d0148fdd.png?mw=700" alt="image.png" /></a></p> <pre><code>Fatal error: Uncaught TypeError: mb_detect_encoding() expects parameter 1 to be string, bool given in C:\xampp\phpMyAdmin\libraries\classes\Util.php:1620 Stack trace: #0 C:\xampp\phpMyAdmin\libraries\classes\Util.php(1620): mb_detect_encoding(false, 'UTF-8', true) #1 C:\xampp\phpMyAdmin\libraries\classes\Controllers\Server\Status\StatusController.php(51): PhpMyAdmin\Util::localisedDate(1580669987) #2 C:\xampp\phpMyAdmin\server_status.php(35): PhpMyAdmin\Controllers\Server\Status\StatusController->index(Object(PhpMyAdmin\ReplicationGui)) #3 {main} thrown in C:\xampp\phpMyAdmin\libraries\classes\Util.php on line 1620 </code></pre> <p>と表示され、閲覧できない。<br /> 当該ソース(C:\xampp\phpMyAdmin\libraries\classes\Util.php)はこちら。</p> <p><a href="https://crieit.now.sh/upload_images/f37dd84b36bcbac2749ed7d9d5d7127f5e371f3f568f7.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/f37dd84b36bcbac2749ed7d9d5d7127f5e371f3f568f7.png?mw=700" alt="image.png" /></a></p> <pre><code class="php">/* Fill in AM/PM */ $hours = (int) date('H', (int) $timestamp); if ($hours >= 12) { $am_pm = _pgettext('AM/PM indication in time', 'PM'); } else { $am_pm = _pgettext('AM/PM indication in time', 'AM'); } $date = preg_replace('@%[pP]@', $am_pm, $date); $ret = strftime($date, (int) $timestamp); // Some OSes such as Win8.1 Traditional Chinese version did not produce UTF-8 // output here. See https://github.com/phpmyadmin/phpmyadmin/issues/10598 if (mb_detect_encoding($ret, 'UTF-8', true) != 'UTF-8') { $ret = date('Y-m-d H:i:s', (int) $timestamp); } </code></pre> <p>これはphpMyAdminを構成するコアな部分で、もちろん一切コードは変更していない。</p> <p>このバグは言語が日本語の時だけ発生する。他の言語では発生しないため、発見が遅れている可能性が高い。</p> <h1 id="原因"><a href="#%E5%8E%9F%E5%9B%A0">原因</a></h1> <p>該当ソースのユーザー定義関数<code>localisedDate()</code>の中で発生している。直接的には、1617行目のstrftime()がfalseを返していることが原因。php.netによれば</p> <blockquote> <p>エラー / 例外<br /> 出力内容は元となった C ライブラリに依存するため、サポートしていない変換指定子もあります。 Windows では、対応していない変換指定子を渡すと 5 つの E_WARNING メッセージが出て FALSE を返します。 その他のオペレーティングシステムでは特に E_WARNING メッセージは出ず、 変換指定子が (変換されずに) そのまま出力されます。</p> </blockquote> <p>とあり、Windows依存のバグ。</p> <p>先ほどのコードでは、strftime()の第一引数に、<code>$date</code>の中身として<code>%Y 年 2 月 %d 日 %H:%M</code>が渡されている。これ自体は仕様通りで問題が無いように見える。</p> <p>しかし、strftimeを処理する際に、依存するWindowsのCライブラリでは、<strong>文字列を一度Shift-JISに変換している</strong>。つまり、UTF-8→SJIS→UTF-8の変換が行われている。ここで、漢字の「月」の字が文字化けを起こし、不正なマルチバイト文字と認識され、strftime()がfalseを吐いている、というのが真相である。</p> <p>実際に、UTF-8→Shift-JIS→UTF-8変換を行うと、「%Y 年 2 � %d 日 %H:%M」となる。<br /> また、<code>$date</code>フォーマット文字列において漢字の「月」を削除すればこのバグは発生しなくなる。</p> <h1 id="結論"><a href="#%E7%B5%90%E8%AB%96">結論</a></h1> <p>要するに、このバグはOS依存の関数を用いるコードのほうに問題がある。strftime()ではなくdate()に書き換えるべきである。</p> <p>とりあえず日本語ユーザーができる仮対応としては、C:\xampp\phpMyAdmin\libraries\classes\Util.phpの1617行目を「$ret = strftime('%Y-%B-%d %H:%M', (int) $timestamp);」に書き換えることで閲覧できるようになる。</p> <p>Fatal Errorが出るのは相当だと思うので、もし他の方が同じバグに遭遇したら是非プロジェクトに熱いissueを送ってほしい。私はやり方がよくわからないので原因究明で終わりにします。</p> <p>【追記】<br /> だらさんに報告して頂きました、ありがとうございます。<br /> <a target="_blank" rel="nofollow noopener" href="https://github.com/phpmyadmin/phpmyadmin/issues/15830">https://github.com/phpmyadmin/phpmyadmin/issues/15830</a></p> ウラル tag:crieit.net,2005:PublicArticle/15576 2019-12-03T07:00:09+09:00 2019-12-03T07:00:09+09:00 https://crieit.net/posts/18776fe4237fa152b2d01ae84c654a81 どうして物を作ってしまうのか <p>去年僕は<a target="_blank" rel="nofollow noopener" href="https://splamp.info/octo/">オクトチャット</a>というサービスを作った。そして、それを2019年始めに公開したのを最後に、今年は受験などで忙しいので何も作らないようにするつもりだった。それどころではないだろう、と思っていた。<br /> ──はずなのに。</p> <p>なぜか今年も色々な物を作ってしまった。もしかすると去年よりもたくさん。あまり一つ一つに時間をかけられなかった分、ポコポコしたものがたくさんできた気がする。</p> <p>これは仕方なかった、そう、仕方なかったのだ!<br /> 不可抗力だった。物作りは自分にとっての呼吸のようなもので、作らなければ死んでしまうのだ。何かを作らないと怖いし、しんどいし、何より脳の容量が圧迫されていくのを感じる。物事に集中できなくなる。地上に打ち上げられて死を待つ魚のような気持ちになる。</p> <p>物作り中毒とでも言うべきだろうか。<br /> 正直、自分でも少しおかしいというか、病的な気はする。なるほど、そういう人間なのかもしれない。だが、それでいいのか?都合のいいように考えすぎではないか?</p> <p>もっとよく考えるべきだ。<br /> Twitterプロフィールの一番最初に書いてある「作るの大好き」という言葉。<br /> どうして自分は物を作ってしまうのか。思いつく限りの言い訳を考えることにした。</p> <h1 id="そもそも「作る」とは何なのか?"><a href="#%E3%81%9D%E3%82%82%E3%81%9D%E3%82%82%E3%80%8C%E4%BD%9C%E3%82%8B%E3%80%8D%E3%81%A8%E3%81%AF%E4%BD%95%E3%81%AA%E3%81%AE%E3%81%8B%EF%BC%9F">そもそも「作る」とは何なのか?</a></h1> <p>自分にとって、「作る」とは何を意味しているのか。</p> <p>Webサービスを作ることか?いや違う。自分はWebサービスが作りたいわけではない。<br /> 確かにWebが大好きで、今はWebにしか関わっていない。でも、Webエンジニアになりたいわけではないし、難しいことに挑戦したいという気持ちなどさらさらない。</p> <p>Webサービスを作ること。絵を描くこと。曲を作ること。小説を書くこと。ゲームを作ること。<br /> 実現できるかは置いといて、これらは一様に大好きで、どれも同じ「作りたい」という気持ちがある。<br /> より正確に書くなら、「創りたい」という気持ちだ。</p> <h2 id="創る、伝える、幸せにする"><a href="#%E5%89%B5%E3%82%8B%E3%80%81%E4%BC%9D%E3%81%88%E3%82%8B%E3%80%81%E5%B9%B8%E3%81%9B%E3%81%AB%E3%81%99%E3%82%8B">創る、伝える、幸せにする</a></h2> <p>自分には三信条がある。「<strong>創る、伝える、幸せにする</strong>」だ。<br /> 中学生のときに考えた。これは、自分の生きる指標であり、自分の考える正義である。</p> <p>「<strong>創る</strong>」は、文字通りCreateのことだ。Makeとはやや趣旨が異なる。<br /> 無から有を生み出す作業。世の中にないものを作り出すことだ。</p> <p>「<strong>伝える</strong>」は伝達すること。自分の意見をごまかしても相手には伝わらないのだから、まずは相手に自分の素直な気持ちを伝えろ、誤解させるな、という意味だ。</p> <p>「<strong>幸せにする</strong>」はそのまんまである。困難に直面したとき、選択肢を天秤にかけ、どちらがより多くの人を幸せにできるかを考えろ、という意味だ。</p> <p>つまり、自分の「作りたい」という欲求は、一番最初の「世の中にないものを作り出すこと」への欲求に違いない。<br /> 本題に入る。</p> <h1 id="どうして物を作ってしまうのか"><a href="#%E3%81%A9%E3%81%86%E3%81%97%E3%81%A6%E7%89%A9%E3%82%92%E4%BD%9C%E3%81%A3%E3%81%A6%E3%81%97%E3%81%BE%E3%81%86%E3%81%AE%E3%81%8B">どうして物を作ってしまうのか</a></h1> <h2 id="現実逃避のため?"><a href="#%E7%8F%BE%E5%AE%9F%E9%80%83%E9%81%BF%E3%81%AE%E3%81%9F%E3%82%81%EF%BC%9F">現実逃避のため?</a></h2> <p>それを言ったらおしまいだ!!<br /> と思ったが、よく考えると少し違うかもしれない。</p> <p>確かに、差し迫った現実から逃れるために、物を作ることは多い。<br /> 現実がしんどければしんどいほど、物作りは捗るものだ。締め切り前に何かを作り始めると、夢中になって課題のプレッシャーなどすっかり頭から消え去ってしまう。</p> <p>だが、幼いころを思い返すと、現実で特に逃げたいことがあったわけではないのに、何かを作り続けていた。保育園、小学校の休み時間、学童。特に課題に追われることもなかったから、何でもできた。</p> <p>そう考えると、やはり物作りは現実に左右されない麻薬のようなものなのだろうか。現実逃避が目的ではないが、現実逃避の役には立っている。</p> <h2 id="ストレス発散のため?"><a href="#%E3%82%B9%E3%83%88%E3%83%AC%E3%82%B9%E7%99%BA%E6%95%A3%E3%81%AE%E3%81%9F%E3%82%81%EF%BC%9F">ストレス発散のため?</a></h2> <p>自分は昔から物事を記憶するのが苦手だった。<br /> 人の顔を覚えられない。名前、誕生日、どれも中々覚えられない。数学の公式、英単語、過去のことから未来の予定まで、頭の中に残っていることはまずない。</p> <p>自分の頭の中はハンガーフックのようになっていて、考えていることをそこに引っかけておくのだが、すぐに忘れてしまうので、常に頭の片隅に意識し続けなければならない。だが、このフックは脳のメモリーを占めてしまっていて、これが本当にストレスで、とにかく頭の中は自分の考えたいことだけが占めるようにしたいのである。</p> <p>例えば、生きる意味について考えているときに、帰ってからするタスクに頭を奪われるのが許せるだろうか?だったらメモに書いて記憶から消したほうが得策である。というか、放っといたら生きる意味について考え始めてしまい、タスクのことはすっかり忘れてしまうので、メモしなければならないのだが。</p> <p>さらに、考え事というのは連想ゲームのようなもので、どんどん別のことを考え始めてしまう。困ったことに、生きる意味について答えを見つけたとして、そのまま次のことを考え始めると、さっきと同様にしてその答えは頭から消えてしまう。だから、覚えておきたいなら、メモするか、次のことを考えるのをやめてフックに引っかけて忘れないように唱え続けるしかない。</p> <p>これは考え事のメモばかりの話ではない。アイディアにしても同じなのだ。<br /> 自分の思いついたアイディアが、外部(現実)に存在していなければ、自分がそれを記憶していなければならない。ところがアイディアというのは自然に湧き出てくるものである。自分がそれを止める術はない。そして一度思いつくと、それが世の中に存在していないことがどうにも耐えられなくなる。そして脳にフックで引っかかったまま、解放できないモヤモヤが残る。</p> <p>悲しいことに、そのアイディアを捨てるだけの勇気もないのだ。中途半端に忘れて、ふと思い出した時、うまく思い出せなくてそれが逆に気になって、さらにモヤモヤが深まることは間違いない。<br /> だから少なくともメモはする。同時に連想ゲームが始まって、仕様も大体そのとき作る。ある程度、そこで忘れてしまうことへの恐怖とストレスからは解放される。</p> <p>で、だ。アイディアがあり、仕様がある。逃避したい現実もある(無くてもよい)。作れるだけの技術的目処が立っている。<br /> 少し手を動かすだけでこの想像は実体化され、脳から解放される。さもなくば現実に出力されるまでずっと脳に保持していなければならない。放っておくと時間経過で積み重なっていくストレス、使える分が減っていく脳のメモリー。…どうして作らない理由があるだろうか??</p> <p>・・・と、こんな感じで、気が付いたらアイディアがまた一つ形になっているのである。<br /> 考えていることを形にすることで、溢れかえる脳を解放していると言っていい。これは生きるために必要なことであり、呼吸と同じ、努力してすることではなく、むしろしないと息が詰まって死んでしまう、そういう類の作業。</p> <p>これを、もっとラフに言うなら「ストレス発散」なのである。</p> <h1 id="目的がない"><a href="#%E7%9B%AE%E7%9A%84%E3%81%8C%E3%81%AA%E3%81%84">目的がない</a></h1> <p>こんな基本的なことでも、丁寧に振り返らないと自分でもわからないものだ。やりたくなくてもやってしまう理由を考えていたから、ある意味無意識的なもので、仕方ないのかもしれない。<br /> 誰かにちやほやされなくてもいい。目的論も何も要らない。自己完結するルーティーン。</p> <p>ただ、まあこんな感じで意識低めでやってるもんだから、デメリットもある。<br /> それは、他人が先に作ってしまうとモチベがゼロになること。実在しないものを実在させるために作っていたのだから、他人が作ったらその時点で自分の動機は達成されてしまう。作りかけの時間が無駄になるのは嫌なので、そうならないようにさっさと仕上げるか、そういうときは見なかったことにして自分の分を急いで完成させるか、で何とか凌いでいる。</p> <h1 id="消えたくない"><a href="#%E6%B6%88%E3%81%88%E3%81%9F%E3%81%8F%E3%81%AA%E3%81%84">消えたくない</a></h1> <p>自分は、アイディアを忘れたくないから、逐一形にしている。形にする作業は没頭でき、それが完成することで、脳が解放され、ストレス発散することができる。</p> <p>記憶したくないから物作りとは、随分と変な過程である。これもこじつけなのだろうか?<br /> いや、昔の自分も作ったものを壊したくなかった。ずっと残しておきたかった。記憶の記録として。きっと、昔からそうだったに違いない。思いついたことを友達に話し、先生に話し、誰かに話すか書き残して、自分の記憶を残そうとしていた気がする。</p> <p>少し前にTwitterに投稿したことがある。</p> <blockquote class="twitter-tweet"><p lang="ja" dir="ltr">自分は死んだあともアカウント遺っててほしいなぁ…自分の考えてることが他人に伝えられないといつも感じてるせいかな、自分の思考の全てが外部に記録されていてほしいんだよね</p>— ウラル (@barley_ural) <a target="_blank" rel="nofollow noopener" href="https://twitter.com/barley_ural/status/1199551621060947969?ref_src=twsrc%5Etfw">November 27, 2019</a></blockquote> <blockquote class="twitter-tweet"><p lang="ja" dir="ltr">やっぱり存在への執着なんだろうなぁ、モノが存在しているかしていないかがすごく大切に感じていて、自分という存在が確実に証拠として残っていることに執着があるんだろな…</p>— ウラル (@barley_ural) <a target="_blank" rel="nofollow noopener" href="https://twitter.com/barley_ural/status/1199552373120626688?ref_src=twsrc%5Etfw">November 27, 2019</a></blockquote> <blockquote class="twitter-tweet"><p lang="ja" dir="ltr">過去の自分が嫌いでもあり愛おしくもあって。その瞬間の自分は別の一瞬の自分とは完全に別人だと思ってるから、過去に自分が別人であったことを残しておかなければその自分は死んでしまうから、ありとあらゆる自分の欠片を寄せ集めて取っておきたい、そんな感じ</p>— ウラル (@barley_ural) <a target="_blank" rel="nofollow noopener" href="https://twitter.com/barley_ural/status/1199558894453805057?ref_src=twsrc%5Etfw">November 27, 2019</a></blockquote> <p>自分は過去の自分とは違う人間だ。字も違うし考え方も違う。思い込みで怒ることなく、まずは受け容れることを大切にしようと思って、柔軟に自分自身を変えてきたと思う。でも、そのときその瞬間考えていたことは、事実であり、誰も否定できないものであり、なおかつ自分の記憶の中にしか生きることができない。だから、自分は過去の自分を大切しているし、絶対に忘れたくない。醜い姿も、ダメな自分も、全部全部実在した自分だから。</p> <p>自分が存在した理由を確かに証明してくれるのが制作物であり、創作物であって、それは自分自身にしか残せないのである。</p> <p>だから、私は創作をする。物を作る。</p> ウラル tag:crieit.net,2005:PublicArticle/15505 2019-10-26T04:35:45+09:00 2019-10-26T04:35:45+09:00 https://crieit.net/posts/Wayback-Machine-POST Wayback Machineに今のサイトをPOSTして登録するブックマークレット <p>Wayback Machine(<a target="_blank" rel="nofollow noopener" href="https://web.archive.org">https://web.archive.org</a>)に開いているサイトを一発で登録するブックマークレットを作りました。<br /> オプションの「Save outlinks」「Save error pages (HTTP Status=4xx, 5xx)」「Save screen shot」は全てオンにした状態です。</p> <pre><code class="javascript">javascript: (function() { var form = document.createElement('form'); form.action = 'https://web.archive.org/save'; form.method = 'post'; form.target = '_blank'; form.id = 'wmbl'; var hid = document.createElement('input'); hid.type = 'hidden'; hid.name = 'url'; hid.value = document.location; form.appendChild(hid); var hid2 = document.createElement('input'); hid2.type = 'hidden'; hid2.name = 'capture_outlinks'; hid2.value = 'on'; form.appendChild(hid2); var hid3 = document.createElement('input'); hid3.type = 'hidden'; hid3.name = 'capture_all'; hid3.value = 'on'; form.appendChild(hid3); var hid4 = document.createElement('input'); hid4.type = 'hidden'; hid4.name = 'capture_screenshot'; hid4.value = 'on'; form.appendChild(hid4); document.body.appendChild(form); document.getElementById('wmbl').submit(); } )(); </code></pre> <p>縮小版</p> <pre><code class="javascript">javascript:(function(){var c=document.createElement("form");c.action="https://web.archive.org/save";c.method="post";c.target="_blank";c.id="wmbl";var b=document.createElement("input");b.type="hidden";b.name="url";b.value=document.location;c.appendChild(b);var a=document.createElement("input");a.type="hidden";a.name="capture_outlinks";a.value="on";c.appendChild(a);var e=document.createElement("input");e.type="hidden";e.name="capture_all";e.value="on";c.appendChild(e);var d=document.createElement("input");d.type="hidden";d.name="capture_screenshot";d.value="on";c.appendChild(d);document.body.appendChild(c);document.getElementById("wmbl").submit()})(); </code></pre> <p>こちらを参考に作りました。<br /> <a target="_blank" rel="nofollow noopener" href="https://q.hatena.ne.jp/1213176191">https://q.hatena.ne.jp/1213176191</a></p> <p>汎用的に使えるようになってますので、サーバーにPOSTする系のブックマークレットを作りたくなったときは<code>value</code>などを改変して使ってください。</p> <p>ちなみに書いてから調べて知りましたが、昔の仕様の名残りでWayback Machineは/save/の下に直接URLを貼っても登録できるようです。オプションはどうなってるんだろう…。</p> <pre><code class="javascript">javascript:var u=location.href;w='http://web.archive.org/save/'+u;window.open(w, null); </code></pre> <p>引用元:<a target="_blank" rel="nofollow noopener" href="http://blog.tainakanchu.pw/article/waybackbookmarklet.html">http://blog.tainakanchu.pw/article/waybackbookmarklet.html</a></p> <p><a target="_blank" rel="nofollow noopener" href="http://archive.is">archive.is</a>も簡単に登録できます。(公式ブックマークレットがあります。)</p> <pre><code class="javascript">javascript:void(open('http://archive.today/?run=1&url='+encodeURIComponent(document.location))) </code></pre> <p>以上です!</p> ウラル tag:crieit.net,2005:PublicArticle/15498 2019-10-22T16:42:46+09:00 2019-10-22T17:08:45+09:00 https://crieit.net/posts/SNS-URL ドメイン殺しにやられた話。SNS時代の外部URL遷移は超キケン <p>先日、長いこと閉鎖していたサービスを復活させました。</p> <p><strong>OGP changer</strong><br /> <a target="_blank" rel="nofollow noopener" href="https://ogpc.ga/">https://ogpc.ga/</a></p> <p>これは、SNS共有用のURLを発行するサービスです。閉鎖に至ったのには大きな理由がありました。</p> <h1 id="短縮URLサービスのドメインを殺された"><a href="#%E7%9F%AD%E7%B8%AEURL%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9%E3%81%AE%E3%83%89%E3%83%A1%E3%82%A4%E3%83%B3%E3%82%92%E6%AE%BA%E3%81%95%E3%82%8C%E3%81%9F">短縮URLサービスのドメインを殺された</a></h1> <p>きっかけは<a target="_blank" rel="nofollow noopener" href="https://splamp.info/">弊サイト</a>が管理する別の短縮URLサービス「URL Shortener」(<a target="_blank" rel="nofollow noopener" href="https://lmp.tw">lmp.tw</a>)の悪用でした。</p> <p>URL Shortenerは純粋な短縮URLサービスです。ユーザー数が少ないため、他よりも短いURLを取得できるのがウリでした。</p> <p>ところが。</p> <blockquote class="twitter-tweet"><p lang="ja" dir="ltr">短縮URLサービスでテロられたこんなことあるんか</p>— ウラル (@barley_ural) <a target="_blank" rel="nofollow noopener" href="https://twitter.com/barley_ural/status/1116611726206627844?ref_src=twsrc%5Etfw">April 12, 2019</a></blockquote> <p>まさかの事態に。</p> <h2 id="何が起きたのか?"><a href="#%E4%BD%95%E3%81%8C%E8%B5%B7%E3%81%8D%E3%81%9F%E3%81%AE%E3%81%8B%EF%BC%9F">何が起きたのか?</a></h2> <p>lmp.twドメインが全てTwitterに弾かれ、一切ツイートできないようになっていました。既にツイートされたリンクを踏むと、全てのページでセキュリティエラーが表示されるようになりました。<br /> 一言でいうと、<strong>ブラックリストに登録された</strong>のです。</p> <p><img src="https://pbs.twimg.com/media/D38RHjhVUAAX00G?format=jpg&name=medium" alt="image" /></p> <blockquote class="twitter-tweet"><p lang="ja" dir="ltr">危険なサイトへのリダイレクトを作られてしまって、そのせいで短縮URLサービスのドメイン丸ごとTwitterに弾かれてる他のリンクも全部ブラックリスト扱いされてる…</p>— ウラル (@barley_ural) <a target="_blank" rel="nofollow noopener" href="https://twitter.com/barley_ural/status/1116612307151245319?ref_src=twsrc%5Etfw">April 12, 2019</a></blockquote> <blockquote class="twitter-tweet"><p lang="ja" dir="ltr">経緯を説明すると、lmp. tw配下全てのリンクに警告表示が出ることを発見→危険なサイトにリダイレクトさせるツイートが投稿されていた→直ちに該当ツイートのリンクの無効化と管理する全てのURLリダイレクトサービス新規登録を停止 <a target="_blank" rel="nofollow noopener" href="https://t.co/ZsERui2Jp2">pic.twitter.com/ZsERui2Jp2</a></p>— ウラル (@barley_ural) <a target="_blank" rel="nofollow noopener" href="https://twitter.com/barley_ural/status/1116630154342572034?ref_src=twsrc%5Etfw">April 12, 2019</a></blockquote> <p>この人物は、lmp.twの他にも複数の短縮URLサービスで危険なリンクを作成し、ツイートすることで、サービスを殺しているようでした。悪意があるとしか言いようがありません。</p> <p><a href="https://crieit.now.sh/upload_images/f37dd84b36bcbac2749ed7d9d5d7127f5dae73e69bd42.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/f37dd84b36bcbac2749ed7d9d5d7127f5dae73e69bd42.png?mw=700" alt="image.png" /></a></p> <p>これまでも数多くの短縮URLサービスが出来ては消えていきましたが、このような悪意ある利用がその主な原因だったと言えるでしょう。</p> <p>私はすぐにサービスを停止せざるを得ませんでした。そして、同様のリスクがある以上、他のURLリダイレクトサービスも動かすわけにはいかず、OGP changer(<a target="_blank" rel="nofollow noopener" href="https://ogpc.ga/">ogpc.ga</a>)も停止することになったのでした。</p> <h2 id="ドメインを取り戻すまで"><a href="#%E3%83%89%E3%83%A1%E3%82%A4%E3%83%B3%E3%82%92%E5%8F%96%E3%82%8A%E6%88%BB%E3%81%99%E3%81%BE%E3%81%A7">ドメインを取り戻すまで</a></h2> <p>lmp.twがブラックリスト入りしたことで、私のTwitterアカウントのプロフィールのリンクが使えなくなりました。</p> <p><a href="https://crieit.now.sh/upload_images/f37dd84b36bcbac2749ed7d9d5d7127f5dae7a6d335a5.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/f37dd84b36bcbac2749ed7d9d5d7127f5dae7a6d335a5.png?mw=700" alt="image.png" /></a></p> <p>これでは信用に関わります。泣く泣くリンクを削除しましたが、このまま放置するわけにはいきません。危険なリンクは既に削除したので、まずはドメインを回復させます。</p> <p>危険なURLと判断された流れは次の通りでしょう。</p> <ol> <li>元々「Google Safe Browsing」(URL安全性確認サービス)によって危険と判断されていたURLを、悪意のあるユーザーが短縮URLサービスに登録し、ツイートする。</li> <li>GoogleによってURLが認識され、Google Safe Browsingでドメイン全体に危険ステータスが付与される。</li> <li>Twitterや他のセキュリティサービスがその情報を元にドメイン全体をブロックする。</li> </ol> <p>ですから、まずはGoogle Safe Browsingによるスパム判定を解除する必要があります。</p> <p><img src="https://pbs.twimg.com/media/D38RHjhU8AASaw8?format=jpg&name=medium" alt="image" /></p> <p>サイトが危険と見なされているかどうかは<a target="_blank" rel="nofollow noopener" href="https://transparencyreport.google.com/safe-browsing/search"><strong>このページ</strong></a>から確認できます。</p> <p><a href="https://crieit.now.sh/upload_images/f37dd84b36bcbac2749ed7d9d5d7127f5daea6e72e057.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/f37dd84b36bcbac2749ed7d9d5d7127f5daea6e72e057.png?mw=700" alt="image.png" /></a></p> <p>Google Safe Browsingに再審査を要求する手順は<a target="_blank" rel="nofollow noopener" href="https://developers.google.com/web/fundamentals/security/hacked/request_review"><strong>こちら</strong></a>です。<br /> 審査に合格するとステータスが解除されます。</p> <p>Google Safe Browsingによるステータスが解除され次第、マカフィーなど他のセキュリティサービスの解除申請を送ると良いでしょう。</p> <p>Twitterでは、「<a target="_blank" rel="nofollow noopener" href="https://help.twitter.com/forms/spam"><strong>スパムを報告</strong></a>」というページから、「Twitterがスパムと判断したため、リンクをツイートできません。」を選ぶことで、スパム判定を食らったリンクを回復できます。</p> <p><a href="https://crieit.now.sh/upload_images/f37dd84b36bcbac2749ed7d9d5d7127f5dae7d48c419b.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/f37dd84b36bcbac2749ed7d9d5d7127f5dae7d48c419b.png?mw=700" alt="image.png" /></a></p> <p>色々やった結果、lmp.tw自体は数週間で回復しました。<br /> 信用を失うのは一瞬だが、取り戻すのは本当に手間と時間がかかる、と実感しました。</p> <h1 id="対策"><a href="#%E5%AF%BE%E7%AD%96">対策</a></h1> <p><a target="_blank" rel="nofollow noopener" href="https://ogpc.ga/">OGP changer</a>はそんなわけで、同様のリスクがある以上復活させられないことから、長い間「改修中」と称し閉鎖していました。<br /> ちなみにずっと404を返していたところ、ドメイン管理サービスに「使用実態がない」と見なされ、ドメインを剥奪されるハプニングもありました…。</p> <h2 id="Safe Browsing APIの利用"><a href="#Safe+Browsing+API%E3%81%AE%E5%88%A9%E7%94%A8">Safe Browsing APIの利用</a></h2> <p>Google Safe Browsingによって弾かれたのですから、同じ基準で守ればよいのです。<br /> GoogleのSafe Browsing APIを使うと、そのリンクが安全かどうかを判定できます。</p> <p>こちらのブログを参考に、PHPでURLの判定を行なうようにしました。</p> <blockquote> <p>サイトの安全性を確かめるSafe Browsing APIをPHPで試す<br /> <a target="_blank" rel="nofollow noopener" href="https://minory.org/safe-browsing-api.html">https://minory.org/safe-browsing-api.html</a></p> </blockquote> <p>APIキーなどはGoogle Cloud Platformでプロジェクトを作成して取得します。</p> <h1 id="感想"><a href="#%E6%84%9F%E6%83%B3">感想</a></h1> <p>外部へのURL遷移には今後注意していこうと思いました。無意味な遷移はできるだけ控えたほうがよいでしょう。もしサービスの中で外部URL遷移を利用している場合は、何かしらの対策を行うべきです。<br /> Safe Browsing APIの利用の他、HTTPのLocationによる遷移ではなく、HTMLのmetaタグによる遷移や、javascriptによる遷移に変更するのも良いでしょう。<br /> まだまだ自分の知らないセキュリティリスクは多いなと感じました。</p> <p>最後に、半年ぶりに復活を果たした<a target="_blank" rel="nofollow noopener" href="https://ogpc.ga/">OGP changer</a>をよろしくお願いします。</p> <p><a href="https://crieit.now.sh/upload_images/f37dd84b36bcbac2749ed7d9d5d7127f5daeb2e276daf.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/f37dd84b36bcbac2749ed7d9d5d7127f5daeb2e276daf.png?mw=700" alt="image.png" /></a></p> ウラル tag:crieit.net,2005:PublicArticle/15419 2019-09-24T04:49:33+09:00 2019-09-24T04:52:06+09:00 https://crieit.net/posts/3196986630826e80e694cf840dd8f493 意外と簡単!自分のサイトをダークモード対応してみた【技術編】 <p><a href="https://crieit.now.sh/upload_images/ab7fe98314e669b78bed589d5154e4b05d89099d3d9f8.PNG" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/ab7fe98314e669b78bed589d5154e4b05d89099d3d9f8.PNG?mw=700" alt="IMG_8789.PNG" /></a></p> <p>スプランプ(<a target="_blank" rel="nofollow noopener" href="https://splamp.info">splamp.info</a>)というサイトの管理人をしているウラルです。</p> <p>来ましたよ、ダークモードの時代が。2019年はダークモード元年です。</p> <h1 id="ダークモードの時代が来た"><a href="#%E3%83%80%E3%83%BC%E3%82%AF%E3%83%A2%E3%83%BC%E3%83%89%E3%81%AE%E6%99%82%E4%BB%A3%E3%81%8C%E6%9D%A5%E3%81%9F">ダークモードの時代が来た</a></h1> <p>何を言っているかお分かりでしょうか。<br /> 実は、2019年はWindows・iOS・Androidという三大OSがダークモード対応をするという、大きな時代の節目なのです。</p> <h2 id="OSの更新日とダークモード設定方法"><a href="#OS%E3%81%AE%E6%9B%B4%E6%96%B0%E6%97%A5%E3%81%A8%E3%83%80%E3%83%BC%E3%82%AF%E3%83%A2%E3%83%BC%E3%83%89%E8%A8%AD%E5%AE%9A%E6%96%B9%E6%B3%95">OSの更新日とダークモード設定方法</a></h2> <p><strong>2019年5月22日</strong><br /> Windows 10 May 2019 Update配信開始。「個人用設定 > 色」から「既定のアプリモード」を「黒」にする。</p> <p><strong>2019年9月4日</strong><br /> Android10配信開始。「設定 > ディスプレイ」から「ダークテーマ」をオンにする。</p> <p><strong>2019年9月20日</strong><br /> iOS13配信開始。「設定 > 画面表示と明るさ」から「外観モード」を「ダーク」にする。</p> <h1 id="Webが対応しないでどうする"><a href="#Web%E3%81%8C%E5%AF%BE%E5%BF%9C%E3%81%97%E3%81%AA%E3%81%84%E3%81%A7%E3%81%A9%E3%81%86%E3%81%99%E3%82%8B">Webが対応しないでどうする</a></h1> <p>個人的な所感ですが、開発者の中でダークモード対応は興味としては薄いように感じます。</p> <p>しかし、Webこそダークモードの流れに乗るべきなのです。なぜなら、ほとんどのWebサイトの背景は真っ白じゃないですか!目が眩むし、夜眠れなくなりそうです。</p> <p>私は、以前からWebサイトでダークモードの対応をしたかったのですが、OSレベルでのダークモード対応が普及していなかったため、ずっと待っていました。サイト内にダークモードボタンを設置しても、ブラウザが真っ白では意味がありませんからね。</p> <p>三大OSが対応した今、もう何も遮るものはありません。</p> <h1 id="CSSだけで実装できる"><a href="#CSS%E3%81%A0%E3%81%91%E3%81%A7%E5%AE%9F%E8%A3%85%E3%81%A7%E3%81%8D%E3%82%8B">CSSだけで実装できる</a></h1> <p>ダークモード対応は、難しい技術が必要なのでしょうか。いいえ、CSSだけでいいんです。</p> <p>CSSのメディアクエリの1つである、<code>prefers-color-scheme</code>を使います。</p> <blockquote> <p><code>prefers-color-scheme</code> は CSS のメディア特性で、ユーザーがシステムに要求したカラーテーマが明色か暗色かを検出するために使用します。<br /> <a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/ja/docs/Web/CSS/@media/prefers-color-scheme">prefers-color-scheme - CSS: カスケーディングスタイルシート | MDN</a></p> </blockquote> <p>構文によると、ダークモードがオンになると<code>prefers-color-scheme</code>が<code>dark</code>になり、オフだと<code>light</code>、そもそも機能を持ち合わせていない場合は<code>no-preference</code>という値になります。</p> <p>実際に利用する場合はどう使うのでしょうか。<br /> おすすめは、CSS変数との併用です。既存の構造を崩さずに、ダークモード用の色彩設定を追加することができます。</p> <p><em>例</em></p> <pre><code class="css">:root { --header-bg: white; --body-bg: white; --body-color: black; } @media (prefers-color-scheme: dark) { :root { --header-bg: hsl(212, 38%, 12%); --wrapper-bg: hsl(212, 40%, 17%); --body-color: #e4e4e4; } } header { background: var(--header-bg); } body { background: var(--body-bg); color: var(--body-color); } </code></pre> <p>CSS変数を参照させることで、ダークモードに切り替わると一斉に色が変わります。</p> <p><img src="https://splamp.info/shed/assets/IMG_8788.GIF" alt="gif" /></p> <p>imgタグの画像の切り替えは若干面倒ですが、私はライト用とダーク用の2つを連続で並べて、CSSで<code>display: none</code>と<code>display: inline</code>を入れ替えるようにしました。</p> <h1 id="大変だけど"><a href="#%E5%A4%A7%E5%A4%89%E3%81%A0%E3%81%91%E3%81%A9">大変だけど</a></h1> <p>これまで白ベースで作ってきたサイトにとっては、ダークモードの対応は破壊的な修正にあたります。角丸で四隅が白い画像だとか、色でごまかしていた要素の境目などを、見直さなければいけません。</p> <p>しかし、それはデメリットばかりではありません。それは、負の遺産になりかけていたCSSを直すチャンスかもしれません。(少なくともうちのサイトは構造から改善しました)</p> <p>三大OSがダークモードに対応した今、時代の流れはダークモード対応です。<br /> 実装自体は簡単ではあるものの、色使いの検討や構造の見直しに割と時間がかかるので、早め早めに手を付けたいものです。<br /> アクセシビリティとか言って、やがて対応に迫られてくるのは間違いありません。</p> <p>次回は【デザイン編】として、ダークモードデザインについて記事にしようと思います。</p> ウラル tag:crieit.net,2005:PublicArticle/15418 2019-09-24T00:05:17+09:00 2019-09-24T00:20:41+09:00 https://crieit.net/posts/Chrome-placeholder-757575 Chromeのplaceholderの色は#757575 <p>HTMLの<code><input></code>や<code><textarea></code>に使われる<code>placeholder</code>属性の文字色は、CSSでは疑似要素<code>::placeholder</code>の中で設定できますが、デフォルトの色が分からなかったので調べました。</p> <p><a href="https://crieit.now.sh/upload_images/f37dd84b36bcbac2749ed7d9d5d7127f5d88e29c1b118.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/f37dd84b36bcbac2749ed7d9d5d7127f5d88e29c1b118.png?mw=700" alt="image.png" /></a><br /> ▲これのことね</p> <h1 id="結果"><a href="#%E7%B5%90%E6%9E%9C">結果</a></h1> <p>Chromeでスポイトで色を取って確認したところ、<code><input></code>も<code><textarea></code>も<code>#757575</code>=<code>rgb(117, 117, 117)</code>=<code>hsl(0, 0%, 46%)</code>でした。</p> <h1 id="感想"><a href="#%E6%84%9F%E6%83%B3">感想</a></h1> <p>デフォルトがCSSでは定義されておらず開発者ツールではわからないので、スポイト使うしかありませんでした(´・ω・`)</p> <p><a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/ja/docs/Web/CSS/::placeholder">https://developer.mozilla.org/ja/docs/Web/CSS/::placeholder</a></p> ウラル tag:crieit.net,2005:PublicArticle/15398 2019-09-16T00:18:49+09:00 2019-09-16T00:30:47+09:00 https://crieit.net/posts/0ad3fb07d90d16ac9612cde5d92f6bed オブジェクト指向とは何だったのか(初心者として) <p>極度の面倒くさがりにより、漠然とオブジェクト指向から距離を置いてきたが、最近初めて触れ、やっと理解できた気がするので書く。</p> <p>世の人間はオブジェクト指向を説明するとき、たい焼きだの設計図だの何だのと言い始める。<br /> そうじゃないんだ。読めるようになりたいんだ。</p> <p>ということで、自分なりにまとめてみる。PHPを例にする。誰かの役に立てたら嬉しい。</p> <h1 id="オブジェクトって何"><a href="#%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%81%A3%E3%81%A6%E4%BD%95">オブジェクトって何</a></h1> <p>まず、「オブジェクト指向」とはそもそも何かと言うと、設計思想の1つのことだ。<br /> そしてこのオブジェクト指向を実現するため、「クラス構文」という書式がある。要はこれが使えればいいのだ。</p> <p><a href="https://crieit.now.sh/upload_images/f37dd84b36bcbac2749ed7d9d5d7127f5d7cecde17e98.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/f37dd84b36bcbac2749ed7d9d5d7127f5d7cecde17e98.png?mw=700" alt="image.png" /></a></p> <p>「オブジェクト」とは関数と変数のカタマリの事である。関数と変数が梱包された状態だと思ってほしい。</p> <p><a href="https://crieit.now.sh/upload_images/f37dd84b36bcbac2749ed7d9d5d7127f5d7ceecf2278b.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/f37dd84b36bcbac2749ed7d9d5d7127f5d7ceecf2278b.png?mw=700" alt="image.png" /></a></p> <p>このオブジェクトのことを「クラス」とも呼ぶ。「クラス構文」とは、つまりオブジェクトを定義する書式だと思えばいい。</p> <p style="font-size:smaller;margin:0;padding:0;">※わかりやすくするため、ここでは「オブジェクト」=「クラス」とする。後述する「インスタンス」はオブジェクトと呼ばれやすいが、クラスとは別物。「オブジェクト」=関数と変数のカタマリの総称、「クラス」=オブジェクトを定義したもの、「インスタンス」=クラスをインスタンス化したもの、なんだなーと思っておけば大丈夫。</p> <h1 id="オブジェクトを使っていく流れ"><a href="#%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E3%81%84%E3%81%8F%E6%B5%81%E3%82%8C">オブジェクトを使っていく流れ</a></h1> <p>オブジェクトは定義して呼び出すのが基本で、こういう流れになる。</p> <p><strong>【定義する】</strong></p> <p>空っぽのオブジェクトを用意する。<br /> ⇓<br /> オブジェクトの中に変数や関数を入れていく。<br /> ⇓<br /> オブジェクトが定義できた!(オブジェクト完成!)</p> <p><strong>【呼び出す】</strong></p> <p>定義されたオブジェクトを読み込む。<br /> ⇓<br /> オブジェクトから変数や関数を引っ張りだして使う。</p> <h1 id="オブジェクトの中身を呼び出すには"><a href="#%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%81%AE%E4%B8%AD%E8%BA%AB%E3%82%92%E5%91%BC%E3%81%B3%E5%87%BA%E3%81%99%E3%81%AB%E3%81%AF">オブジェクトの中身を呼び出すには</a></h1> <p>オブジェクトは、中身の呼び出し方を考えてから定義することが多いので、先に呼び出し方を確認しておこう。</p> <h2 id="例"><a href="#%E4%BE%8B">例</a></h2> <p>例として、引数に与えた文字をそのまま表示するユーザー定義関数<code>foo()</code>を用意した。</p> <pre><code class="php">function foo($str) { echo $str; } </code></pre> <p>この関数を、どんな名前でもいいが、ここでは<code>MyProject</code>という名前のオブジェクトに入れることにする。(「オブジェクトを定義するには」で手順は後述)<br /> 早速オブジェクト<code>MyProject</code>から<code>foo()</code>を呼び出してみる。</p> <pre><code class="php">// 事前にオブジェクトが定義されているとする $obj = new MyProject; $obj->foo('おはよう!'); // おはよう!が表示される </code></pre> <p>最初の行を見てほしい。<br /> <code>$obj</code>という変数の中に、オブジェクトが代入されている。</p> <p>そう、オブジェクトは<code>new 〇〇</code>という書式で、変数に一度代入することで使えるようになる。これを「インスタンス化」と呼び、このとき、変数は「オブジェクト型」という状態になっている。</p> <p>つまり、上記の例ではオブジェクト型の変数<code>$obj</code>に、事前に定義したオブジェクト<code>MyProject</code>の中身がそのまま入っているのだ。</p> <p><a href="https://crieit.now.sh/upload_images/f37dd84b36bcbac2749ed7d9d5d7127f5d7d037044f18.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/f37dd84b36bcbac2749ed7d9d5d7127f5d7d037044f18.png?mw=700" alt="image.png" /></a></p> <p>この代入された変数のことを「インスタンス」という。インスタンス=オブジェクト型の変数のことである。</p> <p>続きを見てほしい。<br /> <code>$obj->foo('おはよう!')</code>とある。これは、オブジェクトのインスタンスから<code>foo()</code>というユーザー定義関数を呼び出している。</p> <p><a href="https://crieit.now.sh/upload_images/f37dd84b36bcbac2749ed7d9d5d7127f5d7d072b8b744.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/f37dd84b36bcbac2749ed7d9d5d7127f5d7d072b8b744.png?mw=700" alt="image.png" /></a></p> <p>このとき、普通の関数とは呼び出し方が異なるので、この関数のことを特別に「メソッド」と呼ぶ。</p> <p>やたら用語ばかり出てきて申し訳ない。先ほどのコードはこういうことである。<br /> <a href="https://crieit.now.sh/upload_images/f37dd84b36bcbac2749ed7d9d5d7127f5d7e3c4152609.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/f37dd84b36bcbac2749ed7d9d5d7127f5d7e3c4152609.png?mw=700" alt="image.png" /></a></p> <h1 id="オブジェクトを定義するには"><a href="#%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%82%92%E5%AE%9A%E7%BE%A9%E3%81%99%E3%82%8B%E3%81%AB%E3%81%AF">オブジェクトを定義するには</a></h1> <p>さて、ここで先ほどの例で使ったオブジェクト<code>MyProject</code>を定義する手順だ。</p> <p>最初に空っぽのオブジェクトを用意する。こう書く。</p> <pre><code class="php">class MyProject { } </code></pre> <p>この中にさっき作ったユーザー定義関数<code>foo()</code>を入れてみると…</p> <pre><code class="php">class MyProject { function foo($str) { echo $str; } } </code></pre> <p>オブジェクトが完成!<br /> これだけ?これだけ!<br /> これで、<code>MyProject</code>というオブジェクトの中に<code>foo()</code>というメソッドが定義されたことになる。</p> <p>さっき呼び出しの部分で書いたものと組み合わせてみよう。</p> <pre><code class="php">class MyProject { function foo($str) { echo $str; } } $obj = new MyProject; $obj->foo('おはよう!'); // おはよう!が表示される </code></pre> <p>これで一段落。これが理解できたあなたは、とても基本的なクラス構文をマスターしたことになる。</p> <h1 id="オブジェクトにしかできないこと…プロパティ"><a href="#%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%81%AB%E3%81%97%E3%81%8B%E3%81%A7%E3%81%8D%E3%81%AA%E3%81%84%E3%81%93%E3%81%A8%E2%80%A6%E3%83%97%E3%83%AD%E3%83%91%E3%83%86%E3%82%A3">オブジェクトにしかできないこと…プロパティ</a></h1> <p>ここまで来た人は疑問に思うかもしれない。<br /> 「関数をメソッドにする意味って何なの?」「それ、ユーザー定義関数で良くね?」と。</p> <p>ここからは、オブジェクト指向だからできること、プロパティの仕組みを説明する。</p> <p>「オブジェクトって何?」で説明した図を思い出してほしい。</p> <p><a href="https://crieit.now.sh/upload_images/f37dd84b36bcbac2749ed7d9d5d7127f5d7ceecf2278b.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/f37dd84b36bcbac2749ed7d9d5d7127f5d7ceecf2278b.png?mw=700" alt="image.png" /></a></p> <p>そう、梱包されてるのは関数(メソッド)だけじゃなかった。変数もいた。<br /> このオブジェクトの中で定義された変数のことを、「プロパティ」と呼ぶ。</p> <p>プロパティの基本的な定義の仕方と呼び出し方は次の通り。</p> <pre><code class="php">class MyProject { public $text = 'おはよう!'; // これがプロパティ } $obj = new MyProject; echo $obj->text; // プロパティの呼び出し </code></pre> <p>メソッドと似ていることがわかるだろうか?<br /> そう、プロパティとメソッドは扱い方がとてもよく似ている。</p> <p>今回は、最初に少し見慣れないキーワード「public」について説明しよう。<br /> これは、メソッド(関数)やプロパティ(変数)のアクセス権を示している。<br /> PHPではpublic, protected, privateの3つがある。</p> <div class="table-responsive"><table> <thead> <tr> <th>アクセス権</th> <th>範囲</th> </tr> </thead> <tbody> <tr> <td>public</td> <td>オブジェクトの外部から呼び出せる。継承されたオブジェクトから呼び出せる。オブジェクト内のメソッドから呼び出せる。</td> </tr> <tr> <td>protected</td> <td>オブジェクトの外部から呼び出せない。継承されたオブジェクトから呼び出せる。オブジェクト内のメソッドから呼び出せる。</td> </tr> <tr> <td>private</td> <td>オブジェクトの外部から呼び出せない。継承されたオブジェクトから呼び出せない。オブジェクト内のメソッドから呼び出せる。</td> </tr> </tbody> </table></div> <p>プロパティではこれらのアクセス権を必ず明示する必要がある。メソッドでは、省略した場合publicとしてみなされる。<br /> つまり、前項の例で作ったメソッドは、publicを省略していたということだ。省略できるのだが、アクセス権はちゃんと書いておいたほうが喜ばれるだろう。</p> <p>さて、プロパティで注意してほしいのは、メソッドの外でないと宣言できないということだ。<br /> 例えば、以下のようなことはできない。</p> <pre><code class="php">class MyProject { // publicなメソッドを宣言 public function foo() { public $text = 'おはよう!'; // メソッド内なのでエラー echo $text; } } $obj = new MyProject; echo $obj->foo(); </code></pre> <p>これは、ユーザー定義関数の特性上、その中で使われる変数はローカルスコープでないといけないからだ。<br /> もしメソッドの中でプロパティを使いたいのなら、<code>$obj->text</code>みたいな感じで変数を呼び出さなければならない。だが、自分のオブジェクトの中で自分のオブジェクトを指定するのもおかしいよな…ということで、擬似変数<code>$this</code>が登場する。</p> <pre><code class="php">class MyProject { public $text = 'おはよう!'; public function foo() { echo $this->text; // $thisは自分自身を表す } } $obj = new MyProject; echo $obj->foo(); // おはよう! </code></pre> <p><code>$this</code>を使うと、プロパティを活かして、例えばこんなことができる。</p> <h2 id="カートに商品を追加する例"><a href="#%E3%82%AB%E3%83%BC%E3%83%88%E3%81%AB%E5%95%86%E5%93%81%E3%82%92%E8%BF%BD%E5%8A%A0%E3%81%99%E3%82%8B%E4%BE%8B">カートに商品を追加する例</a></h2> <pre><code class="php">class Cart { protected $quantities = []; // 商品ごとの量が入る public function add($product, $quantity) { $this->quantities[$product] = $quantity; } public function get_quantity($product) { return $product . 'は' . $this->quantities[$product] . '個'; } } $my_cart = new Cart; $my_cart->add('バター', 1); $my_cart->add('牛乳', 3); $my_cart->add('卵', 6); echo $my_cart->get_quantity('牛乳'); // 牛乳は3個 </code></pre> <p>合計金額を表示するメソッドも作れる。</p> <pre><code class="php">class Cart { protected $prices = [ 'バター' => 600, '牛乳' => 180, '卵' => 200, 'アイス' => 100 ]; protected $quantities = []; public function add($product, $quantity) { if(isset($this->prices[$product])) { $this->quantities[$product] = $quantity; } } public function get_quantity($product) { return $product . 'は' . $this->quantities[$product] . '個'; } public function get_total_price() { $total_price = 0; foreach($this->quantities as $key => $quantity) { $total_price += $this->prices[$key] * $quantity; } return $total_price; } } $my_cart = new Cart; $my_cart->add('バター', 1); $my_cart->add('牛乳', 3); $my_cart->add('卵', 6); echo $my_cart->get_total_price() . '円'; // 2340円 </code></pre> <p>オブジェクト指向の便利さはここからだ。上の例の後半だけ次のように書き換える。</p> <pre><code class="php">$cart1 = new Cart; $cart1->add('バター', 1); $cart1->add('バター', 3); $cart1->add('卵', 6); $cart1->add('卵', -3); $cart2 = new Cart; $cart2->add('アイス', 2); $cart2->add('牛乳', 1); echo $cart1->get_total_price() . '円'; // 1200円 echo $cart2->get_total_price() . '円'; // 380円 </code></pre> <p>インスタンス<code>$cart1</code>, <code>$cart2</code>が別々のものとして計算されている。つまり、2つのオブジェクトは独立して動作しているのだ。</p> <p>これがインスタンス化の最大のメリットである。もしこの処理をユーザー定義関数で無理やり実現しようとすると、それぞれの処理が複雑に干渉してしまい、たちまち地獄が訪れるだろう。</p> <p>処理をいい感じに独立させることは、プログラミングをする上でとても大切だ。また、プログラムに将来への拡張性があることも、どれほど大切かわかる人は多いだろう。</p> <p>オブジェクト指向はこれを実現するためにある。<br /> インスタンス化で複数の機能をいい感じに独立させられる。またクラスの中にメソッドを追加しても他の処理に影響を与えないため、将来への拡張性も確保できる。</p> <h1 id="おわりに"><a href="#%E3%81%8A%E3%82%8F%E3%82%8A%E3%81%AB">おわりに</a></h1> <p>オブジェクト指向はプログラマの「あったらいいな」が詰め込まれているため、機能が多すぎて面食らってしまいやすいが、本質的な部分はとても簡単なものである。<br /> 継承や無名関数、コンストラクタなど、面白い機能が他にもたくさんあるので、興味があるものから一つずつ調べてみるのがいいだろう。</p> <h1 id="PHPマニュアルおすすめ部分"><a href="#PHP%E3%83%9E%E3%83%8B%E3%83%A5%E3%82%A2%E3%83%AB%E3%81%8A%E3%81%99%E3%81%99%E3%82%81%E9%83%A8%E5%88%86">PHPマニュアルおすすめ部分</a></h1> <p><a target="_blank" rel="nofollow noopener" href="https://www.php.net/manual/ja/language.oop5.basic.php">PHP: クラスの基礎</a></p> <p><a target="_blank" rel="nofollow noopener" href="https://www.php.net/manual/ja/language.oop5.properties.php">PHP: プロパティ</a></p> <p><a target="_blank" rel="nofollow noopener" href="https://www.php.net/manual/ja/language.oop5.visibility.php">PHP: アクセス権</a></p> <p><a target="_blank" rel="nofollow noopener" href="https://www.php.net/manual/ja/functions.anonymous.php">PHP: 無名関数</a></p> ウラル tag:crieit.net,2005:PublicArticle/15345 2019-08-23T18:16:09+09:00 2019-08-23T18:16:09+09:00 https://crieit.net/posts/form-input-submit-Enter form inputタグでsubmitボタンが無いとEnterしても送信されない現象の原因 <h1 id="現象"><a href="#%E7%8F%BE%E8%B1%A1">現象</a></h1> <p><a href="https://crieit.now.sh/upload_images/f37dd84b36bcbac2749ed7d9d5d7127f5d5ec894c9348.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/f37dd84b36bcbac2749ed7d9d5d7127f5d5ec894c9348.png?mw=700" alt="image.png" /></a></p> <ul> <li>HTMLで入力フォームを作成する際、<code><input type="submit"></code>(送信ボタン)を使わずに記述したところ、Enterを押してもページが遷移せず、送信されなかった。</li> <li><code><input type="submit"></code>のスタイルに<code>display:none</code>を設定したところ、Chromeでは送信できるようになったがSafariでは送信できないままだった。</li> </ul> <h2 id="該当のコード"><a href="#%E8%A9%B2%E5%BD%93%E3%81%AE%E3%82%B3%E3%83%BC%E3%83%89">該当のコード</a></h2> <pre><code class="html"><form accept-charset="UTF-8" method="post" action=""> <input name="name"> <input name="name2"> </form> </code></pre> <p>実際の動作は<a target="_blank" rel="nofollow noopener" href="https://splamp.info/tests/crieit/form_input.php">用意したこちらのページ</a>で確認してほしい。「項目が複数でsubmitなし」の部分です。</p> <h1 id="原因"><a href="#%E5%8E%9F%E5%9B%A0">原因</a></h1> <p><strong>仕様でした。</strong></p> <p>WHATWGの仕様書を見てみます。<br /> <a target="_blank" rel="nofollow noopener" href="https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#implicit-submission">https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#implicit-submission</a></p> <p>4.10.21 Form submission(以下和訳)</p> <blockquote> <p><strong>4.10.21.2 暗黙的送信</strong></p> <p>form要素の<strong>デフォルトボタン</strong>は、そのform要素に登場する最初のsubmitボタンです。</p> <p>もしブラウザがユーザーの暗黙的送信(例えば、いくつかのプラットフォームではテキスト項目でEnterキーを押すとフォームが送信される)をサポートするのであれば、そのデフォルトボタンが有効であるフォームに対して、デフォルトボタンでclickイベントが発火したように振舞わなければいけません。</p> <blockquote> <p>注:暗黙的送信しか使えないような(submitボタンの無い)ページがWebにはあるので、ブラウザはこれをサポートすることを強く推奨します。</p> </blockquote> <p>フォームにsubmitボタンがない場合、そのフォームが複数の <em>暗黙的送信を妨げるフィールド</em> を持つならば、暗黙的送信メカニズムは何もしてはいけません。そうでないならば、form要素はフォームの内容を送信しなければなりません。</p> <p>form要素を親に持つinput要素で、属性が以下のどれかであれば、それはform要素の暗黙的送信を妨げるフィールドです。 Text, Search, URL, Telephone, E-mail, Password, Date, Month, Week, Time, Local Date and Time, Number</p> </blockquote> <p><strong>暗黙的送信</strong>(Implicit submission)とは、text属性などのinput要素の中でEnterを押すと送信できるように、submitボタンを押さなくても送信できる補助機能のことを指します。</p> <p>ざっくり言うと、フォームが複数のinput要素を持っている場合、submitボタンが無い限り暗黙的送信は行ってはならないと定義されているのです。</p> <p>逆に言えば、submitボタンは無いがフォームのinput要素が1つだけの場合や、フォームのinput要素が複数あるが有効なsubmitボタンが存在する場合は、暗黙的送信ができるということです。</p> <h1 id="対策"><a href="#%E5%AF%BE%E7%AD%96">対策</a></h1> <p>もしあなたが複数のinput要素を送信しようとしているのなら、できる限りsubmitボタンを用意すべきでしょう。submitボタンの書き方は2通りあります。</p> <h3 id="input要素を使う場合"><a href="#input%E8%A6%81%E7%B4%A0%E3%82%92%E4%BD%BF%E3%81%86%E5%A0%B4%E5%90%88">input要素を使う場合</a></h3> <pre><code class="html"><input type="submit"> </code></pre> <p>value属性でボタンに表示されるテキストを編集できます。デフォルトでは「送信」が表示されます。</p> <h3 id="button要素を使う場合"><a href="#button%E8%A6%81%E7%B4%A0%E3%82%92%E4%BD%BF%E3%81%86%E5%A0%B4%E5%90%88">button要素を使う場合</a></h3> <pre><code class="html"><button type="submit">送信</button> </code></pre> <p>input要素と異なり、ボタンの中身はHTMLで記述できます。</p> <p>それでもsubmitボタンを表示したくない場合は、次のようにCSSで消すのが良いでしょう。</p> <pre><code class="html"><button type="submit" style="padding:0;border:0"></button> </code></pre> <p>以上</p> ウラル tag:crieit.net,2005:PublicArticle/15275 2019-07-25T03:03:37+09:00 2019-07-25T03:18:33+09:00 https://crieit.net/posts/PHP-API PHPで爆速APIを作るとき工夫したこと <p>「サーモンランAPI」という配信専用のAPIです。<br /> <a target="_blank" rel="nofollow noopener" href="https://splamp.info/salmon/api/">https://splamp.info/salmon/api/</a></p> <p>任天堂ソフト「Splatoon2」のサーモンランの最新情報を取得できます。</p> <h1 id="爆速のために"><a href="#%E7%88%86%E9%80%9F%E3%81%AE%E3%81%9F%E3%82%81%E3%81%AB">爆速のために</a></h1> <p>Apacheサーバー上で、JSONデータを静的ファイルとして配信しています。</p> <p>これまでスプランプ(※うちのサイト)では「<a target="_blank" rel="nofollow noopener" href="https://splamp.info/spstage2/">スプステージ2</a>」という名前でバトルステージの情報を返すAPIを運用してきたのですが、こちらはオプションの充実のために、PHPファイルにアクセスさせる動的な仕組みでした。</p> <p>今回静的ファイルにした理由です。</p> <ol> <li><p>オプションを付けないほうがシンプル<br /> … タイムゾーンなどの混乱の解消のため、時間データはUnixtimeだけにしました。言語も日英同時に配信することにしました。</p></li> <li><p>サーバー負荷が激減する<br /> … 毎回DBにアクセスしたりせずに済むのでレスポンス速度が向上します。</p></li> <li><p>クライアントでキャッシュが有効になる<br /> … ヘッダの<code>Last-Modified</code>や<code>E-tag</code>により、クライアントでキャッシュが有効になります。</p></li> </ol> <h1 id="仕組み"><a href="#%E4%BB%95%E7%B5%84%E3%81%BF">仕組み</a></h1> <p>APIのアクセス先は一見「/salmon/api/now」や「/salmon/api/all」のように見えますが、実は<code>.htaccess</code>で「/salmon/api/」の別の子フォルダにあるJSONファイルにリライトしています。<br /> 例:</p> <pre><code class="apache">RewriteEngine On RewriteRule ^all$ xxx.xxx [L] </code></pre> <p>また、JSONファイルは更新用プログラムによって、定期的に新しいファイルに上書きされます。</p> <p>更新用プログラムはcronで定期的に更新すべきかチェックしていて、更新があるとDBからデータを取得・整形して、出力します。</p> <p>JSONは何となく可読性を高めたくなったので、インデントを付けたり無意味なエスケープを消したりしてみました。</p> <pre><code class="php">$json = json_encode($output, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); </code></pre> <h1 id="ヘッダ問題"><a href="#%E3%83%98%E3%83%83%E3%83%80%E5%95%8F%E9%A1%8C">ヘッダ問題</a></h1> <p>今回一番欠かせなかったのはヘッダの付与でした。</p> <p>毎アクセス動的生成していた頃はPHPの<code>header()</code>なんかを使っていたのですが、今回は静的ファイルですから、Apacheの力を借りる他ありません。</p> <p>Apacheでは、<code>.htaccess</code>を使うと配下のファイルに対して指定したヘッダを付与することができます。</p> <pre><code class="apache">Header set (ヘッダ名) "(値)" </code></pre> <p>今回加えたヘッダは以下の二つです。</p> <pre><code class="apache">Header set Access-Control-Allow-Origin "*" Header set Content-Type "application/json; charset=utf-8" </code></pre> <p><code>Content-Type</code>では、文字化けしたりしないようにUTF-8を明示しています。</p> <p><code>Access-Control-Allow-Origin</code>というのは見たことない人もいるのではないでしょうか。これは、JavaScriptにおいてクロスオリジンでアクセスしてもよいかの指定になります。</p> <p>これをサーバーが明示的に許可しないとjsがアクセスすることはできません。jsでオープンなAPIが使えないなんてもったいない。<code>*</code>を指定することで誰でもアクセスできるようになります。(CORS / Cross-Origin Resource Sharingの許可、と呼ばれたりします。)</p> <h1 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h1> <ul> <li>サーバー上でキャッシュ(静的ファイル)を生成した</li> <li><code>.htaccess</code>でヘッダを指定した</li> <li><code>Access-Control-Allow-Origin</code>の指定でjs勢にも利用してもらえる</li> </ul> <h1 id="感想"><a href="#%E6%84%9F%E6%83%B3">感想</a></h1> <p>JavaScriptからのアクセス許可については、実際にjs勢の方から問い合わせがあって初めて気づきました。意外と見落としている方は多いんじゃないでしょうか。</p> <p>個人的な信条として「サーバーサイドの魔法」というのがあります。サーバーサイドは制約が少なく、予想外のことができるということです。Apacheの設定ファイル<code>.htaccess</code>は特にその幅を広げてくれるので気に入っています。<br /> 他にも色々な機能があるので、調べてみると面白いと思います。<br /> <a target="_blank" rel="nofollow noopener" href="https://murashun.jp/blog/20141229-01.html">.htaccess の書き方 | murashun.jp</a></p> <h1 id="サーモンランAPIの他の記事"><a href="#%E3%82%B5%E3%83%BC%E3%83%A2%E3%83%B3%E3%83%A9%E3%83%B3API%E3%81%AE%E4%BB%96%E3%81%AE%E8%A8%98%E4%BA%8B">サーモンランAPIの他の記事</a></h1> <p>サーモンランAPIについて<br /> <a target="_blank" rel="nofollow noopener" href="https://splamp.info/salmon/api/">https://splamp.info/salmon/api/</a></p> <p>サーモンランAPIサンプル集(連載)<br /> <a href="https://crieit.net/magazines/barley_ural/サーモンランAPIサンプル集">https://crieit.net/magazines/barley_ural/サーモンランAPIサンプル集</a></p> ウラル tag:crieit.net,2005:PublicArticle/15272 2019-07-23T04:30:25+09:00 2019-07-23T04:34:47+09:00 https://crieit.net/posts/PHP-5d360ed14c851 PHPで新しいプロセスに処理を引き継ぐ方法 <h1 id="動機"><a href="#%E5%8B%95%E6%A9%9F">動機</a></h1> <p>PHPで無限ループ処理をしたかったのですが、サーバーの都合上5分でプロセスがタイムアウトしてしまうので、PHPからコマンドを呼び出して、新しくPHPプロセスを立てて処理を引き継ぐことにしました。</p> <h1 id="方法"><a href="#%E6%96%B9%E6%B3%95">方法</a></h1> <p>PHPで外部コマンドを実行するには、<code>exec()</code>を使います。</p> <p>ところで、PHPがインストールされているOSで<code>/home/example.php</code>を実行するなら、次のようなコマンドを打ちますよね。</p> <pre><code class="console">php /home/example.php </code></pre> <p>( ˘ω˘) 。o(つまりこうすれば…)</p> <pre><code class="php">exec('php /home/example.php'); </code></pre> <p>残念!!</p> <blockquote class="twitter-tweet"><p lang="ja" dir="ltr">ねえプロセス殺せないんだけどなんで??????</p>— ウラル (@barley_ural) <a target="_blank" rel="nofollow noopener" href="https://twitter.com/barley_ural/status/1152569096367591425?ref_src=twsrc%5Etfw">July 20, 2019</a></blockquote> <p>処理中のプロセスを見ると、大量のPHPが。そして、手元には大量のエラー通知メールが…。</p> <p>ここで、<code>exec()</code>のPHPリファレンスをもう一度よく見てみましょう。</p> <blockquote> <p>注意:</p> <p>プログラムがこの関数で始まる場合、 バックグラウンドで処理を続けさせるには、 プログラムの出力をファイルや別の出力ストリームにリダイレクトする必要があります。 そうしないと、プログラムが実行を終えるまで PHP はハングしてしまいます。</p> <p><a target="_blank" rel="nofollow noopener" href="https://www.php.net/manual/ja/function.exec.php">https://www.php.net/manual/ja/function.exec.php</a></p> </blockquote> <p>つまり<code>exec()</code>からコマンドを実行すると、子プロセスの出力があるまでこのPHPはkillされないということです。これは、<code>exec()</code>が返り値を待っているためでしょう。</p> <p>しかし厄介なのは、この待機中のプロセスは子プロセスが終了しない限り、あらゆるタイムアウト処理を無視する点です。いつまでもプロセスに残り続け、メモリを食い続けるのです。</p> <p>これを防ぐためには、以下のようにします。</p> <pre><code class="php">exec('php /home/example.php > /dev/null &'); </code></pre> <p><code>></code>を使うと、左側(ここでは<code>php /home/example.php</code>)の出力を右側(ここでは<code>/dev/null</code>)にリダイレクトします。</p> <p><code>/dev/null</code>は虚無を表すスペシャルファイルで、ここを出力先に指定すると、その出力は破棄されます。</p> <p><code>&</code>はコマンドの後ろに付けることで、バックグラウンド実行を示します。これにより、非同期でその処理を扱うことができます。</p> <h1 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h1> <p>PHPで新しいプロセスを立ててバックグラウンド実行するには次のコマンドを使います。</p> <pre><code class="php">exec('php /home/example.php > /dev/null &'); </code></pre> <p>重たい処理だけバックグラウンドに逃がしたいときにも使えそうです。</p> <h1 id="参考"><a href="#%E5%8F%82%E8%80%83">参考</a></h1> <p><a target="_blank" rel="nofollow noopener" href="https://qiita.com/inosy22/items/341cfc589494b8211844">Linuxコマンド(Bash)でバックグラウンド実行する方法のまとめメモ</a></p> <p><a target="_blank" rel="nofollow noopener" href="https://www.softel.co.jp/blogs/tech/archives/5697">【php】 exec()関数などでバックグラウンドでコマンドを実行するとき</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/15191 2019-07-03T02:31:47+09:00 2019-07-16T01:50:20+09:00 https://crieit.net/posts/PHP-IP 【PHP】IPアドレスから地域・郵便番号・経緯度座標などを調べる <p>地域に応じてWebサイトの表示が変わると面白いと思ったので、PHPを使ってIPアドレスから地域を推定します。</p> <p>IPアドレスから住所を調べられるサービスはいくつかありますが、今回は<a target="_blank" rel="nofollow noopener" href="https://www.ip-adress.com">www.ip-adress.com</a>を使います。</p> <h2 id="スクレイピング"><a href="#%E3%82%B9%E3%82%AF%E3%83%AC%E3%82%A4%E3%83%94%E3%83%B3%E3%82%B0">スクレイピング</a></h2> <p>もちろんスクレイピングでやります。スクレイピングとは、Webサイトのソースコードから欲しい情報だけを抜き出して利用することです。APIのようなものです。()</p> <p>スクレイピングで注意すべき点は、絶対に過剰なアクセスをかけないことです。<strong>あなたのサーバーがDoS攻撃を受けると、間接的にスクレイピング先のサーバーにもDoS攻撃がかかります。</strong></p> <p>私はかつてこれをやらかし、とあるAPIからサーバーごとBANされたことがあります。向こうからすれば私のサーバーがDoS攻撃をしたように見えるわけです。</p> <p>キャッシュを取るか、アクセスに制限を設けましょう。</p> <p>今回は、0.6秒以上間隔を空けないとスクレイピング処理が行われないようにします。これは、<a target="_blank" rel="nofollow noopener" href="https://ipinfodb.com/">IPinfoDB</a>の無料APIの上限が秒間2アクセスであることを参考にしています。</p> <h2 id="コード"><a href="#%E3%82%B3%E3%83%BC%E3%83%89">コード</a></h2> <p>動作デモはこちらです。<br /> <a target="_blank" rel="nofollow noopener" href="https://reiwa.cf/get_ip.php">https://reiwa.cf/get_ip.php</a></p> <pre><code class="php"><?php define('WAIT', 0.6); $last_updated = @file_get_contents('get_ip_last_updated'); if($last_updated !== false && $_SERVER["REQUEST_TIME_FLOAT"] - $last_updated < WAIT) { echo '<meta http-equiv="refresh" content="'.WAIT.';URL=">'; return; } $remote_addr = $_SERVER['REMOTE_ADDR']; $ipv4 = '^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?).){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$'; if (!preg_match('/'.$ipv4.'/', $remote_addr)) { echo 'IPv4しか扱えません'; return; } $ip_html = file_get_contents('https://www.ip-adress.com/ip-address/ipv4/'.$remote_addr); $regex = <<<EOT |<tr><th>IP Address</th><td>(.*)</td></tr> <tr><th>Decimal Representation</th><td>(?:.*)</td></tr> <tr><th><abbr title="Autonomous System Number">ASN</abbr></th><td>(?:.*)</td></tr> <tr><th>City</th><td>(.*)</td></tr> <tr><th>Country</th><td>(.*)</td></tr> <tr><th>Country Code</th><td>(?:.*)</td></tr> <tr><th><abbr title="Internet Service Provider">ISP</abbr></th><td>(?:.*)</td></tr> <tr><th>Latitude</th><td>(.*)</td></tr> <tr><th>Longitude</th><td>(.*)</td></tr> <tr><th>Organization</th><td>(.*)</td></tr> <tr><th>Postal Code</th><td>(.*)</td></tr> <tr><th>Is Private IP Address</th><td>(?:.*)</td></tr> <tr><th>PTR Resource Record</th><td>(?:.*)</td></tr> <tr><th>Is Reserved IP Address</th><td>(?:.*)</td></tr> <tr><th>State</th><td>(.*)</td></tr> <tr><th>State Code</th><td>(?:.*)</td></tr> <tr><th>Timezone</th><td>(.*)</td></tr>|U EOT; $is_match = preg_match($regex, $ip_html, $matches); if(!$is_match) { echo 'わかりませんでした'; return; } $ip_addr = $matches[1]; $postal = $matches[7]; $country = $matches[3]; $state = $matches[8]; $city = $matches[2]; $ido = strtok($matches[4], '&'); $keido = strtok($matches[5], '&'); $org = $matches[6]; $timezone = $matches[9]; echo <<<EOT IPアドレス: {$ip_addr}<br> 郵便番号: {$postal}<br> 地域: {$city}, {$state}, {$country}<br> 緯度: {$ido}°<br> 経度: {$keido}°<br> 組織: {$org}<br> タイムゾーン: {$timezone}<br> EOT; file_put_contents('get_ip_last_updated', $_SERVER["REQUEST_TIME_FLOAT"]); </code></pre> <p>返り値はこんな感じになります。</p> <pre><code>IPアドレス: 111.101.7.185 郵便番号: 220-0023 地域: Yokohama, Kanagawa, Japan 緯度: 35.4500° 経度: 139.6333° 組織: au one net タイムゾーン: Asia/Tokyo </code></pre> <p>最後の更新時間(Unixtime)を「get_ip_last_updated」に保存していて、もしも0.6秒経たないうちにアクセスしてしまった場合は、0.6秒後にリフレッシュするHTMLを送り返しています。<br /> IPv4しか扱えないので、その他の書式のものは正規表現で除外しています。</p> <h2 id="使い道"><a href="#%E4%BD%BF%E3%81%84%E9%81%93">使い道</a></h2> <p>僕は、近所の天気予報を表示したかったので作りました。座標情報が出ているので、最寄りの何かを検索したりもできるだろうし、「北海道から新しいアクセスがありました。」みたいな通知メールを送ったりにも使えそうですね。</p> ウラル tag:crieit.net,2005:PublicArticle/15087 2019-06-10T23:57:24+09:00 2019-06-10T23:58:52+09:00 https://crieit.net/posts/100-5cfe6fd4312ce 挑戦!100万番目の素数を求めろ <p><a href="https://crieit.now.sh/upload_images/f37dd84b36bcbac2749ed7d9d5d7127f5cfe66aad8f39.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/f37dd84b36bcbac2749ed7d9d5d7127f5cfe66aad8f39.png?mw=700" alt="image.png" /></a></p> <p>きっかけは<a target="_blank" rel="nofollow noopener" href="https://twitter.com/tkgling">@tkgling</a>さんのツイートでした。</p> <h3 id="100万番目の素数を返すプログラム、どのくらいで計算できるだろう?"><a href="#100%E4%B8%87%E7%95%AA%E7%9B%AE%E3%81%AE%E7%B4%A0%E6%95%B0%E3%82%92%E8%BF%94%E3%81%99%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%A0%E3%80%81%E3%81%A9%E3%81%AE%E3%81%8F%E3%82%89%E3%81%84%E3%81%A7%E8%A8%88%E7%AE%97%E3%81%A7%E3%81%8D%E3%82%8B%E3%81%A0%E3%82%8D%E3%81%86%EF%BC%9F">100万番目の素数を返すプログラム、どのくらいで計算できるだろう?</a></h3> <hr /> <blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">100万番目の素数を返すプログラム、どのくらいで計算できるだろう?</p>— nme (@tkgling) <a target="_blank" rel="nofollow noopener" href="https://twitter.com/tkgling/status/1136679237484630017?ref_src=twsrc%5Etfw">2019年6月6日</a></blockquote> <blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">100万番目の素数を返すコードをC++, Python, Javascriptの三つで対決させよう。</p>— nme (@tkgling) <a target="_blank" rel="nofollow noopener" href="https://twitter.com/tkgling/status/1136679897726824448?ref_src=twsrc%5Etfw">2019年6月6日</a></blockquote> <h3 id="100万番目の素数かぁ…"><a href="#100%E4%B8%87%E7%95%AA%E7%9B%AE%E3%81%AE%E7%B4%A0%E6%95%B0%E3%81%8B%E3%81%81%E2%80%A6">100万番目の素数かぁ…</a></h3> <hr /> <blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">100万番目の素数かぁ…</p>— ウラル (@barley_ural) <a target="_blank" rel="nofollow noopener" href="https://twitter.com/barley_ural/status/1136694572283678721?ref_src=twsrc%5Etfw">2019年6月6日</a></blockquote> <h3 id="こうかな?"><a href="#%E3%81%93%E3%81%86%E3%81%8B%E3%81%AA%EF%BC%9F">こうかな?</a></h3> <hr /> <p>GMP(GNU MP)関数を使ってみました。これは、PHPに標準で含まれるライブラリです。<br /> PHPはお呼びでないって?うるせーーー!知らねーーー!(PHPの苦手分野だけどな!)</p> <blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr"><a target="_blank" rel="nofollow noopener" href="https://twitter.com/search?q=%24prime&src=ctag&ref_src=twsrc%5Etfw">$prime</a> = 1;for($i=0;$i<1000000;$i++){<a target="_blank" rel="nofollow noopener" href="https://twitter.com/search?q=%24prime&src=ctag&ref_src=twsrc%5Etfw">$prime</a> = gmp_nextprime($prime);}echo <a target="_blank" rel="nofollow noopener" href="https://twitter.com/search?q=%24prime&src=ctag&ref_src=twsrc%5Etfw">$prime</a>; // 100万番目の素数</p>— ウラル (@barley_ural) <a target="_blank" rel="nofollow noopener" href="https://twitter.com/barley_ural/status/1136695918932004865?ref_src=twsrc%5Etfw">2019年6月6日</a></blockquote> <pre><code><?php $prime = 1; for($i=0;$i<1000000;$i++){ $prime = gmp_nextprime($prime); } echo $prime; // 100万番目の素数 </code></pre> <p><a target="_blank" rel="nofollow noopener" href="https://www.php.net/manual/ja/function.gmp-nextprime.php">gmp_nextprime()</a>は、引数の次の素数を返す関数です。これなら、100万回繰り返すだけで求まりますね。</p> <h3 id="100万番目の素数の答え"><a href="#100%E4%B8%87%E7%95%AA%E7%9B%AE%E3%81%AE%E7%B4%A0%E6%95%B0%E3%81%AE%E7%AD%94%E3%81%88">100万番目の素数の答え</a></h3> <hr /> <p>上記のコードを実行します。</p> <pre><code>15485863 </code></pre> <p><strong>15485863</strong>!!</p> <blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">64秒かかった…</p>— ウラル (@barley_ural) <a target="_blank" rel="nofollow noopener" href="https://twitter.com/barley_ural/status/1136722104630427648?ref_src=twsrc%5Etfw">2019年6月6日</a></blockquote> <p>set_time_limit(0);を書かないとタイムアウトするくらいにはかかりますね。<br /> PCのスペックと実行中の負荷によってもかなり変わります。僕のPCは<br /> 記事執筆中、3回実行して1番早かったのは72.1秒でした。</p> <h3 id="実コードで実装よろぴく"><a href="#%E5%AE%9F%E3%82%B3%E3%83%BC%E3%83%89%E3%81%A7%E5%AE%9F%E8%A3%85%E3%82%88%E3%82%8D%E3%81%B4%E3%81%8F">実コードで実装よろぴく</a></h3> <hr /> <blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">実コードで実装よろぴく。</p>— nme (@tkgling) <a target="_blank" rel="nofollow noopener" href="https://twitter.com/tkgling/status/1136696256590254080?ref_src=twsrc%5Etfw">2019年6月6日</a></blockquote> <p>ですよねーーー。GMPライブラリは、標準で切られている環境が多いです。それにGMP関数使うの、ちょっとずるい。</p> <h3 id="gmp_nextprime()はどうやって素数を求めてるの?"><a href="#gmp_nextprime%28%29%E3%81%AF%E3%81%A9%E3%81%86%E3%82%84%E3%81%A3%E3%81%A6%E7%B4%A0%E6%95%B0%E3%82%92%E6%B1%82%E3%82%81%E3%81%A6%E3%82%8B%E3%81%AE%EF%BC%9F">gmp_nextprime()はどうやって素数を求めてるの?</a></h3> <hr /> <p>ところで、さっきのgmp_nextprime()はどうやって素数を求めているんでしょうか。</p> <p><a target="_blank" rel="nofollow noopener" href="https://www.php.net/manual/ja/function.gmp-nextprime.php">公式リファレンス</a>の一番下に、こう書かれています。</p> <blockquote> <p>この関数は素数を識別するのに確率的アルゴリズムを使用します。 誤って合成数を取得してしまうことは、まずありません。</p> </blockquote> <h3 id="「確率的アルゴリズム」…?"><a href="#%E3%80%8C%E7%A2%BA%E7%8E%87%E7%9A%84%E3%82%A2%E3%83%AB%E3%82%B4%E3%83%AA%E3%82%BA%E3%83%A0%E3%80%8D%E2%80%A6%EF%BC%9F">「確率的アルゴリズム」…?</a></h3> <hr /> <p>さっきの「確率的アルゴリズム」とは何なのでしょうか。Wikipediaで調べてみました。</p> <p><a target="_blank" rel="nofollow noopener" href="https://ja.wikipedia.org/wiki/%E7%B4%A0%E6%95%B0%E5%88%A4%E5%AE%9A"><strong>素数判定</strong></a><br /> <a href="https://crieit.now.sh/upload_images/f37dd84b36bcbac2749ed7d9d5d7127f5cfe40f6c09da.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/f37dd84b36bcbac2749ed7d9d5d7127f5cfe40f6c09da.png?mw=700" alt="image.png" /></a></p> <p><a target="_blank" rel="nofollow noopener" href="https://ja.wikipedia.org/wiki/%E3%83%9F%E3%83%A9%E3%83%BC%E2%80%93%E3%83%A9%E3%83%93%E3%83%B3%E7%B4%A0%E6%95%B0%E5%88%A4%E5%AE%9A%E6%B3%95"><strong>ミラー–ラビン素数判定法</strong></a><br /> <a href="https://crieit.now.sh/upload_images/f37dd84b36bcbac2749ed7d9d5d7127f5cfe40caaa669.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/f37dd84b36bcbac2749ed7d9d5d7127f5cfe40caaa669.png?mw=700" alt="image.png" /></a><br /> <a href="https://crieit.now.sh/upload_images/f37dd84b36bcbac2749ed7d9d5d7127f5cfe4121063bb.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/f37dd84b36bcbac2749ed7d9d5d7127f5cfe4121063bb.png?mw=700" alt="image.png" /></a></p> <p>説明を読んでもサッパリですね。<br /> ざっくり言うと、確率的アルゴリズムとは、「おそらく素数」を高速で求める方法らしいです。なぜ「おそらく」なのかと言うと、この方法では(多分)フェルマーの小定理を用いた合同式の対偶を利用して計算していて、その中で乱数を使用するからです。ごく稀に、合成数なのに奇数として出してしまうことがあるということですね。実用上は問題ないらしいです。</p> <h3 id="決定的アルゴリズムで考えよう"><a href="#%E6%B1%BA%E5%AE%9A%E7%9A%84%E3%82%A2%E3%83%AB%E3%82%B4%E3%83%AA%E3%82%BA%E3%83%A0%E3%81%A7%E8%80%83%E3%81%88%E3%82%88%E3%81%86">決定的アルゴリズムで考えよう</a></h3> <hr /> <p>確率的アルゴリズムは難しいからやめましょう。地道に確実な方法で求めようよ、ね?<br /> このような方法を確率的アルゴリズムと比較して「決定的アルゴリズム」と言います。</p> <h3 id="こうだっ!"><a href="#%E3%81%93%E3%81%86%E3%81%A0%E3%81%A3%EF%BC%81">こうだっ!</a></h3> <hr /> <blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">ファーストトライとりあえず動いた… <a target="_blank" rel="nofollow noopener" href="https://t.co/h0P6m6m4BL">pic.twitter.com/h0P6m6m4BL</a></p>— ウラル (@barley_ural) <a target="_blank" rel="nofollow noopener" href="https://twitter.com/barley_ural/status/1136925853659492352?ref_src=twsrc%5Etfw">2019年6月7日</a></blockquote> <p>少し考えて作ってみました。</p> <pre><code><?php set_time_limit(0); $debug_start = microtime(true); $a = 2;//~無限 $primes = [2]; for($cnt=0;$cnt<10000;$cnt++){ //素数が見つかるまでの処理 $flg = false; while($flg == false){ $a++; $flg = true; for($i=0;$i<=$cnt;$i++){ if($a % $primes[$i] == 0){ $flg = false; break; } } } $primes[$cnt + 1] = $a; } echo $primes[$cnt - 1] . '<br>' . (microtime(true) - $debug_start) . 's'; </code></pre> <p>とりあえず1万まで。</p> <p>最初に、<code>$debug_start = microtime(true);</code>で開始時間を記録します。<br /> <code>$a = 2;</code>は、素数かどうか判定される自然数です。これを増やしていきます。<br /> <code>$primes = [2];</code>は素数リストです。素数を見つけたらここに追加していきます。<br /> <code>for($cnt=0;$cnt<10000;$cnt++)</code>で、素数リストが1万個になるまで素数判定を繰り返します。<br /> ここからは素数判定の方法です。<br /> <code>$flg = false;</code>で最初に$aが素数かどうかのフラグを偽にします。<br /> <code>while($flg == false)</code>で、「$aが素数でなければ繰り返す」ようにします。<br /> <code>$a++;</code>します。これは単に順番の問題でここにあります。<br /> これから合成数かどうか判定するので、<code>$flg = true;</code>で最初にこれが素数であると仮定します。<br /> <code>for($i=0;$i<=$cnt;$i++)</code>は、素数リストの手前から順番に取り出すための繰り返しです。<br /> <code>if($a % $primes[$i] == 0)</code>で、「もし$aを素数リストから取り出した数で割った余りが0」になったら、$aは合成数だとわかります。<br /> 合成数なので<code>$flg = false;</code>で素数フラグは偽です。<br /> 素数リストを辿る繰り返しを抜け、$a+1して次のループに入ります。<br /> $aがそれまで発見したどの素数でも割れなかったら、それは素数です。<br /> <code>$flg = true;</code>だと仮定されたまま、<code>while($flg == false)</code>に入らずに進みます。<br /> <code>$primes[$cnt + 1] = $a;</code>で素数リストに新たにその数が追加されます。<br /> 1万回繰り返したら、出力します。</p> <p>この方法なら、奇数だけに絞る意味はあまりありません。なぜなら、素数リストの1番目が2なので一瞬で合成数だと分かるからです。</p> <h3 id="これで100万回やってみようとした"><a href="#%E3%81%93%E3%82%8C%E3%81%A7100%E4%B8%87%E5%9B%9E%E3%82%84%E3%81%A3%E3%81%A6%E3%81%BF%E3%82%88%E3%81%86%E3%81%A8%E3%81%97%E3%81%9F">これで100万回やってみようとした</a></h3> <hr /> <blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">これワンチャン30分くらいかかるな やべえ</p>— ウラル (@barley_ural) <a target="_blank" rel="nofollow noopener" href="https://twitter.com/barley_ural/status/1136933706642276352?ref_src=twsrc%5Etfw">2019年6月7日</a></blockquote> <p>(´・ω・`)諦めた</p> <h3 id="平方根まででいいのでは…?"><a href="#%E5%B9%B3%E6%96%B9%E6%A0%B9%E3%81%BE%E3%81%A7%E3%81%A7%E3%81%84%E3%81%84%E3%81%AE%E3%81%A7%E3%81%AF%E2%80%A6%EF%BC%9F">平方根まででいいのでは…?</a></h3> <hr /> <p>ちょっと待ってよ。素数か判定するために素数リストを全部当てはめて確かめたけど、合成数か確かめるには「平方根×平方根」までで良くない?<br /> 「11」が素数か判定したいとき、5以上の素数で割れないのは明らかだよね。<br /> ということで</p> <blockquote class="twitter-tweet" data-conversation="none" data-lang="ja"><p lang="ja" dir="ltr">平方根までにするだけでめちゃくちゃ早くなった <a target="_blank" rel="nofollow noopener" href="https://t.co/ZqWbRMtKDz">pic.twitter.com/ZqWbRMtKDz</a></p>— ウラル (@barley_ural) <a target="_blank" rel="nofollow noopener" href="https://twitter.com/barley_ural/status/1136934482173341696?ref_src=twsrc%5Etfw">2019年6月7日</a></blockquote> <p><img src="https://pbs.twimg.com/media/D8cz1lcUYAAXcmY.jpg" alt="image" /></p> <h3 id="これで100万番目の素数は求まるだろ!"><a href="#%E3%81%93%E3%82%8C%E3%81%A7100%E4%B8%87%E7%95%AA%E7%9B%AE%E3%81%AE%E7%B4%A0%E6%95%B0%E3%81%AF%E6%B1%82%E3%81%BE%E3%82%8B%E3%81%A0%E3%82%8D%EF%BC%81">これで100万番目の素数は求まるだろ!</a></h3> <hr /> <blockquote class="twitter-tweet" data-conversation="none" data-lang="ja"><p lang="ja" dir="ltr">それでも100万番目の素数を出すのにうちのPCだと206秒かかる</p>— ウラル (@barley_ural) <a target="_blank" rel="nofollow noopener" href="https://twitter.com/barley_ural/status/1136935634969300998?ref_src=twsrc%5Etfw">2019年6月7日</a></blockquote> <p>求まった。でも206秒かかる…!</p> <blockquote class="twitter-tweet" data-conversation="none" data-lang="ja"><p lang="ja" dir="ltr">for文の中に関数を入れると毎回呼び出すので(しかもsqrt()は遅い)、外で一回変数定義しておくか$prime[$k]**みたいに平方根でなくて平方を使うのがオススメです。</p>— nme (@tkgling) <a target="_blank" rel="nofollow noopener" href="https://twitter.com/tkgling/status/1137042460314443776?ref_src=twsrc%5Etfw">2019年6月7日</a></blockquote> <blockquote class="twitter-tweet" data-conversation="none" data-lang="ja"><p lang="ja" dir="ltr">forループの中にいるsqrt($a)を変数に入れるとちょっと早くなるんじゃないかな</p>— もぎゃ🔌(daisuke furukawa) (@mogya) <a target="_blank" rel="nofollow noopener" href="https://twitter.com/mogya/status/1136981595640877057?ref_src=twsrc%5Etfw">2019年6月7日</a></blockquote> <p>for文は毎回判定されるので、確かに関数入れたら毎回実行されちゃう…!</p> <h3 id="直した"><a href="#%E7%9B%B4%E3%81%97%E3%81%9F">直した</a></h3> <hr /> <p>平方根を出す処理をfor文の外に出しました。<br /> ついでにインクリメントを前置きにすると早いと聞いたのでそこも直しました。(実測すると意外と逆だったりするのでおまじない程度です)</p> <blockquote class="twitter-tweet" data-conversation="none" data-lang="ja"><p lang="ja" dir="ltr">アドバイスを受けて <a target="_blank" rel="nofollow noopener" href="https://t.co/1ySLeLUcOI">pic.twitter.com/1ySLeLUcOI</a></p>— ウラル (@barley_ural) <a target="_blank" rel="nofollow noopener" href="https://twitter.com/barley_ural/status/1137263445391241216?ref_src=twsrc%5Etfw">2019年6月8日</a></blockquote> <pre><code><?php set_time_limit(0); $debug_start = microtime(true); $a = 2;//~無限 $primes = [2]; for($cnt=0;$cnt<1000000;++$cnt){ //素数が見つかるまでの処理 $flg = false; while($flg == false){ ++$a; $flg = true; $sqrted = sqrt($a); for($i=0;$primes[$i]<=$sqrted;++$i){ if($a % $primes[$i] == 0){ $flg = false; break; } } } $primes[$cnt + 1] = $a; } echo $primes[$cnt - 1] . "\n" . (microtime(true) - $debug_start) . 's'; </code></pre> <p>85.7秒!めちゃくちゃ早くなった!!</p> <p>これにてウラルはターンエンド!!</p> <p>(補足:記事執筆中、3回実行して1番早かったのは90.6秒でした)</p> <h3 id="他の人の実装紹介"><a href="#%E4%BB%96%E3%81%AE%E4%BA%BA%E3%81%AE%E5%AE%9F%E8%A3%85%E7%B4%B9%E4%BB%8B">他の人の実装紹介</a></h3> <hr /> <p>まずはネタ元@tkglingさん!<br /> 記事にされていたので、ここで書くよりずっと分かりやすいので読んで下さい。</p> <blockquote> <p>[PHP] 1,000,000番目の素数を求める<br /> <a target="_blank" rel="nofollow noopener" href="https://tkgstrator.work/?p=16234">https://tkgstrator.work/?p=16234</a></p> </blockquote> <p>細かい部分は違いますが、同様に素数リストを使った方法でした。<br /> 記事執筆中、素数リストの最終形を3回実行して1番早かったのは115.4秒でした。</p> <p>エラトステネスの篩やライブラリを用いたミラーラビン素数判定法のほか、決定的ミラーラビン判定法についても書かれてる…!!</p> <p>次に、組み込み系の<a target="_blank" rel="nofollow noopener" href="https://twitter.com/21692fa">くわい</a>(@21692fa)さん!</p> <blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">100万番目の素数計算ページ100万番目の素数と計算時間が出るよ(※読み込みに数秒かかります)<a target="_blank" rel="nofollow noopener" href="https://t.co/dOdg754j0a">https://t.co/dOdg754j0a</a></p>— くわい👎@赤眼鏡 (@21692fa) <a target="_blank" rel="nofollow noopener" href="https://twitter.com/21692fa/status/1137298387529637888?ref_src=twsrc%5Etfw">2019年6月8日</a></blockquote> <blockquote> <p><a target="_blank" rel="nofollow noopener" href="https://kunow.ml/PrimeNum.html">https://kunow.ml/PrimeNum.html</a></p> </blockquote> <p>jsはっやい。うちのPCだと14.5秒でした。iPhoneだと数秒です。</p> <p><del>嫌々</del>PHPでも書いてくれました。申し訳ない…</p> <pre><code><?php set_time_limit(0); $PrimeNumbers = [2]; $PrimeNumCount = 1; $CurrentNumber = 3; $RoundUpSqRoot = 2; $RoundUpSqNum = $RoundUpSqRoot * $RoundUpSqRoot; $IsPrimeNumber = true; $ExecuteTime = microtime(true); for (; $PrimeNumCount < 1000000; $CurrentNumber += 2) { if ($CurrentNumber > $RoundUpSqNum) { $RoundUpSqRoot++; $RoundUpSqNum = $RoundUpSqRoot * $RoundUpSqRoot; } for ($i = 0; $i < $PrimeNumCount; ++$i) { if ($CurrentNumber % $PrimeNumbers[$i] === 0) { $IsPrimeNumber = false; break; } if ($PrimeNumbers[$i] > $RoundUpSqRoot) { $IsPrimeNumber = true; break; } } if ($IsPrimeNumber) { $PrimeNumCount++; $PrimeNumbers[] = $CurrentNumber; } } $ExecuteTime = microtime(true) - $ExecuteTime; echo $PrimeNumbers[$PrimeNumCount - 1] . "\n" . $ExecuteTime . "s"; </code></pre> <p>記事執筆中、3回実行して1番早かったのは99.1秒でした。<br /> 関数も何も使ってないのに思ったほど早くない…という不思議な結果に。PHPは組み込み系言語の常識が通用しないと聞いたことあるけど、それなんだろか…。</p> <p>ということで、楽しかったです。定期的にこういうのやりたい。</p> ウラル tag:crieit.net,2005:PublicArticle/15059 2019-06-04T23:07:05+09:00 2019-06-06T14:39:36+09:00 https://crieit.net/posts/13ea918e8d47fc1e3b51dce98c2efd73 「はてブ」は誰にも伝わらない <p><a href="https://crieit.now.sh/upload_images/f37dd84b36bcbac2749ed7d9d5d7127f5cf6765f533b9.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/f37dd84b36bcbac2749ed7d9d5d7127f5cf6765f533b9.png?mw=700" alt="image.png" /></a></p> <p>3分の2以上が「はてなブログ」だと思ってるよ。</p> <blockquote> <h5 id="はてなブックマーク"><a href="#%E3%81%AF%E3%81%A6%E3%81%AA%E3%83%96%E3%83%83%E3%82%AF%E3%83%9E%E3%83%BC%E3%82%AF">はてなブックマーク</a></h5> <p>「ソーシャルブックマーク」、「ソーシャルニュース」サービスである。略称は『<strong>はてブ</strong>』。<br /> (<a target="_blank" rel="nofollow noopener" href="https://ja.wikipedia.org/wiki/%E3%81%AF%E3%81%A6%E3%81%AA%E3%81%AE%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9%E4%B8%80%E8%A6%A7#%E3%81%AF%E3%81%A6%E3%81%AA%E3%83%96%E3%83%83%E3%82%AF%E3%83%9E%E3%83%BC%E3%82%AF">Wikipedia「はてなのサービス一覧」</a>)</p> </blockquote> <p><a target="_blank" rel="nofollow noopener" href="https://twitter.com/dnm_b/status/1135494306737737728">ツイートURL</a></p> ウラル tag:crieit.net,2005:PublicArticle/15053 2019-06-02T16:49:14+09:00 2019-06-02T17:07:08+09:00 https://crieit.net/posts/bot botでシフト情報を返信するサンプル <p><a target="_blank" rel="nofollow noopener" href="https://splamp.info/salmon/api/">サーモンランAPI</a>を使って、シフト情報を返信するbotを作る方法をご紹介します。</p> <h2 id="指針"><a href="#%E6%8C%87%E9%87%9D">指針</a></h2> <p>今回は、<a target="_blank" rel="nofollow noopener" href="https://metabirds.net/">metabirds</a>というbot作成サービスを利用します。<br /> metabirdsでは、Twitter・LINE・Facebookメッセンジャー・Discord・Skype・Google Homeなど、様々なサービス上でbotを作ることができます。</p> <p>metabirdsには、外部のJSONファイルを利用して返信をする機能があります。そこで、サーモンランAPIを利用してbotに返信させてみます。</p> <h2 id="動作"><a href="#%E5%8B%95%E4%BD%9C">動作</a></h2> <p>LINEで動作させてみたところ、こんな感じになりました。</p> <p><a href="https://crieit.now.sh/upload_images/ac2a2c44e0f035ec2de0dc0e6589a83f5cf36ed3de0fa.PNG" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/ac2a2c44e0f035ec2de0dc0e6589a83f5cf36ed3de0fa.PNG?mw=700" alt="IMG_6383.PNG" /></a></p> <p><a href="https://crieit.now.sh/upload_images/7a06d9e02beb58796d747af3996b73a85cf370701e19e.PNG" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/7a06d9e02beb58796d747af3996b73a85cf370701e19e.PNG?mw=700" alt="IMG_6390.PNG" /></a></p> <p>「今のサーモンランは?」「次のサーモンラン」などと送ると、そのシフト情報を返信で返します。<br /> 打つのが面倒だったので、リッチメニューを使った入力補助も付けてみました。</p> <p>ここではLINEで試しましたが、設定すればTwitterなど他のサービスでも同様に動作します(しています)。<br /> 同じbotを複数のサービスで展開できるのもmetabirdsの強みです。</p> <h2 id="作り方"><a href="#%E4%BD%9C%E3%82%8A%E6%96%B9">作り方</a></h2> <p>まずは<a target="_blank" rel="nofollow noopener" href="https://metabirds.net/">metabirds</a>に登録しましょう。<br /> 登録したら、管理画面の「<a target="_blank" rel="nofollow noopener" href="https://metabirds.net/admin/bot_mention.php">@返信ボット</a>」というページを開きます。</p> <h3 id="「反応ワード」の設定"><a href="#%E3%80%8C%E5%8F%8D%E5%BF%9C%E3%83%AF%E3%83%BC%E3%83%89%E3%80%8D%E3%81%AE%E8%A8%AD%E5%AE%9A">「反応ワード」の設定</a></h3> <p>反応ワードでは、正規表現を使うことができます。<br /> 今回は、「やあ、今のサーモンランを教えて」「今やってるサーモンラン」などの入力を想定し、「今.*サーモンラン」としましょう。</p> <p>正規表現では、「.」は任意の1文字を表し、「*」はその1つ前の文字が無い、もしくは無限に連続することを表します。<br /> ですから、「.*」というのはその部分に何が入っても一致することを表しています。</p> <h3 id="「返信メッセージ」の設定"><a href="#%E3%80%8C%E8%BF%94%E4%BF%A1%E3%83%A1%E3%83%83%E3%82%BB%E3%83%BC%E3%82%B8%E3%80%8D%E3%81%AE%E8%A8%AD%E5%AE%9A">「返信メッセージ」の設定</a></h3> <p>返信メッセージでは、「置き換えタグ」という機能を利用して複雑な出力をすることができます。<br /> ここでは、こんな感じで入力してみました。</p> <p><a href="https://crieit.now.sh/upload_images/f5bd3e223829d669bee4658c2523c3f65cf374b373501.PNG" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/f5bd3e223829d669bee4658c2523c3f65cf374b373501.PNG?mw=700" alt="キャプチャ.PNG" /></a></p> <pre><code>{@} 【第{json:0,num:https://splamp.info/salmon/api/now}回】 日時︰{utstrftime:{json:0,start:https://splamp.info/salmon/api/now}:%-m/%-d %-H:00} 〜 {utstrftime:{json:0,end:https://splamp.info/salmon/api/now}:%-m/%-d %-H:00} 場所︰{json:0,stage_ja:https://splamp.info/salmon/api/now} <支給ブキ> ・{json:0,w1_ja:https://splamp.info/salmon/api/now} ・{json:0,w2_ja:https://splamp.info/salmon/api/now} ・{json:0,w3_ja:https://splamp.info/salmon/api/now} ・{json:0,w4_ja:https://splamp.info/salmon/api/now} [https://lmp.tw/sch](https://lmp.tw/sch) </code></pre> <p>置き換えタグについては、metabirdsのヘルプに<a target="_blank" rel="nofollow noopener" href="http://help.metabirds.net/e1236143.html">Botbird置き換えタグ一覧</a>というページがありますが、ざっくり説明しておきます。</p> <h4 id="{@}"><a href="#%7B%40%7D">{@}</a></h4> <p>かつてのTwitterで返信するときに必要でした。Twitter以外では無視されます。</p> <h4 id="{json:パラメータ:参照URL}"><a href="#%7Bjson%3A%E3%83%91%E3%83%A9%E3%83%A1%E3%83%BC%E3%82%BF%3A%E5%8F%82%E7%85%A7URL%7D">{json:パラメータ:参照URL}</a></h4> <p>パラメータでは、カンマで区切ることで下層の指定をすることができます。<br /> 例えば、[0]['num']を指定したいときは「0,num」となります。</p> <h4 id="{utstrftime:UnixTime:汎用時刻format}"><a href="#%7Butstrftime%3AUnixTime%3A%E6%B1%8E%E7%94%A8%E6%99%82%E5%88%BBformat%7D">{utstrftime:UnixTime:汎用時刻format}</a></h4> <p>UnixTimeで指定された時刻を、汎用時刻formatで指定された形式で出力します。<br /> metabirdsでは入れ子ができるので、ここではUnixTimeの部分に先ほどのJSON置き換えタグを指定しました。<br /> 汎用時刻formatでは、strftime関数で指定できる変換指定子を指定できます。<br /> ちなみに、metabirdsのヘルプページではPHPリファレンスを参照先として紹介していましたが、実際はRubyで使われる構文として解釈されるようです。そのため、PHPにはありませんが、「%-m」「%-d」「%-H」のように指定すると、先頭の0が除去された値(01→1など)を指定できます。</p> <p>最後に貼ったリンクは、サーモンランのシフト一覧ページです。</p> <h2 id="次のサーモンランも表示"><a href="#%E6%AC%A1%E3%81%AE%E3%82%B5%E3%83%BC%E3%83%A2%E3%83%B3%E3%83%A9%E3%83%B3%E3%82%82%E8%A1%A8%E7%A4%BA">次のサーモンランも表示</a></h2> <p>上のやつで今のサーモンランは表示できたので、次のサーモンランも表示できるようにしましょう。<br /> JSONのパラメータ部分を変えるだけです。</p> <h4 id="反応ワード"><a href="#%E5%8F%8D%E5%BF%9C%E3%83%AF%E3%83%BC%E3%83%89">反応ワード</a></h4> <pre><code>次.*サーモンラン </code></pre> <h4 id="返信メッセージ"><a href="#%E8%BF%94%E4%BF%A1%E3%83%A1%E3%83%83%E3%82%BB%E3%83%BC%E3%82%B8">返信メッセージ</a></h4> <pre><code>{@} 【第{json:1,num:https://splamp.info/salmon/api/now}回】 日時︰{utstrftime:{json:1,start:https://splamp.info/salmon/api/now}:%-m/%-d %-H:00} 〜 {utstrftime:{json:1,end:https://splamp.info/salmon/api/now}:%-m/%-d %-H:00} 場所︰{json:1,stage_ja:https://splamp.info/salmon/api/now} <支給ブキ> ・{json:1,w1_ja:https://splamp.info/salmon/api/now} ・{json:1,w2_ja:https://splamp.info/salmon/api/now} ・{json:1,w3_ja:https://splamp.info/salmon/api/now} ・{json:1,w4_ja:https://splamp.info/salmon/api/now} [https://lmp.tw/sch](https://lmp.tw/sch) </code></pre> <h2 id="LINEで動かしてみる"><a href="#LINE%E3%81%A7%E5%8B%95%E3%81%8B%E3%81%97%E3%81%A6%E3%81%BF%E3%82%8B">LINEで動かしてみる</a></h2> <p>Twitterについてはアプリ連携するだけで動くと思います。</p> <p>LINE APIとの接続方法については<a target="_blank" rel="nofollow noopener" href="http://help.metabirds.net/e1578068.html">metabirdsのヘルプ</a>をお読みください。</p> <h2 id="LINEのリッチメニュー"><a href="#LINE%E3%81%AE%E3%83%AA%E3%83%83%E3%83%81%E3%83%A1%E3%83%8B%E3%83%A5%E3%83%BC">LINEのリッチメニュー</a></h2> <p>なんかボタンが出てきてそれを押すと勝手に文字が送信されるやつはリッチメニューと言います。<br /> <a target="_blank" rel="nofollow noopener" href="https://admin-official.line.me/">LINE@MANAGER</a>でアカウントを選択し、「リッチコンテンツ作成」を押すと作成できます。</p> <p>botの動作確認をしたいときは、LINE Developersのチャネル基本設定ページの下のほうにあるQRコードを使って友達追加をして、トーク画面を開くことができます。</p> <h2 id="その他"><a href="#%E3%81%9D%E3%81%AE%E4%BB%96">その他</a></h2> <p>metabirdsで一度作ってしまえば、Twitter・LINE・Facebookメッセンジャー・Discord・Skype・Google Homeなど、様々なサービス上でそのbotを動かせます。<br /> 必要なのはトップページのAPI連携だけ!</p> <p>metabirdsでできることは他にもたくさんあります。この記事がAPI活用の参考になりましたら幸いです。</p> ウラル