Symfoware

Symfowareについての考察blog

PythonでUPnPによるポートマップ(なぜなに Torrent)

なぜなに Torrent ( P2P プログラム入門 )

こちらを参考に、PythonでTorrentクライアントを作ってみます。
最終目標は、Debianのインストーラーをダウンロードしてみること。

最小の CD を使って、ネットワークインストールする


前回までで、Torrentファイルの内容を読み取ることができました。
PythonでBencodingのデコード(なぜなに Torrent)

今回は、UPnPによるポートマップを試してみます。
UPnPによるポートマップ



UPnPによるポートマップ




UDP Multicast を利用して、使用中のルーターに、ポートマッピングを依頼するためのアドレスを教えてもらう。
TCP を使って、教えてもらったアドレスからポートマッピングの依頼をだす。



ルーターにこんな機能があったとは。知りませんでした。

Pythonでの実装はこちらを参考にしています。
DLNAってなんじゃらほい? - SSDPを喋ってみる -
UPnPを使ってルータの外向けIPアドレスを取得する




依頼先を調べる



UDPマルチキャストでUPnPに対応しているルーターの情報を調べます。

送信するデータ


M-SEARCH * HTTP/1.1
MX: 3
HOST: 239.255.255.250:1900
MAN: "ssdp:discover"
ST: urn:schemas-upnp-org:service:WANIPConnection:1





Pythonでの実装はこうなりました。


  1. # -*- coding:utf-8 -*-
  2. import socket
  3. host = '239.255.255.250'
  4. port = 1900
  5. messages = [
  6.     'M-SEARCH * HTTP/1.1',
  7.     'MX: 3',
  8.     'HOST: 239.255.255.250:1900',
  9.     'MAN: "ssdp:discover"',
  10.     'ST: urn:schemas-upnp-org:service:WANIPConnection:1',
  11. ]
  12. message = '\r\n'.join(messages)
  13. message += '\r\n\r\n' # 末尾に改行コードを2つ付与
  14. sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
  15. sock.sendto(message, (host, port))
  16. res = sock.recv(4096)
  17. print(res)
  18. sock.close()




実行してみるとこんな応答があります。


$ python sample.py
HTTP/1.1 200 OK
CACHE-CONTROL: max-age=1800
DATE: Thu, 11 May 2017 13:08:30 GMT
EXT:
LOCATION: http://192.168.1.1:49152/gatedesc.xml
SERVER: Linux/3.2.26, UPnP/1.0, Intel SDK for UPnP devices/1.3.1
ST: urn:schemas-upnp-org:service:WANIPConnection:1
USN: uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx::urn:schemas-upnp-org:service:WANIPConnection:1





重要なのは「LOCATION」の情報です。
このURLをブラウザで開くと、xmlで情報が表示されます。


「serviceType」が「urn:schemas-upnp-org:service:WANIPConnection:1」となっているタグの、
「controlURL」が重要です。

このURLに対して、SOAP通信で要求を行っていくことになります。

751_01.png



UDPマルチキャストでUPnPに対応しているルーターの情報を取得。
応答の「LOCATION」からxmlを取得・解析し、controlURLを取得するまでのプログラムはこうなりました。


  1. # -*- coding:utf-8 -*-
  2. import socket
  3. import urllib
  4. import urlparse
  5. import xml.etree.ElementTree as ET
  6. # --- ルーターの情報を取得
  7. host = '239.255.255.250'
  8. port = 1900
  9. messages = [
  10.     'M-SEARCH * HTTP/1.1',
  11.     'MX: 3',
  12.     'HOST: 239.255.255.250:1900',
  13.     'MAN: "ssdp:discover"',
  14.     'ST: urn:schemas-upnp-org:service:WANIPConnection:1',
  15. ]
  16. message = '\r\n'.join(messages)
  17. message += '\r\n\r\n' # 末尾に改行コードを2つ付与
  18. sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
  19. sock.sendto(message, (host, port))
  20. res = sock.recv(4096)
  21. sock.close()
  22. for line in res.split('\n'):
  23.     info = line.strip()
  24.     if not info.startswith('LOCATION:'):
  25.         continue
  26.     
  27.     locaction = info[len('LOCATION:'):].strip()
  28.     break
  29.     
  30. print 'locaction:', locaction
  31. # --- XMLの解析
  32. f = urllib.urlopen(locaction)
  33. xml_string = f.read()
  34. f.close()
  35. xml = ET.fromstring(xml_string)
  36. ns = {
  37.     'ns' : 'urn:schemas-upnp-org:device-1-0'
  38. }
  39. # controlURLを取得する
  40. for child in xml.findall(".//ns:service", ns):
  41.     if child.find('ns:serviceType', ns).text == 'urn:schemas-upnp-org:service:WANIPConnection:1':
  42.         controlURL = child.find('ns:controlURL', ns).text
  43.         break
  44.     
  45. print controlURL
  46. print urlparse.urljoin(locaction, controlURL)





