RubyのNokogiriでスクレイピングしてSlackに通知&Qiita記事更新

[わたモテ13巻「遠足編」発売記念 もこっちとくっつくのは誰だ!?選手権 | 電子書籍ストア-BOOK☆WALKER](https://bookwalker.jp/ex/feature/watamote-vote/)
が目に入ってしましい、一時間ごとのリアルタイム更新では目が離せない！
という言い訳を得まして。

今回はRubyでスクレイピングしてSlackで監視するとともに、
なんとなくAPIを使いQiitaの記事の更新を試してみることにしました。

7月29日までの期間限定デス。

# 動作環境

* docker ruby:latest
* ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-linux]
* nokogiri (1.8.4)
* slack-incoming-webhooks (0.2.0)
* qiita (1.3.5)

# 成果物

## Slack通知

![image.png](https://qiita-image-store.s3.amazonaws.com/0/62354/fcb35c1e-55a2-bca9-140c-3da45e944430.png)

普通にPOSTするとURLが多すぎて展開されませんでした。
attachmentのtextでもURL画像が展開されず、authorとimageで無理やり2つの画像を詰め込むように。
たくさん画像を表示させたいときが課題ですね…

## Qiita

[RubyのNokogiriでスクレイピングしてSlackに通知&Qiita記事更新 - Qiita](https://qiita.com/khsk/private/9f34f79aee34f5fc56ed)
コピーしたprivate記事

# コード


```rb
require 'nokogiri'
require 'open-uri'
require 'qiita'
require 'slack/incoming/webhooks'


# Qiita投稿前に書き換えるものはグローバル変数にしておこう(悪手)
$incoming_url  = 'secret'
$qiita_token   = 'secret'
$qiita_item_id = '9f34f79aee34f5fc56ed'

def scraping()
    charset = nil
    url = 'https://bookwalker.jp/ex/feature/watamote-vote/'
    html = open(url) do |f|
        charset = f.charset
        f.read
    end

    members = []
    doc = Nokogiri::HTML.parse(html, nil, charset)
    doc.css('.vote-box').each do |vote|
        members.push({
            rank: url + vote.css('.rank-box>img').first.attributes['src'].value,
            name: vote.css('.border').first.attributes['alt'].value,
            icon: icon = url +  vote.css('.border').first.attributes['src'].value,
        })
    end
    members
end

def makeAttachments(members)
    attachments = []
    members.each do |member|
        attachments.push({
            author_name: member[:name],
            image_url: member[:icon],
            author_icon: member[:rank],
        })
    end
    attachments
end

def postSlack(attachments)
    incoming_url = $incoming_url
    channel      = '#hitorigoto'
    username     = 'bot'
    slack = Slack::Incoming::Webhooks.new incoming_url, channel: channel, username: username, attachments: attachments
    slack.attachments = attachments
    slack.post "もこっちとくっつくのは誰だ!?選手権\nhttps://bookwalker.jp/ex/feature/watamote-vote/"
end

def postQiita(body)
    title = 'RubyのNokogiriでスクレイピングしてSlackに通知&Qiita記事更新'
    client = Qiita::Client.new(access_token: $qiita_token)
    client.patch('/api/v2/items/' + $qiita_item_id, title: title, body: body)
end

def makeBody(members)
    ranking = <<~EOT
    | ランク | エントリー |
    |:-:|:-:|
    EOT
    members.each do |member|
        ranking = ranking + "|<img src=\"#{member[:rank]}\">|<img src=\"#{member[:icon]}\">|\n"
    end
    ranking

    client = Qiita::Client.new(access_token: $qiita_token)
    body = client.get_item($qiita_item_id).body['body']
    body.gsub(/(?<=\*{10}\n).*/m, ranking)
end


# rubyはメソッドを先に宣言しておかないといけない
members = scraping()

attachments = makeAttachments(members)
postSlack(attachments)

body = makeBody(members)
#p body
res = postQiita(body)
#p res
```

rubyは初心者ですが、コピペでなんとかなりました。
ハッシュの文字列とシンボルはショートハンド含めて混乱のもとですね。

当初はこの記事まるごとコードに持たせるつもりでしたが、記事取得の置換で書き換えるようにしました。

# 参考記事

* [配列とハッシュとシンボルは紛らわしいので整理！！ - Qiita](https://qiita.com/iron-breaker/items/32710004f0bb2e2babb6)
* [ChefTips: RubyのArray(配列)とHash(連想配列)入門 - Qiita](https://qiita.com/kentarok/items/5d38c3f7df37a7396ef9#_reference-21495ff590d32afc6374)

>更にややこしいことに、HashのKeyにSymbolを使う場合、KeyとValueの紐付けに `=>` (ハッシュロケット)を使わず以下のようなシンプルな表記が可能になります。
>
記載はシンプルになって見やすいのですが、この表記法を知らない人には混乱のタネになります。また、あくまでも紐付けの表記が変わるだけで、値を取り出す際のリテラルは変わらないことにご注意ください。

ﾂﾗﾐ!

***

* [メタ文字(.)が改行にマッチする(/m修飾子) - 正規表現のオプション修飾子 - Ruby正規表現の使い方](https://www.rubylife.jp/regexp/option/index4.html)

`m`がマルチラインモードじゃないのですねえ。
じゃあ`^$`がどうなるかというと、普段から各行を表し、ファイル先頭・末尾は`\A\z(\Z)`を使うようです。
わざわざ`m`つかわないほうが混乱しないのかも。

* [Railsの正規表現でよく使われる \A \z って何？？ - Qiita](https://qiita.com/jnchito/items/ea7832df6f64a9034872)

* [Ruby の正規表現を複数行で書く - Qiita](https://qiita.com/scivola/items/04270095eb4c17ab8c92)

こういうのもあるらしい。Rubyはなかなか深そうですね…

***

* [increments/qiita-rb: Qiita API v2 client library and CLI tool, written in Ruby](https://github.com/increments/qiita-rb)
* [shoyan/slack-incoming-webhooks: A simple wrapper of slack incoming webhooks in Ruby.](https://github.com/shoyan/slack-incoming-webhooks)

***

* [Ruby + Nokogiriでスクレイピング - Qiita](https://qiita.com/kumamonmaster/items/9bb2aadde56c956fdc9f)
* [Nokogiriでスクレイピング - Qiita](https://qiita.com/w650/items/e663fa2430145c456c4d)  
xpathは苦手だったのでCSSで書けるのは助かります。

***

* [Qiitaに1件も投稿していない時の画像アップロード容量の上限は月間2MBまで - Qiita](https://qiita.com/unsoluble_sugar/items/f61be798972883e44877)

***

* [cronでRubyを動かそうとしてハマる - ぱらつり記](http://paralleltree.hatenablog.com/entry/2015/04/27/225313)

cronが`require cannot load such file`エラーを吐いていので、`crontab`に`GEM_HOME="/usr/local/bundle"`を追加しました。


# 知見

## QiitaのPatchは差分更新

APIでの更新は、毎回タグや限定公開状況も含め、完璧な記事パラメーターにしないとダメだと思っていました。
しかし、渡したパラメーターだけが更新され、渡されなかったものは最初の投稿時に決めればプログラム側で持つ必要がないことがわかりました。
(タグパラメーターの渡し方がわからずずっとbad requestでてたので助かりました)

### ただし、タイトルと本文は必須

ここでかなり詰まったのですが、JSONには`title`と`body`を含めなければ、`Bad request`になるようです。
更新したいものは本文だけだったので、かなり困りました。
APIを見ても、必須指定の情報を見いだせなかったです。(雑に見ていたので見落としかもしれません(必須|必要で検索してみたけど…))


### 無変更なら非更新

毎回bodyとtitleを渡す必要がありますが、もともとの内容と差異がなければ更新自体が行われませんでした。
ちゃんと動いているか不安になる場合もあるかもしれませんが、
履歴が無駄に汚れないので良いです。
これも差分更新という言い方であってるかな？


## Qiitaは直リン不可

`img`タグで画像を表示していますが、投稿時するとQiita側に自動でアップロードされるようです。
もともとの`src`属性値は`data-canonical-src`として保存されるようです。

## 多分アップロードがスマート

画像を含む更新を頻繁にしていれば、月のアップロード上限に引っかかるのではないか？
と不安になりましたが、画像以外の部分を多少変えた更新では
実際の画像`src` = `https://camo.qiitausercontent.com/～`
が不変でしたので、再度アップロードせずに前回の画像を使いまわしてくれている気がします。
ただし、それだけで枠消費を回避しているのかはわかりませんが…要経過観察。

## 関係ない疑問

限定共有投稿内のQiitaリンクやメンションは通知されるのかしらん。


private記事なら↓↓にランキングが挿入・更新される予定(これランキングほぼ不動のやつや)
**********