Help us understand the problem. What is going on with this article?

自社のIRをGoogle Apps ScriptでChatWorkに通知する

More than 3 years have passed since last update.

Google Apps Script(以下GAS)を用いて、自社(株式会社ネクスト)のIR情報をチャットワークに通知するように設定しました。

新卒1年目の頃、「ふつうに仕事してるだけじゃ、他部署の人が何やってんのかわかんねえな」って思ったことがコードを書いた動機です。

スクレイピングを行う

以下の記事を参考に、正規表現を用いてHTMLをパースする方法を選びました。

HTMLのパース用のライブラリ(PythonであればBeautiful Soup4)があればそちらを利用するつもりだったのですが、私が探した限りスクレイピング用のライブラリは見つかりませんでした。

また、XmlServiceを利用する手もあったのですが、スクレイピングでは多少間違った形式のHTMLも存在するため、読み込みに失敗する場合があります。

また、「正規表現の最短マッチ」と「改行も含めた正規表現」で多少詰まりました。主に以下のサイトを参考にしました。

チャットワークに通知する

ChatWorkの方が開発されたアドオンを使いました。

最新のURLだけを通知する

ただ、上記のChatWorkのアドオンには「room_idからメッセージを取得する」というメソッドは未実装だったようなので、(本当は自分の投稿から、既に投稿したURLを判断したかったのですが)スプレッドシートに別に保存するようにしました。

「Googleスプレッドシートに最新のURLを残しておき、それ以前のURLは投稿しない」というように設定しています。

コード例

私はGoogle Apps Scriptや、JavaScriptやnode.jsを書き慣れていないので書き方が変かもしれません…。

var ROOM_ID = '12345678'; // 投稿する部屋のID
var TOKEN = 'xxxxxxx'; // チャットワークのアクセストークン
var PICT_ID_DICT = {
  'next': '11111111' // あらかじめアップロードしているホームズくんのアイコン画像のid
};
var TITLE_DICT = {
  'next': 'ネクストニュースが更新しました'
};

/**
 * 実行する関数
 */
function myFunction() {
  try {
    postNextNews();
  } catch(e) {
    Logger.log(e);
    postChatWork(String(e));
    throw e;
  }
}

/**
 * ネクストのニュースを投稿する
 */
function postNextNews() {
  var target_news = selectLatestNews(fetchNextIr(), 'B2');
  if (target_news.length < 1) return;
  postNews(target_news, 'next');
}

/**
 * 投稿したことないニュースだけを返し、スプレッドシートに最新のURLを残す
 */
function selectLatestNews(news_list, cell_address) {
  var sheet = SpreadsheetApp.getActive().getSheetByName('シート1');
  var cell = sheet.getRange(cell_address);
  var latest_url = cell.getValues()[0][0];
  var target_news = takeUntilLastNews(news_list, latest_url);
  if (target_news.length < 1) return [];
  cell.setValues([[target_news[0]['url']]]); // save latest url
  return target_news;
}

/**
 * 既に投稿された最新のニュースが出るまでNewsを取得する
 */
function takeUntilLastNews(news_list, latest_url) {
  result = [];
  for (var i in news_list) {
    var news = news_list[i];
    if (news['url'] == latest_url) break;
    result.push(news);
  };
  return result;
}

/**
 * 株式会社ネクストのニュースを取得する
 */
function fetchNextIr() {
  return parseXmlToDict(
    fetchUrl('http://www.next-group.jp/news/'),
    /<ul class="news_list">([\s\S]*?)<\/ul>/,
    /<li class="clf">([\s\S]*?)<\/li>/g,
    /<span class="date">(\d{4}\d{2}\d{2})<\/span>/,
    /<span class="[\s\S]*?"><a href="[\s\S]*?".*?>([\s\S]*?)<\/a>[\s\S]*?<\/span>/,
    /<span class="[\s\S]*?"><a href="([\s\S]*?)".*?>[\s\S]*?<\/a>[\s\S]*?<\/span>/
  );
}