実行結果


$ python sample.py
locaction: http://192.168.1.1:49152/gatedesc.xml
/upnp/control/WANIPConn1
http://192.168.1.1:49152/upnp/control/WANIPConn1




試行錯誤しながらなので不格好ですが、希望通りSOAPアクションを送信する
http://192.168.1.1:49152/upnp/control/WANIPConn1
というURLが取得できました。





外部の端末から見えているアドレスを聞く



SOAP通信、何かライブラリを使用しても良かったのですが、urllibだけでも行けそうだったので、
素のpythonだけで実装してみます。

Can I use urllib to submit a SOAP request?
4.2. Python XML HTTP Post to send a SOAP message to a JWSDP or to a .NET web service.


  1. # -*- coding:utf-8 -*-
  2. import socket
  3. import urllib2
  4. import urlparse
  5. import xml.etree.ElementTree as ET
  6. # --- ルーターの情報を取得
  7. host = '239.255.255.250'
  8. port = 1900
  9. messages = [
  10.     'M-SEARCH * HTTP/1.1',
  11.     'MX: 3',
  12.     'HOST: 239.255.255.250:1900',
  13.     'MAN: "ssdp:discover"',
  14.     'ST: urn:schemas-upnp-org:service:WANIPConnection:1',
  15. ]
  16. message = '\r\n'.join(messages)
  17. message += '\r\n\r\n' # 末尾に改行コードを2つ付与
  18. sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
  19. sock.sendto(message, (host, port))
  20. res = sock.recv(4096)
  21. sock.close()
  22. for line in res.split('\n'):
  23.     info = line.strip()
  24.     if not info.startswith('LOCATION:'):
  25.         continue
  26.     
  27.     locaction = info[len('LOCATION:'):].strip()
  28.     break
  29.     
  30. print 'locaction:', locaction
  31. # --- XMLの解析
  32. f = urllib2.urlopen(locaction)
  33. xml_string = f.read()
  34. f.close()
  35. xml = ET.fromstring(xml_string)
  36. ns = {
  37.     'ns' : 'urn:schemas-upnp-org:device-1-0'
  38. }
  39. # controlURLを取得する
  40. for child in xml.findall(".//ns:service", ns):
  41.     if child.find('ns:serviceType', ns).text == 'urn:schemas-upnp-org:service:WANIPConnection:1':
  42.         controlURL = child.find('ns:controlURL', ns).text
  43.         break
  44.     
  45. print controlURL
  46. get_ip_soap = """<?xml version="1.0"?>
  47. <s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"
  48.      s:encodingStyle="http://schemas.xmlsoap.org/encoding/">
  49. <s:Body>
  50.     <m:GetExternalIPAddress xmlns:m="urn:schemas-upnp-org:service:WANIPConnection:1">
  51.     </m:GetExternalIPAddress>
  52. </s:Body>
  53. </s:Envelope>"""
  54. soap_url = urlparse.urljoin(locaction, controlURL)
  55. req = urllib2.Request(soap_url, data=get_ip_soap)
  56. req.add_header("Content-type", "text/xml; charset=\"UTF-8\"")
  57. req.add_header("SOAPAction", '"urn:schemas-upnp-org:service:WANIPConnection:1#GetExternalIPAddress"')
  58. response = urllib2.urlopen(req)
  59. ret = response.read()
  60. print 'Response:' + '-' * 10
  61. print ret




実行結果


  1. $ python sample.py
  2. locaction: http://192.168.1.1:49152/gatedesc.xml
  3. /upnp/control/WANIPConn1
  4. Response:----------
  5. <s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><s:Body>
  6. <u:GetExternalIPAddressResponse xmlns:u="urn:schemas-upnp-org:service:WANIPConnection:1">
  7. <NewExternalIPAddress>93.184.216.34/NewExternalIPAddress>
  8. </u:GetExternalIPAddressResponse>
  9. </s:Body> </s:Envelope>




ちゃんと外向きのIPアドレスが取得できました。





