セブンだけでなくファミマの今週の新商品もスクレイピングしてみた

## はじめに

以下の記事を読んで面白そうだと思い、実際に組んでみました。
[GASでセブンイレブンの今週の新商品をスクレイピングして通知する](https://qiita.com/khsk/items/a3d942a2d091abed7c4e)

GASは初めてだったので戸惑いましたが、何とか組むことができました。
生意気にも品数を出したりとちょっとしたカスタマイズまでしています。
<img width="554" alt="スクリーンショット 2018-05-17 1.25.42.png" src="https://qiita-image-store.s3.amazonaws.com/0/138245/da6e0f3e-6ff7-fe2a-3958-5ce7f46cc0b4.png">

ここで私は思いました。
「 __これ他のコンビニでもできるんじゃね？__ 」

そう思ってファミリーマートのサイトを見てみました。
http://www.family.co.jp/goods/newgoods.html

<img width="989" alt="スクリーンショット 2018-05-17 22.21.12.png" src="https://qiita-image-store.s3.amazonaws.com/0/138245/c9198c94-7032-8bd2-c633-59b0232dc93e.png">

セブンとほぼ同じようなサイトです。
「 __これはいける！ファミマだってセブンに負けちゃられねぇぜ！__ 」

ということでファミマ版も組んでみましたので紹介します。

## リファクタリング

まずはセブンのソースを使い回せるようにリファクタリングします。
Slackの通知部分は共通化できそうなので、セブン固有の処理と分離します。

### セブン固有の処理

```js
// ----------------------------------------------------------------------------
// Post Seven Products
//
// セブンイレブンの新商品をSlackに通知する
// 参考：https://qiita.com/khsk/items/a3d942a2d091abed7c4e
// 　　：http://www.sej.co.jp/i/products/thisweek/?page=1&sort=f&limit=100
// ----------------------------------------------------------------------------

// 定数
var MY_SEVEN_REGION = '関東'
var SEVEN_INCOMING_URL = 'https://hooks.slack.com/services/…'
var SEVEN_TERMS = {
  '今週': 'thisweek',
  '来週': 'nextweek'
}
var SEVEN_REGIONS = {
  '北海道': 'hokkaido',
  '東北': 'tohoku',
  '関東': 'kanto',
  '甲信越・北陸': 'koshinetsu',
  '東海': 'tokai',
  '近畿': 'kinki',
  '中国・四国': 'chugoku',
  '九州': 'kyushu'
}
var SEVEN_SORTS = {
  'おすすめ': 'f',
  '新商品': 'n'
}
var SEVEN_LIMITS = {
  '15件': 15,
  '50件': 50,
  '100件': 100
}
var SEVEN_COLORS = [
  '#f58220', // セブンオレンジ
  '#00a54f', // セブングリーン
  '#ee1c23' // セブンレッド
]
var SEVEN_BASE_URL = 'http://www.sej.co.jp'
var SEVEN_PRODUCTS_DIR = '/i/products/'

// メソッド
function postSevenThisWeekNewProducts() {
  postSevenProducts('今週')
}

function postSevenNextWeekNewProducts() {
  postSevenProducts('来週')
}

function postSevenProducts(term) {
  var query = sevenQuery(SEVEN_SORTS['おすすめ'], SEVEN_LIMITS['100件'])
  var url = sevenURL(SEVEN_TERMS[term], SEVEN_REGIONS[MY_SEVEN_REGION], query)
  text = term + 'の新商品です。(' + MY_SEVEN_REGION + ')'
  postSevenSlack(text, makeSevenAttachments(url))
}

function sevenQuery(sort, limit) {
  return '?page=1' + '&sort=' + sort + '&limit=' + limit
}

function sevenURL(term, region, query) {
  return SEVEN_BASE_URL + SEVEN_PRODUCTS_DIR + term + '/' + region + query
}

function postSevenSlack(text, attachments) {
  text += '\n全' + attachments.length + '品'
  postSlack(SEVEN_INCOMING_URL, '711', 'セブン新商品通知bot', ':711:', text, attachments)
}

function makeSevenAttachments(url) {
  var attachments = []
  var html = UrlFetchApp.fetch(url).getContentText()
  var items = Parser.data(html).from('<li class="item">').to('</div>\n</li>').iterate()
  for (var i = 0; i < items.length; i++) {
    var link = SEVEN_BASE_URL + items[i].match(/<a href="(.+)">/)[1]
    var image = items[i].match(/src="([^"]+)" alt="商品画像"/)[1]
    var name = items[i].match(/<div class="itemName">.+">(.+?)<\/a><\/strong>/)[1]
    var price = items[i].match(/<li class="price">(.+?)<\/li>/)[1]
    var launch = items[i].match(/<li class="launch">(.+?)<\/li>/)[1]
    var region = items[i].match(/<li class="region">(.+?)<\/li>/)[1].replace('<em>販売地域</em>', '')
    attachments.push(makeSevenAttachment(link, image, name, price, launch, region, i))
  }
  return attachments
}

function makeSevenAttachment(link, image, name, price, launch, region, i) {
  return makeAttachment(SEVEN_COLORS[i % SEVEN_COLORS.length],
                        'No.' + (i + 1),
                        name,
                        link,
                        image,
                        makeSevenFields(price, launch, region))
}

function makeSevenFields(price, launch, region) {
  return [makeField('値段', price, true),
          makeField('販売時期', launch, true),
          makeField('販売地域', region, true)]
}
```

`postSevenThisWeekNewProducts()` メソッドを実行すると今週の新商品がSlackに通知されます。

`postSevenNextWeekNewProducts()` メソッドだと来週の新商品が通知されます。

### Slack通知処理

```js
// ----------------------------------------------------------------------------
// Post Slack
// ----------------------------------------------------------------------------

function makeAttachment(color, pretext, title, titleLink, thumbURL, fields) {
  return {
    color: color,
    pretext: pretext,
    title: title,
    title_link: titleLink,
//    image_url: imageURL,
    thumb_url: thumbURL,
    fields: fields
  }
}

function makeField(title, value, short) {
  return {
    title: title,
    value: value,
    short: short,
  }
}

function postSlack(incomingURL, channel, username, iconEmoji, text, attachments) {
  var jsonData = {
    channel: channel,
    username: username,
    icon_emoji: iconEmoji,
    text: text,
    attachments: attachments,
  }
  var options = {
    "method" : "post",
    "contentType" : "application/json",
    "payload" : JSON.stringify(jsonData)
  }
  UrlFetchApp.fetch(incomingURL, options)
}
```

## 実装

ここまでリファクタリングできたら、セブン固有の処理をコピーしてファミマ版とするだけです。
URLの構成や表示項目が異なるので、そこに気をつけて実装していきます。

本当はクラス化して継承したかったのですが、生のJSではできないようなので断念しました。

### ファミマ固有の処理

```js
// ----------------------------------------------------------------------------
// Post Famima Products
//
// ファミリーマートの新商品をSlackに通知する
// 参考：http://www.family.co.jp/goods/newgoods.html
// ----------------------------------------------------------------------------

var FAMIMA_INCOMING_URL = 'https://hooks.slack.com/services/…'
var FAMIMA_TERMS = {
  '先週': 'lastweek',
  '今週': 'newgoods',
  '来週': 'nextweek'
}
//var CATEGORIES = {
//}
var FAMIMA_COLORS = [
  '#36aa57', // ファミマグリーン
//  '#ffffff', // ファミマホワイト // 白だと線が見えないので使わない
  '#009ec6' // ファミマブルー
]
var FAMIMA_BASE_URL = 'http://www.family.co.jp'
var FAMIMA_GOODS_DIR = '/goods/'
var FAMIMA_NEW_GOODS_DIR = 'newgoods/'

// メソッド
function postFamimaThisWeekNewProducts() {
  postFamimaProducts('今週')
}

function postFamimaNextWeekNewProducts() {
  postFamimaProducts('来週')
}

function postFamimaLastWeekNewProducts() {
  postFamimaProducts('先週')
}

function postFamimaProducts(term) {
  var url = famimaURL(term)
  text = term + 'の新商品です。'
  postFamimaSlack(text, makeFamimaAttachments(url))
}

function famimaURL(term) {
  if (term == '今週') {
    return FAMIMA_BASE_URL + FAMIMA_GOODS_DIR + FAMIMA_TERMS[term] + '.html'
  }
  if (term == '来週'|| term == '先週') {
    return FAMIMA_BASE_URL + FAMIMA_GOODS_DIR + FAMIMA_NEW_GOODS_DIR + FAMIMA_TERMS[term] + '.html'
  }
}

function postFamimaSlack(text, attachments) {
  text += '\n全' + attachments.length + '品'
  postSlack(FAMIMA_INCOMING_URL, 'famima', 'ファミマ新商品通知bot', ':famima:', text, attachments)
}

function makeFamimaAttachments(url) {
  var attachments = []
  var html = UrlFetchApp.fetch(url).getContentText()
  var items = Parser.data(html).from('<div class="ly-mod-infoset4 js-imgbox-size-rel ">').to('</p> </a>').iterate()
  for (var i = 0; i < items.length; i++) {
    var link = FAMIMA_BASE_URL + items[i].match(/<a href="(.+?)" class="ly-mod-infoset4-link">/)[1]
    var image = FAMIMA_BASE_URL + items[i].match(/src="([^"]+)" alt="/)[1]
    var name = items[i].match(/<h3 class="ly-mod-infoset4-ttl">\n\s*(.+?)\n*\s*<\/h3>/)[1]
    var category = items[i].match(/<p class="ly-mod-infoset4-cate">\n\s*(.+?)<\/p>/)[1]
    var price = items[i].match(/<p class="ly-mod-infoset4-txt">\n\s*(.+?<br>\n\t*<span>.+?)<\/span>/)[1].replace(/<br>\n\t*<span>/, '')
    attachments.push(makeFamimaAttachment(link, image, name, category, price, i))
  }
  return attachments
}

function makeFamimaAttachment(link, image, name, category, price, i) {
  return makeAttachment(FAMIMA_COLORS[i % FAMIMA_COLORS.length],
                        'No.' + (i + 1),
                        name,
                        link,
                        image,
                        makeFamimaFields(category, price))
}

function makeFamimaFields(category, price) {    
  return [makeField('種類', category, true), makeField('値段', price, true)]
}
```

## 完成！

`postFamimaThisWeekNewProducts()` メソッドを実行します。

<img width="554" alt="スクリーンショット 2018-05-17 1.26.10.png" src="https://qiita-image-store.s3.amazonaws.com/0/138245/3e0a4d22-3aeb-fc04-e447-1810a8252f9c.png">

ファミマの新商品もスクレイピングしてSlackに通知することができました。

ちなみに `postFamimaNextWeekNewProducts()` メソッドを実行すると来週の新商品、 `postFamimaLastWeekNewProducts()` メソッドを実行すると先週の新商品が通知されます。

## トリガーの設定

スクリプトが完成したら、あとは定期的に実行させるよう指定するだけです。

各ページを手動でリロードして調べたところ、更新される時間は以下のようでした。
 
|ページ|更新日時|
|:---|:---|
|今週の新商品|毎週火曜日0時頃|
|来週の新商品|毎週金曜日15時頃|

これを踏まえ、私は以下のようにトリガーを設定しています。

<img width="863" alt="スクリーンショット_2018-05-22_19_56_32.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/138245/1e67c74d-6d3a-61c7-4ff3-3ca916657266.jpeg">

これで今週の新商品は毎週火曜日の6〜7時の間、来週の新商品は毎週金曜日の18〜19時の間に通知されます。
起床時間と就業時間に合わせたことにより、更新されてからのタイムラグが発生しているので、時間帯はお好みで変更してください。

トリガーは今週 or 来週のどちらか片方でもよさそうです。

## おわりに

GASもJSも正規表現も全くの素人だったのでめちゃくちゃ苦戦しました。

特に正規表現で名前を取ってくるところがやっかいでした。
今週(2018/05/15〜05/21)だと「ふんわりしっとりチーズスフレ（デンマーク産クリームチーズ使用）」だけなぜか名前と `</h3>` タグの間に改行と半角スペースが入っていたので、それを考慮するために以下のコードとなっています。

```diff_javascript
- var name = items[i].match(/<h3 class="ly-mod-infoset4-ttl">\n\s*(.+?)<\/h3>/)[1]
+ var name = items[i].match(/<h3 class="ly-mod-infoset4-ttl">\n\s*(.+?)\n*\s*<\/h3>/)[1]
```

<img width="948" alt="スクリーンショット_2018-05-17_10_15_54.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/138245/2100679f-b209-fee9-cc3f-d1f0d3824080.jpeg">

<img width="501" alt="スクリーンショット 2018-05-17 1.57.41.png" src="https://qiita-image-store.s3.amazonaws.com/0/138245/af8cf712-2118-4d6e-b126-2998a9b72052.png">


ふんわりしっとりチーズスフレめ！
ちょっと高いけどうまそうだな:custard:

今度買ってみようかな…笑

## (追記)ふんわりしっとりチーズスフレ食べました:custard:

有言実行ということで、実際にふんわりしっとりチーズスフレを食べました。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">ふんわりしっとりチーズスフレ食べました🍮 <br>文字通りふんわりしっとりしていてボリュームもあり、満足感いっぱいでした☺️ <a href="https://t.co/61Im4LmpbJ">pic.twitter.com/61Im4LmpbJ</a></p>&mdash; ウホーイ (@the_uhooi) <a href="https://twitter.com/the_uhooi/status/1002028592300109825?ref_src=twsrc%5Etfw">May 31, 2018</a></blockquote>

ボリュームは見た目以上でした。
店員さんが __パスタとかに使うフォークを付ける__ くらいでした。

満足な一品でした:relaxed: