- 投稿日:2021-03-25T21:29:51+09:00
高校生が2ヶ月で教育用プログラミング言語を作ってU22プロコンで賞をとった話
前置き
2020/11/29に開催された U22プログラミングコンテスト2020 において経済産業省政務局長賞プロダクト賞及びOBC賞を頂きましたので、開発経緯をまとめてみたいと思います。拙い文章だとは思いますが、U22プログラミングコンテストに応募してみようと考えている方や自作言語に興味がある方への参考になれば幸いです。
自己紹介
最初にちょっとした自己紹介を...
プログラミングが趣味な一般高校二年生(4月から3年生)です。
プログラミングはScratchを小学生4年生から、JavaScriptを中学1年生からはじめました。高校に入ってからプログラミング学習を本格的に初め、C, C++, Swift, PHP, (SQL), Go などを学習しました。(まだマスターには程遠いですが..)
一番なれている分野はサーバーサイドで、文化祭の物品管理データベースの作成やスーパー混雑共有サービスなどを今までに作りました。応募作品について
今回のU22プログラミングコンテスト(以下U22プロコン)で提出した作品は以下の2つです。簡単に紹介します。(詳しくはPeriDotのホームページをご覧ください。)
まず30秒ほどの動画で大まかな特徴を紹介しますイメージビデオ
PeriDot
種類
プログラミング言語(インタプリタ型/手続き型/動的型付け)
概要
Scratchなどのビジュアルプログラミングから、C, JavaScript, Pythonなどのテキストプログラミングへのステップアップ を目的とした、教育、学習用として最適化されたシンプルなインタプリタ型言語。
また、日本語でのエラーやインタプリタの評価過程の表示が可能。開発言語/環境
GO言語/Mac OS
開発期間
1ヶ月半
Peree
種類
WEBサービス
概要
PeriDotをweb上で実行可能にしたサービス。アカウント登録をすることでweb上にプロジェクトを保存し他者とシェアすることもできる。一つの画面でソースコードの編集,実行,実行過程の確認までできる。
開発言語/環境
フロントエンド HTML/CSS/JavaScript(JQuery)
バックエンド(CentOS) PHP/Go/MySQL/Apache開発期間
1ヶ月
受賞までの道のり
作品の締め切りが9/14で提出したのは9/13でした(ギリギリ!)。 そのため、応募すると決めた7月中盤から9/13までの開発過程とその後のプレゼンの準備までどういうことをしたのか書いていきます。詳しい開発方法などを書くときりがないのでざっと紹介します。
何を開発するか決める(7月中盤)
7月中盤ごろに自分の力試しを兼ねてU22プログラミングコンテストを応募してみようと思いたちました。何を作るか考える中で、どうせなら世の中の課題を解決するものを作ろうと思い、プログラミング教育に焦点をあてたものを作ろうと決めました。
教育現場ではよくJavaScriptや日本語プログラミング言語が使われるのですがどれもいイマイチ教育向きではない。そのため、あ〜もう自分で作ればいいんだな。となって教育用言語を開発することに決めました。言語設計及び機能決め(7月後半~8月)
締め切りまで2ヶ月もなかったので開発と言語設計は同時進行で行いました。機能と設計の考案は以下のような流れで進めました。
- 課題を明確に設定
- 1の原因を分析
- 2を踏まえて課題を解決できる設計や機能を考案
個人的にはこの言語設計が最も評価されたのだと思います。
課題
ビジュアルプログラミングからテキストプログラミングへステップアップできない人が多い
原因
以下のようなテキストプログラミングの特徴
- 複雑なルール
- 実務でも使えるように普通の言語は設計されているためどうしても機能が多くなり複雑になっている
- 予約語の多さ
- 使いやすさや多機能さを追求するあまり予約後が多くなり、覚える事柄が多くなってしまっている
- 英語でのエラー
- ビジュアルプログラミングにはなかったエラーが出るようになりしかも英語で解読が難しい
- 実行過程の不明調さ
- どういうふうに処理が進んでいったのかわかりにくく、デバッグも難しい
- CUIへの抵抗
- いきなりテキストだけの世界へ踏み出すのはかなり勇気のいること
- 開発環境構築の難しさ
- ダウンロードして環境変数を通して...インストール時点で難しいことをしなければならない
解決策
解決するための設計/機能
- 最低限の機能
- 逐次,繰り返し,分岐処理を学ぶための最低限の機能しかいれない
- 少い予約語
- 予約語の数を極力減らし,覚えやすいよう簡単な英語を名前に用いる。
- 書きやすい文法
- 直感的でシンプルな書きやすい文法を目指す
- 柔軟な型
- 型に極度に配慮する必要のない動的型付け言語
- 従来の言語から逸脱しすぎない
- あくまでもステップアップが目的なので、そのまま一般の言語へ移行できるよう特徴的すぎる設計にしない
- エラーを日本語で表示
- エラーへの抵抗を少なくするために親切な日本語でのエラーを実装する
- 処理過程の表示
- インタプリタがどうソースを評価していったか表示することで理解及びデバッグを助ける
- web上(GUI)で実行可能
- 開発環境構築の必要性をなくす
以上をもとに開発しました。参考書籍探し(7月後半)
インタプリタ自作本とかあるかなっと探してみたらこんなものが...!
Go言語で作るインタプリタ (Thorsten Ball 著、設樂 洋爾 訳)
しかも大好きなGo言語なのでこれはいいと思い即買いしました。
それに加え,サーバーサイドのGo言語も学ぶ必要があったため、こちらの本も購入。
Goプログラミング実践入門 標準ライブラリでゼロからWebアプリを作る impress top gearシリーズ (au Sheong Chang (著), 武舎 広幸 (著), 阿部 和也 (著), 上西 昌弘 (著) )
主にこちらの二冊を参考にしながら開発を進めていきました。言語コア部分作成(7月後半~8月中旬)
GO言語で作るインタプリタをもとに字句解析器,構文解析器,評価器を作成していきます。書籍のサンプルコードがとても優秀なのでほぼそれを流用という形にはなってしまいますが、単に写すのではなく確実に理解してから写しました。そのため思ったより時間がかかってしまい(夏休み全部かけたのですが)あっという間に8月中旬です。
オリジナル設計/機能を追加(8月中旬~9/10)
コア部分はできたので、前述したオリジナルの設計と機能を組み込んでいきます。ここで、写しながらも完璧にソースを理解していたことが役立ち、案外すいすい組み込むことがきました。1日1機能追加を目標に開発を進めていきました。
具体的な改造内容
- 繰り返し処理がなかったためloop文を作成
PeriDotmake i = 0 loop i < 10{ <<10回繰り返す>> i++ } <<簡略化版>> loop 10 { <<10回繰り返す>> }
- 予約語の名称の変更(1例)
改造前let a = 1;PeriDotmake a = 1
- 関数の定義方法の変更
改造前let add = fn(a,b){ return a+b; }PeriDotfunc add(a,b) { return a+b; }
- セミコロン,カッコの削除
改造前let a = 2; if (a%2 == 0) { print("aは偶数です"); }PeriDotmake a = 2 if a%2 == 0 { SAY("aは偶数です") }
- 日本語対応
PeriDotmake りんごの個数 = 2 SAY(りんご)
- 文字列+数の連結可能
PeriDotmake りんごの個数 = 2 SAY("りんごは"+りんごの個数+"個です")
- エラーを日本語で表示
PeriDotmake Array = [1,2,3] SAY(Array[3])エラー配列から値を取り出せませんでした。[ 3 ]に対応する値がみつかりません。 (添字は2以下である必要があります)
- 処理過程の表示
PeriDotmake a = 102 if a%2 == 0 { SAY("aは偶数です") }実行過程[1] 1行目: a -> 102 「変数(a)を定義し(102)を代入」 [2] 2行目: a -> 102 「変数(a)を参照」 [3] 2行目: 102 % 2 -> 0 「計算」 [4] 2行目: 0 == 0 -> true 「評価(true or false)」 [5] 2行目: ((a % 2) == 0) -> true 「条件がtrueであったためif内を実行」 [6] 3行目: SAY -> 実行 「関数を実行(関数:SAY)」webサービス作成(8月中旬~9/10)
言語作成と並行してwebサービス(Peree)も作りました。
フロントエンドはJQuery、PeriDotの実行/結果返却やプロジェクトの保存/読み出しなどはGo言語、ユーザーのログイン処理などにはMySQLとPHPを使いました。通信ではAjaxを用いることでUXの向上を図り、SQLインジェクション対策やOSコマンドインジェクション対策などのセキュリティ対策も行いました。
図に表すと以下のような感じです。
作品完成(9/10)
マニュアル&資料作成(9/10~締め切り)
応募のために必要な資料や説明の動画をこの期間に作りました。また、オンラインマニュアルも作成しました。
第一次審査通過(10月中旬),第二次審査通過(10月下旬)
最終審査(11/29)にむけて
第二次審査通過をすると、プレゼンテーション(今年はオンライン)にむけての案内が届くのでそれに従って、プレゼン資料を作っていきます。持ち時間は約10分なのでそれに収まりながらも工夫点を最大限伝えられるよう練習しました。
最終審査(11/29)
ニコニコ生放送されるゆえにすごい方々に見られながらなのでとても緊張しましたが、なんとか乗り越えることができ、受賞することができました。
まとめ
簡単に受賞までの経緯をまとめてみました。
U22プログラミングコンテストは、技術に加え課題解決能力も非常に問われるコンテストだと思います。もし応募してみようと思っている方は是非参考にしてください!
最後まで呼んでいただきありがとうございますPeriDotホームページ
フィードバックやご意見/ご感想をこちらに寄せていただけると大変嬉しいです
- 投稿日:2021-03-25T19:36:03+09:00
【HTTPヘッダー】CORSの仕組みとGo+GinによるCors設定の実践
はじめに
localhost:80
で立てたWebサーバー(nginx)から
localhost:8080
に立てたAPIサーバー(golang)にリクエストを送った時に、
CORSエラーが発生し、APIが叩けないところでハマったので、
学びとしてまとめます。そもそもHTTPとは?
HTTP は、 HTML 文書などのリソースを取り出すことを可能にするプロトコルです。これはウェブにおけるデータ交換の基礎をなし、クライアントサーバープロトコルであり、リクエストは受け取り者 (一般にはウェブブラウザー) が生成します。文書全体は、テキスト、レイアウトの定義、画像、動画、スクリプトなど、取り込まれたさまざまなサブ文書から再構成されます。
参考元:HTTP の概要
- プロトコルの一種
- ウェブにおけるデータ交換の基礎
- シンプルで人間が読めるように設計されている
- HTTPヘッダーで、プロトコルの拡張や検証が簡単
- ステートレスだけど、HTTP Cookie によってステートフルなセッションを実現
- HTTP は TCP 標準に依存。
- TCPはハンドシェイクによって通信の信頼性を担保、つまり通信相手の応答があってはじめて通信を開始するプロトコルを採用
HTTPで制御できること
ざっくりと
- キャッシュ: html documentsとか
- オリジン制約のコントロール: CORS(Cross Origin Resource Sharing)の話
- 認証: Basic認証とか
- プロキシとトンネリング: World Wide Webにアクセスする時にプロキシや別のネットワークを経由してデータを送ったりできるようにする
- セッション:クッキーを使ってセッションを保てる
HTTPヘッダーの役割
HTTP ヘッダーにより、クライアントやサーバーが HTTP リクエストやレスポンスで追加情報を渡すことができます。 HTTP ヘッダーは、大文字小文字を区別しないヘッダー名とそれに続くコロン (:)、 値で構成されます。値の前にあるホワイトスペースは無視されます。
参考元:HTTP ヘッダーなるほど。
HTTPヘッダーはそもそも拡張用のもので、
情報を追加して、独自の制約を持たせたり、
通信のルールを定めて、安心安全かつ用途に合わせたやりとりを実現できる
ってことなんだろうな。
種類 説明 一般ヘッダー /
Generalsリクエストとレスポンスの両方に適用される。
本文で転送されるデータとは関係ないリクエストヘッダー /
Request Headers読込むリソースやリクエストしているクライアントに関する詳細な情報を保持 レスポンスヘッダー /
Response Headersレスポンスに関する追加情報
例えば場所や提供しているサーバーに関するものを保持CORSとHTTPヘッダーの関係
オリジン間リソース共有Cross-Origin Resource Sharing (CORS) は、追加の HTTP ヘッダーを使用して、あるオリジンで動作しているウェブアプリケーションに、異なるオリジンにある選択されたリソースへのアクセス権を与えるようブラウザーに指示するための仕組みです。ウェブアプリケーションは、自分とは異なるオリジン (ドメイン、プロトコル、ポート番号) にあるリソースをリクエストするとき、オリジン間 HTTP リクエストを実行します。
参考元:オリジン間リソース共有 (CORS)つまり、CORSに関連する専用のHTTPヘッダー情報があるってことで、
その情報を使って、CORSを実現、かつ制御できると言うことだな。セキュリティ上の理由から、ブラウザーは、スクリプトによって開始されるオリジン間 HTTP リクエストを制限しています。例えば、 XMLHttpRequestや Fetch API は同一オリジンポリシーsame-origin policyに従います。つまり、これらの API を使用するウェブアプリケーションは、そのアプリケーションが読み込まれたのと同じオリジンに対してのみリソースのリクエストを行うことができ、それ以外のオリジンの場合は正しい CORS ヘッダーを含んでいることが必要です。
参考元:オリジン間リソース共有 (CORS)知らなかった〜〜〜〜〜orz
自分は、JavascriptのFetch APIを使ってHTTPリクエストを投げたから、
same-origin policy
に反して、CORS制限が発動し、
かつ、HTTPヘッダーに適切な設定をしてなかったから、
APIサーバーはリクエストを拒否ってきたのか。CORSリクエストを成功させるには?
各種HTTPヘッダー情報を適切に設定する必要がある。
種類 説明 例 Access-Control-Allow-Origin リクエストの送信元の指定 http://localhost Access-Control-Allow-Credentials 資格情報(Cookie、認証ヘッダー、TLSクライアント証明書)の送信をOKするか true Access-Control-Allow-Headers リクエスト間に使用できるHTTPヘッダーを指定 Accept, Content-Type Access-Control-Allow-Methods 使用できるメソッドを指定 GET, POST, HEAD Access-Control-Expose-Headers ヘッダー名を羅列して、レスポンスの一部として開示するものを指定
既定のセーフリストは7つだけだからContent-Length Access-Control-Max-Age プリフライトリクエストの結果をキャッシュしてよい期間を指定 86400 Access-Control-Request-Headers 実際のリクエストで使うHTTPヘッダーをサーバーに知らせる目的
プリフライトリクエストで使用Accept, Content-Type Access-Control-Request-Method 実際のリクエストで使うHTTPメソッドをサーバーに知らせる目的
プリフライトリクエストで使用GET, POST, HEAD CORSで
プリフライト
を引き起こさないためにはそもそもプリフライトとは
「プリフライト」リクエストは始めに OPTIONS メソッドによる HTTP リクエストを他のドメインにあるリソースに向けて送り、実際のリクエストを送信しても安全かどうかを確かめます。サイト間リクエストがユーザーデータに影響を与える可能性があるような場合に、このようにプリフライトを行います。
参考元:プリフライトリクエスト安全確認のためのリクエストってことすな。
プリフライトしない条件
以下のすべての条件を満たすもの。
- メソッド
- GET
- HEAD
- POST
- HTTPヘッダー
- Accept
- Accept-Language
- Content-Language
- Content-Type
- application/x-www-form-urlencoded
- multipart/form-data
- text/plain
- DPR
- Downlink
- Save-Data
- Viewport-Width
- Width
- XMLHttpRequestUpload にもイベントリスナーが登録されていない
- ReadableStream オブジェクトが使用されていない
実際に確認してみる。
とりあえず
gin
+cors
で確認してみる。
※下記、コードは色々、省略してるので、コピーでは動きません。fetch//一部の記述です fetch(url_string + queryParams, { method: 'GET',//<-これをいじって検証 mode: 'cors', headers: { 'Content-Type': 'application/json',//<-これをいじって検証 } })main.goimport ( "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" ) func main() { // Start HTTP server r := gin.Default() // ここからCorsの設定 // *****CORS設定をいじくって検証******** r.Use(cors.New(cors.Config{ // アクセス許可するオリジン AllowOrigins: []string{ "http://localhost", }, // アクセス許可するHTTPメソッド AllowMethods: []string{ "POST", "GET", "OPTIONS", }, // 許可するHTTPリクエストヘッダ AllowHeaders: []string{ "Content-Type", }, // cookieなどの情報を必要とするかどうか AllowCredentials: false, // preflightリクエストの結果をキャッシュする時間 MaxAge: 24 * time.Hour, })) r.GET("/scrape", scrapeText) if err := r.Run(":8080"); err != nil { log.Fatal(err) } }リクエストでは
Content-Type
を送るけど、サーバーサイドでは許していないパターン
Request header field content-type is not allowed by Access-Control-Allow-Headers in preflight response
ヘッダーを許可してないよと怒られた。
リクエストではPUTで送るけど、サーバーサイドでは、PUTメソッドを許していないパターン
Method PUT is not allowed by Access-Control-Allow-Methods in preflight response
PUTメソッド許可してないよと怒られた。
"リクエストでは
Content-Type
を送るけど、サーバーサイドでは許していないパターン
Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource...
Originを許可してないよって怒られた。
※オリジンとリファラーって最後の/
があるかないかで微妙に違うから気をつけないと。。。ちなみに、条件に合わせたらプリフライトが飛ばなくなった
'Content-Type': 'text/plain',
に変更してみたら、リクエストが1回のみになった。変更前は、プリフライトある場合は、2回APIがたかれている。
application/json
は許可されていないので、
いわゆるSimple requests
(プリフライトが怒らないやつ)の条件から外れてしまうわけですねぇまとめ
CORSでハマって、なんやねんなんやねん、パソコンたたき割ったろかぁぁぁあああ!!!
って数時間ずっと苦しみましたが、なかなかの学びと成果につながりました。検証したから、しっかりと身を持って体験できたことで、実感が湧きました。
変毒為薬っすな。
HTTPヘッダーさんとCORSさんが、こんな仕組みになってるなんて知らなかったワイ✨
以上、ありがとうございました。
引き続き頑張ります٩( ᐛ )و
- 投稿日:2021-03-25T13:27:48+09:00
【Go】「binary.Write: invalid type 」が発生してバイナリ書き込みができない
問題
以下のような構造体をバッファに書き込もうとしてエラーが発生した。
main.gotype T1 struct { A int8 B int8 } type T2 struct { T1 C string D int8 } var ( t1 T1 t2 T2 ) t2.T1 = t1 t2.C = "hoge" t2.D = 1234 buf := new(bytes.Buffer) // ここで該当のエラーが発生 // // binary.Write: invalid type *main.T2 err := binary.Write(buf, binary.LittleEndian, &t2)原因
埋め込みがまずいと予想していたが、どうやらstringを書きこもうとして出力されるエラーだとわかった。
ドキュメントによると、1
Write は data のバイナリ表現を w に書き込みます。 data は,固定サイズ値または固定サイズ値のスライス,あるいはそのようなデータへのポインタでなければなりません。
対処法
なので、stringを固定長byteに変換して、構造体へコピーすれば良さそう
main.gotype T1 struct { A int8 B int8 } type T2 struct { T1 C [10]byte D int8 } var ( t2 T2 // 書き込み用 r2 T2 // 読み込み用 ) // 固定長byteにコピー copy(t2.C[:], "hoge") buf := new(bytes.Buffer) // バッファに書き込み err := binary.Write(buf, binary.LittleEndian, &t2) // バッファから読み込み err = binary.Read(buf, binary.LittleEndian, &r2) // 固定長byteをstringに変換 fmt.Println(string(r2.C[:]))参考
- 投稿日:2021-03-25T13:05:30+09:00
【並列・並行解説付き】goroutineでマルチスレッド処理を試してみた
はじめに
タイトルをあえて、"マルチスレッド"とだけ書き、
並行・並列
という言葉を使うのを避けました。
理由としては、
goroutine
はruntime.NumCPU()
の値(論理コア数)が複数だった場合、並列処理となる。- 論理コアごとに
gorotine
と紐づくカーネルスレッドが実行されるM:Nモデル
を採用しているので、同時に複数の処理が走る。(並行処理)ただ、並行は並列を包含するので、
強いて言えば、goroutineは、並行処理というのがいいのかもしれませんね。すでに「並」でゲシュタルト崩壊発生( ;´Д`)
マルチスレッドとは
すでに、並列処理・並行処理ってなんやねん!!って状態だと思いますが、
図をご覧下さい。
並行(concurrent)
並列とは違い、複数のタスクを1人が処理するイメージ。
シングルコアのCPUが複数の処理を同時に動かすことなどが挙げられる。たとえば、主婦が、炊事、洗濯、掃除を切り替えながら同時かつ並行的に家事を進める感じ。
並列(parallel)
並行とは違い、複数人が同時に複数のタスクを処理すること。
複数のコアのCPUが、それぞれのスレッドを同時に動かすのであれば、それは並列処理
※「並行かつ並列」という状態は起こりうるたとえば、主婦が家政婦を引き連れて陣形を組み、
主婦:炊事、家政婦A:洗濯、家政婦B:掃除
みたいに担当を決めて、同時かつ並列的に家事を進める感じ。
ただ、主婦が買出しも引き受けて、炊事と並行的に二つを進めれば、並行かつ並列の状態。goroutineの特長
ざっくり言うと、マルチスレッド処理が簡単に実装できてかつ、高速に実行できるすごいやーつ。
理由としては、
- 複数のユーザ・スレッドに対して、複数のカーネル・スレッドをマップする
M:Nモデル
を使用してるので、ユーザスレッドであるgoroutine
はコンテクストスイッチするが、カーネルスレッドはコンテクストスイッチせずに、オーバーヘッドが発生しづらいから。goroutine
のスタックはガードページを使わない&初期サイズが2KBだから、メモリ使用量が少ない用語
コンテキストスイッチとは、コンピュータの処理装置(CPU)が現在実行している処理の流れ(プロセス、スレッド)を一時停止し、別のものに切り替えて実行を再開すること。
IT用語辞典 e-Words
ガードページは、ヒープブロック間にガードページと呼ばれるアクセスを禁止した領域を置くことで、ヒープ領域を超えて書き込みが行われた場合、プログラムを異常終了させるものです。
ヒープに対する攻撃とその対策goroutineの構成要素
goroutine
:マルチスレッド化channel
:スレッド同士で値のやりとりをできる経路の役割select
:複数のチャネル操作を待つことができるという3つの機能を駆使するといいらしい。
※本編では
goroutine
のみで実験します。channel
とか別で挑戦したい。書き方
goroutine// function定義をしてgoroutine実行 func goroutine(){ //処理 } func main() { go goroutine() }goroutine// 無名関数を定義して func main() { goroutine := func() { //処理 } }goroutine// 無名関数的に(省略形) func main() { go func() { //処理 }() //←()忘れないように } // ちなみにループだと、コピーを引数に渡さないと、参照になってしまい、想定しない数字が出力されるよ for _, val := range values { go func(i int) { // 引数を追加 fmt.Println(i) }(val) // 関数実行時に現在の値を渡す }コード
今回は、go gin のフレームワークを使って、
REST API
を作成し、複数のURLを[,]区切りで入力すると、そのページ先からタイトルを抽出して、かかった時間と一緒に結果を返すというmain.go
にしてます。
Webスクレイピングには、goquery という便利なライブラリを使いました。
(※エディタはgoland
を使いました。Go Modules
とかの管理が楽)
main.go
main.gopackage main import ( "bytes" "fmt" "golang.org/x/net/html/charset" "io/ioutil" "log" "net/http" "strconv" "strings" "sync" "time" "github.com/PuerkitoBio/goquery" "github.com/gin-gonic/gin" "github.com/saintfish/chardet" ) func main() { // Start HTTP server r := gin.Default() r.GET("/scrape", scrapeText) if err := r.Run(":8080"); err != nil { log.Fatal(err) } } // ページタイトルと取得時間の構造体 type Title struct { Title string Time string } func scrapeText(c *gin.Context) { //クエリストリングから値を受け取り配列化 var urls []string urls = strings.Split(c.Query("urls"), ",") //syncでURLの数だけ待ち合わせするWaitGroupの宣言 var wg sync.WaitGroup wg.Add(len(urls)) //titlesを格納する変数宣言 var results []Title //titleをfetchする無名関数を代入 fetchTitle := func(url string) { start := time.Now() defer wg.Done() // Getリクエスト res, _ := http.Get(url) defer res.Body.Close() // 読み取り buffer, _ := ioutil.ReadAll(res.Body) // 文字コード判定 detector := chardet.NewTextDetector() detectResult, _ := detector.DetectBest(buffer) fmt.Println(detectResult.Charset) // 文字コード変換 bufferReader := bytes.NewReader(buffer) reader, _ := charset.NewReaderLabel(detectResult.Charset, bufferReader) // HTMLパース document, _ := goquery.NewDocumentFromReader(reader) // titleを抜き出し title := document.Find("title").Text() end := time.Now(); time := (end.Sub(start)).Seconds() result := Title{title, strconv.FormatFloat(time, 'f', -1, 64) } results= append(results, result) } //urlsの回数分スレッド実行 for _, url := range urls { go fetchTitle(url) } //fetchTitle goroutineが終わるまで、wg.Wait()で待つ wg.Wait() c.JSON(http.StatusOK, results) }
index.html
Fetch APIをつかってリクエストを送ってみました。
※'Content-Type': 'application/json'
なので、プリフライトリクエストが送られる模様。
参考元:CORSindex.html<!DOCTYPE html> <html lang="ja"> <head> <meta http-equiv="content-type" charset="utf-8"> <title>Test Application</title> </head> <body> <form name="multiScraping" id="multiScraping"> <p>URL:[,]カンマ区切りで複数入力</p> <p><textarea name="urls" rows="10" cols="80" placeholder="URL記入"></textarea></p> <p><input type="button" value="scrape" onclick='getTitles()'></p> </form> <ul id="titles"></ul> <script type="text/javascript"> function getTitles() { const urls = document.forms['multiScraping'].elements['urls'].value; const params = { // 渡したいパラメータをJSON形式で書く urls: urls, }; const queryParams = new URLSearchParams(params); const url_string = 'http://localhost:8080/scrape?'; fetch(url_string + queryParams, { method: 'GET', headers: { 'Content-Type': 'application/json', } }) .then((res) => { if (!res.ok) { throw new Error(`${res.status} ${res.statusText}`); } return res.json();//.json()にするとパースされたJSONが取得できる(promiseが返される) }) .then((data) => { console.log(data); // 詰め替え処理して表示させる for(let i=0, len=data.length;i<len;i++) { let title = document.createElement("li"); title.setAttribute("id", "title-"+i); title.innerHTML = data[i]['Title'] + ':' + data[i]['Time'] + '秒'; document.getElementById("titles").appendChild(title); } }) .catch((reason) => { console.log(reason); }); } </script> </body> </html>早速試してみる
まずは、URLをカンマ区切りで複数入力してみる。
とりあえず主要どころの
Yahoo
,Amazon
、もちろんQiita
さんもいれてみました。
scrape
ボタンをポチッとなと...おおお、タイトルと掛かった時間が取得できた。
URLの順番的には、
- Yahoo
- Qiita
- Amazon
と入力したけど、
返ってきた結果は、
- Yahoo
- Qiita
- Amazon
の順番でした。
スレッドだから順不同なので、早く終わったものから、
レスポンス用の配列に追加されてった感じですな。2回目やると
- Yahoo
- Qiita
- Amazon
順番が変わった。
まとめ
goroutine
奥が深すぎて、難しい印象。だけど記述自体はものすごく簡単だ。マルチスレッドプログラミングができるようになるには、
メモリのこととか、処理の待ちとか、値の受け渡し方、チャンネルのこととか色々考えていかないといけないんだな〜
って思いました。
まだまだ習得には時間がかかりそうです。頑張ってまいります!
以上、ありがとうございました!