【参考URL】

DLNAってなんじゃらほい? - SSDPを喋ってみる -
UPnPを使ってルータの外向けIPアドレスを取得する
Can I use urllib to submit a SOAP request?
4.2. Python XML HTTP Post to send a SOAP message to a JWSDP or to a .NET web service.
UPnPのNAT越えについて調べてみた

テーマ:プログラミング - ジャンル:コンピュータ

  1. 2017/05/11(木) 23:37:29|
  2. Python
  3. | トラックバック:0
  4. | コメント:0
  5. | 編集

PythonでBencodingのデコード(なぜなに Torrent)

なぜなに Torrent ( P2P プログラム入門 )

こちらを参考に、PythonでTorrentクライアントを作ってみます。
最終目標は、Debianのインストーラーをダウンロードしてみること。

最小の CD を使って、ネットワークインストールする



Torrentファイルを読み込む



https://nazenani-torrent.firefirestyle.net/torrentfile/Torrentfile.html

Torrentファイルは、bencodeという形式で記載されているそうです。
データ型は4つ。


・文字列(String)

[文字の長さ(バイト数)]:[文字]

という形式で表現。

「Hello」は、「5:Hello」
「こんにちは」は、「10:こんにちは」
となるようです。



・整数(Number)

i[数字]e

とい形式で表現。

「123」は「i123e」
「1024」は「i1024e」
となるようです。



・リスト(List)

順序を持った複数のデータを保持します。


l[要素1][要素2]...[要素n]e

という形式で表現。


Pythonでの配列
['Hello', 'こんにちは']、これは「l5:Hello10:こんにちはe」
[123, 1024]、これは「li123ei1024ee」
となるようです。



・辞書(Dict)

キーワードと値を関連付けてデータを保持します。

d[キー要素1][値要素1][キー要素2][値要素2]...[キー要素n][値要素n]e

という形式で表現。

キーが「Hello」、値が「こんにちは」の場合「d5:Hello10:こんにちはe」
となるようです。





torrentファイルの内容




実際のtorrentファイルの中身を確認してみます。

最小の CD を使って、ネットワークインストールする
こちらから「debian-8.7.1-amd64-netinst.iso.torrent」をダウンロード。

バイナリエディタで内容を確認してみます。

750_01.png

「d」から始まるので辞書型で、次は数値なので、コロン以降に文字列が続いて...
確かに読み取れますね。




パーサー



適当にパーサーを書いて、ファイルの中身を解析してみます。


  1. # -*- coding:utf-8 -*-
  2. class MyTorrent(object):
  3.     def parse_file(self, torrent_file):
  4.         
  5.         with open(torrent_file, 'rb') as f:
  6.             result = self.parse(f.read())
  7.             
  8.         return result
  9.     
  10.     
  11.     def parse(self, bencode_bytes):
  12.         
  13.         result = []
  14.         while(bencode_bytes):
  15.         
  16.             # 数値
  17.             if bencode_bytes[0] == 'i':
  18.                 v,bencode_bytes = self.parse_number(bencode_bytes)
  19.                 
  20.             # リスト
  21.             elif bencode_bytes[0] == 'l':
  22.                 v,bencode_bytes = self.parse_list(bencode_bytes)
  23.                 
  24.             # 辞書
  25.             elif bencode_bytes[0] == 'd':
  26.                 v,bencode_bytes = self.parse_dict(bencode_bytes)
  27.             
  28.             # 解析途中で打ち切り
  29.             elif bencode_bytes[0] == 'e':
  30.                 return (result, bencode_bytes[1:])
  31.             
  32.             # 文字列
  33.             else:
  34.                 v,bencode_bytes = self.parse_string(bencode_bytes)
  35.             
  36.             result.append(v)
  37.             
  38.         return result
  39.     
  40.     
  41.     def parse_number(self, bencode_bytes):
  42.         """ 数値部分を解析し、解析結果と未解析部分を返す """
  43.         index = bencode_bytes.find('e')
  44.         value = int(bencode_bytes[1:index])
  45.         return (value, bencode_bytes[index + 1:])
  46.         
  47.     
  48.     def parse_string(self, bencode_bytes):
  49.         """ 文字列部分を解析し、解析結果と未解析部分を返す """
  50.         index = bencode_bytes.find(':')
  51.         last_index = index + int(bencode_bytes[:index]) + 1
  52.         value = bencode_bytes[index + 1:last_index]
  53.         return (value, bencode_bytes[last_index:])
  54.         
  55.     def parse_list(self, bencode_bytes):
  56.         """ リスト部分を解析し、解析結果と未解析部分を返す """
  57.         return self.parse(bencode_bytes[1:])
  58.         
  59.     def parse_dict(self, bencode_bytes):
  60.         """ 辞書部分を解析し、解析結果と未解析部分を返す """
  61.         list_value, bencode_bytes = self.parse(bencode_bytes[1:])
  62.         value = {}
  63.         for i in range(0, len(list_value), 2):
  64.             value[list_value[i]] = list_value[i + 1]
  65.         
  66.         return value, bencode_bytes
  67. if __name__ == '__main__':
  68.     t = MyTorrent()
  69.     result = t.parse_file('debian-8.7.1-amd64-netinst.iso.torrent')
  70.     info = result[0]
  71.     
  72.     print('announce', info['announce'])
  73.     print('creation date', info['creation date'])
  74.     print('comment', info['comment'])
  75.     print('httpseeds', info['httpseeds'])
  76.     print('info.length', info['info']['length'])
  77.     print('info.piece length', info['info']['piece length'])
  78.     print('info.name', info['info']['name'])
  79.     print('info.pieces', len(info['info']['pieces']))





実行結果


$ python sample.py
('announce', 'http://bttracker.debian.org:6969/announce')
('creation date', 1484567799)
('comment', '"Debian CD from cdimage.debian.org"')
('httpseeds', ['http://cdimage.debian.org/cdimage/release/8.7.1/iso-cd/debian-8.7.1-amd64-netinst.iso', 'http://cdimage.debian.org/cdimage/archive/8.7.1/iso-cd/debian-8.7.1-amd64-netinst.iso'])
('info.length', 260046848)
('info.piece length', 262144)
('info.name', 'debian-8.7.1-amd64-netinst.iso')
('info.pieces', 19840)





・announce:Tracker サーバーのアドレス

http://bttracker.debian.org:6969/announce
ここに問い合わせれば、データを配信してくれる端末を紹介してくれるようです。


・creation date:ファイルの作成日


・comment:コメント



・info.length:ダウンロード後のサイズ

260046848 byteなので、260.0 MBぐらいになります。


・info.piece length:データを配信する際に分割するサイズ

262144 byteなので、256KBごとにファイルが分割されているようです。

ファイルの総容量 260046848 byte / 分割サイズ 262144 byte = 992

992個のファイルに分割されていると読み取れるのかと思います。


・info.name:ファイル名

debian-8.7.1-amd64-netinst.iso


・info.pieces:分割されたデータごとのHash値

20バイト単位のバイナリデータとのこと。

19840 byte / 20 byte = 992

上記で計算した分割ファイル数と一致しますね。




次は、UPnPによるポートマップを試してみようと思います。




テーマ:プログラミング - ジャンル:コンピュータ

  1. 2017/05/05(金) 18:28:42|
  2. Python
  3. | トラックバック:0
  4. | コメント:0
  5. | 編集

Python 組み込みのCGIHTTPServerやhttp.serverでcgiを動かす

javascriptライブラリの使い方を調べている時、ローカルに簡単なwebサーバーが欲しくなる時があります。

普段はPythonの組み込みモジュールSimpleHTTPServerを使用し
htmlファイルを保存したディレクトリで


$ python -m SimpleHTTPServer



Python 3の場合は


$ python3 -m http.server



でwebサーバーを起動。
ブラウザでhttp://localhost:8000を表示し、動作を確認していました。

ajax通信を扱うライブラリの場合は、送信したパラメーターに応じて
異なる応答を返してほしい。

簡単なcgiプログラムでアプリケーション側を代用できないか調べてみます。





CGIHTTPServer



Pythonにはcgiを動作させるための組み込みモジュール
「CGIHTTPServer」があります。(Python 2の場合)

CGIHTTPServer - CGI 実行機能付き HTTP リクエスト処理機構

これを使えば、簡単にアプリケーションサーバーが動かせそうです。

cgiスクリプトを配置するディレクトリの名称はデフォルトで
「cgi-bin」です。


path/cgi-bin/sample.pyを作成。


  1. # -*- coding:utf-8 -*-
  2. print('Content-type: text/html; charset=UTF-8\r\n')
  3. print('Hello, World!')




ターミナルを起動して、pathに移動。
CGIサーバーを起動します。


$ python -m CGIHTTPServer
Serving HTTP on 0.0.0.0 port 8000 ...




