Life is Like a Boat

忘備録や経済、投資、プログラミングに関するメモやtipsなど

TwitterのDeveloperアカウントを取得、Appを作って自動投稿してみる

Twitterへの投稿を自動化したくてAPIキーの発行をしようとしたらいつの間にか制限が厳しくなったようで、はじめにDeveloperアカウントの取得が必要です。

下記のリンクからいくつかの質問に回答する必要があります。審査後、アカウント発行という流れです。

https://developer.twitter.com/en/apply/user

f:id:nerimplo:20181105231951p:plain 正直に理由を書けばいいだけです。

公式からアカウントできたよとメールで知らせてきます。

f:id:nerimplo:20181105232123p:plain

ログインしたらAppを作ってConsumer API key, API secret key, Access token, Access token secretが発行されます。

環境変数に充ててあげて、Node.jsのTwitterモジュールを使って投稿テストです。

test.js

const Twitter = require("twitter");
require('dotenv').config()
const twtr_client = new Twitter({
    consumer_key: process.env.TWTR_CONSUMER_KEY,
    consumer_secret: process.env.TWTR_CONSUMER_SECRET,
    access_token_key: process.env.TWTR_TOKEN_KEY,
    access_token_secret: process.env.TWTR_TOKEN_SECRET
});

(async () => {
await twtr_client.post('statuses/update', {
        status: `YOUR_TWEET!!!`,
    }, (error, tweet, response) => {
        if (error) throw error;
        console.log(tweet);  // Tweet body.
        console.log(response);  // Raw response object.
    })
})()

Herokuにデプロイして作ったBotはこんな感じです。

f:id:nerimplo:20181105232840p:plain

他にも色々ツイートの投稿を自動化したいです。

マルハニチロ 2Qメモ

5日発表の2Q決算短信より。

養殖と商事セグメントでの営業益減少が大きい印象。通期予想は変更せず。 保険金の受け取りと関係会社出資分の売却益で2,605百万の特別利益あり。 通期予想は

  • 売上高 920,000百万
  • 営業益 25,000百万
  • ESP 323円

としている。

漁業養殖事業

単位は百万、カッコ内は前年同期比

売上高 営業利益
16,789 (-15.2%) 739 (-41.8%)
  • 台風の影響による養殖マグロの出荷減、カツオの取扱減

  • マグロ・カツオの魚価安による利益率低下

商事事業

売上高 営業利益
215,817 (+0.7%) 1,708 (-44.4%)
  • [水産商事] 魚価高や円安による調達コスト増加の影響もあり減益

  • [荷受] 台風の来襲など夏場に天候不順が続き、鮮魚の取扱高が減り、また冷凍魚の魚価高を売価に転嫁できず、減収減益

  • [畜産商事] 牛肉・豚肉・加工品で取扱増となるも、牛肉・豚肉・鶏肉の利益率低下により、増収減益

海外事業

売上高 営業利益
83,228 (+5.4%) 2,776 (-19.9%)
  • [海外ユニット] タイでのペットフード事業、日本産水産物の輸出事業、ニュージーランドでの操業漁船1隻追加が売上増に寄与。NZD・豪州にて漁獲が振るわず。米ドルに対するタイバーツ高の影響により、増収減益。
  • [北米ユニット] 助宗すりみの効率的な生産と日欧米主体の順調な販売、及びエビ・タコなど欧州での販売拡大により、増収増益。

加工事業

売上高 営業利益
117,177 (+0.7%) 3,080 (-6.5%)
  • [家庭用冷凍食品] 食卓惣菜向け商品や冷凍野菜の販売は増加。お弁当のおかず向け商品は減。

  • [業務用食品] 介護食、コンビニエンスストア等の取り組みが下支えして売上は前年並み。水産原料、畜産原料等の価格高騰に加え、自社工場製品の販売が低調に推移したことにより減益。

  • [化成] フリーズドライ製品及び機能性表示食品制度を追い風としたDHA・EPAの販売が好調に推移し、増収増益となりました。

物流事業

売上高 営業利益
8,240 (+5.0%) 791 (-2.2%)
  • [増収要因] 積極的な集荷活動による取扱貨物の増加や輸配送事業の伸長

  • [コスト要因] 燃料調整費の上昇に伴う動力費の増加や労務コストの増加、平和島物流センターの新規稼働に伴う賃借料の増加等

https://www.release.tdnet.info/inbs/140120181102428838.pdf

ハロウィーングッズの価格トレンド

先月31日はハロウィンでした。

本来は子供が仮装してトリックオアトリートと言ってお菓子をもらう風習です。私の実家の地域では十五夜の夜に子供らが家々を回ってお菓子をもらう風習がありました。

地域によっては「お月見どろぼう」という名前が付いているそうです。

matome.naver.jp

そういやどんなお菓子もらっていたかなと25年くらい前のことですが思い出してみると、お菓子はもちろん、手作りの餅や笹団子もありました。 子供らは家々を回った後、公民館で収穫物を山分けです。ビックリマンチョコが当時好きでお目当てにしていた記憶があります。

私の育った地区には小学校全学年で20人くらいはいたので、お菓子揃えるにも結構な出費だっただろうなと今になって思いますw

そこで、ハロウィーンに関係しそうなお菓子の価格トレンドはどうなっているか、調べてみました。

元データは先月発表された東京都区部の消費者物価指数です。

キャンディ

f:id:nerimplo:20181101120220p:plain

黒田日銀の大規模金融緩和が始まって以降、ほとんど動いていません。

ビスケット

f:id:nerimplo:20181101121155p:plain

だいふく

f:id:nerimplo:20181101120211p:plain

都内だと大福売ってるのは和菓子屋くらいですかね〜

チョコレート

f:id:nerimplo:20181101120214p:plain

調査対象となるのは、 板チョコレート,50g,「明治ミルクチョコレート」,「ロッテガーナミルクチョコレート」又は「森永ミルクチョコレート」 だそうです。

まんじゅう

f:id:nerimplo:20181101120207p:plain

まんじゅうは子供が食べるのか。。。?と思いますが、お菓子系の品目なので調べてみました。

年齢と紐つけたPOSデータがあれば、消費世代を加味した価格指数ができるのではないかなぁ。

Heroku + Puppeteer + SendGridでスクレイピングした結果を自分のメアドに送る

ネタはなんでもいいのですが、日々更新されるサイトのキャプチャを取ってメールで送って欲しいことがあると思います。

例えば、

  1. 日経平均の業種別騰落率がヒートマップになっている画像を取得
  2. 騰落率のベスト・ワースト3をサマリーとして記載
  3. 自分のメールアドレスに送信

と、下記の画像のようなことを実現したいです。

f:id:nerimplo:20181103164628j:plain

1は 日経平均採用銘柄の株価一覧 :株式 :マーケット :日経電子版 のヒートマップ画像です。

方法としては

  1. Puppeteerで上記URLにアクセス。
  2. 画像部分の要素をスクショ
  3. 添付ファイルとする
  4. SendGrid経由で自分のメアドに送信

となります。

ローカルでサクサクっとメール受信までできたので、HerokuにDeployするのは楽勝だろうと思っていましたが、下記の点でハマりました。

HerokuにPuppeteerのBuildpackを充てる

$ heroku buildpacks:add buildpack_nameのコマンドでnodejs, puppeteer, 日本語対応用のパックのbuildpackをインストールします。 Puppeteer buildpackの作者(@jontewks)によるとこの順番が問題になるケースもあるようです。理由はよくわからないが私の環境ではこうなってます。

=== YOUR_APP_NAME Buildpack URLs
1. heroku/nodejs
2. jontewks/puppeteer
3. https://github.com/CoffeeAndCode/puppeteer-heroku-buildpack.git

Heroku Appの環境は以下の通りです。

