7は孤独な数字

森博嗣さんの『すべてがFになる』という作品の冒頭で、こんな会話があります。

「1から10までの数字を二組に分けてごらんなさい。そして、両方とも、グループの数字を全部かけ合わせるの。二つの積が等しくなることがありますか?」

「ありません」萌絵は即答した。「片方のグループには7がありますから、積は7の倍数になりますけど、もう片方には7がないから、等しくはなりません」

「ほら、7だけが孤独でしょう?」真賀田女史が言った。

もし二つのグループの積が等しいなら、全ての数字の総積は必ず平方数になります。

7は素数だし、1から10までの中で自分自身以外の倍数はないので、二つのグループの積が等しくなることはありません。

それなら、7を除くと、積が等しくなることはありますか?と、自然と考えたくなりますね。

検証してみましょう。

f:id:michiru_7:20210525201619p:plain

確かに真賀田四季が言った通りですw

ちょっと考えてみたら、実はこの問題はある種の Subset sum problem の 積バージョンとなり、NP完全問題ですね。

Google ColabでMeCabを使ってみよう

MeCab とは日本語形態素解析エンジンである。

詳しくは MeCab 公式ホームページをご参照ください。

taku910.github.io

  • mecab-python3 のインストール
!pip install mecab-python3
  • mecab-ipadic-NEologd のインストール
!apt install mecab libmecab-dev git make curl xz-utils file
!git clone --depth 1 https://github.com/neologd/mecab-unidic-neologd.git
!echo yes | mecab-unidic-neologd/bin/install-mecab-unidic-neologd -n

MeCab.Tagger クラスに-dオプションで NEologd 辞書のパスを指定することができます。

import MeCab
tagger = MeCab.Tagger("-d /usr/lib/x86_64-linux-gnu/mecab/dic/mecab-unidic-neologd")
print(tagger.parse('フシギダネは不思議だね。'))
  • 実行結果 f:id:michiru_7:20210516165552p:plain

AtCoder Regular Contest 077 C - pushpush

atcoder.jp

数列 a の長さが奇数の時、反転した偶数 index の要素 + 奇数 index の要素が数列 b になります。

数列 a の長さが偶数の時、反転した奇数 index の要素 + 偶数 index の要素が数列 b になります。

例えば、

b index
3 1 2 2 0 1
4 2 1 3 3 1 0 2
5 3 1 2 4 4 2 0 1 3
n = int(input())
a = list(map(int, input().split()))
b = []
odd_idx_list = a[1::2]
even_idx_list = a[::2]
 
if n % 2== 0:
    odd_idx_list.reverse()
else:
    even_idx_list.reverse()
b = even_idx_list + odd_idx_list
 
print(*b)

PythonとHerokuでKindle日替わりセールの情報を送ってくれるslackbotを作ってみる

まずはslackbotライブラリとスクレイピングするために必要なライブラリ(requestsBeautiful Soup)をインストールする。

$ pip install slackbot
$ pip install requests beautifulsoup4

ディレクトリ構造

$ tree
.
├── Procfile
├── libs
│   ├── __init__.py
│   └── my_functions.py
├── plugins
│   ├── __init__.py
│   └── my_mention.py
├── requirements.txt
├── run.py
└── slackbot_settings.py

実装

  • run.pyslackbot_settings.py
# -*- coding: utf-8 -*-

from slackbot.bot import Bot

def main():
    bot = Bot()
    bot.run()

if __name__ == "__main__":
    print('start slackbot')
    main()
# -*- coding: utf-8 -*-

import os

# botアカウントのトークンを指定
API_TOKEN = os.environ.get('SLACKBOT_API_TOKEN')

# このbot宛のメッセージで、どの応答にも当てはまらない場合の応答文字列
DEFAULT_REPLY = "何言ってんだこいつ"

# プラグインスクリプトを置いてあるサブディレクトリ名のリスト
PLUGINS = ['plugins']
  • my_functions.pyは以下の通りです。

Kindle日替わりセールの情報をスクレイピングする。

# -*- coding: utf-8 -*-

import re
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
from bs4 import BeautifulSoup

headers = {'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2227.0 Safari/537.36'}

def kindle_daily_deal(url):
    text = ''

    s = requests.Session()
    retries = Retry(total=5,
                    backoff_factor=0.1,
                    status_forcelist=[500, 502, 503, 504])

    s.mount("https://", HTTPAdapter(max_retries=retries))

    html = s.get(url, headers=headers).text.encode('utf-8')
    soup = BeautifulSoup(html, features="html.parser")

    # CSSセレクターを使って日替わりセールリストを取得する
    carousel_list = soup.find('ol', attrs={'class': 'a-carousel'})

    # 本のタイトルと著者名を取得する
    title_author_list = carousel_list.select("[class='a-truncate-full']")

    # 本の価格を取得する
    price_list = carousel_list.select("[class='a-price-whole']")

    res = {}
    res['title'] = ['『{}』'.format(t.get_text()) for i, t in enumerate(title_author_list) if i % 2 == 0]
    res['author'] = [re.sub(r"[\n ]", "", a.get_text()) for i, a in enumerate(title_author_list) if i % 2 == 1]
    res['price'] = ['{}\n'.format(p.get_text()) for p in price_list]

    for i in range(len(price_list)):
        text += '📚 '+ res['title'][i] + '\n著者:' + res['author'][i] + '\n価格:' + res['price'][i]

    return text
  • my_mention.py