ブラウザでhttp://localhost:8000/cgi-bin/sample.pyを表示しようとすると、
コンソールにこんなエラーが。


Traceback (most recent call last):
File "/usr/lib/python2.7/CGIHTTPServer.py", line 248, in run_cgi
    os.execve(scriptfile, args, env)
OSError: [Errno 13] Permission denied




権限が足りないのかと思い、sudoで実行してもダメ。


$ sudo python -m CGIHTTPServer




cgiで実行しようとするスクリプトに実行権限が必要でした。


$ chmod +x cgi-bin/sample.py




権限をつけて再度アクセスすると次はこんなエラー。


Traceback (most recent call last):
File "/usr/lib/python2.7/CGIHTTPServer.py", line 248, in run_cgi
    os.execve(scriptfile, args, env)
OSError: [Errno 8] Exec format error




ファイルの先頭にシェバン (shebang) が必要でした。


  1. #!/usr/bin/env python
  2. # -*- coding:utf-8 -*-
  3. print('Content-type: text/html; charset=UTF-8\r\n')
  4. print('Hello, World!')




これで表示できました。

743_01.png



Python 3の場合は、http.serverに「--cgi」オプションをつけて起動すれば
同様の動作となります。


$ python3 -m http.server --cgi
Serving HTTP on 0.0.0.0 port 8000 ...







cgi



こちらのドキュメントを参考にしました。
cgi - CGI (ゲートウェイインタフェース規格) のサポート

デバッグ用にcgitbを使用すると便利とのこと。
わざとエラーになるプログラムで表示してみます。


  1. #!/usr/bin/env python
  2. # -*- coding:utf-8 -*-
  3. import cgi
  4. import cgitb
  5. cgitb.enable()
  6. print('Content-type: text/html; charset=UTF-8\r\n')
  7. print('Hello, World!')
  8. # わざとエラーを発生させる
  9. error is here




こんな感じで、エラーとなった箇所を表示してくれました。

743_02.png


引数を追加すると、エラー内容をhtmlで保存してくれます。


  1. cgitb.enable(display=0, logdir='./')



ajax通信時のエラーデバッグで便利でした。






get値の取得




getされた値を取得してみます。
ブラウザで表示するURL
http://localhost:8000/cgi-bin/sample.py?query=symfo


  1. #!/usr/bin/env python
  2. # -*- coding:utf-8 -*-
  3. import cgi
  4. import cgitb
  5. cgitb.enable()
  6. print('Content-type: text/html; charset=UTF-8\r\n')
  7. # パラメーターを取得
  8. form = cgi.FieldStorage()
  9. print(form['query'].value)




cgi.FieldStorage()で送信されたパラメーターを解析。
内容は辞書型に変換されるので、その中から値を取り出します。

743_03.png





post値の取得



postの場合もFieldStorageを使用した同様の処理となります。
postを送信するindex.htmlを作成。


  1. <!doctype html>
  2. <html lang="ja">
  3.     
  4. <html>
  5. <head>
  6.     <meta charset="UTF-8">
  7.     <title>cgi</title>
  8.     <meta http-equiv="X-UA-Compatible" content="IE=edge">
  9. </head>
  10. <body>
  11.     <form method="post" action="./cgi-bin/sample.py">
  12.         <input type="text" name="form_text"><input type="submit" value="send">
  13.     </form>
  14. </body>
  15.     
  16. </html>




cgi-bin/sample.py


  1. #!/usr/bin/env python
  2. # -*- coding:utf-8 -*-
  3. import cgi
  4. import cgitb
  5. cgitb.enable()
  6. print('Content-type: text/html; charset=UTF-8\r\n')
  7. # パラメーターを取得
  8. form = cgi.FieldStorage()
  9. print(form['form_text'].value)




ちゃんとpostしたデータが受け取れていますね。

743_04.png

743_05.png




ajax通信



ここで使用したaxiosを使用し、ajax通信を行うサンプルを作ってみます。
Riot ajaxで取得したjsonデータをテーブルに表示する(axios使用)
https://github.com/mzabriskie/axios

ここではまったのですが、axios.postした値はform.fileに
json形式の文字列で送信されてきます。

form値をprintしてみて気が付きました。


FieldStorage(None, None, '{"ping":"value"}')




・index.html


  1. <!doctype html>
  2. <html lang="ja">
  3.     
  4. <html>
  5. <head>
  6.     <meta charset="UTF-8">
  7.     <title>cgi</title>
  8.     <meta http-equiv="X-UA-Compatible" content="IE=edge">
  9.     <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.16.1/axios.min.js"></script>
  10.     <script>
  11.     function button_click() {
  12.         axios.post('/cgi-bin/sample.py', {
  13.                 ping : document.getElementById('form_text').value
  14.             })
  15.             .then(function (response) {
  16.                 var result = document.getElementById('result');
  17.                 result.innerHTML = response.data['pong'];
  18.             })
  19.             .catch(function (error) {
  20.                 console.log(error)
  21.             });
  22.         
  23.         
  24.     }
  25.     </script>
  26. </head>
  27. <body>
  28.     <input type="text" name="form_text" id="form_text">
  29.     <input type="button" value="send" onclick="javascript:button_click()">
  30.     <div id="result"></div>
  31. </body>
  32.     
  33. </html>




・cgi-bin/sample.py


  1. #!/usr/bin/env python
  2. # -*- coding:utf-8 -*-
  3. import json
  4. import cgi
  5. import cgitb
  6. cgitb.enable(display=0, logdir='./')
  7. print('Content-type: text/javascript; charset=UTF-8\r\n')
  8. # パラメーターを取得
  9. form = cgi.FieldStorage()
  10. # axiosからpostされた値は、fileに格納されている
  11. # json形式に変換
  12. post_json = json.load(form.file)
  13. # 送信データの先頭に「echo」をつけて返却
  14. result = {
  15.     u'pong' : u'echo -> ' + post_json['ping']
  16. }
  17. print(json.dumps(result))




適当な文字列を入力してsend。

743_06.png


狙い通りの応答が得られました。

743_07.png


【参考URL】

PythonでCGIを動かす(Python2, Python3)
Python: CGIHTTPServerでCGIスクリプトを処理

テーマ:プログラミング - ジャンル:コンピュータ

  1. 2017/04/11(火) 22:32:38|
  2. Python
  3. | トラックバック:0
  4. | コメント:0
  5. | 編集

PythonからMariaDB(MySQL)に接続する(mysql-connector-python)

PythonからMariaDB 10に接続してみます。
以前はMySQLdbを使用していたのですが、今回はmysql-connector-pythonを試してみます。

https://pypi.python.org/pypi/mysql-connector-python
https://dev.mysql.com/doc/connector-python/en/


Pythonを動かす端末はUbuntu 16.04。
MariaDBが動いているサーバーはFreeBSD 11.0 + MariaDB 10.1です。

ここで使用した環境です。
http://symfoware.blog68.fc2.com/blog-entry-1966.html



pipによるインストール



pipをインストールしていない場合はインストール。
続いて、mysql-connector-pythonをインストールします。


$ sudo apt install python-pip
$ sudo pip install mysql-connector




mysql.connectorをimport出来るか試してみます。


$ python
Python 2.7.12 (default, Nov 19 2016, 06:48:10)
[GCC 5.4.0 20160609] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import mysql.connector
>>>







接続サンプル



MariaDB側には「test」データベースに「sample」テーブルを作成済です。


  1. create table sample(
  2.     id int,
  3.     val varchar(100)
  4. );




接続情報は以下の通り。


server ip:192.168.1.101
id:admin
password:P@ssw0rd





データベースに接続し、登録や検索を行うサンプルはこんな感じになります。


  1. # -*- coding:utf-8 -*-
  2. import mysql.connector
  3. # データベース接続
  4. con = mysql.connector.connect(
  5.     host='192.168.1.101',
  6.     db='test',
  7.     user='admin',
  8.     passwd='P@ssw0rd'
  9. )
  10. # 辞書型カーソル取得
  11. cur = con.cursor(dictionary=True)
  12. # データの削除
  13. cur.execute('delete from sample')
  14. # 登録(すっぴん)
  15. cur.execute("insert into sample (id, val) values (1, 'テスト1')")
  16. # 登録(プレースフォルダ)
  17. cur.execute("insert into sample (id, val) values (%s, %s)", [2, 'テスト2'])
  18. # 登録(一括)
  19. insert_values = [
  20.     [3, 'テスト3'],
  21.     [4, 'テスト4'],
  22.     [5, 'テスト5']
  23. ]
  24. cur.executemany("insert into sample (id, val) values (%s, %s)", insert_values)
  25. # コミットして変更を確定
  26. con.commit()
  27. # 検索
  28. cur.execute("select * from sample")
  29. # 表示
  30. for row in cur:
  31.     print("id:%d, val:%s" % (row['id'], row['val']))
  32. # 切断
  33. cur.close()
  34. con.close()




カーソル取得時、dictionary=Trueとすることで、フィールド名をキーとした辞書型でデータが取り出せます。
これを指定していないと配列でのデータ取得となるため、
row[0], row[1]のような指定でデータを参照することになります。

自動的にトランザクションが開始されているため、明示的にcommitしないと
変更が確定しません。


実行結果


$ python sample.py
id:1, val:テスト1
id:2, val:テスト2
id:3, val:テスト3
id:4, val:テスト4
id:5, val:テスト5







データ取得方法あれこれ



selectを実行した後のカーソルはそのままイテレートできます。


  1. # -*- coding:utf-8 -*-
  2. import mysql.connector
  3. # データベース接続
  4. con = mysql.connector.connect(
  5.     host='192.168.1.101',
  6.     db='test',
  7.     user='admin',
  8.     passwd='P@ssw0rd'
  9. )
  10. # 辞書型カーソル取得
  11. cur = con.cursor(dictionary=True)
  12. # 検索
  13. cur.execute("select * from sample")
  14. # 表示
  15. for row in cur:
  16.     print("id:%d, val:%s" % (row['id'], row['val']))





fetchoneで先頭のレコードを取り出せます。


  1. # -*- coding:utf-8 -*-
  2. import mysql.connector
  3. # データベース接続
  4. con = mysql.connector.connect(
  5.     host='192.168.1.101',
  6.     db='test',
  7.     user='admin',
  8.     passwd='P@ssw0rd'
  9. )
  10. # 辞書型カーソル取得
  11. cur = con.cursor(dictionary=True)
  12. # 検索
  13. cur.execute("select * from sample")
  14. # 最初の行を取得
  15. row = cur.fetchone()
  16. # 表示
  17. print("id:%d, val:%s" % (row['id'], row['val']))




$ python sample.py
id:1, val:テスト1




fetchoneすると読み込む行が1つ進みます。
次の行がない状態でfetchoneすると「None」になります。

fetchoneしながらすべての行を読み取るプログラムは
こんな感じになります。


  1. # -*- coding:utf-8 -*-
  2. import mysql.connector
  3. # データベース接続
  4. con = mysql.connector.connect(
  5.     host='192.168.1.101',
  6.     db='test',
  7.     user='admin',
  8.     passwd='P@ssw0rd'
  9. )
  10. # 辞書型カーソル取得
  11. cur = con.cursor(dictionary=True)
  12. # 検索
  13. cur.execute("select * from sample")
  14. # 最初の行を取得
  15. row = cur.fetchone()
  16. # データが取得できている間はループ
  17. while row:
  18.     # 表示
  19.     print("id:%d, val:%s" % (row['id'], row['val']))
  20.     # 次の行を読み取り
  21.     row = cur.fetchone()




$ python sample.py
id:1, val:テスト1
id:2, val:テスト2
id:3, val:テスト3
id:4, val:テスト4
id:5, val:テスト5




fetchallですべての行を変数に代入できます。


  1. # -*- coding:utf-8 -*-
  2. import mysql.connector
  3. # データベース接続
  4. con = mysql.connector.connect(
  5.     host='192.168.1.101',
  6.     db='test',
  7.     user='admin',
  8.     passwd='P@ssw0rd'
  9. )
  10. # 辞書型カーソル取得
  11. cur = con.cursor(dictionary=True)
  12. # 検索
  13. cur.execute("select * from sample")
  14. # すべての行を取得
  15. rows = cur.fetchall()
  16. for row in rows:
  17.     # 表示
  18.     print("id:%d, val:%s" % (row['id'], row['val']))




$ python sample.py
id:1, val:テスト1
id:2, val:テスト2
id:3, val:テスト3
id:4, val:テスト4
id:5, val:テスト5




テーマ:プログラミング - ジャンル:コンピュータ

  1. 2017/03/23(木) 22:48:03|
  2. Python
  3. | トラックバック:0
  4. | コメント:0
  5. | 編集

Python webサーバー、フレームワークを使用せずにwebアプリ作成

個人的なツール用として簡単なwebアプリを作成したかったのですが、
なんとか素のPythonだけで実装できないか考えてみました。


BaseHTTPServer,SimpleHTTPServer



Python 2系の場合、BaseHTTPServerが標準で組み込まれています。
※Python 3系ではhttp.server

