Wikipediaから検索するLINE botを作った

flask, line-bot-sdkwikipediaで作って,herokuにデプロイします。

ぜひ使って下さい。


完成図

screenshot

レポジトリはこちら


準備

pip install -r requirements.txtさせたいので,上記3つをrequirements.txtへ記述し,herokuの設定とLINE@の設定を終わらせておく (アクセストークンとチャンネルシークレットあたりを入手しておく)。runtime.txtprocfileを置く。基本HerokuでLINE BOT(python)を動かしてみたを真似た。


Wikipedia について

みんなだいすきWikipedia。

1
pip instal wikipedia

で入るライブラリであるWikipediaは非常に便利で,Python用のMedia Wiki APIのラッパーだと解釈している。requestsでAPI叩いて,BeautifulSoup4か何かでマークアップをバラして,返してくれるものだったはず。

1
2
3
import wikipedia

wikipedia.set_lang("ja")

で日本語Wikipediaに設定した後,

1
wikipedia.search("文字列")

をすることで各ページ名のリストが返り,書くWikipediaのページはその名前(タイトル)がIDとなっており,

1
wikipedia.page("ページ名")

wikipedia.WikipediaPageオブジェクトを取得できる。このpageオブジェクトがcategories, links, content, summaryなどのattributesをもっており,これらは基本的にURLか文字列かのリストである。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
>>> help(wikipedia.WikipediaPage)
>>>
"""
categories
List of categories of a page.
content
Plain text content of the page, excluding images, tables, and other data.
coordinates
Tuple of Decimals in the form of (lat, lon) or None
images
List of URLs of images on the page.
links
List of titles of Wikipedia page links on a page.
Only includes articles from namespace 0, meaning no Category, User talk, or other meta-Wikipedia pages.
parent_id
Revision ID of the parent version of the current revision of this
page. See ``revision_id`` for more information.
references
List of URLs of external links on a page.
May include external links within page that aren't technically cited anywhere.
revision_id
Revision ID of the page.
The revision ID is a number that uniquely identifies the current
"""

今回つくるLINE Botでは,検索単語に対して取得した候補の中から1ページ選び,そのページのsummary (タイトル直後の概要・OGPとかに表示される?)とページへのリンクをLINEトークルームにかえしてあげよう,というものを作ることにした。


ファイルとか

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.
├── Procfile
├── README.md
├── __pycache__
│   ├── app.cpython-36.pyc
│   └── parser.cpython-36.pyc
├── app.py
├── assets
│   └── img
│   └── linebot-icon.png
├── messenger.py
├── parser.py
├── requirements.txt
├── runtime.txt
└── test.py

app.py

次に,flaskベースでアプリケーション部分をササっと書きますが,これもほぼコピペ。面倒な部分をlinebotが隠してくれていて,非常に便利。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
from flask import Flask, request, abort

from linebot import (
LineBotApi, WebhookHandler
)
from linebot.exceptions import (
InvalidSignatureError
)
from linebot.models import (
MessageEvent, TextMessage, TextSendMessage,
)

import parser
import os

app = Flask(__name__)

YOUR_CHANNEL_ACCESS_TOKEN = os.environ.get("YOUR_CHANNEL_ACCESS_TOKEN")
YOUR_CHANNEL_SECRET = os.environ.get("YOUR_CHANNEL_SECRET")

line_bot_api = LineBotApi(YOUR_CHANNEL_ACCESS_TOKEN)
handler = WebhookHandler(YOUR_CHANNEL_SECRET)


@app.route("/callback", methods=['POST'])
def callback():
# get X-Line-Signature header value
signature = request.headers['X-Line-Signature']

# get request body as text
body = request.get_data(as_text=True)
app.logger.info("Request body: " + body)

# handle webhook body
try:
handler.handle(body, signature)
except InvalidSignatureError:
print("Invalid signature. Please check your channel access token/channel secret.")
abort(400)

return 'OK'


@handler.add(MessageEvent, message=TextMessage)
def handle_message(event):
line_bot_api.reply_message(
event.reply_token,
TextSendMessage(text=parser.answer(event.message.text)))


if __name__ == "__main__":
port = int(os.getenv("PORT", 5000))
app.run(host="0.0.0.0", port=port)

line-bot-sdk-pythonの公式リポジトリに,app.pyとしてflaskでのサンプルが公開されている。
[sample] app.py

また,今回はそもそもREADMEにガッツリ載っていたものを使った。sample code on GitHub


parser.py

parserはモジュールの変数として言語設定を持っている。設計として微妙ですかね?モジュール(グローバル)変数は。
なんか使い方間違えたりとか,ヘルプ出したい時のためのusage()は未実装。
意外とWikipediaPage.summaryの文字数が長く,Messaging APIの上限を叩いてしまったときのために,1500文字以上は切っている。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
import wikipedia


# init language setting
lang = "ja"
wikipedia.set_lang(lang)

def init() -> None:
global lang
wikipedia.set_lang(lang)


def tokenize(text: str) -> list:
"""Tokenize input Sentence to list of word"""
splited = text.split()
if len(splited) == 1:
return splited
elif len(splited) == 2:
if splited[0] in wikipedia.languages.fn().keys():
change_lang(splited[0])
return splited[1]
else:
usage()

def search(text: str, rank=0) -> "wikipedia.wikipedia.WikipediaPage":
"""Search Wikipedia page by Word
arg
---
rank : int : Return the contents of the search result of the set rank.
"""
try:
page = wikipedia.page(wikipedia.search(text)[rank])
except wikipedia.exceptions.DisambiguationError:
page = wikipedia.page(wikipedia.search(text)[rank+1])
return page


def encode(page: "wikipedia.wikipedia.WikipediaPage", threshold=1500) -> str:
"""Transform data into the text for LINE message
"""
summary = page.summary
if len(summary) > threshold:
summary = summary[:threshold] + "..."

return f"Result: {page.title}\n\n{summary}\n\n{page.url}"


def answer(text: str) -> str:
init()
word = tokenize(text)
page = search(word)
return encode(page)


def change_lang(language: str) -> None:
wikipedia.set_lang(language)
return

def usage():
pass

if __name__ == "__main__":
import argparse

parser = argparse.ArgumentParser()
parser.parse_args()

一応ちゃんとPEP8スタイルだし,type hintsもdocstring書いている。クセにしとおきたい。


デプロイ等

1
2
3
4
5
$ heroku login
$ heroku create heroku-line-bot
$ heroku config:set LINE_CHANNEL_SECRET="<Channel Secret>"
$ heroku config:set LINE_CHANNEL_ACCESS_TOKEN="<アクセストークン>"
$ git push heroku master

references