趣味でローカルなWebサービスを作ってみました。
仕事場などに掛かってくる電話は基本的に決まっているのですが、滅多にかけてこない相手もいます。
そして電話機能が持っている電話帳って数が少ないため、全部登録できません。
かといってネットの電話番号検索は、電話が掛かっている最中の検索としてちょっと遅い。
よってある程度の地域やローカルな番号はあらかじめcsvファイルとして保存しておく方法を取りました。
Python3+Pandas+Flask+JQuery+Ajaxという構成。 データはそれほど多くないのでDataFrameに読み込ませました。
検索する所に番号を入力していくと、段々と候補が絞られていく感じ。 リストになければ、リストをゼロにする感じです。
実は追加で、リストになければ他の迷惑電話検索サービスに検索にいくようにしようかと思っています。
が、それはそのサイトの利用規約によるのでどうしようかなー、という感じです。
また検索した電話がリストになければ登録フォームを表示するようにしようかと思っています。
なぜこれがローカルなのかというと、ユーザー登録管理は考えてないから。
一応ルーターで外からアクセスは出来なくしているし、自分のパソコンのみのアクセスにしている。
ここまで来たらユーザー管理してログイン関係も出来るんだけど、満足している所があるしなぁ。
おかげでFlaskはUIとして使っているだけです。 python部分は全部乗せます。訳わからなくなってくるかもしれない。
ソートとかcsvの読み書きについてはpandasに任せます。
実はpandasにする理由がないと思う。
検索はstr.containsまかせだし。
フォルダ構成はこんな感じ。
-app.py -tel_data.csv +-static +-css -bootstrap.min.css -bootstrap.min.css.map +-js -bootstrap.min.js -jquery-2.1.4.min.js -usrlocal.js +-templates -add.html -index.html -layout.html tel_data.csvの中身 tel,name,desc,flag 011,北海道,国際電話もあり,1 0120987654,リサイクル買い取り,,2 0120123456,宣伝FAX請負業者,悪質な宣伝FAX 2099/09/09 2回目,2 0333334444,,,2 0120999,契約している電気業者系?,,0
ソースに書いてありますが、csvのフラグは0で標準、1で怪しい、2で迷惑電話という一覧にしてます。
このデータはWebでは編集できません、追加だけです。
編集したきゃCSVでやってくれという投げっぱなし。
以下、ソースを一部張っていきます。
from flask import Flask, session, render_template, request, redirect, url_for, jsonify import pandas as pd import os class TellData: """ 電話データの読み書きを行う データは同じ場所にある'tel_data.csv' """ __tel_data = "./tel_data.csv" __ci = ["tel", "name", "desc", "flag"] def __init__(self, fname=__tel_data, keta=3): """ 初期に'./tel_data.csv'のデータをDataFrameに読み込んでいます。 読み込まれたデータは df にDataFrameで格納されています。 """ super(TellData, self).__init__() self.__fname = fname self.reload() self.__keta = keta def reload(self): tmp_df = pd.read_csv(self.__fname, header=0, dtype=str) assert isinstance(tmp_df, pd.DataFrame) tmp_df["tel"] = tmp_df["tel"].str.replace("-", "") self.df = tmp_df.fillna("") # Jquery.parseで"Non"だとエラーとなる def save(self): """ DataFrameに定義されたファイルを、読み込んだファイルに上書き保存する。 sortして保存する。 """ self.df.sort_values(by=self.__ci[0]).to_csv(self.__fname, index=False, encoding="utf-8") def etcsort(self, ans): # 短い順に並べ替え ans.flag = ans.flag.astype(int) ans = ans.sort_values(by="flag", ascending=True)[self.__ci] ret = [] for key, col in ans.iterrows(): ret.append(dict(col)) return ret def topfilter(self, telnum): """ 引数では途中まででも入力されたデータを文字列で読み込みます。 先頭から一致した一覧を戻す ハイフンがあっても、削除しますので入力はどちらでも可能。 戻り値は配列で、中はdictになっている。 dictの中身は["tel","name","desc", "flag"] なければ空の配列が戻る """ # 数値タイプだった場合、文字列変換する telnum = self.__check_type(telnum) if len(telnum) < self.__keta: return [] ans = self.df[self.df["tel"].str.contains(telnum)] ret = self.etcsort(ans) return ret def namefilter(self, nstr): """ 名前のフィルタ """ ans = self.df[self.df["name"].str.contains(nstr)] ret = self.etcsort(ans) return ret def add(self, tel, name, desc, flag=0): """ 入力した電話番号、名前、コメントをDataFrameに入れる その結果はself.saveでcsvファイルに保存される。 telのみNoneならエラー """ if not tel: print("tel number is None, please input number.") return # 数値タイプだった場合、文字列変換する tel = self.__check_type(tel) # 同じデータがあるか確認 ans = self.df[self.df.tel == tel] if len(ans) == 1: print("重複データがあるので、登録しません。編集してください") return y = pd.Series([tel, name, desc, flag], index=self.__ci) self.df = self.df.append(y, ignore_index=True) def __check_type(self, indata): if isinstance(indata, int): indata = str(indata) elif isinstance(indata, str): indata = indata.replace("-", "") return indata app = Flask(__name__) td = None @app.route("/") def index(): return render_template('index.html') @app.route("/reload", methods=['POST']) def reload(): td.reload() return redirect(url_for('index')) @app.route('/post', methods=['POST']) def post(): gdata = request.json # 以下ここで電話番号計算の処理をする gtel = td.topfilter(gdata) result = { "teldata": gtel } return jsonify(result) @app.route('/npost', methods=['POST']) def npost(): gdata = request.json gname = td.namefilter(gdata) result = { "namedata": gname } return jsonify(result) @app.route('/add_data') def add_data(): return render_template('add.html') @app.route('/add', methods=['POST']) def add(): if request.method == 'POST': if 0 <= len(request.form["tel"]) <= 8: session['verror'] = "電話番号だけは入力してください" return render_template('add.html') else: session.pop('veeror', None) td.add(request.form['tel'], request.form['name'], request.form['desc'], request.form['number']) td.save() return render_template('index.html') if __name__ == "__main__": td = TellData(keta=0) app.secret_key = os.urandom(24) app.run(host='0.0.0.0', debug=True)
と、flask関係はこれだけ。
ワンファイルに書いてあり、デバッグモードで動かしています。
あとはpythonで電話関係のクラスを一つ作って中で処理して、表示はJQueryにお任せです。
気が向いたら改造しよう。
ちなみにJavascriptの中身がなかなか濃い。
なんせテーブル部分は全てJavascriptで書いているから。
enterやボタンを押して検索するのは、ローカルでは手間だから入力された時のkeyupのタイミングでテーブル描画してみた。
速度が大事なのでhtmlの追加部分はJQueryのappendではなく、innerHTMLを使ってみた。
デザインは苦手な為、Bootstrapを使う。
$(function() { $('#tel').each(function() { $(this).bind('keyup', cht(this)); function cht(elm) { var v, old = elm.value; return function() { if (old != (v=elm.value)) { old = v; str = $(this).val(); $.ajax({ type: "POST", url: '/post', data: JSON.stringify(str), dataType: 'json', processData : true, contentType : 'application/json', success: function(result) { $('table tbody#ial').empty(); // $('#ial').children().remove(); addlist(result.teldata); }, error: function(xhr, textStatus, errorThrown) { console.log(xhr.responseText); var res = $.parseJSON(xhr.responseText); console.dir(res); } }); } } } }); $('#nid').each(function() { $(this).bind('keyup', cht(this)); function cht(elm) { var v, old = elm.value; return function() { if (old != (v=elm.value)) { old = v; str = $(this).val(); $.ajax({ type: "POST", url: '/npost', data: JSON.stringify(str), dataType: 'json', processData : true, contentType : 'application/json', success: function(result) { $('table tbody#ial').empty(); // $('#ial').children().remove(); addlist(result.namedata); }, error: function(xhr, textStatus, errorThrown) { console.log(xhr.responseText); var res = $.parseJSON(xhr.responseText); console.dir(res); } }); } } } }); function addlist(dd) { var insert = ""; for(var i = 0; i < dd.length; i++) { // tdタグを生成してテキスト追加 var flang = ""; if (dd[i]["flag"] == "2") { insert += "<tr class='danger'>"; flang = "迷惑電話"; } else if (dd[i]["flag"] == "1") { insert += "<tr class='warning'>" flang = "怪しい"; } else { insert += "<tr class='info'>"; flang = "OK"; } insert += "<td>" + dd[i]["tel"] + "</td>"; insert += "<td>" + dd[i]["name"] + "</td>"; insert += "<td>" + dd[i]["desc"] + "</td>"; insert += "<td>" + flang + "</td>"; insert += "</tr>" } // insertを#ial内に追加 $('table tbody#ial')[0].innerHTML = insert; } $('#reload').click(function(){ $.ajax({ type: "POST", url: '/reload', dataType: false, processData : false, contentType : 'application/json', success: function(result) { } }) }) } );
どなたか忘れましたが、ネットにあるサンプルをいじった程度。
Javascripは詳しくないので分かるところだけいじりました。
人のサンプルを見ていると、本体のhtmlにデータを書いて、省略しているとか表示を絞っている感じの物が多い。
私は検索するたびに、thタグ以下をempty()して描画したhtmlを追加という形にした。ローカルじゃないと出来ない贅沢だね!
どちらが正しいのかは不明。たぶんデータ件数によってかわるんだろうなぁ。
index.htmlがどうなっているのか書いてみる。
{% extends "layout.html" %} {% block content %} <!-- Form ================================================== --> <script src="static/js/usrlocal.js"></script> <div class="container-fluid"> <div class="row"> <div class="col-md-1"> </div> <div class="col-md-10"> <div class="page-header"> <h1>電話番号検索 <small id='reload' type="button" class="btn btn-primary">csv読み直し</small></h1> <a href="./add_data">電話番号追加</a> </div> <p></p> <div class="form-group input-group input-group-lg"> <span class="input-group-addon">検索する電話番号</span> <input type="text" id="tel" style="ime-mode: disabled" class="form-control" placeholder="電話番号を入力してください"> <span class="input-group-addon">検索する名前</span> <input type="text" id="nid" style="ime-mode: enable" class="form-control" placeholder="名前を入力してください"> </div> <div class="table-responsive"> <table class="table table-bordered table-hover table-condensed"> <thead class="thead-inverse"> <tr> <th class="col-md-2">電話番号</th> <th class="col-md-2">名前</th> <th class="col-md-3">説明</th> <th class="col-md-3">状態</th> </tr> </thead> <tbody id='ial'> </tbody> </table> </div> </div> <div class="col-md-1"> </div> </div> </div> {% endblock %}