2020/12/2 追記
記事中で書いている g-i-s を使って情報を取得できなくなっていました。
なので改修しました。
flex message になったり、結構変わってます。
やばそうな部分は消したので、変な部分もありますがご了承ください。
'use strict';
const HTMLParser = require('fast-html-parser');
const urllib = require('urllib');
const Client = require('@line/bot-sdk').Client;
const client = new Client({
channelAccessToken: process.env['ch_access_token'],
channelSecret: process.env['ch_secret']
});
const num_max_result = 10;
exports.handler = (event, context) => {
// Line の内容受け取り
const event_data = JSON.parse(event.body);
console.log(event_data);
const messageData = event_data.events && event_data.events[0];
const replyToken = messageData.replyToken;
// 頭に検索の文字がある場合のみ実行
const raw_search_word = messageData.message.text;
const search_word = trimStrings(raw_search_word);
switch (true) {
// google 画像検索
case /^検索[\x20\u3000\n]*.*/.test(raw_search_word):
console.log(`検索語 = ${search_word}`);
const base_url2 = 'https://www.google.co.jp/search';
const search_url2 = `${base_url2}?q=${encodeURIComponent(search_word)}&tbm=isch&hl=ja&oe=utf-8&tbs=qdr:m`;
// 画像検索実行
google_search(search_url2, search_word, replyToken);
break;
}
};
// google 検索メイン処理
function google_search(url, search_word, replyToken, flg=0) {
urllib.request(url, function(err, data, res) {
if (err) {
console.log(err);
} else {
var root = HTMLParser.parse(data.toString());
var links = root.querySelectorAll('table.TxbwNb');
var MessageObj2 = [];
links = random_sort(links);
links.forEach((l, idx) => {
if (MessageObj2.length < num_max_result) {
var tmp = l.querySelectorAll('a');
var title2 = tmp[1].querySelector('span.fYyStc').childNodes[0].rawText;
var img = tmp[0].querySelector('img.t0fcAb').rawAttrs.match(/src="(.+)"/)[1];
var link2 = tmp[0].rawAttrs.match(/q=(.+?)&/)[1];
// URL 内のスペースを置換
img = img.replace(/[\x20\u3000]/g, '%20');
link2 = link2.replace(/[\x20\u3000]/g, '%20');
// Line の要件check
if ((/^https.*$/.test(img)) && (encodeURIComponent(img).length < 1000) &&
(/^https.*$/.test(link2)) && (encodeURIComponent(link2).length < 1000)) {
MessageObj2.push(createMessageObj(title2, img, link2));
}
} else {
return true; // foreach では break が使えないのでこうする
}
});
console.log(MessageObj2);
// Line返信
reply_line(search_word, MessageObj2, replyToken, flg);
}
});
}
// search_word から不要文字列を削除
var trimStrings = function(message) {
message = message.replace(/^検索[\x20\u3000\n]*/, '');
switch (message) {
default:
return message;
}
};
// ランダムソート
var random_sort = function(array) {
for(var i = array.length - 1; i > 0; i--){
var r = Math.floor(Math.random() * (i + 1));
var tmp = array[i];
array[i] = array[r];
array[r] = tmp;
}
return array
};
// Line に返すオブジェクト作成
var createMessageObj = function(title, img, link) {
return {
'type': 'bubble',
'size': 'micro',
'hero': {
'type': 'image',
'url': img,
'size': 'full',
'aspectMode': 'cover',
'aspectRatio': '1.51:1'
},
'body': {
'type': 'box',
'layout': 'vertical',
'contents': [
{
'type': 'text',
'text': title,
'wrap': true,
'maxLines': 3,
'size': 'xs',
'gravity': 'center',
'margin': 'md'
}
]
},
'action': {
'type':'uri',
'label':'View details',
'uri': link
},
};
};
// LINE に返信する関数定義
var reply_line = (search_word, MessageObj, replyToken, flg) => {
// 返信するオブジェクト
var SendMessageObject;
if (MessageObj.length == 0) {
SendMessageObject = [
{
type: 'text',
text: '検索結果がゼロ件でした。'
}];
} else {
SendMessageObject = [
{
type: 'text',
text: search_word + ' で検索しました'
},
{
"type": "flex",
"altText": "this is a flex message",
"contents": {
'type': 'carousel',
'contents': MessageObj
}
}
];
}
client
.replyMessage(replyToken, SendMessageObject)
.catch((err) => {
console.log('failed in reply post');
console.log(err);
});
};
はじめに
Google 画像検索をする LINE BOTを作ってみました。
gif を見ればわかると思いますが、こういうことをやってくれます。
- 「検索」の後に続く文字列で Google 画像検索し、結果を5つ画像カルーセルで返す
- 画像をクリックするとその画像元サイトに飛ぶ
完成までのアレコレを書いていきたいと思います。
モチベーション
- APIGateway と lambda 使ってサーバレスな BOT を作ってみたいなぁと思っていた
- 普段 python ばっかりなので、たまには別の言語を使ってみたかった(Node.js にしたのは何となく)
やったこと
1. LINE チャンネル作成(Messaging API)
こちら を参考に作成しました。
プランは Developer Trial にしました。
2. Node.js でコーディング
コーディングに入る前に
自分の Node.js バージョン はこのようになってました。
node --version
v6.11.2
もしも未インストールの場合はこちらから DL してインストール出来ます。(インストール手順は割愛)
https://nodejs.org/en/download/
line_bot_test というフォルダを作り、その中で作業をしていきます。
とりあえず必要なパッケージをインストールしましょう。
mkdir line_bot_test
cd line_bot_test
npm install superagent
npm install g-i-s
今回、 Google 画像検索をするにあたり g-i-s を使いました。
他にも色々画像検索パッケージがあったんですが、自分が見た限りではそれらの殆どが
Google Custom Search Engine を使っており、無課金だと API 上限(100回/day)があるので
今回は採用しませんでした。
しかし、g-i-s も問題があり、検索すると結果が以下のような json で返ってきます。
※「Qiita」で検索した例
{ url: 'https://cdn.qiita.com/assets/qiita-fb-2887e7b4aad86fd8c25cea84846f2236.png',
width: 200,
height: 200 }
自分は 画像をクリックしたら元のサイトに飛ばしたい のですが、今のままではその要件を満たせない…
どうにかなんねぇかなぁ~と思いソースコードを見に行くと node_modules/g-i-s/index.js
にこのような個所が。
59 var result = {
60 url: metadata.ou,
61 width: metadata.ow,
62 height: metadata.oh
63 };
ここで元のリンクも返すようにすれば良さそう。
59 var result = {
60 url: metadata.ou,
61 width: metadata.ow,
62 height: metadata.oh,
63 link: metadata.ru ← 追加
64 };
無事、画像元のリンクを取得できるようになりました!
(他にどういう情報が取得できそうか気になった方は metadata
を確認してみてください)
※「Qiita」で検索した例
{ url: 'https://cdn.qiita.com/assets/qiita-fb-2887e7b4aad86fd8c25cea84846f2236.png',
width: 200,
height: 200,
link: 'https://qiita.com/' }
コーディング
いきなり結論。こんな感じに作りました。
初 Node.js なので、変なところもあるかと思いますがご容赦ください(指摘歓迎)
'use strict';
var gis = require('g-i-s');
var request = require('superagent');
var crypto = require('crypto');
var webclient = require('request');
exports.handler = (event, context) => {
// line の内容受け取り
var event_data = JSON.parse(event.body);
console.log(event_data);
var messageData = event_data.events && event_data.events[0];
// 頭に検索の文字がある場合のみ実行
var flg_check = messageData.message.text;
var search_word = trimStrings(flg_check);
switch (true){
// 画像検索
case /^検索[ \n]*.*/.test(flg_check):
gis(search_word, function(error, results) {
if (error) {
console.log(error);
} else {
console.log(results);
lineclient_img(search_word, results, messageData.replyToken);
}
});
break;
}
};
// search_word から不要文字列を削除
var trimStrings = function(message) {
message = message.replace(/^検索[ \n]*/, '');
switch (message) {
default:
return message;
}
};
// LINE に返信する関数定義
var lineclient_img = (search_word, res, replyToken) => {
const reply_url = process.env['reply_url'];
const ch_secret = process.env['ch_secret'];
const signature = crypto.createHmac('sha256', ch_secret);
const ch_access_token = process.env['ch_access_token'];
// https画像を探す
var urls = [];
var url_count = 0;
var i = 0;
while(i < res.length) {
if (res[i].url.match(/^https:.*(jpg|jpeg|png)$/)) {
urls[url_count] = res[i];
url_count = url_count + 1;
}
if (urls.length == 5) {
break;
}
i = i + 1;
}
console.log('selected imade: ', urls);
// 返信するオブジェクト
var SendMessageObject;
if (urls.length < 5) {
SendMessageObject = [
{
type: 'text',
text: '検索結果が少なすぎます。ワードを変えて検索してみてね!'
}];
} else {
SendMessageObject = [
{
type: 'text',
text: search_word + ' で画像検索しました'
},
{
"type": "template",
"altText": search_word + ' で画像検索しました',
"template": {
"type": "image_carousel",
"columns": [
{
"imageUrl": encodeURI(urls[0].url),
"action": {
"type": "uri",
"label": "Open Link",
"uri": encodeURI(urls[0].link)
}
},
{
"imageUrl": encodeURI(urls[1].url),
"action": {
"type": "uri",
"label": "Open Link",
"uri": encodeURI(urls[1].link)
}
},
{
"imageUrl": encodeURI(urls[2].url),
"action": {
"type": "uri",
"label": "Open Link",
"uri": encodeURI(urls[2].link)
}
},
{
"imageUrl": encodeURI(urls[3].url),
"action": {
"type": "uri",
"label": "Open Link",
"uri": encodeURI(urls[3].link)
}
},
{
"imageUrl": encodeURI(urls[4].url),
"action": {
"type": "uri",
"label": "Open Link",
"uri": encodeURI(urls[4].link)
}
}
]
}
}
];
}
request
.post(reply_url)
.send({ replyToken: replyToken, messages: SendMessageObject })
.set('X-Line-signature', signature)
.set('Content-Type', 'application/json')
.set('Authorization', `Bearer ${ch_access_token}`)
.end(function(err, res){
console.log(res);
if (res.ok) {
console.log('succeed in reply post');
} else {
console.log('failed in reply post');
console.log(err);
// 検索失敗の通知
request
.post(reply_url)
.send({ replyToken: replyToken, messages: [{'type': 'text', 'text': '見つからないので検索ワードを変えてみてね'}] })
.set('X-Line-signature', signature)
.set('Content-Type', 'application/json')
.set('Authorization', `Bearer ${ch_access_token}`)
.end(function(err, res){
console.log(res);
if (res.ok) {
console.log('succeed in error post');
} else {
console.log('failed in error post');
console.log(err);
}
});
}
});
};
少し補足
- LINE 画像カルーセルの仕様に合わせて、 https 画像のサイトを選択して返すようにしています。
https://developers.line.me/ja/reference/messaging-api/#image-carousel
- process.env['reply_url'];
は次回、AWS lambda のところで説明します。
AWS 環境にデプロイ
あとはこれを AWS 環境にデプロイすればOKです。
…が少し長くなって疲れたので続きは別途書きます。
【追記】書きました
https://qiita.com/dasu1982/items/cd31d4a6b755e133c08f