#AndroidでWeb開発入門!
近年、スマートフォンなどのモバイル機器の発展が目覚ましく、最早ノートPCにも迫る性能を持っています。
しかしながらAndroidで開発、という意識はあまり無いのではないでしょうか。自分もやむにやまれぬ事情がなければ(修学旅行にPCを持っていくことを拒否された)、そのような意識を持つことはなかっただろうと思います。
自分はコードを1日20行以上読まないと気が変になりそうになる非常に残念な人種なのでこれは死活問題だったのです。
まあ、そんなわけでPythonのAndroid上の実行環境を探せばこんなものがありました。
QPython3 - Google Play
これはAndroidのターミナル上で動かせるPython3系の実行環境です。
#サーバーを実装しよう!
何はともあれサーバーがなくては開発なんてできません。作りましょう!
ファイル構造はこんな感じ。
- httpd
- main.py
- conf
- httpd.yml
- mime.typesdef
- libs
- altlogger.py
- mimeParser.py
- RequestHandler.py
- ServerCore.py
#!/usr/bin/env python
# encoding: utf-8
"""
main.py
Created by Frodo on 2017/12/11.
Copyright (c) 2017 BouFraw. All rights reserved.
"""
import sys
import os.path as path
wdir = path.dirname(__file__)
sys.path.append(path.join(wdir, 'libs'))
service = None
from ServerCore import server
try:
service = server()
service.serve_forever()
except KeyboardInterrupt:
service.shutdown()
except Exception as e:
print(e)
#!/usr/bin/env python
# encoding: utf-8
"""
Logger Module
Created by Frodo821 on 2017/12/05.
Copyright (c) 2017 Copyright BowFraw. All rights reserved.
"""
from time import strftime as stm
from xml.sax.saxutils import escape
import sys
__modname__ = "Logger/Logging"
class Level:
Debug = 0
Info = 1
Warn = 2
Error = 3
Fatal = 4
@classmethod
def asName(cls, val):
for k, v in cls.__dict__.items():
if v == val:
return k
return None
@classmethod
def asValue(cls, key):
try:
return cls.__dict__[key]
except KeyError:
return None
class logger:
def __init__(self, log, level, show_in_console = True, time_format = "%Y-%m-%d %H:%M:%S"):
self.log = ""
self.file = log
self.sic = show_in_console
self.level = level
self.time_format = time_format
self.info("Logging system initialized")
def __log__(self, msg, level, caller):
if level < self.level:
return
time = stm(self.time_format)
message = "[{} in {} at {}] ".format(Level.asName(level), caller, time) + str(msg)
if self.sic:
print(message)
self.log += message + "\n"
def debug(self, mes):
caller = self.__getcaller__()
self.__log__(mes, Level.Debug, caller)
def info(self, mes):
caller = self.__getcaller__()
self.__log__(mes, Level.Info, caller)
def warn(self, mes):
caller = self.__getcaller__()
self.__log__(mes, Level.Warn, caller)
def error(self, mes):
caller = self.__getcaller__()
self.__log__(mes, Level.Error, caller)
def fatal(self, mes):
caller = self.__getcaller__()
self.__log__(mes, Level.Fatal, caller)
def save(self):
self.debug("saving log to " + self.file)
with open(self.file, 'w') as f:
f.write(self.log)
def export_to_html(self):
from os.path import join, dirname, basename
hf = join(dirname(self.file), basename(self.file) + '.html')
self.debug("exporting log into html and will be saved to " + hf)
html = "<!doctype html><html><head><title>%s</title><style>body{background: #000;}code{font-size: 14px; line-height: 1.1em; white-space: pre;}.Debug{color: #a9a9a9;}.Info{color: #fff;}.Warn{color: #ffd700;}.Error{color: #ff4500;}.Fatal{color: #b22222;}</style></head><body><code>{{body}}</code></body></html>" % basename(self.file)
body = ""
last = ""
for l in self.log.split("\n"):
typ = l.split(" ").pop(0)
if typ == '' or typ[0] != '[':
typ = last
else:
last = typ[1:]
line = "<span class=\"%s\">%s</span><br />" % (last, escape(l).replace(' ', ' '))
body += line
with open(hf, 'w') as f:
f.write(html.replace("{{body}}", body))
def __getcaller__(self):
try:
return sys._getframe(2).f_globals['__modname__']
except ValueError:
return 'Error Module'
except KeyError:
return 'Unknown'
if __name__ == "__main__":
pass
#!/usr/bin/env python
# encoding: utf-8
"""
mimeParser.py
Created by Frodo821 on 2017/12/11.
Copyright (c) 2017 BouFraw. All rights reserved.
"""
import sys
import os.path as path
__modname__ = "Server/MIMEInit"
class mime:
def __init__(self, p, svc):
self.svc = svc
svc.info('loading server MIME types definition from configuration file...')
svc.debug('MIME configuration file path was given, that was "{}"'.format(p))
if not path.exists(p):
raise FileNotFoundError("The MIME definition file '{}' was not found.".format(p))
svc.debug("MIME configuration file was found.")
svc.info("trying to parse the configuration...")
self.types = dict()
with open(p, 'r') as f:
lines = f.readlines()
lc = 1
tc = 0
for line in lines:
lc += 1
com = line.find('#')
if com != -1:
line = line[:com]
if line == '' or line.strip() == '':
continue
sep = line.find('=')
if sep == -1:
raise ParseError("Unexpected line was found.", lc, 1)
dic = {e: line[:sep].strip() for e in line[sep + 1:].strip().split(' ')}
tc += 1
svc.debug("<mime loaded #{}> {}".format(tc, line.strip()))
for k in dic.keys():
if k in self.types.keys():
raise ParseError("The extension '{}' was already registered!".format(k), lc, line.find(k))
self.types.update(dic)
svc.info("MIME types loading successfully completed.")
def getType(self, ext):
if ext == '':
ext = 'noext'
elif not ext in self.types.keys():
self.svc.warn("Unknown file extension was given, and recognize as 'unknown'. Please register '{}' as soon as possible!".format(ext))
ext = 'unknown'
t = self.types[ext]
self.svc.debug("{} -> {}".format(ext, t))
return t
def registerType(self, ext, mt):
if ext in self.types.keys():
self.svc.error("the extension '{}' was already registered.".format(ext))
return False
self.types[ext] = mt
self.debug("registration successful.")
return True
def updateType(self, ext, mt):
self.debug("types library force updating.")
self.types[ext] = mt
class ParseError(Exception):
def __init__(self, msg, line, column):
self.line = line
self.column = column
super(ParseError, self).__init__(msg + " Error occurred at line {}, column {}".format(self.line, self.column))
#!/usr/bin/env python
# encoding: utf-8
"""
RequestHandler.py
Created by Frodo821 on 2017/12/11.
Copyright (c) 2017 BouFraw. All rights reserved.
"""
if __name__ == '__main__':
print("Can't run itself.")
exit()
from http.server import BaseHTTPRequestHandler
from urllib.parse import unquote as uq
from os.path import join
from __main__ import service
from os.path import splitext as ext
__modname__ = "Server/Response"
class handler(BaseHTTPRequestHandler):
def do_GET(self, parg = None, res = 200):
if parg is not None: self.server.info(parg)
if res != 200:
self.server.info("%s - - %s %d -" % (self.client_address[0], self.requestline, res))
doc = self.server.getErrorDocument(res, uq(path) + ('?' + arg if arg is not None else ''))
if doc is None:
self.send_error(res)
return
self.send_response(res)
self.send_header('Content-Type', 'text/html')
self.end_headers()
self.wfile.write(doc)
return
path, arg = self.separate_path()
np = self.server.normalize_path(uq(path))
if type(np) is int:
self.server.info("%s - - %s %d -" % (self.client_address[0], self.requestline, np))
doc = self.server.getErrorDocument(np, uq(path) + ('?' + arg if arg is not None else ''))
if doc is None:
self.send_error(np)
return
self.send_response(np)
self.send_header('Content-Type', 'text/html')
self.end_headers()
self.wfile.write(doc)
return
t = self.server.mime.getType(ext(np)[1])
if t in self.server.special_handler.keys():
self.server.special_handler[t](self)
return
self.send_response(200)
self.send_header('Content-Type',t)
self.end_headers()
with open(np, 'rb') as f:
self.wfile.write(f.read())
self.server.info("%s - - %s %d -" % (self.client_address[0], self.requestline, 200))
return
def do_POST(self):
args = self.rfile.read(int(self.headers.get('Content-Length'), 10)).decode('utf-8')
pargs = {uq(k.replace('+', ' ')):uq(v.replace('+', ' ')) for k, v in map(lambda x: x.split('='), args.split('&'))}
self.do_GET(parg = pargs)
def log_message(self, fmt, *args):
with open(join(self.server.logs, 'access.log'), 'a') as f:
f.write("%s - - [%s] %s\n" %
(self.address_string(),
self.log_date_time_string(),
fmt%args))
def separate_path(self):
sep = self.path.find('?')
if sep == -1:
return self.path, None
return self.path[:sep], self.path[sep + 1:]
#-*- coding: utf-8;-*-
"""
server.py
Created by Frodo821 on 2017/12/05.
Copyright (c) 2017 Copyright BowFraw. All rights reserved.
"""
import sys
import os
import os.path as path
from threading import Timer, Thread
from altlogger import logger, Level
from yaml import safe_load as load
from http.server import HTTPServer
from RequestHandler import handler
from mimeParser import mime
from traceback import format_exception as f_exp
from __main__ import wdir
__modname__ = "Server/InitCore"
SDIR = wdir
initial_config = """general:
enable_alternative_settings: yes
console_quiet_mode: no
enable_logging: yes
log_time_format: $Y-$m-$d $H:$M:$S
enable_advanced_logging: no
service_root: /sdcard/httpd
modules:
core:
paths:
document_root: docroot
errors: errdocs
logs: logs
modules: libs
directory_index: [index.html, main.css]
bindings:
host: 127.0.0.1
port: 8000
clients:
allow: []
deny: []
typedef:
def: mime.typesdef
"""
class server(HTTPServer):
def __init__(self):
global __modname__
try:
self.cfgdir = path.join(SDIR, "conf")
self.cfgpth = path.join(self.cfgdir, 'httpd.yml')
if not path.exists(self.cfgdir):
os.mkdir(self.cfgdir)
if not path.exists(self.cfgpth):
with open(self.cfgpth, 'w') as c:
c.write(initial_config)
with open(self.cfgpth, 'r') as f:
self.config = load(f)
self.core = self.config['modules']['core']
self.general = self.config['general']
self.svcroot = self.general['service_root']
self.docroot = path.join(self.svcroot, self.core['paths']['document_root'])
self.errdocs = path.join(self.svcroot, self.core['paths']['errors'])
self.logs = path.join(self.svcroot, self.core['paths']['logs'])
self.modules = path.join(self.svcroot, self.core['paths']['modules'])
self.directory_index = self.core['paths']['directory_index']
bind = tuple([c for c in self.core['bindings'].values()])
self.lg = self.general['enable_logging']
self.advlog = self.general['enable_advanced_logging']
self.quiet_mode = self.general['console_quiet_mode']
self.special_handler = dict()
if not path.exists(self.svcroot):
os.mkdir(self.svcroot)
if self.lg:
if not path.exists(self.logs):
os.mkdir(self.logs)
lvl = Level.Info
if self.advlog:
lvl = Level.Debug
self.logger = logger(path.join(self.logs, 'server.log'), lvl, not self.quiet_mode, self.general['log_time_format'].replace('$', '%'))
self.debug = self.logger.debug
self.info = self.logger.info
self.warn = self.logger.warn
self.error = self.logger.error
self.fatal = self.logger.fatal
elif self.quiet_mode:
self.debug = lambda x: None
self.info = lambda x: None
self.warn = lambda x: None
self.error = lambda x: None
self.fatal = lambda x: None
else:
self.debug = print
self.info = print
self.warn = print
self.error = print
self.fatal = print
try:
self.mime = mime(path.join(self.cfgdir, self.core['typedef']['def']), self)
except Exception:
if self.advlog:
self.fatal(''.join(f_exp(*sys.exc_info()))[:-1])
else:
self.fatal(sys.exc_info()[1])
self.fatal('uncaught error was occurred, server could not continue.')
return
if not path.exists(self.docroot):
os.mkdir(self.docroot)
self.info("created " + self.docroot)
if not path.exists(self.errdocs):
os.mkdir(self.errdocs)
self.info("created " + self.errdocs)
if not path.exists(self.modules):
os.mkdir(self.modules)
self.info("created " + self.modules)
self.debug("server service root directory = " + self.svcroot)
self.debug("server document root directory = " + self.docroot)
self.debug("server error docs directory = " + self.errdocs)
self.debug("server log files directory = " + self.logs)
self.debug("server modules directory = " + self.modules)
self.debug("server logging status = " + str(self.lg))
self.debug("server debug logging status = " + str(self.advlog))
self.debug("server listening port binding = " + str(bind))
self.info("server configuration loading finished. prepare to launch server...")
super(server, self).__init__(bind, handler)
self.log_backup = None
self.save_logs()
self.info("server startup sequence completed.")
__modname__ = "Server/Core"
try:
self.logger.save()
self.logger.export_to_html()
except AttributeError:
pass
#raise NotImplementedError("Server core function is not implemented yet.")
except Exception:
if self.advlog:
self.fatal(''.join(f_exp(*sys.exc_info()))[:-1])
else:
self.fatal(sys.exc_info()[1])
self.fatal('uncaught error was occurred, server could not continue.')
self.shutdown()
def save_logs(self):
self.logger.save()
self.logger.export_to_html()
self.log_backup = Timer(600, self.save_logs)
self.log_backup.start()
def getErrorDocument(self, state, orgpth):
edp = path.join(self.errdocs, str(state) + '.perm')
doc = None
if path.exists(edp):
with open(edp, 'r') as f:
doc = bytes(f.read().replace('$path', orgpth), 'utf-8')
return doc
def normalize_path(self, rp):
if rp.find("/..") != -1 or rp.find("/../") != -1 or rp[:3] == '..':
np = self.docroot
for i in self.directory_index:
ret = path.join(np, i)
if path.exists(ret):
return ret
return 404
np = path.join(self.docroot, rp[1:])
if path.isdir(np):
for i in self.directory_index:
ret = path.join(np, i)
if path.exists(ret):
return ret
return 404
if path.exists(np):
return np
return 404
def shutdown(self):
self.info("shutting down the server.")
try:
self.log_backup.cancel()
self.logger.save()
self.logger.export_to_html()
except AttributeError:
pass
sys.exit()
Pythonはここまでです。標準モジュールにhttp.serverというものがあるのですが、使い勝手が悪かったので継承して実装しなおし。
#application
application/xhtml+xml = .xhtml
application/rss+xml = .rss
application/atom+xml = .atom
application/msexcel = .xls .xlsx
application/epub+zip = .epub
application/x-shar = .shar
application/msword = .doc .docx
application/postscript = .ai .ps .eps
application/x-dvi = .dvi
application/x-sh = .sh
application/x-texinfo = .texi .texinfo
application/x-latex = .latex
application/java = .class
application/vnd.wap.wmlscriptc = .wmlscriptc
application/vnd.rn-realmedia = .rm
application/x-aim = .aim
application/x-troff-me = .me
application/x-shockwave-flash = .swf
application/x-sv4cpio = .sv4cpio
application/x-mif = .mif
application/vnd.wap.wmlc = .wmlc
application/pdf = .pdf
application/x-tcl = .tcl
application/x-compress = .Z .z
application/x-ustar = .ustar
application/x-java-jnlp-file = .jnlp
application/x-hdf = .hdf
application/x-netcdf = .nc
application/oda = .oda
application/x-gtar = .gtar
application/octet-stream = .jar .bin .exe
application/rtf = .rtf
application/x-sv4crc = .sv4crc
application/x-wais-source = .src .ms
application/x-tar = .tar
application/x-gzip = .gz
application/x-troff-man = .man
application/x-tex = .tex
application/x-bcpio = .bcpio
application/x-x509-ca-cert = .cer
application/x-troff = .roff .tr .t
application/x-cdf = .cdf
application/zip = .zip
application/x-csh = .csh
application/mac-binhex40 = .hqx
application/x-cpio = .cpio
#audio
audio/ogg = .ogg
audio/webm = .webm
audio/aac = .m4a
audio/vnd.rn-realaudio = .ra
audio/x-mpeg = .mpa .mpega .abs .mp2 .mp3 .mp1
audio/x-scpls = .pls
audio/basic = .snd .ulw .au
audio/x-midi = .mid .midi .kar .smf
audio/x-wav = .wav
audio/x-mpegurl = .m3u
audio/x-aiff = .aiff .aifc .aif
#image
image/svg+xml = .svg
image/vnd.microsoft.icon = .ico
image/x-jg = .art
image/x-cmu-raster = .ras
image/x-xbitmap = .xbm
image/x-portable-graymap = .pgm
image/x-portable-bitmap = .pbm
image/jpeg = .jpe .jpeg .jpg
image/gif = .gif
image/x-xwindowdump = .xwd
image/png = .png
image/x-portable-anymap = .pnm
image/x-quicktime = .qti .qtif
image/x-portable-pixmap = .ppm
image/x-xpixmap = .xpm
image/x-photoshop = .psd
image/ief = .ief
image/pict = .pct .pic .pict
image/vnd.wap.wbmp = .wbmp
image/bmp = .bmp .dib
image/x-rgb = .rgb
image/x-macpaint = .pnt .mac
image/tiff = .tiff .tif
#text
text/xml = .xml
text/csv = .csv
text/javascript = .js
text/html = .body .htm .html
text/x-setext = .etx
text/vnd.wap.wmlscript = .wmls
text/vnd.sun.j2me.app-descriptor = .jad
text/richtext = .rtx
text/vnd.wap.wml = .wml
text/x-component = .htc
text/tab-separated-values = .tsv
text/css = .css
text/plain = .dtd .java .txt unknown noext
#video
video/x-dv = .dv
video/x-sgi-movie = .movie
video/mpeg2 = .mpv2
video/quicktime = .qt .mov
video/x-ms-asf = .asx .asf
video/x-rad-screenplay = .avx
video/mpeg = .mpg .mpeg .mpe
video/x-msvideo = .avi
#x-world
x-world/x-vrml = .wrl
general:
enable_alternative_settings: yes
console_quiet_mode: no
enable_logging: yes
log_time_format: $Y-$m-$d $H:$M:$S
enable_advanced_logging: no
service_root: /sdcard/httpd/
modules:
core:
paths:
document_root: docroot
errors: errdocs
logs: logs
modules: libs
directory_index: [index.html, main.html]
bindings:
host: 127.0.0.1
port: 8000
clients:
allow: []
deny: []
typedef:
def: mime.typesdef
これらのファイルを作ってQPythonのスクリプトディレクトリに配置したら、configを自分の環境に合わせて編集します。次のセクションでは、configの解説をします。
#設定の解説
##httpd.yml
general
####enable_alternative_settings
未使用
####console_quiet_mode
コンソールにログを表示するか
####enable_logging
ロギングを有効にするか
####log_time_format
ログの時間のフォーマット
####enable_advanced_logging
デバッグログを有効にするか
####service_root
サーバーのユーザーデータを置くディレクトリ。
###modules.core
####paths
このセクションのパスはすべてgeneral.service_rootからの相対パス。
document_root: Webドキュメントのルートディレクトリ。
errors: エラードキュメントを置くディレクトリ。エラードキュメントは、「{HTTP_status_code}.perm」の形で保存。
logs: ログの出力ファイルが保存されるディレクトリ。
modules: 未使用
directory_index: ディレクトリインデックスのリスト。
####bindings
サーバーにバインドするホストとポートを設定する。
####clients
未使用
####typedef
def: MIMETypeの設定ファイル名。
##mime.typesdef
[MIME_Type] = [file_extension1 file_extension2...]
の形で書けばおkです。
設定が完了したら、次は起動です。
#サーバーを起動しよう。
QPythonを起動してトップの「Programs」をタップします。するとScriptsタブの中にhttpdというフォルダがありましたか? あればタップして開いた後、「main.py」のコンテキストメニューから「Run」を選んで実行しましょう。
なかった人はソースの配置を見直してからやり直してください。
するとservice_rootに選択したディレクトリの下に4つディレクトリができているはずです。
そのうちの、document_rootを開いて、「index.html」という名前のファイルを作成します。
<!doctype html>
<html>
<head>
<meta charset="utf8" />
<meta name="viewport" content="initial-scale=1.0, user-scalable=no" />
<title>Powered by Python 3</title>
</head>
<body>
<h1>It works!</h1>
</body>
</html>
はい。恒例の「It works!」です。
できたら、バインドしたアドレスにアクセスしてみましょう。
こんな感じの画面が表示されたら成功です。
気が向いたら続投があるかも。