これらを使用すれば良さそうです。

作成するwebアプリでは、htmlやjsファイルは通常通りget。
javascriptからのデータ取得はPOSTのみで行うことにし、
実装を簡単にしました。



POSTデータの解析



POSTデータの解析をどうやれば良いかわからず探したところ、
このドキュメントが参考になりました。

BaseHTTPServer – web サーバを実装するベースクラス

cgi.FieldStorageを使用すれば解析できそうです。



サンプルアプリケーション



アプリケーションは
・index.html
・app.js
・view.py
の3つのファイルで構成しました。

名前を入力すると、応答してくれる簡単なものです。

731_01.png


・index.html


  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4.     <meta charset="UTF-8">
  5.     <title>Simple Web</title>
  6.     <script src="//ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
  7.     <script type="text/javascript" src="./static/js/app.js"></script>
  8. </head>
  9. <body>
  10. <h3>Pythonのみで簡易Webアプリ</h3>
  11. <input type="text" id="name">
  12. <input type="button" id="echo" value="ajaxでデータ取得">
  13. <div id="result"></div>
  14. </body>
  15. </html>




・app.js


  1. $(function() {
  2.     
  3.     // 入力された値をPOSTで送信
  4.     // 応答を表示する
  5.     $('#echo').on('click', function(){
  6.         var name = $('#name').val();
  7.         $.ajax({
  8.             type: 'POST',
  9.             dataType: 'json',
  10.             data: {'name':name},
  11.             url: 'echo',
  12.         }).done(function (data) {
  13.             $('#result').html(data.message);
  14.         });
  15.     });
  16.     
  17. });




・view.py


  1. # -*- coding: utf-8 -*-
  2. import cgi
  3. import json
  4. import BaseHTTPServer
  5. import SimpleHTTPServer
  6. class MyHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
  7.     
  8.     # POSTの実装(GETは継承元にある)
  9.     def do_POST(self):
  10.         
  11.         # リクエストに対応したメソッドを呼び出す
  12.         route = self.path.split('/')[-1]
  13.         if not hasattr(self, route):
  14.             self.send_response(500)
  15.             self.end_headers()
  16.             return
  17.         
  18.         # POSTデータの解析
  19.         form = self._parse_form()
  20.         response = getattr(self, route)(form)
  21.         
  22.         # レスポンス作成
  23.         self.send_response(200)
  24.         self.send_header("Content-type", 'application/json')
  25.         self.send_header("Content-Length", len(response))
  26.         self.end_headers()
  27.         self.wfile.write(response)
  28.         
  29.         
  30.     def _parse_form(self):
  31.         if 'Content-Type' not in self.headers:
  32.             return None
  33.         
  34.         # POSTデータがあれば内容を解析する
  35.         return cgi.FieldStorage(
  36.             fp=self.rfile,
  37.             headers=self.headers,
  38.             environ={
  39.                 'REQUEST_METHOD':'POST',
  40.                 'CONTENT_TYPE':self.headers['Content-Type'],
  41.             }
  42.         )
  43.     
  44.         
  45.         
  46.     # --- router
  47.     def echo(self, form):
  48.         
  49.         name = u'名無し'
  50.         
  51.         # データが送信されていれば、内容を応答
  52.         if 'name' in form:
  53.             name = form['name'].value
  54.         
  55.         message = u'%sさんこんにちは' % name
  56.         data = {'message' : message}
  57.         return json.dumps(data)
  58.     
  59.     
  60. if __name__ == '__main__':
  61.     
  62.     server_address = ('0.0.0.0', 8000)
  63.     httpd = BaseHTTPServer.HTTPServer(server_address, MyHandler)
  64.     print('Serving HTTP on 0.0.0.0 port %d ...' % server_address[1])
  65.     print('use <Ctrl-(C or break)> to stop')
  66.     httpd.serve_forever()
  67.     





$ python view.py

と実行して、http://localhost:8000にアクセスします。

731_02.png


適当な文字列を入力してボタンをクリックすると、応答が得られました。

731_03.png


なんとかPython単独でwebアプリケーションが作成できそうです。



【参考URL】

SimpleHTTPServer — 簡潔な HTTP リクエストハンドラ
BaseHTTPServer – web サーバを実装するベースクラス

テーマ:プログラミング - ジャンル:コンピュータ

  1. 2017/01/30(月) 23:53:09|
  2. Python
  3. | トラックバック:0
  4. | コメント:0
  5. | 編集
次のページ