窓際BLOG

プログラミングの学習メモや書籍の感想等を公開していきます。

Ruby:はてなブログAPIを使って記事の一覧をJSON形式で取得する

最近、技術系の記事はQiitaへの移行を考えているのですが、過去の記事の移行がめんどくさいなぁ。。。と思っていたので、記事の一覧をはてなブログAPIで取得するスクリプトを作ってみたので、その手順をメモしておきますφ(..)

ちなみに成果物は下記に公開してます。ソースコードだけ見たいという人は下記を、ご参照ください。

はてなブログAPIを使っての記事一覧をJSONで取得する · GitHub

やり方

APIKEYとルートエンドポイントを取得する

まずは、自分のブログにアクセスするためのAPIKEYとルートエンドポイントを取得します。それぞれの役割は下記の通りです。

名前 役割
APY KEY APIで自身のブログにアクセスする際のパスワード
ルートエンドポイント APIへRESTでアクセスする際に指定するURL

APIKEY及びルートエンドポイントは、自分のブログの詳細設定画面で確認することが出来ます。

https://blog.hatena.ne.jp/アカウント/ブログID.hatenablog.com/config/detail

f:id:madogiwa0124:20171210224903p:plain

記事を取得してみる

それでは、実際にはてなブログAPIを使って記事を取得していきます。
基本的な流れは下記の通りです。

  1. ルートエンドポイント/entryにGETリクエストを作成
  2. ベーシック認証でのアカウント情報をGETリクエストに付与
  3. GETリクエストを発行
  4. レスポンスボディに設定された記事を取得

実際のソースコードと解説は下記の通りです。

require 'uri'
require 'net/http'

# ルートエンドポイントからURLを生成
uri = URI(ROOT_END_POINT)
# GETリクエストを作成
req = Net::HTTP::Get.new(uri)
# ベーシック認証でアカウント情報を付与
req.basic_auth(USER_ID, APY_KEY)
# HTTPリクエストを送付
res = Net::HTTP.start(uri.host,
                      uri.port,
                      use_ssl: true) { |http| http.request(req) }
# 記事情報を取得(XML形式)
res.body

記事のタイトルと本文を取得する

取得結果からタイトルと本文といった特定部分を取得するためには、取得したXML形式の記事情報を解析する必要があります。
今回はNokogoriを使用してXMLを解析してタイトルと本文を取得しました。Nokogiriの説明は下記の記事にまとめておりますので、そちらを参照してください。

madogiwa0124.hatenablog.com

実際にレスポンスボディに設定される記事(XML形式)は下記のような形になります。

<entry>
  <id>tag:blog.hatena.ne.jp,2013:blog-madogiwa0124-6653812171402746641-8599973812325431185</id>
  <link rel="edit" href="https://blog.hatena.ne.jp/madogiwa0124/madogiwa0124.hatenablog.com/atom/entry/8599973812325431185"/>
  <link rel="alternate" type="text/html" href="http://madogiwa0124.hatenablog.com/entry/2017/12/10/232320"/>
  <author><name>madogiwa0124</name></author>
  <title>タイトル</title>
  <updated>2017-12-10T23:23:20+09:00</updated>
  <published>2017-12-10T23:23:20+09:00</published>
  <app:edited>2017-12-10T23:23:20+09:00</app:edited>
  <summary type="text">本文</summary>
  <content type="text/x-markdown">本文</content>
  <hatena:formatted-content type="text/html" xmlns:hatena="http://www.hatena.ne.jp/info/xmlns#">&lt;p&gt;本文&lt;/p&gt;</hatena:formatted-content>
  <app:control>
    <app:draft>no</app:draft>
  </app:control>
</entry>

そのため、entry配下のtitlecontentを取得するには下記のようなコードで行うことが出来ます。今回はタイトルと本文をHash形式で取得出来るようにしてみました。

# Nokogiriで取得したXMLをパース
entry = Nokogiri::XML.parse(res.body, nil, 'utf-8')
# タイトルの一覧を取得
titles = entry.css('entry title').map(&:text)
# 本文の一覧を取得
contents = entry.css('entry content').map(&:text)
# Hash形式でタイトルと本文のリストを作成
titles.length.times do |i|
  entry_hash_list << { title: titles[i], content: contents[i]}
end

全ての記事を取得する

はてなブログAPIでは、1回のレスポンスで10記事までしか返却出来ないため、全ての記事を取得するためには次の記事の一覧が取得出来るURLを取得し、リクエストを複数回送らないといけません。

次の記事の一覧へのURLは、レスポンスボディに下記のような形で設定されます。

<link rel="next" href="https://blog.hatena.ne.jp/madogiwa0124/madogiwa0124.hatenablog.com/atom/entry?page=1509279003" />

上記XMLを解析し、URL部分を取得するコードは下記の通りです。

# 次の記事一覧へのリンク部分を取得
link_urls = entry.css("link[rel^='next']")
# リンク部分からURLの文字列を取得※取得でいなかった場合はnil を設定
next_url = link_urls.empty? ? nil : link_urls.attribute('href').value

上記で取得したURLに再度記事を取得してみるで記載した処理を行えば、次の記事の一覧が取得することが出来るので、次のページのURLが取得出来なくなるまで処理を繰り返せば全ての記事を取得することが出来ます。

全ての記事のタイトル、本文、カテゴリをJSON形式で取得する

下記は私が作成したはてなブログAPIを使った記事のタイトル、本文、カテゴリを取得するスクリプトです。

gist.github.com

参考

はてなブログAtomPub - Hatena Developer Center

cartman0.hatenablog.com

以上です

Ruby:seleniumを使って動的ページをスクレイピングしてみる

以前、Nokogiriを使ったスクレイピングについてブログを書きましたが、JavaScriptで初期表示時にページを動的に変更しているページ等上手く値を取得するこが出来ません。。。
※httpレスポンスに設定されたHTMLを取得しているため

madogiwa0124.hatenablog.com

動的ページのスクレイピングには、Webブラウザでの操作を自動化できるseleniumが便利です。

github.com

はじめに

今回は、Google翻訳の結果をスクレイピングするツールを作ったので、それをもとにseleniumの使い方を書いていきます。 ちなみにソースコードRubyです。

使い方

環境構築

seleniumを使うのに必要なのは、selenium用のgemとWebDriverです。今回はGoogleChromeのドライバをインストールしてます。

# selenium用のgemをインストール
$ gem install selenium-webdriver
# Chrome用のドライバをインストール
$ brew install chromedriver

これで準備完了です!

実際にスクレイピングしてみる

スクレイピングの基本的な手順は、下記の通りです。

  1. ドライバーの起動
  2. URLへアクセス
  3. 要素を取得

実際にGoogle翻訳の結果を取得するソースコードが以下のものです。

# Google翻訳にアクセスして、翻訳結果を取得
# target_lang:翻訳前言語、result_lang:翻訳後言語、word:翻訳対象文字列
def get_translate_result(target_lang, result_lang, word)
  # URL生成
  url = "#{GOOGLE_TRANSLATE_URL}##{target_lang}/#{result_lang}/#{word}"
  # ドライバーの起動
  driver = Selenium::WebDriver.for :chrome
  # URLへアクセス
  driver.navigate.to(url)
  # タイムアウト値の設定
  wait = Selenium::WebDriver::Wait.new(timeout: 10)
  begin
    # タイムアウトになるまで、翻訳結果部からテキスト情報を取得
    wait.until{ html = driver.find_element(id: 'result_box'); html.text }
  rescue RuntimeError => e
    # ERROR処理、メッセージを出力してドライバーを終了
    puts e.message
    driver.quit
  end
end

ヘッドレスブラウザを使ってみる

ヘッドレスブラウザを使うと、ブラウザを起動せずにスクレイピングを行うことができます。
先程のソースコードをヘッドレスブラウザを使用するように書き替えたものが以下です。

# Google翻訳にアクセスして、翻訳結果を取得
def get_translate_result(target_lang, result_lang, word)
  # URL生成
  url = "#{GOOGLE_TRANSLATE_URL}##{target_lang}/#{result_lang}/#{word}"
  # オプションの生成(ヘッドレスブラウザで動作するように)
  options = Selenium::WebDriver::Remote::Capabilities
  options = options.chrome('chromeOptions' => { args: ['--headless'] })
  # ドライバーの起動
  driver = Selenium::WebDriver.for :chrome, desired_capabilities: options
  # URLへアクセス
  driver.navigate.to(url)
  # タイムアウト値の設定
  wait = Selenium::WebDriver::Wait.new(timeout: 10)
  begin
    # タイムアウトになるまで、翻訳結果部からテキスト情報を取得
    wait.until{ html = driver.find_element(id: 'result_box'); html.text }
  rescue RuntimeError => e
    # ERROR処理、メッセージを出力してドライバーを終了
    puts e.message
    driver.quit
  end
end

ソースコードの完全版は下記です。

github.com

参考

qiita.com

qiita.com

RubyonRails:deviseのControllerをちょっとだけカスタマイズする

