やわらかテック

興味のあること。業務を通して得られた発見。個人的に試してみたことをアウトプットしています🍵

elixirでcsvファイルにデータを書き出す

mixのプロジェクトの用意

前提としてmix newでプロジェクトが作成されている状態とします
mixコマンドを使ってプロジェクトを作成するまでの手順は こちらの前半部分で触れていますので
必要であればご覧ください

テキトーにgitを覗いてみる

あった(秒速

beatrichartz/csv

リポジトリ名見ただけで分かりましたわ
starもかなりあるのでこちらのcsvモジュールを使用します

ライブラリのダウンロード

READ.MEに従いmix.exsファイルに

{:csv, "~> 2.3"}

を追記します

./project_name/mix.exs

defp deps do
    [
      # {:dep_from_hexpm, "~> 0.3.0"},
      # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
      {:csv, "~> 2.3"} #2019/04/06時のREAD.MEを参照
    ]
  end

あとはいつものように

$ mix deps.get
$ mix deps.compile #やらなくてもok

コマンドを叩いてライブラリをダウンロードしましょう
これでcsvライブラリを使う準備はできました

csvモジュールの使い方

ざっくりとREAD.MEを見た感じ、decodeとencodeに対応しているようです
今回はcsvファイルへの書き込みなのでencode部分のみを取り扱います

どんなデータを用意すればいいのかという話になりますがREAD.MEに記述あります

Encoding
Do this to encode a table (two-dimensional list):

table_data |> CSV.encode

二次元のリスト用意して、パイプでencodeすればええでーってことですね
今回もテキトーなデータ用意しました。らきすたは初めてハマったアニメです
このデータをcsv形式で書き出してみます

[
  ["name", "class", "height", "food"],
  ["泉こなた", "2年E組", "142", "チョココロネ"],
  ["柊つかさ", "2年E組", "158", "バルサミコ酢"],
  ["柊かがみ", "2年D組", "159", "ヨーグルト"],
  ["高良みゆき", "2年E組", "166", "うなぎの卵焼き"],
]

READ.MEに従い記述

lst_data
  |> CSV.encode

あー、でもこれだけだとファイルは生成されません
ファイル読み込みと書き込みの操作と追記します
ついでに関数化しておきましょう

defmodule ProjectCSV do
  def output_csv(file_name) do
    lst_data = [
      ["name", "class", "height", "food"],
      ["泉こなた", "2年E組", "142", "チョココロネ"],
      ["柊つかさ", "2年E組", "158", "バルサミコ酢"],
      ["柊かがみ", "2年D組", "159", "ヨーグルト"],
      ["高良みゆき", "2年E組", "166", "うなぎの卵焼き"],
    ]
    file = File.open!(file_name, [:write, :utf8])
    lst_data
      |> CSV.encode
      |> Enum.each(&IO.write(file, &1))
    IO.puts("--> output csv file")
  end
end

本来ならファイル名と共に2次元リストも引数として渡すべきですが
今回は関数内に変数を置いてます
さっそくこいつが上手く動くかを確かめてみます

iex -S mix

mix deps.compileをしていなければライブラリのコンパイルが始まります

iex > ProjectCSV.output_csv("lucky_star.csv")
--> output csv file
:ok

今回は特にパスで出力先を指定しないのでプロジェクトディレクトリの直下にcsvファイルが生成されます
お、無事に生成されました!!!

f:id:takamizawa46:20190406102812p:plain
elixirでcsvファイルを出力した結果

おまけ

同一のcsvにデータを追記していく場合には上のサンプルでは毎度上書きされてしまいます
csvモジュールの話ではないですが、同一のcsvファイルに追記していきたい場合には
optionのパラメータの:writeを:appendに変更します

#変更前
file = File.open!(file_name, [:write, :utf8])

#変更後
file = File.open!(file_name, [:append, :utf8])

:appendに変更してもう一度実行すると同じデータが追記されます

f:id:takamizawa46:20190406104020p:plain:w300
同じファイルに上書きする

あら^〜、headerが2回も書き込まれている...
公式ドキュメントをみた感じpandasみたいにheaderをセットする引数はなさそう
ファイル読み込み時に対象のheaderが存在しているかをチェックしつつ
headerを挿入するかどうかという処理を組み込みます

def is_exist_header_in_file(file_name, header) do
    read_file = File.stream!(file_name) |> CSV.decode! |> Enum.to_list
    List.first(read_file) === header
  end

この関数にcsvファイル名とheaderのリストを渡すことで対象のheaderが
存在するかしないかをbool値でreturnしてくれます
その戻り値をcaseで場合分けさせて

  • trueであればheaderは追記しない
  • falseであればheaderを追記

するようにoutput_csv関数を変更しました

defmodule ProjectCSV do
  def is_exist_header_in_file(file_name, header) do
    read_file = File.stream!(file_name) |> CSV.decode! |> Enum.to_list
    List.first(read_file) === header
  end

  def output_csv(file_name) do
    header = ["name", "class", "height", "food"]
    lst_data = [
      ["泉こなた", "2年E組", "142", "チョココロネ"],
      ["柊つかさ", "2年E組", "158", "バルサミコ酢"],
      ["柊かがみ", "2年D組", "159", "ヨーグルト"],
      ["高良みゆき", "2年E組", "166", "うなぎの卵焼き"],
    ]

    file = File.open!(file_name, [:append, :utf8])
    case is_exist_header_in_file(file_name, header) do
      true ->
        lst_data
          |> CSV.encode
          |> Enum.each(&IO.write(file, &1))
      false ->
        List.insert_at(lst_data, 0, header)
          |> CSV.encode
          |> Enum.each(&IO.write(file, &1))
    end
    IO.puts("--> output csv file")
  end
end

上手くいきました!!

f:id:takamizawa46:20190406112112p:plain:w350
headerが2回書き込まれないように変更

正直caseやなくてifでもええかなとは思う...
elixirではifを極力使わないようにした方がいいと偉い人が言っていました
関数でのパターンマッチもありだと今更思えてきた

elixirで簡単にAmazonレビューをスクレイピングする

用意するもの

  • elixir(1.8.1でやってます)
  • mix(elixirのビルドツール。クソ便利)
  • HTTPoison(HTTPクライアント)
  • Floki(HTMLパーサー)

事前準備について

elixirのインストールについては割愛します
公式のドキュメントみた方が圧倒的ッ!に早いです こちらに各環境でのインストール方法が記載されています

mixについてはelixirをインストールした際に共にインストールされています
nodeインストールするとnpm入ってるのと同じですね
試しに以下コマンドを打つ

mix help
mix                   # Runs the default task (current: "mix run")
mix app.start         # Starts all registered apps
mix app.tree          # Prints the application tree
mix archive           # Lists installed archives
mix archive.build     # Archives this project into a .ez file
mix archive.install   # Installs an archive locally
mix archive.uninstall # Uninstalls archives
mix clean             # Deletes generated application files
mix cmd               # Executes the given command
mix compile           # Compiles source files
mix deps              # Lists dependencies and their status
:
:

大丈夫そうですね

新規のプロジェクトを立ち上げる

mixのコマンドを使うことでプロジェクトを生成することが可能です
今回は「amazon_scraping」という名前のプロジェクトを立ち上げるとしましょう

mix new amazon_scraping

mix newコマンドによって各ファイルが生成されました

* creating README.md
* creating .formatter.exs
* creating .gitignore
* creating mix.exs
* creating config
* creating config/config.exs
* creating lib
* creating lib/amazon_scraping.ex
* creating test
* creating test/test_helper.exs
* creating test/amazon_scraping_test.exs

Your Mix project was created successfully.
You can use "mix" to compile it, test it, and more:

    cd amazon_scraping
    mix test

Run "mix help" for more commands.

とりあえずcdコマンドで場所だけ移動しておきましょう

cd amazon_scraping

現状ではtestはどちらでもいいかな(後にちゃんとやる

ライブラリのインストール

生成されたプロジェクトディレクトリの直下にmix.exsというファイルがあります
このファイルを開きます ./amazon_scraping/mix.exs

defmodule AmazonScraping.MixProject do
  use Mix.Project

  def project do
    [
      app: :amazon_scraping,
      version: "0.1.0",
      elixir: "~> 1.8",
      start_permanent: Mix.env() == :prod,
      deps: deps()
    ]
  end

  # Run "mix help compile.app" to learn about applications.
  def application do
    [
      extra_applications: [:logger]
    ]
  end

  # Run "mix help deps" to learn about dependencies.
  defp deps do
    [
      # {:dep_from_hexpm, "~> 0.3.0"},
      # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
    ]
  end
end

これ何なん?という質問に直球で答えると設定ファイル(プロジェクトの情報群)だと思ってください
今回の目的に合わせて答えると、このファイルに使用したい外部ライブラリを記述することでinstallがすることが可能となります
なので、この関数内に

# Run "mix help deps" to learn about dependencies.
  defp deps do
    [
      # {:dep_from_hexpm, "~> 0.3.0"},
      # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
    ]
  end

HTTPoisonFlokiを追加
インストールするバージョンは上記リンクgitのREAD.MEを見れば大体分かります
こちらは2019年4月での最新バージョンをインストールしています

# Run "mix help deps" to learn about dependencies.
  defp deps do
    [
      # {:dep_from_hexpm, "~> 0.3.0"},
      # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
      {:httpoison, "~> 1.4"},
      {:floki, "~> 0.20.0"}
    ]
  end

ファイルを保存して、下記コマンドを叩きます

mix deps.get

このコマンドを打つことでmix.exsファイルに記述されている外部ファイルをダウンロードしてくれます
当然ながら、通信環境下でないと失敗します

Resolving Hex dependencies...
Dependency resolution completed:
Unchanged:
  certifi 2.5.1
  floki 0.20.4
  hackney 1.15.1
  html_entities 0.4.0
  httpoison 1.5.0
  idna 6.0.0
  metrics 1.0.1
  mimerl 1.2.0
  mochiweb 2.18.0
  parse_trans 3.3.0
  ssl_verify_fun 1.1.4
  unicode_util_compat 0.4.1
All dependencies are up to date

ついでに依存関係のダウンロードもしてくれるので色々とゴチャゴチャしてますが
無事にダウンロードしてくれたようですね
ただ、まだコンパイルされていないので

mix deps

と叩くと

:
:
* httpoison (Hex package) (mix)
  locked at 1.5.0 (httpoison) 71ae9f30
  the dependency build is outdated, please run "mix deps.compile"
:
:
* floki (Hex package) (mix)
  locked at 0.20.4 (floki) be42ac91
  the dependency build is outdated, please run "mix deps.compile"

コンパイルオナシャスと返ってきますが自動的にコンパイルされるのでどちらでも大丈夫です
いますぐコンパイルしたいという方は

mix deps.cpmpile

でmixを満足させてあげてください

いよいよコードを書いていく

生成されたこちらのファイルに記述をしていきます
./amazon_scraping/lib/amazon_scraping.ex

defmodule AmazonScraping do
  @moduledoc """
  Documentation for AmazonScraping.
  """

  @doc """
  Hello world.

  ## Examples

      iex> AmazonScraping.hello()
      :world

  """
  def hello do
    :world
  end
end

手始めにAmazonScrapingモジュールのhelloコマンドを呼び出してみます

iex -S mix

と叩くとプロジェクトを立ち上げてiexプロンプトを起動することが出来ます
つまりはインストールした外部ファイル使えまっせということ(怪しい
先ほどmix deps.compileコマンドを叩いていない場合にコンパイルが自動的に走ります

:
:
==> httpoison
Compiling 3 files (.ex)
Generated httpoison app
===> Compiling mochiweb
==> floki
Compiling 1 file (.erl)
Compiling 21 files (.ex)
Generated floki app
==> amazon_scraping

ではさっそく呼び出してみます

iex(1)> AmazonScraping.hello()
:world

うまく呼び出すことが出来ました
ではさっくとスクレイピングの処理を記述します
今回は宣伝も兼ねて「プログラミングelixir」のレビューを取得します

2019年4月4日で3件のレビューがあります(増えろ増えろ~
このレビューのテキストを3件取得してみます
プログラムの流れはこんな感じになりそうです

  1. 対象のページにアクセスしページ情報を取得
  2. 抽出したいdomを指定する
  3. domのテキストを抽出する
  4. etc

とりあえずhelloという名前はふさわしくないのでfetchと改名しつつ上記のフローを形にしてみます

def fetch do
    #取得先のURL
    target_url = "https://www.amazon.co.jp/%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0Elixir-Dave-Thomas/dp/4274219151/ref=sr_1_1?__mk_ja_JP=%E3%82%AB%E3%82%BF%E3%82%AB%E3%83%8A&keywords=%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0elixir&qid=1554332915&s=gateway&sr=8-1"
    fetch_res =
      HTTPoison.get!(target_url).body #ページにアクセスし戻り値のbodyを取得する
      |> Floki.find("div.a-expander-content") #レビュー部分のdomを抽出
      |> Enum.map(&(Floki.text(&1))) #domのテキストを抽出
      |> Enum.map(&(String.replace(&1, "\n", ""))) #改行記号がうざいので除去
    fetch_res
  end

といった形に落ち着きました まず、HTTPoison.get!メゾットを使用して対象ページにアクセスします
このメゾットは例外を発生させる(elixirでは例外を発生させるメゾットには「!」をつける)んですが
HTTPoison.getメゾットを使うよりもメリットがあって
get!メゾットの方がgetメゾットよりも階層が1つ浅いのでbodyの取得が楽です

HTTPoison.get!の場合

AmazonScraping.get()
%HTTPoison.Response{
  body: "information for body....",
  headers: [
    {"Content-Type", "text/html;charset=UTF-8"},
    {"Transfer-Encoding", "chunked"},
    :
    :
  ],
  request: %HTTPoison.Request{
    body: "",
    headers: [],
    method: :get,
    options: [],
    params: %{},
    url: "https://sample.com"
  },
  request_url: "https://sample.com",
  status_code: 200
}

HTTPoison.getの場合

{:ok,
  %HTTPoison.Response{
    body: "information for body....",
    headers: [
      {"Content-Type", "text/html;charset=UTF-8"},
      {"Transfer-Encoding", "chunked"},
      :
      :
    ],
    request: %HTTPoison.Request{
      body: "",
      headers: [],
      method: :get,
      options: [],
      params: %{},
      url: "https://sample.com"
    },
    request_url: "https://sample.com",
    status_code: 200
  }
}

getメゾットは例外を発生させない代わりに:ok, :errorのどちらかのアトムが返ってきています
このreturnのbodyをFlokiを使って解析します
今回の場合はレビューのテキストを取得したいので「div.a-expander-content」 にアクセスします
domについては深いところには触れません。気になる人はブラウザで右クリックして検証画面を開いてみてください
Floki.find()を使用するとこんな感じでわけのわからんデータが返ってきます

[
  {"div",
   [
     {"data-hook", "review-collapsed"},
     {"aria-expanded", "false"},
     {"class",
      "a-expander-content reviewText review-text-content a-expander-partial-collapse-content"}
   ],
   [
     {"span", [{"class", ""}],
      [
        "Ruby On Railsで作ったTodoリストアプリに比べ、最大2,000倍の処理速度で動くなど、Elixirは次に来るWeb言語と言われることがよくわかった。",
        {"br", [], []},
        "昔から処理スピードを気にしてるおっさんなので、最初から速い設計な言語を探していたけど、Elixirは是非習得したいと思わせる魅力がある。",
        {"br", [], []},
        {"br", [], []},
        "残念ながらerlangは全く知らないので活用出来るかどうかわからないが、焦ってもしょうがない。時間をかけてじっくり理解していこうと思う。",
        {"br", [], []},
        {"br", [], []},
        "他にElixirの本が無いのでこの本がしばらくバイブルになりそう。"
      ]}
   ]},
  :
  :
]

この中からテキストのみを抜き出しするにはFloki.text()を使えばOK
ただ、今回は複数データが返ってきてるので普通に戻り値にFloki.text()使うとerrorになります
なのでEnum.map(&(Floki.text(&1))でよしなにやりましょう

["Ruby On Railsで作った...",
 "Elixirの文法部分は前半125ページくらいまでを...",
 "erlangもelixirもろくに触ったことはないですが\n"]

最後に改行記号が邪魔なので「\n」を除去しましょう
あと関数内にurlを定数置きしてますけど、関数の引数に渡す方がcoolですね

def fetch(url) do
  :
  :
end

まとめ

ここまでて最低限のスクレイピングが完了しました
ただ、これだけだと先駆者がいるので次回の記事では

  • ページネーションの対応
  • マルチプロセスでの実行

について触れたいと思います
凄い長くなってしまったので反省

【事後報告】清流elixirというコミュニティを立ち上げました

今更ですが

connpassで東海地方(主に岐阜/名古屋)でelixir勉強しようぜ!!って
安直な気持ちで立ち上げました清流elixirです

名前の由来はアホほど適当で、僕は岐阜出身です
岐阜といえば川でしょ。綺麗だから清流でええや。elixirのアイコンも雫みたいなやつだし...
ほなら岐阜のアイコンの上に川と雫乗せとけばいいやってことでアイコンも出来上がりました

軽い気持ちで作り上げたアイコン
清流elixirのアイコン

アイコン作るのにXD使ったんですけど、いいですね。使いやすい

なぜコミュニティを作ろうと思ったのか

発足したのが2019年の3月20日なので本当に今更ですが
清流elixirを立ち上げた思いとしてはこんな感じ

  • 1人で勉強してるとモチベ管理が難しい
  • 同じことに挑戦してる人と知り合いたい
  • 何かの組織を運営したい
  • 売名

といいつつも、エンジニア特有のマウント取ってくる人が大嫌いなんです
だから皆んなで仲良くできる勉強会にしたいね(小学生並

普通の勉強会とは少し違うものにした理由

いわゆる勉強会ってのは開催側がリソースを用意して
「本日はこちらの資料を元に皆さんと手を動かしていきます」ってイメージ
会社で3回ぐらい勉強会を主催したことがあるのですが
毎回リソースを用意しなければいけないので、やってる方としては結構しんどい
その割には文句ばかり言われる笑
参加するのは簡単なんですよ~(いつも有難うございます

そんな中、とあるフリーランスの方とお話する機会がありまして
面白い話を聞かせてもらいました

僕の友人がpythonというかプログラム全く触ったことない人がいるんだけど、ゼロから皆んなで勉強するぞって勉強会のグループを作った人がいます。今じゃ1000人ぐらいの人が籍を置いてるコミュニティになって当人もpythonバリバリかけるようになったんですよねぇ

fmfm....
えー、そんな感じでやっちゃってええんですね...
何よりも1000人も集まってるのってが凄い。あ、これはパクろう

って流れで運営側が毎回リソースを用意する勉強会ってのをやめまして
参加者と共に何をするかを決めて進めていくという怪しげなスタイルにしました

第1回(2019/03/22)の報告

レンタル会議室を借り...connpassにも登録し.. と分からないことだらけで
かなり不手際だったかと思いますが、初回にも関わらず1人の方に参加頂きました
すでにconnpassの方には4人の方が清流elixirに登録して下さっています
知名度を上げることが第1目標ではないんですが、多くの人に知ってもらって参加してもらえると運営側としては嬉しみの鎌足です

第1では手を動かすことはほとんどなく今後

  • 何を勉強してみたいか
  • 何か作りたいものがあるか

という点をお互いに共有し、色々と妄想が膨らみました
夏頃までにはelixirの良い所が100ぐらい言えるようになりたいなという感じの話にまとまり
まずはelixirの強みを知ってこうぜということに...

後にアプリ開発やサービズ開発もやろうぜとほんまに子供の妄想です

第2回の開催のお知らせ

あれやこれやありまして第2回の告知を出させて頂きました
第2回はjson(マップ)をelixirのパイプ演算子Enumで遊び倒すという内容を予定しています
こちらから参加できますので
elixirに少しでも興味があれば覗いてみて下さい

また清流elixirに関わることは随時、このブログで報告していきます

elixirで2次元のリストを縦方向に結合(merge)する

日本語が下手な件

どういうことかというと...

#この配列を
_lst_data = [
  [1,2,3],
  ["a", "b", "c"],
  [true, false, true]
]

#こうしたい
_output_image = [
  [1, "a", true],
  [2, "b", false],
  [3, "c", false]
]

要するに「2次元のリストを縦方向に結合(merge)する」ってことでしょ(強引
elixirでスクレイピングやった時に結構この手の操作があった
ある商品データの

  • 商品名
  • 金額
  • レビュータイトル
  • レビューテキスト

をdomの属性を使って抽出すると

scraping_res = [
  [4,5,5]
  ["goog", "いい品だ", "ふつくしい"]
  ["手軽に購入できてよかったです", "良いものだ", "強靭無敵最強!!"]
]

こんな感じの配列になって、これを以下のようにすると各商品の情報がまとまり
このままCSVデータとして書き込むことができる(あえてマップにしていない)

adj_res = [
  [4,  "good", "手軽に購入できてよかったです"]
  [5, "いい品だ", "良いものだ"]
  [5, "ふつくしい",  "強靭無敵最強!!"]
]

Enumを使う最も手軽な方法

一番楽なのはおそらくこれ

lst_data = [
  [1,2,3],
  ["a", "b", "c"],
  [true, false, true]
]

cal_res =
  lst_data 
    |> Enum.zip() 
    |> Enum.map(&(Tuple.to_list(&1)))

IO.inspect(cal_res)
#result
#[
#  [1, "a", true],
#  [2, "b", false],
#  [3, "c", false]
#]

もしくは

cal_res =
  lst_data 
    |> Stream.zip() 
    |> Stream.map(&(Tuple.to_list(&1)))

いいね

動き方

Enum.zip()の時点でほとんどの操作は終了している

lst_data = [
  [1,2,3],
  ["a", "b", "c"],
  [true, false, true]
]

cal_res =
  lst_data 
    |> Enum.zip() 

IO.inspect(cal_res)
#result
#[{1, "a", true}, {2, "b", false}, {3, "c", true}]

この時点で縦方向への結合は終了しているわけです(クソ便利
ただ、なぜかタプルに変換されてしまっているので元に戻してやろうねってことで
Enum.mapとTupleのto_listを使ってリストinリストに変換しています

zip_res = [{1, "a", true}, {2, "b", false}, {3, "c", true}]
cal_res =
  zip_res
    |> Enum.map(&(Tuple.to_list(&1)))

# Tuple.to_list({1, "a", true}) -> [1, "a", true]
# Tuple.to_list({2, "b", false}) -> [2, "b", false]
# Tuple.to_list({3, "c", true}) -> [3, "c", false]

手軽にやる方法は以上で終了です
あとは僕の自己満足です

一回タプルに変換されるのキモくない?

これが個人的には納得がいってない
なんでリストを一度、タプルに変換する必要があるのか...
コレガワカラナイ

タプルに変換しないモジュールを自分で作ってみました

とりえあず書いた

lst_data = [
  [1,2,3],
  ["a", "b", "c"],
  [true, false, true]
]


defmodule Merge do
  def zip(lst_data) do
    exe_count = Enum.count(lst_data)
    _zip(lst_data, 0, exe_count, [])
  end
  defp _zip(_lst_data, _searcher, counter, accum) when counter === 0, do: accum
  defp _zip(lst_data, searcher, counter, accum) do
    select_values = zip_helper(lst_data, searcher)
    _zip(lst_data, searcher+1, counter-1, accum ++ [select_values])
  end


  def zip_helper(lst_data, searcher) do
    exe_count = List.first(lst_data) |> Enum.count()
    _zip_helper(lst_data, searcher, exe_count, [])
  end

  defp _zip_helper(_lst_data, _seracher, counter, accum) when counter === 0, do: accum
  defp _zip_helper([head | tail], seracher, counter, accum) do
    target = Enum.at(head, seracher)
    _zip_helper(tail, seracher, counter-1, accum++[target])
  end
end

res = Merge.zip(lst_data)
IO.puts("--> result")
IO.inspect(res)

#result: [[1, "a", true], [2, "b", false], [3, "c", true]]

全体の動きとしては再帰関数の中で再帰関数を呼んでいる
zip関数はリストを受け取り、要素の数だけzip_helper関数をcallする
zip_helper関数では受け取った配列とindex番号から縦方向に結合した配列を返す

#zip_helper
lst_data = [
  [1,2,3],
  ["a", "b", "c"],
  [true, false, true]
]

zip_helper(lst_datam, 0)
# [1, _, _]
# ["a", _, _]
# [true, _, _]
#result: [1, "a", true]

pythonでいうfor in forのような動きをイメージして作りました

for column in data:
  for record in column:
    pass

csvデータを操作する時にこの手の処理はよくやる
ただEnum.reduceとか使えばもっと綺麗にかける気はする....
Enumにaccumulatorを渡す方法はないものなのか
もう少し研究します

追記:もっと簡単にかけるやん

アホみたいに再帰関数使ってたのを反省
Enum.mapで普通に取り出せることに気づきました
ただEnum.reduceかEnum.foldlあたりを使えばもっと短くできる気はする

elixir

lst_data = [
  [1,2,3],
  ["a", "b", "c"],
  [true, false, true]
]

defmodule Merge do
  def easy_zip(lst_data) do
    _easy_zip(lst_data, 0, Enum.count(lst_data), [])
  end
  defp _easy_zip(_lstdata, _searcher, counter, accum) when counter === 0, do: accum
  defp _easy_zip(lst_data, searcher, counter, accum) do
    merge = Enum.map(lst_data, &(Enum.at(&1, searcher)))
    _easy_zip(lst_data, searcher+1, counter-1, accum ++ [merge])
  end
end

ついでにpython

lst_data = [
  [1,2,3],
  ["a", "b", "c"],
  [True, False, True]
]

def merge_column(lst_data, index_num):
    merge = list(map(lambda x: x[index_num], lst_data))
    return merge
 
res = []
for i in range(len(lst_data)):
    merge = merge_column(lst_data, i)
    res.append(merge)
    
print(res)
#result: [[1, 'a', True], [2, 'b', False], [3, 'c', True]]

コメントより: pythonでクールに1行で

lst_data = [
  [1,2,3],
  ["a", "b", "c"],
  [True, False, True]
]

res = list(map(list, zip(*lst_data)))

うーん、zip(*lst_data)ってのは思いつきませんでした
zipから出力された結合されたタプルをmap関数使ってリストに戻して1行で終了
お見事です

親が激推しする公務員を大学3年生の時に目指すのをやめた理由

この記事について

社会経験のない学生が偉そうにイキってる記事です
現職の公務員の方や公務員志望の方を蔑む記事ではありません
こういう考え方も出来るのかという暖かい視点でご覧ください

自分のバックグランドについて

まず自分が何者なのかについてざっくりと触れておきます
その方がより説得力がありそう & 参考になると思うので
ちょっとナゲーかもなので軽く読み流してください

高校: 偏差値50~55辺りの公立高校の普通科(進学)に合格

  • 高校3年間の全てを吹奏楽に注ぐ(打楽器やってました)
  • 音大に行きたいとほざくレベルでハマり勉強を全くしなかった(後悔...

この時点ですでに親から公務員を目指すようにかなり推される

大学: 運良く推薦で名城大学理工学部環境創造学科に合格(部活の特待生とかではない)

  • 1年生: 高校でサボった分、大学で勉強を頑張ろうと思いそこそこの成績を取る
  • 2年生: 大学の勉強つまらなくて、暇なのでなぜか筋トレを始める
  • 3年生: エンジニアになりたくて企業でエンジニア見習いとしてインターンを始める

ざっくりとはこんな感じで別に凄みもない本当に普通の人です
むしろただのバカです。もっと勉強すればよかった

魔法の言葉「安定するから」

親が公務員を激推しする理由はご想像の通り
「安定するから」
「残業がない」
「退職金がたくさん貰える」

ザ・公務員のイメージ通りの利点...
ただ僕は性格がひん曲がっていてよく親に
「安定って何なん?」って聞いてたんです

「毎月決まった給料が確実に手に入ることだ」

というのが我が家のベストアンサーです
なるほど...

本当に?

安定って何だよ。他のメリットは??

毎月xx万を確実にもらえるということは本当に安定なのかということを
当時はよく考えていました
もちろん毎月一定額の収入があることはメリットでしかない
働いた分、決まった収入が貰えるアルバイトも似たようなもんだと思います
じゃあそれで安定かと言われれば違うと思いました

自分なりに色々と考えた結果こんなことを思うように...

収入がいくらかなのかという事よりも
支出がいくらなのかの方が問題なんじゃないの?ハッ!!

とりあえず「安定」の定義を

  • 一点基準の生活ができること
  • 何かに備えるための貯金が最低限できること

と解釈します

とすると...

  • 月の収入が30万の人が30万使って生活することは安定していない
  • 月の収入が10万の人が5万使う生活をすることは安定している
  • 月の収入が100万の人が100万使う生活をすることは安定していない

となりますので、僕の定義では「公務員=安定」ではなくなりました

じゃあ他に何かメリットってあるのかを自分の価値観で考えてみると
特に思いつかなくて。ただ1つ思ったことは
「自分のやりたい仕事なのかと言われればNo」だということでした

公務員になったとして自分が積めるキャリアは何か

よく「民間企業から公務員にシフトチェンジした」という話を耳にします
その一方で「公務員から民間企業にシフトチェンジした」という話は聞いたことがありません

これ、何でやろう?と思って色々調べて分かったことは
公務員のキャリアを積んでも民間企業への転職はおそらく出来ないということです

学部卒で10年、公務員勤務をしても32歳の時に民間企業に転職できるスキルやキャリアはほぼないということです

じゃあ、公務員を辞めたくなったらどうするんだろう?という恐怖が
僕の公務員志望を打ち砕く決定打となりました

昔から飽きやすくて、色んなことをやるのが好きなので

まとめ

結局、色々あって好きなことがやりたいなと思ったので
PCが大好きだったのもありエンジニアという職業に就きました
学部違いでキャリアもなかったので決して楽な道ではありませんでしたが何とかここまで来れました
どうやってエンジニアへの道にたどり着いたのかはまた別記事にでも書こうと思います

珍しく真面目な話を書いてみました
こうやって振り返ってまとめてみると
将来設計においては人がどうのこうのよりも自分の価値観で考えることが一番重要だと思いました

親から公務員になりなさいって言われる人が結構多いと思うので少しでも参考になれば嬉しいですね
以上です