gologiusの巣

プログラミング、モデリングなどのメモです。誰かの役に立てるとうれしいです。

ランダムな画像をDLする 【Python, WIkipedia, Google API】

毎日新しい知識を取り入れたい・・・

ランダムなWikipediaの記事を、画像付きで紹介する、

そんな仕組みを作ってみましょう。

クリスマスだけどな!!!

やること

そんなわけで

  • Wikipediaで適当にワード検索する
  • 検索したワードでGoogle画像検索する
  • 画像をダウンロードする

この三つ、やっていきましょう。

使用したスクリプトは下記URLにおいてます。

適当な画像をDLします · GitHub

1.Wikipediaでランダム記事取得

特定のURLにアクセスすると、ランダムな記事データが取得できます。 取得した記事のタイトルを、画像検索のキーワードとして使用します。

ランダムな記事取得には以下のURLを使用します

https://ja.wikipedia.org/w/api.php?action=query&list=random&rnlimit=1&rnnamespace=0&format=json
パラメータ名 内容
action=query 記事取得
list=random ランダムに記事取得
rnlimit=1 何件取得するか
rnnamespace=0 記事だけ取得する※
format=json JSONで取得するために指定。XMLなどでもOK

※詳しくは以下ページ参照。これを指定しないと誰かのノートやユーザーページも抽出される。邪魔。

https://en.wikipedia.org/wiki/Wikipedia:Namespace

アクセスして取得したJSONはこんな感じ f:id:gologius:20181224162311p:plain

idは記事に振られているIDです。 下記のようなURLでアクセスできます。

https://ja.wikipedia.org/?curid=214545

実際にPythonに落とし込むとこれだけでできます。

import requests

WIKI_RANDOM_URL = 'https://ja.wikipedia.org/w/api.php?action=query&list=random&rnlimit=1&rnnamespace=0&format=json'
response = requests.get(WIKI_RANDOM_URL)
article = response.json()['query']['random']

art_id = article[0]['id']
art_title = article[0]['title']

2.Googleで画像検索

では次に、取得した記事タイトルで画像検索をします。 流れは以下のようになります

  1. (前準備)APIキーの取得
  2. (前準備)カスタム検索の作成(厳密にはsearchidが欲しい)
  3. 画像検索をして、画像URLを取得する
  4. 取得したURLへアクセスし、画像をダウンロードする

前準備

Google画像検索APIを使用するためには前準備が必要になります。 下記URLを参考に取得してください(下記スクリプトAPIKEY,SEARCH_IDに該当する部分です)。

urashita.com

ちなみに私はカスタム画像検索の設定項目「ウェブ全体を検索」を「オン」にしてなかったので、 画像検索でエラーになる凡ミスを起こしました。 また当たり前ですが「画像検索」が「オン」になってないと利用できないので気を付けてください。 f:id:gologius:20181224162342p:plain

画像検索

使用するにはパッケージのインストールが必要になります。

pip install --upgrade google-api-python-client

Google画像検索は以下のようなPythonスクリプトで実行可能です。

※実際の機能としては、service.cse().list()の引数startをインクリメントすることで、 さらに検索結果を取得することができます。 ただし、今回は大量の画像が欲しいわけではないので無視します。

from googleapiclient.discovery import build

APIKEY="your google api key"
SEARCH_ID = "your search id"

# Googleでそのキーワードでイメージ検索する
service = build("customsearch", "v1", developerKey=API_KEY)
responses = []
try:
    response = service.cse().list(q=art_title,cx=SEARCH_ID,num=5,start=1,searchType='image').execute()
    responses.append(response)
except Exception as e:
    print("gmail api error")
    print(e)

#URLのみ抽出
img_urls = []
for r in responses:
    for i in r['items']:
        img_urls.append(i['link'])

3.画像ダウンロード

urllib使用すれば余裕です。 気を付けないといけないのは、「URLにパラメータがついたまま」の場合があることです。 ファイル名をWEB上のファイルと同じものに使用とした場合に問題になります。 適当にパースしてあげましょう。

# 画像をダウンロードする
filepaths = []
for url in img_urls:
    try:
        # ファイル名だけ取り出す
        filename = url.split('/')[-1]
        idx = filename.find("?")
        if idx >= 0:
            filename = filename[:idx]

        filepath = DOWNLOAD_PATH + "\\" + art_title + "_"+  filename
        urllib.request.urlretrieve(url, filepath)        
        filepaths.append(filepath)
        print(filepath)

    except Exception as e:
        print("image download error")
        print(e)

