2
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

画像を検索して貼れるDiscord Botを作る

Last updated at Posted at 2020-11-12

画像検索できるbotを作ったのでその時の知見を書こうと思います。

作った物

findimage

このような感じで、与えられたキーワードから画像を検索し、貼り付けてくれます。
導入はこちらからできるので、よかったらよろしくお願いします!使用方法
ソースコード(github)

環境

  • python 3.7.9
  • discord.py 1.5.1
  • bs4 0.0.1
  • psycopg2 2.8.6
  • urllib3 1.25.11

Discord.pyの使い方

こちらの記事を参考に作成しました。Discord.pyを触ったことない方は、先にこちらを読むことをおすすめします。

画像を検索してurlを取得

画像検索結果の取得

urllibを使用して画像を検索し、htmlを取得します。
この際、htmlの内容が変わってしまうので、User-Agentを必ず指定してください。

find_image.py
from urllib import request as req
from urllib import parse

def find_image(keyword):
	urlKeyword = parse.quote(keyword)
	url = 'https://www.google.com/search?hl=jp&q=' + urlKeyword + '&btnG=Google+Search&tbs=0&safe=off&tbm=isch'
	headers = {"User-Agent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:47.0) Gecko/20100101 Firefox/47.0",}
	request = req.Request(url=url, headers=headers)
	page = req.urlopen(request)
	html = page.read()
	page.close()

	return html

画像urlの取得

通常、スクレイピングで画像を取得する場合、imgタグから取得できますが、Google画像検索の場合それだと圧縮された画像しか取得できないです。
なので、オリジナルの画像を取得するためには、Seleniumなどでこちらの記事のように、クリックして取得しなければいけないのですが、負荷や速度を考慮して今回は別の方法を取っています。
宣伝ですが、BeautifulSoupを高速化するコツについて記事を書いたので、よかったらこちらを参考にしてください

User-Agentをブラウザで指定した場合、下のようにsctiptタグにメソッド呼び出しが実装されているのでそれを使用します。

