Python
Android
HTTP
python3
Web開発

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
main.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)
libs/altlogger.py
#!/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(' ', '&nbsp;'))
            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
libs\mimeParser.py
#!/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))
libs\RequestHandler.py
#!/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:]
libs\ServerCore.py
#-*- 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というものがあるのですが、使い勝手が悪かったので継承して実装しなおし。

conf/mime.typesdef
#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
conf/httpd.yml
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」という名前のファイルを作成します。

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!」です。
できたら、バインドしたアドレスにアクセスしてみましょう。
overview.png
こんな感じの画面が表示されたら成功です。

気が向いたら続投があるかも。