/**
 * 正規表現で与えたXML要素を取り出す
 */
function parseXmlToDict(html, table_regexp, row_regexp, index_regexp, title_regexp, url_regexp) {
  var table = parseMatchedElement(html, table_regexp);
  var rows = parseAllTags(table, row_regexp);
  return rows.map(function(row) {
    return {
      "index": parseMatchedElement(row, index_regexp),
      "title": parseToText(parseMatchedElement(row, title_regexp)),
      "url": parseMatchedElement(row, url_regexp)
    }
  });
}

/**
 * 正規表現にマッチする要素全てを取得する
 */
function parseAllTags(html, regexp) {
  return html.match(regexp);
}

/**
 * 正規表現にマッチした要素を取得する
 */
function parseMatchedElement(html, regexp) {
  var match = regexp.exec(html);
  if (!match) throw String(regexp) + 'にマッチする要素が見つかりませんでした';
  return match[1].replace(/^\s*(.*?)\s*$/, "$1"); // strip
}

/**
 * htmlタグを取り除き、テキストのみを返す
 */
function parseToText(html) {
  return html.replace(/<("[^"]*"|'[^']*'|[^'">])*>/g,'')
}

/**
 * URLからデータを取得する
 */
function fetchUrl(url) {
  var response = UrlFetchApp.fetch(url);
  return response.getContentText();
}

/**
 * チャットワークにメッセージを送信する
 */
function postChatWork(message) {
  var client = ChatWorkClient.factory({token: TOKEN});
  client.sendMessage({room_id: ROOM_ID, body: message});
}

/**
 * ニュースを通知する
 */
function postNews(news_list, media) {
  var title = '[title]' + TITLE_DICT[media] + '[/title]';
  var body = news_list.map(createRowMessage).join("\n\n");
  var pic = '[preview id=' + PICT_ID_DICT[media] + ' ht=120]';
  var message = pic + '[info]' + title +  body + '[/info]';
  postChatWork(message);
}

/**
 * 一行のニュースを成形する
 */
function createRowMessage(news) {
  return news['title'] + '\n' + news['url'];
}

Google Apps Scriptはcronのようにコードを定期実行する機能があるので、↑のコードを設定すると、次のように自動でチャットに通知してくれます。

capture.JPG

また、正規表現で汎用的に取れるようにしたので、例えば競合他社のIR更新通知などの横展開も簡単にできました。(こちらはIR情報がATOM方式で公開されていたため && 万が一不要な負荷をかけて迷惑をかけないようATOMから取得しています)

アイコン画像も表示しているのは、「どの会社のIRなのかひと目で分かるようにするため」という意味もあります。

/**
 * リクルート住まいカンパニーのIRを投稿する
 */
function postSuumoNews() {
  var press_news = selectLatestNews(fetchAtom('http://www.recruit-sumai.co.jp/press/atom.xml'), 'B3');
  var data_news = selectLatestNews(fetchAtom('http://www.recruit-sumai.co.jp/data/atom.xml'), 'B4');
  var target_news = press_news.concat(data_news);
  if (target_news.length < 1) return;
  postNews(target_news, 'suumo');
}

/**
 * Atom形式のニュースを取得する
 */
function fetchAtom(url) {
  return parseXmlToDict(
    fetchUrl(url),
    /<feed.*?>([\s\S]*?)<\/feed>/,
    /<entry>([\s\S]*?)<\/entry>/g,
    /<published>([\s\S]*?)<\/published>/,
    /<title>([\s\S]*?)<\/title>/,
    /<link.*?href="([\s\S]*?)".*?\/>/
  );
}
ninomiyt
株式会社LIFULLの愉快なPythonistaです
https://medium.com/@ninomiyt
lifull
日本最大級の不動産・住宅情報サイト「LIFULL HOME'S」を始め、人々の生活に寄り添う様々な情報サービス事業を展開しています。
https://lifull.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした