gologiusの巣

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

【Python】SQLiteでSQLエラーが発生する

SQLiteプレースホルダー関連でエラーが発生するので解決砲を記載。ハマる人はハマるのではと思ったり。

なお、下記のソースはそのままでは動かないので適宜改変してください。

その1 ''で囲んでいる

ソース

sql = "SELECT NAME FROM MEMBERS WHERE NAME='?'"  #<<<<<<これ
name = "testname"

try :
   #SQL実行
   conn = sqlite3.connect("your db path.db")
   cur = conn.cursor()
   cur.execute(sql,(name, ))

   cur.close()
   conn.close()
except:
   print(traceback.format_exc())
   return DBResult.SQL_ERROR

結果

Traceback (most recent call last):
sqlite3.ProgrammingError: Incorrect number of bindings supplied. The current statement uses 0, and there are 1 supplied.

原因

?を''で囲んだせいで、?が文字列として認識されてしまっているようです。 よって、プレースホルダ(?)が0個なのに、値(name)を一個プレスホルダに入れようとしていてエラーになっています。

SQLをかじった人なら、SQL内で文字列比較する際には''囲みをすることを把握しているかと思います。 それにより引き起こされるミスですね。

解決策

下記のようにする

sql = "SELECT NAME FROM MEMBERS WHERE NAME=?"

その2 タプルで渡していない

ソース

sql = "INSERT INTO MEMBERS (NAME, GROUP) VALUES (?, ?)"
values = ["testname", "A"] #<<<<<<これ

try :
   #SQL実行
   conn = sqlite3.connect("your db path.db")
   cur = conn.cursor()
   cur.execute(sql, values) #<<<<<<これ

   cur.close()
   conn.close()
except:
   print(traceback.format_exc())

エラー

sqlite3.ProgrammingError: Incorrect number of bindings supplied. The current statement uses 1, and there are 2 supplied.

原因

execute()の引数はタプル、executemany()の引数はタプルのリストとなります。 実際の内部処理はよくわかりませんが、プレースホルダ(?)が2に対して、引数がリストでまとめて1つ、 と判断されているのだと思います。

上記のエラーでとりあえずググれば出てくる、有名な話ですね。

解決策

タプルに変換すればいいです

values = ("testname", "A") #タプルにしてあげる
values = ("testname",) #要素が一つの場合は、末尾カンマも忘れずに

#executemanyを使用する場合、タプルのリストに変換してあげる
value_list = ["testname1","testname2","testname3"]
tuple_list = []
   for name in value_list:
       tuple_list.append((name,))

その3 executemanyをselectで使用している

ソース

sql = "SELECT NAME FROM MEMBERS WHERE NAME=?"

name_list = ["aaa","bbb","ccc"]
tuple_list = []
   for name in name_list:
       name_list.append((name,)) #引数がlist[tuple]でないと受け付けないため、変換する

try :
   #SQL実行
   conn = sqlite3.connect("your db path.db")
   cur = conn.cursor()
   cur.executemany(sql, tuple_list)

   cur.close()
   conn.close()
except:
   print(traceback.format_exc())

エラー

sqlite3.ProgrammingError: executemany() can only execute DML statements.

原因

executemanyはDML文でしか使用できません。

DML文とは、表の値を操作したり、削除したりと、「表に変更を与えるようなSQL」です。 最後にcommit()が必要な文ですね。

docs.oracle.com

SELECTは読み取るだけなので、表に変化は与えません。 なのでexecutemanyが使用できないみたいです。

解決策

条件文を駆使して、SQLの構文を用いてまとめて取得しましょう。 下記はIN文での例です

name_list = ["aaa","bbb","ccc"]
sql = "SELECT NAME FROM MEMBERS WHERE NAME IN (" + ",".join(name_list) + ")"

try :
   #SQL実行
   conn = sqlite3.connect("your db path.db")
   cur = conn.cursor()
   cur.execute(sql)

   cur.close()
   conn.close()
except:
   print(traceback.format_exc())

もしくはSQLを都度発行する方法もあります。 パフォーマンス的にはあまりよくないのかもしれませんが・・・

name_list = ["aaa","bbb","ccc"]

try :
    sql = "SELECT NAME, GROUP FROM MEMBER WHERE NAME = ?"    
    conn = sqlite3.connect("your db path.db")
    cur = conn.cursor()

    for name in name_list:    
        cur.execute(sql, (name,))
        records = cur.fetchall()
        print(records)

    cur.close()
    conn.close()
except:
    print(sql)
    print(traceback.format_exc())
    return DBResult.SQL_ERROR

まとめ

最近SQL全然使ってないのでハマった。

XAMMPでWEBサーバーを立てる際に、最低限セキュリティを考慮した設定をしたい

XAMMPでWEBサーバーを立てる機会があったので、 セキュリティについて調べました。

※以下のQiita記事を基に、適用されている作業の中身を独自で調べ直したものになります。

Apacheセキュリティ設定 - Qiita

最低限の知識はあるつもりですが(実は情報セキュリティスペシャリスト所持)、 あくまでネット上の情報をまとめただけです。 以下に記載された情報を基にされた作業に関して一切責任を持ちません。

条件

  • Win10 64bit
  • XAMMP 7.2.7 (PHPのバージョンと同じ)

設定ファイル

超雑に調べた

  • httpd.conf (Apacheのhttp(デフォルトポート80)へのアクセスの設定)
  • httpd-ssl.conf (Apachehttps(デフォルトポート443)へのアクセスの設定)
  • httpd-xammp.conf (PHP関連の設定がされている?PHPのバージョンを変更する際にここを編集するみたい)
  • php.ini (PHP自体の設定)
  • config.inc.phpphpMyAdmin(ブラウザからサーバー設定できるサイト)の設定ファイル)

対策

NW設定

特定のIP、セグメント、ポートからしか見る必要がない場合、設定しておいた方が無難。

httpd.confの設定では、ディレクトリ、ファイル単位で、許可/拒否 制御することが可能。

ファイル一覧出力の禁止

閲覧者(攻撃者含む)に対して余計な情報を与えないようにする。 隠してしまうほうが良い f:id:gologius:20190129120340p:plainf:id:gologius:20190129120401p:plain

バージョン情報の隠ぺい

バージョンが分かると、「この攻撃方法はまだ対策されてないな」と攻撃者に情報を与えてしまう。 隠してしまう方がよいと私は思っています(賛否両論あり) f:id:gologius:20190129120325p:plainf:id:gologius:20190129120428p:plain

httpoxy対策

httpoxy は脆弱性の名前

HTTPリスエストのヘッダの一つにPROXYがある。 脆弱性があるサーバーは、リクエストヘッダの情報を、(環境)変数に入れてしまう。

PROXY情報は通常クライアント側からはアクセスできないが、設定できてしまう。 これにより、不正なプロキシサーバーを設定して、そのサーバーに情報を流し続ける、ということができてしまう

クリックジャギング対策

正常なWEBサイトを、iframeから呼び出す。 さらにそのiframeの上に、透明な悪意のあるリンクをかぶせる これにより、正常なWEBサイトのリンクをクリックしたつもりが、遷移先は別の悪意のあるサイトに飛ばされる

外部から呼び出すことを想定していないサイトについては、ブロックする(特定のWEBサイトのみ、iframe呼び出しを許可する)

XSS対策

XSS=Cross Site Scripting クロスサイトスクリプティング

文字列のエスケープをしていないため、悪意のあるHTMLやJavaScriptが埋め込まれる。

文字入力欄に

名前  「田中 太郎」

とかいれる想定なところに

名前  「田中太郎;<script>alert(`不正なプログラムです`)</script>

などと入力する。

DBにはこの文字列がそのまま登録される。

HTMLとして表示される時に、JSが実行されてしまう。

XST対策

XST=Cross Site Tracing クロスサイトトレーシング

XSSではブラウザ(クライアント)からサーバーへのHTTPリクエストヘッダを取得できない(らしい。なぜ?)。 なので、ヘッダ内に含まれる認証情報(クッキーや認証IDPWなど)は取れない。