kindleというキーワードに反応して、今日のKindle日替わりセール情報を返事する。

# -*- coding: utf-8 -*-

import re
from slackbot.bot import respond_to     # @botname: で反応するデコーダ
from slackbot.bot import listen_to      # チャネル内発言で反応するデコーダ
from libs import my_functions           # 外部関数の読み込み

@respond_to(r'kindle', re.I)
@listen_to(r'kindle', re.I)
def kindle(message):
    url = 'https://www.amazon.co.jp/Kindle%E6%97%A5%E6%9B%BF%E3%82%8F%E3%82%8A/b/?ie=UTF8&node=3338926051&ref_=sv_nav_ebook_4'
    text = my_functions.kindle_daily_deal(url)
    msg = 'ほら!今日のKindle日替わりセールだぞ\n```' + text + '```'
    message.reply(msg)

デプロイ

requirements.txtとHerokuの設定ファイルProcfileを用意する。

$ pip freeze > requirements.txt

Procfile

worker: python run.py

slackのAPIトークンを環境変数として設定する。

$ heroku config:set SLACKBOT_API_TOKEN=xoxb-xxxxx

環境変数の確認。

$ heroku config

Settingsタブでpython buildpackを追加する。

f:id:michiru_7:20210505214020p:plain

あとは、Deployタブで書いた通りにコマンドを実行する。

$ heroku login
$ git init
$ heroku git:remote -a xxx
$ git add .
$ git commit -am "make it better"
$ git push heroku master

さて、プロセスを起動してみる。

$ heroku ps:scale worker=1

プロセスを確認する。

$ heroku ps

プロセスを停止するには、以下のコマンドを実行すれば良い。

$ heroku ps:scale worker=0

ログを見る。

$ heroku logs --tail

定期投稿

日替わりセールなので、やっぱり毎日指定した時間にメッセージを投稿したいです。 Herokuでの定期実行はHeroku Schedulerというアドオンが提供しているが、今回はpython-crontabというライブラリを用いて定期実行を行う。

python-crontabライブラリをインストールする(croniterのインストールも必要になる)。

$ pip install python-crontab
$ pip install croniter
  • cron.py

HerokuのタイムゾーンUTCなので、9時間の時差がある。

# -*- coding: utf-8 -*-

from crontab import CronTab

def main():
    cron = CronTab()
    job1 = cron.new('python3 kindle.py')
    # 毎日の20:00(JST)に実行する
    job1.setall('00 11 * * *')
    for res in cron.run_scheduler():
        print('The schedule has been executed.')

if __name__ == "__main__":
    main()
  • kindle.py
# -*- coding: utf-8 -*-

import re
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
from slacker import Slacker
from bs4 import BeautifulSoup
from slackbot_settings import API_TOKEN

headers = {'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2227.0 Safari/537.36'}

def kindle_daily_deal(url):
    # 省略

def main():
    url = 'https://www.amazon.co.jp/Kindle%E6%97%A5%E6%9B%BF%E3%82%8F%E3%82%8A/b/?ie=UTF8&node=3338926051&ref_=sv_nav_ebook_4'
    text = kindle_daily_deal(url)
    msg = 'ほら!今日のKindle日替わりセールだぞ\n```' + text + '```'
    slack = Slacker(API_TOKEN)
    slack.chat.post_message('#reminder', msg, as_user=True)

if __name__ == "__main__":
    main()
  • Procfile
worker: python run.py
cron: python cron.py

プロセスを起動する。

$ heroku ps:scale cron=1

結果はこんな感じ

f:id:michiru_7:20210505214256p:plain

VimでPythonの入力補完

jedi-vimという python 入力補完プラグインを試してみたいと思って、インストール完了後 Vim でファイルを開くと、

Error detected while processing function jedi#init_python[11]..<SNR>72_display_exception:
line   19:
Error: jedi-vim failed to initialize Python: jedi-vim requires Vim with support for Python 3. (in function jedi#init_python[4]..<SNR>72_init_
python, line 4)

というエラーが出る。 ずっと使ってた VimmacOS の built-in Vim バージョンで、-python3になっていた。

$ vim --version | grep python 
+cmdline_hist      -langmap           +python/dyn        +visual
+cmdline_info      +libcall           -python3           +visualextra

Homebrew には、 +python3コンパイルされた Vim バージョンが入っているので、簡単に Homebrew で新しいバージョンの Vim をインストールする。

$ brew install vim

さっそく+python3 となっていることを確認。

$ /usr/local/bin/vim --version | grep python
+comments          +libcall           -python            +visual
+conceal           +linebreak         +python3           +visualextra

Homebrew でインストールした Vim のパスは/usr/local/bin/vimになっているので、 alias を設定するまたは PATH を変更すれば良い。

alias vim='/usr/local/bin/vim'~/.zshrcに追加した後、source ~/.zshrcを実行して、設定ファイルの内容を反映させる。

PATH を変更する場合は、/usr/local/bin/usr/binより優先されることが必要です。

そして、Vim:echo has('python3')を実行して、1 が返ることを確認。