$ heroku apps:info YOUR_APP_NAME 
=== YOUR_APP_NAME
Auto Cert Mgmt: false
Dynos:          web: 1
Git URL:        https://git.heroku.com/YOUR_APP_NAME.git
Owner:          YOUR_APP_NAME@hogehoge.com
Region:         us
Repo Size:      56 KB
Slug Size:      231 MB
Stack:          heroku-18
Web URL:        https://YOUR_APP_NAME.herokuapp.com/

この後、heroku run node your_puppeteer_app.jsするとエラーが….

Most likely you need to configure your SUID sandbox correctly SUIDってなんやねん….

2018-11-03T06:03:03.423619+00:00 app[web.1]: (node:4) UnhandledPromiseRejectionWarning: Error: Failed to launch chrome!
2018-11-03T06:03:03.423637+00:00 app[web.1]: 
2018-11-03T06:03:03.423639+00:00 app[web.1]: (chrome:21): Gtk-WARNING **: 06:03:03.390: cannot open display:
2018-11-03T06:03:03.423641+00:00 app[web.1]: [1103/060303.407764:ERROR:nacl_helper_linux.cc(310)] NaCl helper process running without a sandbox!
2018-11-03T06:03:03.423643+00:00 app[web.1]: Most likely you need to configure your SUID sandbox correctly
2018-11-03T06:03:03.423645+00:00 app[web.1]: 
2018-11-03T06:03:03.423646+00:00 app[web.1]: 
2018-11-03T06:03:03.423648+00:00 app[web.1]: TROUBLESHOOTING: https://github.com/GoogleChrome/puppeteer/blob/master/docs/troubleshooting.md
2018-11-03T06:03:03.423649+00:00 app[web.1]: 
2018-11-03T06:03:03.423651+00:00 app[web.1]: at onClose (/app/node_modules/puppeteer/lib/Launcher.js:342:14)
2018-11-03T06:03:03.423652+00:00 app[web.1]: at Interface.helper.addEventListener (/app/node_modules/puppeteer/lib/Launcher.js:331:50)
2018-11-03T06:03:03.423654+00:00 app[web.1]: at Interface.emit (events.js:187:15)
2018-11-03T06:03:03.423656+00:00 app[web.1]: at Interface.close (readline.js:379:8)
2018-11-03T06:03:03.423657+00:00 app[web.1]: at Socket.onend (readline.js:157:10)
2018-11-03T06:03:03.423659+00:00 app[web.1]: at Socket.emit (events.js:187:15)
2018-11-03T06:03:03.423660+00:00 app[web.1]: at endReadableNT (_stream_readable.js:1094:12)
2018-11-03T06:03:03.423662+00:00 app[web.1]: at process._tickCallback (internal/process/next_tick.js:63:19)

どうやらpuppeteer起動時の引数が問題なようです。

puppeteer.launch({args: ['--no-sandbox', '--disable-setuid-sandbox']});

この、'--disable-setuid-sandbox’を除いてみるとワークしました!

上記問題の解決に参考にしたリンク

Heroku-16 Failed to launch chrome! error while loading shared libraries: libpng16.so.16 · Issue #14 · jontewks/puppeteer-heroku-buildpack · GitHub

Failed to launch chrome · Issue #807 · GoogleChrome/puppeteer · GitHub

SendGridを使って自分のメアドに送る

このブログが参考になりました。 やはり頼りになるのは公式ブログ。

sendgrid.com

肝となるのは、Puppeteerで取得した画像ファイルをbase64でエンコードしてattachmentsに渡す部分だと思います。 attachmentを複数形にしていなくて30分くらい公式のリファレンスとにらめっこしてました。

こんな感じで送信する内容を作ります。

const msg = {
        to: 'YOUR_EMAIL_ADDRESS',
        from: 'test@example.com',
        subject: `${png_file}`,
        html: `<html><body><pre>${emailBody}</pre><img src="cid:myimagecid"/></body></html>`,
        attachments: [
            {
                filename: png_file,
                contenType: 'image/png',
                content: base64str,
                content_id: 'myimagecid'
            }
        ]
    };