話は変わるが、HTTPリクエストメソッド(GET,POST,PUT,DELETE)などの一種にTRACEがある。 TRACEはHTTPリスエストをオウム返しするメソッドである。

なので、XSSを利用して、TRACEメソッドでリクエストを送ると、リクエストヘッダを取得できる。 前述したクッキーや認証情報も取得できてしまう。

なお、攻撃方法としては現役引退(化石化)している。

現在ではすべてのブラウザでXST対策がとられ、XST攻撃を行うことはできなくなっています https://blog.tokumaru.org/2013/01/TRACE-method-is-not-so-dangerous-in-fact.html

DOS攻撃対策

おなじみのF5アタックなどが該当。

パケットを送りまくって、サーバーに異常な負荷をかける攻撃。

slowloris対策

slowloris=ツール名。スローロリスと読むらしい

Apacheサーバーに不完全なリクエストヘッダーを送ると、サーバー側が最後のヘッダーを待ち続ける。 この脆弱性を利用して、slowlorisを用いて不完全なヘッダーを送り続け、Apacheのプロセスを消費させる。

https://www.drk7.jp/MT/archives/001527.html

作業内容

httpd.conf 設定

例のQiita記事の内容を参考に、httpd.confへ適用します。

Apacheセキュリティ設定 - Qiita

Windowsなのでディレクトリのパスだけ変更する必要があります。

  • /var/www/htmlC:\xampp\htdocs
  • /var/www/cgi-binC:\xampp\cgi-binになるはず

httpd-xammp.conf 設定

PHP無効化

httpd-xammp.conf内に「この拡張子ならPHPファイルとして処理しろ」と設定している箇所がある。 これをコメントアウトすればよい

<FilesMatch "\.php$">
    SetHandler application/x-httpd-php
</FilesMatch>
<FilesMatch "\.phps$">
    SetHandler application/x-httpd-php-source
</FilesMatch>

コメントアウト後は、何も表示されなくなる(正確には<?php ?>で囲まれた部分が全く処理されなくなる)

CGIスクリプト無効化

httpd.conf内で「この拡張子ならCGIとして認識しろ」と設定している箇所がある これをコメントアウトすればよい

AddHandler cgi-script .cgi .pl .asp

コメントアウト後は下記のようになる(ソースがそのまま表示) f:id:gologius:20190129120447p:plain

まとめと感想

XAMMPは手軽に環境を構築できますが、そのまま使ってセキュリティ的に大丈夫なのかずっと不安だったので、 今回調査しました。