上記のスクリプトだと

  • 画像ファイル以外の場合 → 画像ファイルでないものがDLされてしまう
  • アクセス拒否されている場合(403エラー)→ 例外で落ちる となります。注意してください。

真面目にやるなら

  1. URLにアクセス
  2. ファイルが画像か確認
  3. DLを試みる
  4. 成功したら(1.)に戻る * 指定回数繰り返す

なんでしょうね。

注意点

以下注意点

  • サーバー負荷がかからぬように、節度を持ってアクセスしましょう
  • どんな画像がヒットするかわかりません(エログロ個人写真etc)。私はスクリプトを実行したことによる責任は一切持ちません
  • 上記の話があるのでTwitterBotにするのはおすすめしません

まとめ

スクリプトはこちら(記事先頭と同じURLです)

https://gist.github.com/gologius/dffd310024fdc621ec78644a3362a3d1

ランダムな画像をDLする仕組みを構築してみました。

私は毎日一回、メールでその内容を受信しています(GmailAPIで画像添付する方法はまた後日・・・)

一日の始まりが少しだけ面白くなったような、なってないような・・・

メール監視システムを作る その2(Pythonスクリプト編)

前回サーバー構築と設定をしました。 gologius.hatenadiary.com

今回はメールチェックとメール送信用のスクリプトを書いていきます。

使うスクリプト

GMailAPI を叩くPythonスクリプトです · GitHub

実行環境構築

(1) Google API を有効にしておく(下記URL参照)

スクリプト実行に必要なJSONファイルもDLできるはずです。

ゼロからはじめるPython(22) PythonでGmailのメールを確認しよう | マイナビニュース

(2) 以下のコマンドを実行し、モジュールをインストール

pip3 install google-api-python-client
pip3 install oauth2client
pip3 install httplib2

(3) 認証用関数user_auth()を実行する(1回のみ)

(4) JSONファイルcredentials-gmail.jsonが生成されます

メールの送受信

# -*- coding: utf-8 -*-
import httplib2
import base64
import traceback
from googleapiclient import discovery
from oauth2client import client
from oauth2client import tools
from oauth2client.file import Storage
from email.mime.text import MIMEText
from email.utils import formatdate
 
SCOPES = 'https://mail.google.com/' # Gmail権限のスコープを指定
CLIENT_SECRET_FILE = 'client_id.json' # Google AP管理サイトからダウンロードした権限ファイルのパス
USER_SECRET_FILE = 'credentials-gmail.json' # ユーザーごとの設定ファイルの保存パス
USE_ADDR = "test@gmail.com" # 使用するメールアドレス

#認証用(開発環境を整える際に、初回に一回実行する)
def user_auth():
    store = Storage(USER_SECRET_FILE)
    credentials = store.get()

    # ユーザーが認証済みでない場合 新規認証する
    if not credentials or credentials.invalid:
        flow = client.flow_from_clientsecrets(CLIENT_SECRET_FILE, SCOPES)
        flow.user_agent = 'Python Gmail API'
        credentials = tools.run_flow(flow, store, None)
        print('認証結果を保存しました:' + USER_SECRET_FILE)
 
    return credentials

#API使用準備
def get_service():
     credentials = user_auth()
     http = credentials.authorize(httplib2.Http())
     service = discovery.build('gmail', 'v1', http=http)
     return service

#N件メールを取得
def get_messages(maxnum):
    service = get_service()
    messages = service.users().messages()
    msg_list = messages.list(userId='me', maxResults=maxnum).execute()
    print("メールサーバーからの受信完了")
    
    payloads = []
    # 取得したメッセージの一覧を表示
    for msg in msg_list['messages']:
         topid = msg['id']
         payload = messages.get(userId='me', id=topid).execute()
         payloads.append(payload)    
        
         # メール情報を確認 
         item = mail.mail()
         headers = payload['payload']['headers']
         for header in headers:
             if header['name'] == 'Date':
                 print(header['value']) #受信日?
             if header['name'] == 'From':
                 print(header['value']) #from
             if header['name'] == 'To':
                 print(header['value']) #to
             if header['name'] == 'Cc':
                 print(header['value']) #cc
             if header['name'] == 'Subject':
                 print(header['value']) #件名
        
         print(payload['snippet']) #本文の概要(※全文は入らない)
         
    return

# メールを送信する
def send_message(to, subject, body):
    
    #メール作成
    message = MIMEText(body)
    message["from"] = USE_ADDR
    message["to"] = to
    message["subject"] = subject
    message["Date"] = formatdate(localtime=True)

    byte_msg = message.as_string().encode(encoding="UTF-8")
    byte_msg_b64encoded = base64.urlsafe_b64encode(byte_msg)
    str_msg_b64encoded = byte_msg_b64encoded.decode(encoding="UTF-8")
    rawdata =  {"raw": str_msg_b64encoded}
    
    #送信
    service = get_service()
    try:
        service.users().messages().send(
            userId="test@gmail.com",
            body=rawdata
        ).execute()            
        print("メール送信完了")
    except:
        print("メール送信エラー")
        traceback.print_exc()
        
    return 
    

gmail_mailbox.py

  • get_messages()でメール情報を取得できます
  • send_message()でメール送信できます

受信できれば後は好きに弄れば、メール監視スクリプトの完成です。

main.pyを叩くようなバッチを作成し(start.sh)、crontab -eにてそのバッチを叩くように設定します(前回記事参照)。

# -*- coding: utf-8 -*-
# main.py

import datetime

import gmail_mailbox

def job():
    # メール取得+チェック
    mails = gmail_mailbox.get_messages(40)
    print('メール取得完了')
    
    isError = checkError(mails) #適当にチェック
    
    # 現在時間と一緒に、処理結果を表示
    nowtime = datetime.datetime.now();
    timestr = nowtime.strftime('%Y/%m/%d %H:%M:%S')   
    body = u"<チェック結果>\n"
    body += u"isError " + str(isError) + "\n"    
    
    if (isError) == True:
        print("")
        print("【結果】 [" + timestr + "] 何か検知しています")
        print("")
        
        gmail_mailbox.send_message(u"example@gmail.com", u"システムエラー", body) 
        
    else:
        print("")
        print("【結果】 [" + timestr + "] 何もありませんでした")
        print("")
    
    return

job()
#!/bin/sh
cd  `dirname ${0}`
date
datestr=`date '+%Y%m%d'`

echo begin mail checker
python3 main.py >> log/${datestr}.log 2>&1
echo end mail checker

main.pyでは、メールを40通見て、エラーと判定されるとメールを送信するようにしています。

ポイント

  • from apiclientからfrom googleapiclientにモジュール名が変更されています。 古い記事を参考にしている場合、ここでエラーになる場合があります。
  • payload['snippet'] はメールの概要を見るためのものです。実際に全文見ようとすると、 別の項目エンコードする必要があります
  • 調査中ですが、メールを重複して受信する場合があります
    • 恐らく、独自にラベルを設定していると、受信トレイ分とラベル分、二つのメールを受信してしまうようです

メール監視システムを作る その1(サーバー用意編)

要件としては以下の二つです

  • メールを監視したい。 GMailのルールだと限界感があるので、プログラムでゴニョゴニョやりたい。
  • 特定条件ならエラーメールを送信したい。

メールはGmailのメールアドレス(一つ)が対象なので、 GmailAPIを叩くPythonスクリプトを作成します。

作ったスクリプトはサーバー上で一定時間ごとに動かします。

サーバーを用意する

これを買いました。

https://www.amazon.co.jp/gp/product/B07CSN3CLY/ref=oh_aui_detailpage_o03_s00?ie=UTF8&psc=1www.amazon.co.jp

Win10が入ってました(OSなし版もあるみたいです) が容赦なくUbuntu 18.04.1 LTSを上書きインストールします。 デュアルブートにしてもいいんですけど、Win10はメインPCとノートPCに既にあるので今回はなしで。

なぜOSなし版を買わなかった・・・(3000円くらい安い、みたいな意見をどこかで見ました)

環境構築