ソースコード

package.json

{
  "name": "nikkeiheatmap",
  "version": "0.0.1",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start" : "node run.js"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@sendgrid/mail": "^6.3.1",
    "moment": "^2.20.1",
    "puppeteer": "^1.4.0",
    "csv": "^3.1.0",
    "csv-writer": "^1.0.0",
    "dotenv": "^6.1.0"
  }
}

.env

SENDGRID_APIKEY=YOUR_SENDGRID_APIKEY
DYNO=false

your_puppeteer_app.js

const puppeteer = require('puppeteer');
const moment = require('moment');
const sgMail = require('@sendgrid/mail');
require('dotenv').config()
const NikkeiData = require('./nikkeidata.js');
const fs = require("fs");

const FAKE_USERAGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_1) AppleWebKit/604.3.5 (KHTML, like Gecko) Version/11.0.1 Safari/604.3.5';
const NIKKEI_HEATMAP_URL = 'https://www.nikkei.com/markets/kabu/nidxprice/';
const HEATMAP_ELEM = 'div#CONTENTS_MARROW div.m-article';
const LAUNCH_OPTION = process.env.DYNO ? {args: ['--no-sandbox']} : {headless: false};
const TEMP_DIR = process.env.DYNO ? '/tmp/' : ''
sgMail.setApiKey(process.env.SENDGRID_APIKEY);


async function scrape_data_table(page) {
    const data = await page.evaluate(() => {
        const d = Array.from(document.querySelectorAll('div#CONTENTS_MARROW div.m-article div.highcharts-data-labels.highcharts-series-0 div span')).map((e) => {
            return e.innerText.split('\n')
        });
        return d;
    },);
    return data;
}

function to_email_body(dx) {

    const data = dx.map((e) => {
        var d = new NikkeiData(e[0].split('(')[0], Number(e[1].replace('円', '')))
        return d
    }).sort((a, b) => (a.delta < b.delta) ? 1 : ((b.delta < a.delta) ? -1 : 0));

    // get top 3 elements
    const top3 = data.slice(0, 3)

    // get bottom 3 elements
    const bottom3 = data.slice(-3).reverse()

    return `[上昇]\n${top3.reduce((p, c) => {
        return p + '\n' + c
    })} \n\n [下落]\n${bottom3.reduce((p, c) => {
        return p + '\n' + c
    })}`
}

async function base64_encode(file) {
    var bitmap = fs.readFileSync(file);
    return new Buffer(bitmap).toString("base64");
}


(async () => {
    const browser = await puppeteer.launch(LAUNCH_OPTION);
    const page = await browser.newPage();
    await page.setUserAgent(FAKE_USERAGENT);
    await page.setViewport({width: 1000, height: 1000, deviceScaleFactor: 1});
    await page.goto(NIKKEI_HEATMAP_URL, {waitUntil: 'networkidle2'});
    await page.waitFor(1000);
    const element = await page.$(HEATMAP_ELEM);
    const png_file = await `nikkei_heatmap_${moment().format('YY-MM-DD hh:mm')}.png`
    const png_path = await `${TEMP_DIR}${png_file}`
    await element.screenshot({
        path: png_path
    });

    const data = await scrape_data_table(page);
    const emailBody = to_email_body(data);

    const base64str = await base64_encode(png_path);

    const msg = {
        to: 'YOUR_EMAIL_ADDRESS',
        from: 'test@example.com',
        subject: `${png_file}`,
        html: `<html><body><pre>${emailBody}</pre><img src="cid:myimagecid"/></body></html>`,
        attachments: [
            {
                filename: png_file,
                contenType: 'image/png',
                content: base64str,
                content_id: 'myimagecid'
            }
        ]
    };
    await sgMail.send(msg);

    await browser.close();
})();

この前新宿の本屋にいったらPuppeteer本が平積みされていました!