画像ビューワーを作る(.NET C# Windows Form)

晦日なので画像ビューワーを作りましょう(核爆)(錯乱)

完成するとこんな感じになります。 圧縮ファイルの中身を自動展開します。

f:id:gologius:20181231142756g:plain

今回のプロジェクトはこちら。

github.com

※「Windowsフォーム」は技術的に古い、みたいな話がネット上に散見されます。 本当に「Windowsフォームで作成すべきか」は一度検討してみてもよいと思います。

事前準備と画面の説明

開発環境は「Win10、Visual Studio 2017」です。

Visual Studioのインストール方法は割愛します。

プロジェクトを作成する

Visual Studioを開いて、右上の「ファイル」→「新規作成」→「プロジェクト」 でプロジェクト作成します。 f:id:gologius:20181231142929p:plain 今回は「Windows フォーム アプリケーション」を選択します。

フォルダを開くためにNugetからダウンロード

今回やることに必要なパッケージをNuGetからDLします。 今回使うのは下記のパッケージです。

  • Microsoft.WindowsAPICodePack.Core (フォルダを開くダイアログ)
  • Microsoft.WindowsAPICodePack.Shell(フォルダを開くダイアログ)
  • SharpCompress (圧縮ファイルの操作)

  • プロジェクト→NuGetパッケージの管理→参照

  • パッケージソースを「すべて」にして、検索ボックスで上記のパッケージを検索+DL!

f:id:gologius:20181231143112p:plain

f:id:gologius:20181231143045p:plain

(2017になって微妙にUIが変更されててビビった)

迷ったらDL数が多いものを選んでおけばよいのではないでしょうか。

画面の説明

f:id:gologius:20181231144230p:plain

  • 各ウインドウの位置は人によって違うと思います
  • ツールボックスが表示されていない人は上バーの「表示」から表示できます。

ここで覚えておいてほしいのは

ですです。

デザイナー上でコンポーネントをダブルクリックすると、勝手に関数が作成されます。 害はありませんが、コードが汚染されるので私は嫌いです。

フォルダを選択できるようにする

ツールボックスからコンポーネントを追加

ツールボックスからMenuStripを探して、D&Dで追加してください。 追加後、ダブルクリックで項目を追加できます。 ここでは「フォルダを開く」という項目を追加します。 f:id:gologius:20181231144840p:plain

変数名の話

プロパティにて変数名を変更できます。

GUI表示に使用されるのはTextです。 f:id:gologius:20181231144909p:plain

変更しないとコード内に日本語変数が紛れ込みます。 関数名に日本語が混ざります。 動作上問題はないのですが気持ち悪い・・・ f:id:gologius:20181231145001p:plain

イベント追加

MenuStripのイベントClickに対して、関数を設定します。 Clickを選択してダブルクリックで勝手に関数が作成されます。 f:id:gologius:20181231145307p:plain

フォルダを開く コードの追加

private void ****ToolStripMenuItem_Click(object sender, EventArgs e) という関数が自動生成されると思います。

下記のコードのように、クリックしたらフォルダ選択ダイアログが表示されるように実装します。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using Microsoft.WindowsAPICodePack.Dialogs; //これを追加しないと動かない

namespace Viewer
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void DirOpenToolStripMenuItem_Click(object sender, EventArgs e)
        {
            //「フォルダを開く」ダイアログの設定
            var dialog = new CommonOpenFileDialog();
            dialog.IsFolderPicker = true;  // フォルダーを開く設定にする
            dialog.EnsureReadOnly = true;
            dialog.AllowNonFileSystemItems = false;
            dialog.DefaultDirectory = Application.StartupPath;

            //フォルダを開く
            var Result = dialog.ShowDialog();
            if (Result == CommonFileDialogResult.Ok)
            {
              //開いた後の処理
              this.Text = dialog.FileName; //タイトル名変更
              //updateFileList(select_path); //ファイルリスト更新 ※後程使用します
            }
        }
    }
}

※Form1.csがプログラムとして開けない人は、下記の方法で開いてみてください。

Form1.Designer.cs → Form→Form1()をクリック f:id:gologius:20181231150948p:plain

コンパイル

ここまでいろいろやったのでコンパイルして、動くか確認しておきましょう。

コンパイル後、「フォルダを開く」ボタン→「フォルダ選択ダイアログ」が開き、 選択後にフォームのタイトルが「選択したパス」になっていればOKです。

f:id:gologius:20181231145640p:plain

f:id:gologius:20181231145643p:plain

画像ビューワーに仕立て上げる

ガワはできたので、ここから作りこんでいきます。

ここから説明する関数は全てForm1クラスのメンバ関数です。また、適宜イベントを設定する必要があります。

コンポーネント追加

下記の部品を追加します。

  • ListView
  • PictureBox

f:id:gologius:20181231145720p:plain

ListViewはプロパティ「View」を「List」にします(見た目が変わります)。 f:id:gologius:20181231150047p:plain

PictureBoxはプロパティ「SizeMode」を「Zoom」にする(画像の表示方法です) f:id:gologius:20181231150110p:plain

以下のURLに詳しく記載されています。

PictureBoxコントロールに簡単に画像を表示する - .NET Tips (VB.NET,C#...)

画像ビューワーっぽくなってきましたね。

ファイル一覧を表示

ListViewに、選択したフォルダ内のファイル一覧を表示させます。

//ファイルリストを更新する
private void updateFileList(string path)
{
   var fullpaths = System.IO.Directory.GetFiles(path, "*");
   foreach (string filename in fullpaths)
   {
       listView1.Items.Add(filename);
   }
}

※先ほどのDirOpenToolStripMenuItem_Click()//updateFileList(select_path);コメントアウトを外してください

圧縮ファイルを開いて画像を登録する

ListViewの項目をクリックしたら、圧縮ファイル内の「画像ファイル」のみを読み込むようにします。

※読み込みの待ち時間や、IO負荷などはあまり考慮していません。

IArchive archive = null; //圧縮ファイルの実体
List<IArchiveEntry> imgs = null; //画像ファイル群   
int lookPage = 0; //現在閲覧しているページ

//ファイルリストの項目をクリックした時
 private void listView1_Click(object sender, EventArgs e)
 {
     //項目が一つもない場合
     if (listView1.SelectedItems.Count == 0)
     {
         return;
     }

     //選択している最初の行の、最初の列の値を取得
     ListViewItem item = listView1.SelectedItems[0];
     string path = item.SubItems[0].Text;

     bool result = registerImage(path);
     if (result)
     {
         lookPage = 0;
         showImage(lookPage);
     }
 }

 //指定された圧縮ファイル内の画像を表示する
 private bool registerImage(string path)
 {
     Console.WriteLine(path);
     //初期化
     if (archive != null)
     {
         archive.Dispose();
         archive = null;
     }

     //圧縮ファイルから画像ファイルのみ取り出す
     try
     {
         archive = ArchiveFactory.Open(path);
         var entries = archive.Entries.Where(e =>
             e.IsDirectory == false && (
             Path.GetExtension(e.Key).Equals(".jpg") ||
             Path.GetExtension(e.Key).Equals(".jpeg") ||
             Path.GetExtension(e.Key).Equals(".png") ||
             Path.GetExtension(e.Key).Equals(".bmp")));

         imgs = entries.ToList();
     }
     catch (Exception e)
     {
         MessageBox.Show(path + " " + e.ToString(), "ファイル展開エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);

         if (archive != null)
         {
             archive.Dispose();
         }
         archive = null;
         return false;
     }

     //ソート
     imgs.Sort((a, b) => { return a.Key.CompareTo(b.Key); });

     return true;
 }

画像を表示させる+ページめくり機能をつける

いよいよ、画像表示機能を実装します。 といっても、登録している画像を表示させるだけですが・・・

ついでに、矢印キーでページめくりができるようにしておきます。 これは「ListView」に対して「KeyDown」イベントを設定します

//指定ページの画像を表示する
private bool showImage(int index)
{
    if (imgs.Count() == 0)
    {
        return false;
    }

    //圧縮ファイル内のファイル指定
    var entry = imgs[index];
    try
    {
        //ファイルを読み込みビューワーにセット
        pictureBox1.Image = Image.FromStream(entry.OpenEntryStream());
    }
    catch (Exception e)
    {
        MessageBox.Show(e.ToString(), "正常な画像ファイルではありません", MessageBoxButtons.OK, MessageBoxIcon.Error);
        return false;
    }

    return true;
}

//矢印キーでのページめくり
private void listView1_KeyDown(object sender, KeyEventArgs e)
{
    if (archive == null)
    {
        return;
    }

    if (e.KeyCode == Keys.Left)
    {
        lookPage--;
        if (lookPage < 0)
        {
            lookPage = imgs.Count() - 1;
        }
        showImage(lookPage);
    }
    else if (e.KeyCode == Keys.Right)
    {
        lookPage++;
        if (lookPage >= imgs.Count())
        {
            lookPage = 0;
        }
        showImage(lookPage);
    }
}

全てのソースを合体させると、下記のようになります

Viewer/Form1.cs at master · gologius/Viewer · GitHub

まとめ

画像ビューワーを作成しました。

特に SharpCompress の使い方が分からず、最初は苦労しました。

常に圧縮ファイル内の全画像を持つようなコードになっているので、 負荷が気になるところです(といっても私のPCだと全く問題ありませんが・・・)

また、Windowsフォームも技術的には古いらしいので、他の手法も調査もしないとなぁと思いました。

おしまい。

github.com

f:id:gologius:20181231142756g:plain

ランダムな画像を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:の後に問題になっている文字が表示されるようですが、 スペースなので、表示されていないように見えていたわけですね。