Ubuntuのインストール方法や、SSHの構築方法は省略します。 いろんな記事があるので適用に参考にすればいいと思います。

  • GmailAPIを叩くスクリプトPythonで作成(次回紹介)
  • 上記Python環境を叩くためのPython環境(今回紹介
  • 一定時間ごとにバッチを叩く→cronを使用(今回説明

Python環境構築

Ubuntu 18.04.1 LTSPythonが既にインストールされています。 素晴らしいですね。

ただし、Python2系とPython3系が両方入っていますpythonコマンドで実行されるのは、デフォルトは2系です(python --versionで調べられます)

パッケージもPython2系とPython3系で管理が分かれています。 ですので、パッケージインストール時はpip install hogepip3 install hogeか、 どちらのバージョンに入れるのかを、明示的に指定する必要があります。

実行時も同様にpython hoge.pypython3 hoge.pyかどちらで実行するか、 明示的に指定する必要があります。

cron環境+動作設定

cronはスケジュール実行する仕組みです(Windowsでいうタスクスケジューラー) 有名な仕組みなので、色々情報もネット上にありますのでここで詳しく説明はしません。

ただですね、Ubuntu 18.04.1 LTSの情報が全然なく(パス関連が微妙に違う)、 困りました。

cronログ出力設定

デフォルト設定ではcronの実行ログが出力されず、cronが動いているか分からないので、 ログを出力するようにします。

/etc/rsyslog.d/50-default.conf

下記がコメントアウトされているのでコメントイン

cron.*    /var/log/cron.log

スケジュール設定

crontab -e

cron設定用のエディタが開くので、以下のような感じで記述 文法自体は他のサイトを参照してください。

crontabの書き方 | server-memo.net

#10分ごとにメールチェックをするバッチを叩きにいく。絶対パスにすること
*/10 * * * * /home/username/python/mail_checker/start.sh

start.shの中身はこんな感じ。実行日ごとにログファイルを作成し、追記しています。

#!/bin/sh
cd  `dirname ${0}`
date
datestr=`date '+%Y%m%d'`

echo begin mail checker
python3 main.py >> log/${datestr}.log 2>&1
echo end mail checker

GMailAPI を叩くPythonスクリプトです · GitHub

設定後、cronをreloadrestartすると、設定スケジュールどおりに実行される。 ※どちらが良いかは不明

/etc/init.d/cron restart
もしくは
/etc/init.d/cron reload

実行されているかは

/etc/init.d/cron status

で確認可能です。

まとめ

サーバー上で一定時間ごとにPythonスクリプトを起動させる準備が整いました。 次回はPythonでGmailAPIを叩き、受信ボックス内のメールを見ていきます

【Python】datetime.strptime で エラー 「unconverted data remains:」 が発生した場合

背景

Gmailの取得をPythonで開発していました。

取得できる受信日は以下のようなフォーマットで取得できます

Fri, 9 Nov 2018 20:37:10 

String型なのでDate型で色々したいわけですよ。

Pythonでは以下のような方法で「文字列→Date型(厳密には違いますが)」ができます。

lastTimeStr = 'Fri 9 Nov 2018 20:37:10 '
lastitime = datetime.datetime.strptime(lastTimeStr, "%a %d %b %Y %H:%M:%S")

※dateutilなど別の方法でもできます。

問題

以下のエラーが発生しました

ValueError: unconverted data remains:  

なぜでしょうか

原因と解決方法

  • 末尾にスペースが入っていたから
  • カンマが入っていたから

なので以下のように文字を消してあげると正常に動きます。

lastTimeStr = 'Fri, 9 Nov 2018 20:37:10 '
lastTimeStr = lastTimeStr.replace(",", "") #カンマ削除
if lastTimeStr[-1] == " ":
    lastTimeStr = lastTimeStr[:-1] #末尾にスペースが含まれていれば無視する

lastitime = datetime.datetime.strptime(lastTimeStr, "%a %d %b %Y %H:%M:%S")

ちなみに上記のエラー、remains:の後に問題になっている文字が表示されるようですが、 スペースなので、表示されていないように見えていたわけですね。

【JavaScript】JSON.parse() がエラーになる

事象

下記のようなエラーが発生する

JSON.parse: unexpected character at line 1 column 1 of the JSON data

文字列(JSON形式)をJSON.parse()でJSのオブジェクト型に変換する際に、 JSON構文エラーになっている模様。 構文チェッカーでチェックしても普通に問題はない。

JSON Pretty Linter - JSONの整形と構文チェック

環境

  • サーバー側(PHP)とフロントエンド側(JavaScriptJQuery)でAjax的なことをする
  • サーバー側にPOSTでアクセスすると、結果がJSONで返却される

以下JavaScript側サンプルコード

//サーバー上のファイルを削除する
function cancel(){
    
    if(!confirm('サーバー上のファイルを削除します。よろしいですか?')){
        return false;
    }

    //サーバーに受け渡し
    $.ajax({
        type: 'POST',
        url: 'php/cancel.php' <<<<< ★
    }).done((result)=>{
        showResultMsg(result);  
    });
}

function showResultMsg(json){
    
    console.log(json);
    var result = JSON.parse(json); // <<<<<<こいつがエラーを吐く
        
    if (result.result != 'SUCCESS'){
        //エラー時
        $('#result').attr('class','warning');                    
        console.log(result);
    }
    else {
        //成功時
        $('#result').attr('class','');                   
    }
    $('#result').text(result); 
}

原因

サーバー側のcancel.phpが原因。 他ファイルは全てUTF-8で統一されていたが、こいつだけUTF-8-BOMになっていた。 なので、'echo`して返却したJSONがおかしいことに なっていたっぽい

BOMとは

uxmilk.jp

感想

クソハマったのでつらかった。

【コマンドプロンプト(cmd)】AAを表示する

以下のようなスクリプトを作りましたが、その作り方

コマンドプロンプト(cmd.exe)で大きく「本番」と「検証」の文字を表示します · GitHub

f:id:gologius:20180616094423p:plain

画像変換ツールをDL

Unix系だとFIGlet というツールがapt-getで使えるようです。 Win版も配布されていましたが、Win10では使用できませんでした。

なので以下のツールを使用しました 文字絵エディターの詳細情報 : Vector ソフトを探す!

使い方は簡単なので割愛

編集

正直あとは適当に編集してやればできるのですが、私の方法を記載しておきます。

  • notepad++を使用します(そこそこのテキストエディタならなんでもよいですが)
  • Alt+Shitで行、列ごとの選択ができます
  • 先頭行にechoを追加します

まとめ

文字数少ないね ( ^ω^)・・・

【Python】 loggerのログが重複する

ログが再起動のたびに増えていくバグと、運命の出会いを果たしたのでメモします。

Pythonのログの取り方をお勉強しました

以下の記事で、ログの取り方について学びました。

ログ出力のための print と import logging はやめてほしい

超絶的に雑な解釈ですが、以下のようなものだと理解しました

  • print → エラーなのか警告なのかログなのか分からない。ログとる際に使うべきでない
  • logging → グローバル変数的な立ち位置。
  • logger → ローカル変数的な立ち位置。

※詳しくは元記事を参考にしてください・・・。

お、同じログがいっぱいでるぞ~

以下の記事で解決

uyamazak.hatenablog.com

再実行するたびにログが増えていく

上記の記事のようなログ用モジュールを作成して、実行する。

超具体的には、Anacondaで環境構築して、SpyderをIDEとして利用しており、 実行時にF5を使用すると、実行のたびにどんどん増える

# 一回目
[log info] ログテスト
# 二回目
[log info] ログテスト
[log info] ログテスト
#三回目
[log info] ログテスト
[log info] ログテスト
[log info] ログテスト

f:id:gologius:20180606214449p:plain

解決策

終了時に、killLoggers()のようにハンドラを全削除してあげます

from logging import getLogger, Formatter, FileHandler,StreamHandler, DEBUG, shutdown
from logging.handlers import RotatingFileHandler

loggers = {}

def getModuleLogger(moduleName):
    if moduleName is None:
        moduleName = __name__

    if loggers.get(moduleName):
        return loggers.get(moduleName)

    formatter = Formatter('[%(asctime)s | '
                          '%(name)s | '
                          '%(levelname)s] '
                          '%(message)s')

    streamHandler = StreamHandler()
    streamHandler.setFormatter(formatter)
    streamHandler.setLevel(DEBUG)

    fileHandler = RotatingFileHandler("download.log", maxBytes=5000, backupCount=3)    
    fileHandler.setFormatter(formatter)
    fileHandler.setLevel(DEBUG)

    logger = getLogger(moduleName)
    logger.setLevel(DEBUG)
    logger.addHandler(streamHandler)
    logger.addHandler(fileHandler)

    logger.propagate = False
    loggers[moduleName] = logger

    return logger

def killLoggers():

    for l in loggers:
        logger = loggers.get(l)
        for h in logger.handlers:
            logger.removeHandler(h)

    shutdown()

    return

これで重複出力はなくなります。

以上。