Puppeteer入門 スクレイピング+Web操作自動処理プログラミング

Puppeteer入門 スクレイピング+Web操作自動処理プログラミング

次のリセッション

次のリセッションがいつ来るか。わかったら苦労せんがな、と言いたくなりそうですが、こんな記事を見つけました。

The Next U.S. Recession Is Moving Further Away https://www.bloomberg.com/news/articles/2018-10-05/the-next-u-s-recession-is-moving-further-away

f:id:nerimplo:20181101115105p:plain

このBloombergのモデルは

  • 米国債金利
  • SP500
  • ドルインデックス
  • WTI原油価格

を使って次のリセッションがいつ来るか予測しています。

それぞれの線は次のリセッションが何ヶ月後に起こるかという可能性を示しています。

例えば黒の(0-12months)だと8月中旬ごろにピークをつけて、その後下落に転じています。向こう一年は景気後退は起きなさそうだという解釈です。

よく読むと記事の公開日は先月6日で時間軸が10月で終わっているので、先月の値動きは反映されてないと思われます。

アディオス、au!

今日の午後、8年くらい使っていたauを解約しmineoと契約してきました。

ちょうど、auの違約金なしで解約できる期限が今月末まででした。そのうえ、mineoが家電批評のランキングで高評価だった事と11月初旬までやってる6ヶ月割引キャンペーンが後押しになりました。

auが販促で流してる浦島太郎だとかかぐや姫が出てくるようなふざけたCMを見て、その制作費を古参の携帯ユーザに還元しろ!と悶々とすることはもうありません。

非通信分野への進出はわかるが、英会話教室買ったり子供の就業体験施設を買収するのはやり過ぎではないか。キャッシュあるなら、株主に還元しろと言いたくもなります。

話逸れましたが、秋の青空のように今は脱庭できて清々しい気分です。

mineoに切り替えて5時間ほど経ちましたが、とくにネットワークで遅さを感じることはありません。事実上、普段使いなら全く問題ない印象です。

MMS使えないのが唯一の難点ですが、myMailなどサードパーティーのメールアプリを使えばプッシュ通知に対応できるようです。

25日移動平均を上回る銘柄の比率を見てみる

8月の記事ですが、だいたいの相場の過熱感をみる目安として東証1部の銘柄で、株価が過去1カ月の平均値である25日移動平均を上回る比率を示したグラフが日経に載ってました。

www.nikkei.com

手元に分析できる株価データも揃っているので、似たようなグラフを作成してみました。

下記のグラフは全上場企業を対象にしています。

比較対象の指数として、TOPIXを選びました。TOPIXは東証一部市場に上場している全企業の浮動株ベースの時価総額加重型で算出される株価指数なので、本当は市場ごとの指数と各市場に上場している企業の比較にしたいのですが、サクッと入手しやすいTOPIXにしています。

グラフをみていると、今年2月と7月の下落の際は、だいたい25日移動平均を上回る銘柄の比率が15%以下だったことがわかります。その後、TOPIXは反発しています。

26日時点では今年最低の6.2%です。

これはもう「隠の極」水準と言ってもいいのではと思ったのですが、「もうはまだなり」という格言もあるので....。

f:id:nerimplo:20181028225403p:plain

ちなみに全上場企業を時価総額順に並べて10のグループに分けて、その時価総額最上位グループに入る企業の中で、10月26日終値が25日移動平均を上回っている上位10社は以下の通りです。

コード  会社名
3291 飯田グループホールディングス
6701 日本電気
7532 ドンキホーテホールディングス
8028 ユニー・ファミリーマートホールディングス
8282 ケーズホールディングス
8410 セブン銀行
8905 イオンモール
8933 エヌ・ティ・ティ都市開発
9142 九州旅客鉄道
9501 東京電力ホールディングス

ディフェンシブ系が入るのは納得いきますが、NECが意外でした。IoTやセキュリティ関連の需要が堅調だからでしょうか。