Moook(https://moook.herokuapp.com/)では、deviseで認証機能を作っています。
管理用にユーザーが新規登録されたらslack通知が行われるようにしたかったのですが、deviseはdefaultではコントローラーの実体のソースが無いので、一定の手順を踏む必要があります。その手順を備忘目的でメモしておきますφ(..)

やりかた

前提情報

今回手順を実施した環境の情報は下記の通りです。

gem version
rails 5.1.2
devise 4.3.0

また既にrails g devise Userを実施しており、devise用のユーザーモデルが作成済みの状態を想定しています。

コントローラーのソースファイルを生成する

rails g devise:controllers usersを実行し、コントローラーのファイルを作成します。実行するとapp/controller/users配下にdeviseの各種コントローラーが生成されます。

# deviseのコントローラーのファイルを生成
$ rails g devise:controllers users
Running via Spring preloader in process 46167
      create  app/controllers/users/confirmations_controller.rb
      create  app/controllers/users/passwords_controller.rb
      create  app/controllers/users/registrations_controller.rb
      create  app/controllers/users/sessions_controller.rb
      create  app/controllers/users/unlocks_controller.rb
      create  app/controllers/users/omniauth_callbacks_controller.rb

routeの変更

ルーティングの設定を生成したファイルを参照するように変更します。

routes.rb

  devise_for :users, :controllers => {
    :registrations => 'users/registrations'
   }

カスタマイズを加えてみる

カスタマイズを加える前に実際に生成されたファイルを見てみます。
Users::RegistrationsController < Devise::RegistrationsControllerと記載のあるとおり、元のdeviseのユーザー作成のコントローラーを継承したファイルが生成されていると見て取れます。
そのため、各メソッドのコメントアウトを解除(メソッドをオーバーライド)し、super以降に追加したい処理を記載すれば、処理を追加出来ます。

superの意味やrubyのクラスの継承については下記がわかりやすいと思います。 www.rubylife.jp

controllers/users/registrations_controller.rb

class Users::RegistrationsController < Devise::RegistrationsController
  # before_action :configure_sign_up_params, only: [:create]
  # before_action :configure_account_update_params, only: [:update]

  # GET /resource/sign_up
  # def new
  #   super
  # end

# 省略

end

今回は、ユーザー作成処理にカスタマイズを加え、Slack通知を行うようにしたかったので下記のようにしました。

controllers/users/registrations_controller.rb

  def create
    super
    notice_slack("Moookに新しいユーザーが登録されました!")
  end

結果

無事に通知を送ることが出来ましたヽ(´エ`)ノ

f:id:madogiwa0124:20171126221229p:plain

ちなみにSlack通知のやりかたについては下記に記載してます。

madogiwa0124.hatenablog.com

参考

qiita.com

RubyonRails:Slack通知処理を実装する

Moook(https://moook.herokuapp.com/)を運用していて、毎日管理画面を確認するのがめんどくさかったので、新規ページが追加された時にSlackに通知が送られるようにしましたφ(..)

その手順をメモしておきます。

やりかた

gemのインストール

今回は、slack-notifierというgemを使って実装していきます。

github.com

まずは、Gemfileに下記を追記して、bundle installを行います。

gem 'slack-notifier'

Slackの設定

slack-notifierではIncoming WebHooksWeb hook URLを使用して通知を送信します。
通知を送りたいワークスペースのチャネルの歯車マークからAdd an Appをクリックします。

f:id:madogiwa0124:20171126212742p:plain

その後、遷移先の画面にてIncoming WebHooksを検索し、Add Configrarionをクリックします。

f:id:madogiwa0124:20171126210215p:plain

f:id:madogiwa0124:20171126210343p:plain

そしてWebhook URLを取得します。

f:id:madogiwa0124:20171126210601p:plain

ちなみにこの画面では、通知を送る名前やアイコンも設定できます。

項目 内容
Customize Name 通知欄で表示される名前を設定出来ます
Customize Icon 通知欄で表示されるアイコンを設定できます

Moookではこんな感じにしました(・_・)

f:id:madogiwa0124:20171126210850p:plain

通知処理の実装

実装内容はすごく簡単です。

  1. Web hook URLを引数にインスタンスを生成
  2. 本文を引数に通知送信処理を実行

サンプル

def notice_slack
  # 先程取得したWebhook URLを引数にインスタンスを生成
  notifier = Slack::Notifier.new(webhook_url)
  # 通知を送信
  notifier.ping("test")
end

Moookの実際の実装はこんな感じです。slackの通知処理は他でも使いそうだったのでapplication_controller内に記述しました。また、Web hook URLは.envを使って環境変数化しました。

application_controller.rb

  # slack通知用メソッド
  def notice_slack(message)
    notifier = Slack::Notifier.new(ENV['SLACK_WEBHOOK_URL'])
    notifier.ping(message)
  end

page_controller.rb

  def create
    @page = Page.new(page_params)
    @page.user_id = current_user.id
    @page.html = @page.get_html(@page.url)
    if @page.save
      redirect_to @page, notice: '新しいページを登録しました。'
      # ページ追加時に通知を行う
      notice_page_info(@page)
    else
      render :new
    end
  end

  def notice_page_info(page)
    # 本文を生成
    message = <<~"EOS"
      Moookに新規ページが登録されました!
      名前:#{page.name}
      URL:#{page.url}
    EOS
    # Slack通知処理を呼び出し
    notice_slack(message)
  end

実行例

こんな感じでSlackに通知を送ることが出来ましたヽ(´エ`)ノ

f:id:madogiwa0124:20171126211817p:plain

参考

qiita.com

Ruby:Rubyでenum(列挙型)を使う※おまけ:全ての定数の値を取得する方法

下記ツールRubyenum(列挙型)を使いたかったけど、Rubyには明確な列挙型のようなクラスが無く、少しハマったのでφ(..)メモメモ

github.com

Rubyenum(列挙型)を使うには

Rubyにはenumがありません。そのためmoduleを使ってenumを表現します。
下記のように複数の定数値をmoduleの中に定義することで、enumのような振る舞いをさせることが出来ます!

# 定数のため、変更が入らないように`.freeze`を記載しています。
module Lang
  ENG = 'en'.freeze
  JPN = 'ja'.freeze
end

定数値を呼び出すにはモジュール名::定数名というような形で記述します。

puts Lang::ENG
# 結果 : en
puts Lang::JPN
# 結果 : ja

おまけ:全ての定数の値を取得する

moduleクラスに全ての定数値の値を取得するようなメソッドが無さそうだったので、自分で作ってみました。 constantsで全ての定数値の変数名を取得し、それを使ってevalで値を取得し直して配列で返しています。
moduleで定義した定数のリストに属さない入力値のチェック等に使えるのではないかなと思いますφ(..)

module Lang
  ENG = 'en'.freeze
  JPN = 'ja'.freeze

  def self.all_lang
    result = []
    self.constants.each{ |lang| eval("result << #{lang}") }
    result
  end
end

↓動作確認はこちら

参考

qiita.com

以上

Ionic:スワイプでページ更新する方法のメモ

Ionicで下スワイプでページ更新を行う方法をメモφ(..)

↓実装イメージはこんな感じです。

f:id:madogiwa0124:20171112232529g:plain

手順

概要

Ionicでスワイプでページ更新を実装するには、ion-refresherを使用します。
Viewのion-content内にion-refresherを配置し、(ionRefresh)="doRefresh($event)"のように実行メソッドを定義します。

詳細は下記を参照頂ければ!※Ionicの公式ドキュメントです

Refresher - Ionic API Documentation - Ionic Framework

Viewへの配置

下記のようにView内のion-content配下にion-refresherを配置します。

<ion-content no-padding>
  <ion-refresher (ionRefresh)="doRefresh($event)">
    <ion-refresher-content></ion-refresher-content>
  </ion-refresher>
</ion-content>

更新処理の実行及び完了の実装

Viewへの配置が終わったら.ts側にページ更新及び完了の処理を実装していきます。
下記の実装では、API呼び出しによるデータ取得の完了または、3秒経過したら完了とし、ローディングを終了させています。

@IonicPage()
  
@Component({
  selector: 'page-page-index',
  templateUrl: 'page-index.html',
})
export class PageIndexPage {
  pages = [];
  result = '';
  api_url = 'https://moook.herokuapp.com/pages.json';
  refresher = null;

  constructor(public navCtrl: NavController, public navParams: NavParams, private http: Http, public loadingCtrl: LoadingController) {
    // ロード画面を表示
    this.presentLoading();
    // APIからデータを取得
    this.get_pages_data();
  }

  doRefresh(refresher) {
    this.refresher = refresher;
    // APIからデータを取得
    this.get_pages_data();
    // タイムアウトの設定
    setTimeout(() => {
      // ページ更新を完了とし、処理を中断
      refresher.complete();
    }, 3000);
  }

  get_pages_data(){
    this.http.get(this.api_url).subscribe(
      respons => {
        let result = respons.text();
        this.pages = [];
        JSON.parse(result || null).forEach(data => {
          this.pages.push(new Page(data)); 
        });
        if (this.refresher != null) {
          // データ取得が終わったら完了
          this.refresher.complete();
        }
      },
      error => {
        let result = 'faild : '+ error.statusText;
        console.log(result);
      }
    );
  }

}

完全なソースコードは下記に配置してありますφ(..)

github.com

以上です。