<script nonce>
AF_initDataCallback({
			key: 'ds:1',
			isError: false,
			hash: '2',
			data: [null, [
						[
							["g_1", [
									["生 クリーム", ["https://encrypted-tbn0.gstatic.com/images?q\u003dtbn%3AANd9GcR_QK2ghJ5WWcj-Tcf9znnP6_rZwe7f2MCwWUERoVqVLNRFsj4D\u0026usqp\u003dCAU", null, null, true, [null, 0], false, false], "/search?q\u003d%E3%83%97%E3%83%AA%E3%83%B3\u0026tbm\u003disch\u0026chips\u003dq:%E3%83%97%E3%83%AA%E3%83%B3,g_1:%E7%94%9F+%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%A0:FuBfrMHhliU%3D", null, null, [null, null, null, null, "q:プリン,g_1:生 クリーム:FuBfrMHhliU\u003d"], 0],
									["コンビニ", ["https://encrypted-tbn0.gstatic.com/images?q\u003dtbn%3AANd9GcThveHaG9uvSFj6QwXIVDoJPs9P3KjNdnl-I35Wf0WzAKNffK_m\u0026usqp\u003dCAU", null, null, true, [null, 0], false, false], "/search?q\u003d%E3%83%97%E3%83%AA%E3%83%B3\u0026tbm\u003disch\u0026chips\u003dq:%E3%83%97%E3%83%AA%E3%83%B3,g_1:%E3%82%B3%E3%83%B3%E3%83%93%E3%83%8B:tHwRIJyFAco%3D", null, null, [null, null, null, null, "q:プリン,g_1:コンビニ:tHwRIJyFAco\u003d"], 1],
.......

ただ、これが曲者で、検索した時の場所がキーワードごとによって違うため、xpathやcssセレクターでの取得ができず、属性の指定のもないので絞り込みもできず、しかも、変数を展開した上でのjson形式になっているため、簡単にjsonデータに変換することができないです。
なので、ゴリ押ししました。
まず、scriptタグを全て取得し、AF_initDataCallbackから始まる内容を探してそれを無理やり整形してjsonが読み込める形にします。ただ、構造が辞書型ではなく配列のため、indexを決め打ちして取得しています。
正直、スレクイピングのやり方としては下の下ですが、今回のbotのように速度が求められているため、この実装で妥協しました。

find_image.py
import bs4

def scrap_image_urls(html, start = 0, stop = 1)):
	soup = bs4.BeautifulSoup(html, 'html.parser', from_encoding='utf8')
	soup = soup.find_all('script')
	data = [c for s in soup for c in s.contents if c.startswith('AF_initDataCallback')][1]
	data = data[data.find('data:') + 5:data.find('sideChannel') - 2]
	data = json.loads(data)
	data = data[31][0][12][2]
	image_urls= [x[1][3][0] for x in data if x[1]]
	image_urls= [url for url in image_urls if not is_exception_url(url)][start:stop]

	return image_urls

ただ、インスタなどのスクレイピング対策を行っているサイトは、urlを取得できないため弾いています。

find_image.py
exception_urls = [
	'.cdninstagram.com',
	'www.instagram.com'
]

def is_exception_url(str):
	return any([x in str for x in exception_urls])

Prefixを動的に変化させる

prefix(コマンドの最初につける接頭語)を変換させる機能を実装しました。
いろいろ方法はありますが、今回はHeroku Postgresを使用しています。
最初、jsonファイルで管理していましたが、herokuは毎日リセットが入るのでデータが吹っ飛びました。。。
herokuへのデプロイの流れは最初に紹介した記事に詳しく載っているので、そちらを参照してください。
psqlコマンドの導入が必要になります。

Heroku Postgresアドオンの追加

無料プランのhobby-devでアドオンを追加します。最大レコード行数は10Kです。

$ heroku addons:create heroku-postgresql:hobby-dev -a [APP_NAME]

データベースへのアクセス

まず作成したデータベース名を確認します。
Add-onの行を参照してください

heroku pg:info -a [APP_NAME]

次に、データベースにアクセスします。
先ほど取得したデータベース名を使用してください

heroku pg:psql [DATABASE_NAME] -a [APP_NAME]

これでSQLを実行できますので、テーブルを作成してください。

create table guilds (
id varchar(255) not null,
prefix varchar(255) not null,
PRIMARY KEY (id)
);

PythonからPostgreSQLにアクセスする

今回は、psycopg2を使用しています。
データベースのurlは作成した段階で環境変数に定義されているので、それを使用します。

find_image.py
import psycopg2

db_url = os.environ['DATABASE_URL']
conn = psycopg2.connect(db_url)

Discordサーバーごとに動的にprefixを適用する

discord.Clientはコンストラクタでメッソドを渡すことができるので、インスタンス生成の際に渡します。
例では、discord.Clientの子クラスであるdiscord.ext.commands.Botを使用しています。

find_image.py
from discord.ext import commands
import psycopg2

defalut_prefix = '!'
table_name = 'guilds'

async def get_prefix(bot, message):
	return get_prefix_sql(str(message.guild.id))

def get_prefix_sql(key):
	with conn.cursor() as cur:
		cur.execute(f'SELECT * FROM {table_name} WHERE id=%s', (key, ))
		d = cur.fetchone()
		return d[1] if d else defalut_prefix

bot = commands.Bot(command_prefix=get_prefix)

DiscordサーバーごとにPrefixを設定する

今回の場合set_prefixコマンドを実行すると、UPSERTクエリが走るようにしています。

find_image.py
from discord.ext import commands
import psycopg2

table_name = 'guilds'

def set_prefix_sql(key, prefix):
	with conn.cursor() as cur:
		cur.execute(f'INSERT INTO {table_name} VALUES (%s,%s) ON CONFLICT ON CONSTRAINT guilds_pkey DO UPDATE SET prefix=%s', (key, prefix, prefix))
	conn.commit()

@bot.command()
async def set_prefix(ctx, prefix):
	set_prefix_sql(str(ctx.guild.id), prefix)

	await ctx.send(f'The prefix has been changed from {ctx.prefix} to {prefix}')

最後に

ここまで読んでいただきありがとうございました!
Discord Botの作成やスクレイピングの際に参考になれば幸いです!

2
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?