20210325のGoに関する記事は4件です。

高校生が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

ウィンドウのキャプチャ 6.png

種類

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. 1の原因を分析
  3. 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文を作成
PeriDot
make i = 0
loop  i < 10{
  <<10回繰り返す>>
  i++
}
<<簡略化版>>
loop 10 {
  <<10回繰り返す>>
}


予約語の名称の変更(1例)
改造前
let a = 1;
PeriDot
make a = 1


関数の定義方法の変更
改造前
let add = fn(a,b){ return a+b; }
PeriDot
func add(a,b) { return a+b; }


セミコロン,カッコの削除
改造前
let a = 2;
if (a%2 == 0) {
  print("aは偶数です");
}
PeriDot
make a = 2
if a%2 == 0 {
  SAY("aは偶数です")
}


日本語対応
PeriDot
make りんごの個数 = 2
SAY(りんご)


文字列+数の連結可能
PeriDot
make りんごの個数 = 2
SAY("りんごは"+りんごの個数+"個です")


エラーを日本語で表示
PeriDot
make Array = [1,2,3]
SAY(Array[3])
エラー
配列から値を取り出せませんでした。[ 3 ]に対応する値がみつかりません。
(添字は2以下である必要があります)


処理過程の表示
PeriDot
make 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コマンドインジェクション対策などのセキュリティ対策も行いました。
図に表すと以下のような感じです。
Peree仕組み.png

作品完成(9/10)

マニュアル&資料作成(9/10~締め切り)

応募のために必要な資料や説明の動画をこの期間に作りました。また、オンラインマニュアルも作成しました。

第一次審査通過(10月中旬),第二次審査通過(10月下旬)

最終審査(11/29)にむけて

第二次審査通過をすると、プレゼンテーション(今年はオンライン)にむけての案内が届くのでそれに従って、プレゼン資料を作っていきます。持ち時間は約10分なのでそれに収まりながらも工夫点を最大限伝えられるよう練習しました。

最終審査(11/29)

ニコニコ生放送されるゆえにすごい方々に見られながらなのでとても緊張しましたが、なんとか乗り越えることができ、受賞することができました。

まとめ

簡単に受賞までの経緯をまとめてみました。
U22プログラミングコンテストは、技術に加え課題解決能力も非常に問われるコンテストだと思います。もし応募してみようと思っている方は是非参考にしてください!
最後まで呼んでいただきありがとうございます

PeriDotホームページ
フィードバックやご意見/ご感想をこちらに寄せていただけると大変嬉しいです

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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.go
import (
    "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を送るけど、サーバーサイドでは許していないパターン

スクリーンショット 2021-03-25 18.52.14.png

Request header field content-type is not allowed by Access-Control-Allow-Headers in preflight response

ヘッダーを許可してないよと怒られた。

リクエストではPUTで送るけど、サーバーサイドでは、PUTメソッドを許していないパターン

スクリーンショット 2021-03-25 19.09.18.png

Method PUT is not allowed by Access-Control-Allow-Methods in preflight response

PUTメソッド許可してないよと怒られた。

"リクエストではContent-Typeを送るけど、サーバーサイドでは許していないパターン

スクリーンショット 2021-03-25 19.13.47.png

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回のみになった。

スクリーンショット 2021-03-25 19.21.30.png

変更前は、プリフライトある場合は、2回APIがたかれている。
スクリーンショット 2021-03-25 19.24.19.png

application/jsonは許可されていないので、
いわゆるSimple requests(プリフライトが怒らないやつ)の条件から外れてしまうわけですねぇ

まとめ

CORSでハマって、なんやねんなんやねん、パソコンたたき割ったろかぁぁぁあああ!!!
って数時間ずっと苦しみましたが、なかなかの学びと成果につながりました。

検証したから、しっかりと身を持って体験できたことで、実感が湧きました。

変毒為薬っすな。

HTTPヘッダーさんとCORSさんが、こんな仕組みになってるなんて知らなかったワイ✨

以上、ありがとうございました。

引き続き頑張ります٩( ᐛ )و

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Go】「binary.Write: invalid type 」が発生してバイナリ書き込みができない

問題

以下のような構造体をバッファに書き込もうとしてエラーが発生した。

main.go
type 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に変換して、構造体へコピーすれば良さそう

The Go Playground

main.go
type 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[:]))

参考


  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【並列・並行解説付き】goroutineでマルチスレッド処理を試してみた

はじめに

タイトルをあえて、"マルチスレッド"とだけ書き、並行・並列という言葉を使うのを避けました。
理由としては、

  • goroutineruntime.NumCPU()の値(論理コア数)が複数だった場合、並列処理となる。
  • 論理コアごとにgorotineと紐づくカーネルスレッドが実行されるM:Nモデルを採用しているので、同時に複数の処理が走る。(並行処理)

ただ、並行は並列を包含するので、
強いて言えば、goroutineは、並行処理というのがいいのかもしれませんね。

すでに「並」でゲシュタルト崩壊発生( ;´Д`)

マルチスレッドとは

すでに、並列処理・並行処理ってなんやねん!!って状態だと思いますが、
図をご覧下さい。
スクリーンショット 2021-03-25 11.48.52.png

並行(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.go
package 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'なので、プリフライトリクエストが送られる模様。
参考元:CORS

index.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をカンマ区切りで複数入力してみる。

とりあえず主要どころのGoogle,Yahoo,Facebook,Amazon、もちろんQiitaさんもいれてみました。
スクリーンショット 2021-03-25 12.47.07.png

scrapeボタンをポチッとなと...

スクリーンショット 2021-03-25 12.49.20.png

おおお、タイトルと掛かった時間が取得できた。

URLの順番的には、

  1. Google
  2. Yahoo
  3. Qiita
  4. Facebook
  5. Amazon

と入力したけど、

返ってきた結果は、

  1. Yahoo
  2. Google
  3. Qiita
  4. Facebook
  5. Amazon

の順番でした。
スレッドだから順不同なので、早く終わったものから、
レスポンス用の配列に追加されてった感じですな。

2回目やると

スクリーンショット 2021-03-25 13.00.33.png

  1. Yahoo
  2. Google
  3. Facebook
  4. Qiita
  5. Amazon

順番が変わった。

まとめ

goroutine奥が深すぎて、難しい印象。だけど記述自体はものすごく簡単だ。

マルチスレッドプログラミングができるようになるには、
メモリのこととか、処理の待ちとか、値の受け渡し方、チャンネルのこととか色々考えていかないといけないんだな〜
って思いました。
まだまだ習得には時間がかかりそうです。

頑張ってまいります!

以上、ありがとうございました!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む