【Ruby】Google cloud vision APIを使って画像からテキストを抽出する
課題
Google vision apiを使ってドキュメントのスキャン画像からテキストを抽出する。
準備
1. 画像を準備
jpg、pngなどに対応している。pdfの場合はこれらの形式の画像に変換する必要がある。
2. gemをインストール
bundlerを使う場合はGemfileを用意する。
source "https://rubygems.org" gem "google-cloud-vision"
そうしたらbundle install を実行する。
$ bundle install Using public_suffix 3.0.3 Using addressable 2.5.2 Using bundler 1.16.1 Using multipart-post 2.0.0 Using faraday 0.15.4 Using google-cloud-env 1.0.5 Using google-cloud-core 1.2.7 Using google-protobuf 3.6.1 (x86_64-linux) Using googleapis-common-protos-types 1.0.2 Using grpc 1.17.1 (x86_64-linux) Using googleapis-common-protos 1.3.7 Using jwt 2.1.0 Using memoist 0.16.0 Using multi_json 1.13.1 Using os 1.0.0 Using signet 0.11.0 Using googleauth 0.6.7 Using rly 0.2.3 Using google-gax 1.4.0 Using google-cloud-vision 0.28.0 Bundle complete! 1 Gemfile dependency, 20 gems now installed. Use `bundle info [gemname]` to see where a bundled gem is installed.
依存モジュールがまとめてインストールされた。
3. APIの鍵情報を設定
GoogleのAPIをたたくために鍵情報(認証情報)を設定しよう。
認証情報が書かれたJSONファイル(GCPコンソールからダウンロードできる)をスクリプトから参照させる方法をとる。
そのためにはまず.bashrcなどに次の一行をくわえる。
export GOOGLE_APPLICATION_CREDENTIALS="/home/user/wd/credential.json"
credential.jsonのところに実際のファイル名を入れる。このようにするとGOOGLE_APPLICATION_CREDENTIALSという環境変数を自動的にみるようになる。
スクリプト
次にスクリプトの本体を書こう。
require "google/cloud/vision" project_id = "xxxx" # GCPのコンソールで調べる image_path = './doc.jpg' # スキャン画像など vision = Google::Cloud::Vision.new(project: project_id) image = vision.image(image_path) document = image.document puts document.text
実行すると、doc.jpgから抽出されたテキストが出力されるとおもう。
雑感
鍵情報の読み込み方がよくわからなくて困った。公式ドキュメントを探したのだけど、見つからなかった。結局、スクリプトを実行して、エラーに書かれたURLに設定方法が書いてあった。環境変数を設定する の箇所を参照のこと。 https://developers.google.com/accounts/docs/application-default-credentials
Ruby on RailsでアップロードしたCSVを読み込んでDBに書き込む
やりたいこと
ローカルにあるCSVをWEBアプリにアップロードし、DBに書き込む。CSVファイルの文字コードはcp962を想定している。
コード
View
= form_tag csv_upload_path, multipart: true do = file_field_tag :file = submit_tag 'アップロード'
これはhaml。
csv_upload_pathのところはactionに相当するものを書く。アクション先のエイリアスはアプリのルートに/infoを付けるか、コンソールでrake routesとやってルーティングの一覧が得られる。
CSVファイルをアップロードするとき、ファイルそのものが特定のモデルと対応するわけではないので、form_tagを使うのがよいのだと思う。
file_field_tagの引数:fileは、HTMLのnameタグに変換される。コントローラーでこのテキストエリアの内容を受け取るには、params['file']とすればいい。
Controller
次、コントローラー。ビューから送ったCSVファイルを受け取る。params[:file]でファイル情報が取れる。
ここではProductモデルがあるものとし、このモデルに書いたimportメソッドでファイル内容の読み出しと書き込みをする。
そのあとのリダイレクトでは、ビューに表示するメッセージをnoticeに渡している。こうすることで、ビューにnoticeに対応するスニペットを書いておけば、コントローラーで指定したメッセージを画面に表示させることができる。
def import_btob Product.import(params[:file]) redirect_to root_url, notice: "CSVファイルをアップロードしました。" end
Model
コントローラーから呼び出すメソッドを書く。CSVファイルの読み込みとDBへの書き込みをおこなう。
ここでは、読み込みと書き込みのメソッドを別ける。importメソッドとopen_spreadsheetメソッドである。
class Product < ApplicationRecord def self.import(file) spreadsheet = open_spreadsheet(file) header = ["goods_identification_type", "goods_label", "goods_id", "option_1_id", "option_2_id", "selling_price", "quantity", "option_uniq_code", "jan_code"] ActiveRecord::Base.transaction do (2..spreadsheet.count).each do |i| if !spreadsheet[i].nil? row = Hash[[header, spreadsheet[i] ].transpose] option = Product.new(row) option.save! end end end end
肝は、Hash[ [header, spreadsheet[i] ].transpose]だ。こうすると、ヘッダーの要素名をキー、CSVのレコード各要素をバリューとするハッシュができる。それをnewすることでDBにレコードを書き込める。
読み込み処理はこちらでおこなう。
def self.open_spreadsheet(file) case File.extname(file.original_filename) when '.csv' then CSV.parse(File.read(file.path, encoding: 'cp932').encode("UTF-8", :invalid => :replace)) else raise "Unknown file type: #{file.original_filename}" end end end
CSVの読み込みを一行でやっている。このスニペットはQiitaから拾った。とてもいい。
CSV.parse(File.read(file.path, encoding: 'cp932').encode("UTF-8", :invalid => :replace))
エンコードをcp932にしたのは、はじめRooというgemで読み込もうとしたところ、失敗したからだ。
Perl入学式 東京第3回、ピザパーティのお題を解く
Perl入学式 東京の第3回にサポーターとして参加させてもらいました。勉強会終了後のピザパーティーで出たお題をやってみたので、そのことについて書いてみます。
やりたいこと
グーグルの検索トレンドデータからキーワードランキングをつくります。ランキングに加えて前日からの推移も含めます。
Googleトレンドのデータは以下のエンドポイントからJSON形式で取れます。
https://trends.google.com/trends/api/dailytrends?geo=JP&ed=yyyymmdd
わかばたいむすさんの記事に詳しくあります。
wakabatimes.com
やってみてわかったこと
・順位の差を求めることができませんでした。前日だとランキング圏外になっていたからです。
・Googleトレンドをみると正しいランキングがすぐわかります。
Google Trends
コード
月初の順位前日比を求めるところと結果の出力はさぼっています。
use strict; use warnings; use feature 'say'; binmode(STDOUT, ":utf8"); use JSON::XS qw/decode_json/; use LWP::Simple; use DateTime; use DDP; # Googleトレンドのエンドポイントにいれる日付をyyyymmddの形式でつくる my $dt = DateTime->now( time_zone => 'Asia/Tokyo' ); my $yyyymmdd = $dt->ymd('/'); my ($y, $m, $d) = split(/\//, $yyyymmdd); my @days = (); # 月初から本日までの日にちを配列にいれる(例. 1,2,3...) if ( $d ne "1" ) { my $c = 1; my @array; $array[$d - 1] = ''; @days = map { $c++; } @array; } else { @days[0] = "1"; } # yyyymmdd形式の文字列に変換する my @dates = (); for my $day ( @days ) { my $dt = DateTime->new( time_zone => 'Asia/Tokyo', year => $y, month => $m, day => $day, ); push(@dates, $dt->ymd('')); } # 配列に用意した各日付のデータをGoogleに問い合わせる my $rankings = {}; for my $target_day (@dates) { say "day => $target_day"; my $rank = 1; my $content = get("https://trends.google.com/trends/api/dailytrends?geo=JP&ed=$target_day"); my $json = substr($content, 6); # 1行目のごみを取り除く my $data = decode_json($json); # 検索ワードのデータまで掘る my $article_array_ref = $data->{default}->{trendingSearchesDays}[0]->{trendingSearches}; # 検索ワードはランク順にならんでいるので、ループしながらランクを付けられる for my $article ( @$article_array_ref ) { my $query = $article->{title}{query}; say $query; $rankings->{$target_day}{$query}{rank} = $rank; $rank++; } } # ランキングにくわえて前日との順位差を入れるハッシュレフを別につくる my $trends = $rankings; my @dates_copy = @dates; shift @dates_copy; my @from_second_day_to_later_days = @dates_copy; my $prev_day = shift @dates; # 検索ワードごとに前日のランクとの差を求める(ただし、前日だとランク圏外になっていて存在しなかった) for my $target_day (@from_second_day_to_later_days) { for my $query ( keys %{$rankings->{$target_day}} ) { my $trend; if ( defined($rankings->{$prev_day}{$query}) ) { $trend = $rankings->{$prev_day}{$query} - $rankings->{$target_day}{$query}; } else { $trend = "new"; } $trends->{$target_day}{$query}{trend} = $trend; } } # デバッグプリント p $trends;
標準出力(一部抜粋)
20190109 {
NGT {
rank 10,
trend "new"
},
りんご病 {
rank 15,
trend "new"
},
アジアカップ {
rank 9,
trend "new"
},
サッカーアジアカップ {
rank 7,
trend "new"
},
サッカー日本代表 {
rank 5,
trend "new"
},
トルクメニスタン {
rank 3,
trend "new"
},
バーチャルさんは見ている {
rank 14,
trend "new"
},
ピーチジョン {
rank 13,
trend "new"
},
兼高かおる {
rank 6,
trend "new"
},
友井雄亮 {
rank 2,
trend "new"
},
家売る女 {
rank 8,
trend "new"
},
尾田栄一郎 {
rank 4,
trend "new"
},
山口真帆 {
rank 1,
trend "new"
},
志村けん {
rank 12,
trend "new"
},
摂津正 {
rank 17,
trend "new"
},
武田玲奈 {
rank 18,
trend "new"
},
玉川徹 {
rank 16,
trend "new"
},
馬毛島 {
rank 11,
trend "new"
}
},
20190110 {
はあちゅう {
rank 17,
trend "new"
},
スキャンダル専門弁護士 {
rank 5,
trend "new"
},
ディビジョン {
rank 18,
trend "new"
},
ワンピース {
rank 4,
trend "new"
},
京王観光 {
rank 12,
trend "new"
},
刑事ゼロ {
rank 10,
trend "new"
},
宇賀なつみ {
rank 11,
trend "new"
},
川谷絵音 {
rank 13,
trend "new"
},
'日本 対 トルクメニスタン' {
rank 3,
trend "new"
},
星座 {
rank 9,
trend "new"
},
松本人志 {
rank 15,
trend "new"
},
森川葵 {
rank 14,
trend "new"
},
楠ろあ {
rank 7,
trend "new"
},
盾の勇者の成り上がり {
rank 6,
trend "new"
},
福男 {
rank 8,
trend "new"
},
竹下亘 {
rank 16,
trend "new"
},
純烈 {
rank 1,
trend -33088
},
雨宮萌果 {
rank 2,
trend "new"
}
【Ruby】MiniTestでテストを書いてみる
目的
MiniTestをとりあえず使ってみること。
Effective Ruby 第6章「テスティング」の内容そのままを試してみた結果をまとめる。
テストの対象
たとえばバージョンを管理するクラスversion.rbがある。このクラスのメソッドをテストしたい。
version.rb
class Version attr_reader (:major, :minor, :patch) def initialize(version) @major, @minor, @pathch = version.split('.').map(:map.to_i) end end
使用例:
require File.dirname(__FILE__) + "/Version" def major_number v = Version.new('2.1.3') # major: 2, minor: 1, patch: 3 v.major # => 2 end
以下ではこのクラスのmajorの値をテストするテストケースを書く。
手順
1 テストスクリプトの命名
version.rbのテストなのでversion_test.rbという命名しておく。
この通りでなくてもよいて、命名が一貫していればOKだ。
2 必要なライブラリを読み込む
MiniTestを実行するために必要となる。
ライブラリ全体を読み込むには以下の1行を書く。
require 'minitest/autorun'
3 テスト用クラスをつくる
テスト本体のクラスをVersionTest、MiniTestのクラスをMiniTest::Unit::TestCaseとしてクラスを作る。
class VersionTest < MiniTest::Unit::TestCase # テストケース end
4 テストを書く
version_test.rbにテストを書いてく。
require('minitest/autorun') # MiniTestのライブラリを読み込む require File.dirname(__FILE__) + '/Version' # version.rbを読み込む class VersionTest < MiniTest::Unit::TestCase # テストケース def test_major_number v = Version.new('2.1.3') assert_equal(2, v.major) end
5 実行する
以上でテストが完成した。テスト対象のスクリプトとテストスクリプトは同じディレクトリに置かれている。
$ ls version.rb version_test.rb
いよいよ実行する。
$ ruby version_test.rb
.
.
.
成功例: 成功した場合とくに何も言われない。0 failuresというところでエラーがなかったことがわかるようになっている。
$ ruby version_test.rb MiniTest::Unit::TestCase is now Minitest::Test. From version_test.rb:4:in `<main>' Run options: --seed 48129 # Running: . Finished in 0.000882s, 1133.7868 runs/s, 1133.7868 assertions/s. 1 runs, 1 assertions, 0 failures, 0 errors, 0 skips
失敗例: わざと失敗するようにテストを以下のように書き換えてみる。
assert_equal(1, v.major)
MiniTest::Unit::TestCase is now Minitest::Test. From version_test.rb:4:in `<main>' Run options: --seed 52817 # Running: F Failure: VersionTest#test_major_number [version_test.rb:8]: Expected: 1 Actual: 2 bin/rails test version_test.rb:6 Finished in 0.001389s, 719.9424 runs/s, 719.9424 assertions/s. 1 runs, 1 assertions, 1 failures, 0 errors, 0 skips
assert_equalメソッドの第一引数がExpected、v.majorの値がActualで、両者が異なってますよ、ということが表示される。
感想
テストは大切。いちどテストを書いてしまえば、テスト対象が多くてもOK。それに対して、手動でプリントデバッグするのは消耗する。また、変更したあとの再テストがかんたんにできるのも魅力。
ハッシュレファレンスをsprintfでフォーマットしてprintする
課題
こういうハッシュレフを
my $my_info = { name => 'Masato', tall => 169, mass => 55, food => 'Sushi', };
こうやってプリントしたい。
Name Tall Mass Age Food Masato 169 55 33 Sushi
やり方
sprintf関数とハッシュスライスを使う。
sprintf関数の使い方
説明は以下のページがわかりやすい。
sprintf関数 - 文字列の書式指定 - Perlゼミ(サンプルコードPerl入門)
まず、ヘッダーをsprintfで出すにはこうすればできる。
my $format = "%-12s %-7s %-7s %-7s %-7s"; printf $format, qw(Name Tall Mass Age Food);
$formatはsprintf関数の第一引数に渡す書式だ。%-12sなどというのは左詰のxx固定長と解釈される。よってこの例は左詰で12文字の固定長になる。長さは実際にプリントする文字列に合わせる。
ハッシュスライス
printf $format, @$hash_ref{ qw(name tall mass age food) };
javascriptで変数が定義済みかどうかをチェックする
ポイントは、JSでは0が偽になる点です。0は偽ではありますが、未定義ではありません。
if (height) { ... } の条件では0が未定義と判定されてしまいます。0の場合にTrueのブロックに入るようheight == 0を追加しましょう。
var height = 0; if (heigth || height == 0) { console.log('Variable is defined.'); } else { console.log('Variable has NOT been defined.'); }
【Rails】空配列がTrue判定になってはまった件
状況
ActiveRecordでクエリを出して戻り値を変数にいれる。その変数が空かどうかで条件分岐させたい。
解法
books = Book.where(title: 'Great novel') if books.present? # booksの中身がある場合に実行する end
まちがい
books = Book.where(title: 'Great novel') # 該当レコードがない場合 [] (空配列)が返る if books # 空配列はTrueになる # あれ? end
考察
Rubyではnilとfalse以外がすべてTrueになる。つまり、''や[]はTrueだ。空配列がfalseになる言語のノリで書いているとハマる。