使用sys模块的argv属性实现用python3命令运行.py脚本的同时向脚本中传递参数

1. 需求

Python中web服务器、web框架以及WSGI的作用和关系浅析中,实现了简单的遵循WSGI协议的web服务器和web框架。

当使用命令python3 web_server.py启动服务器程序时,如果服务器程序中写死的端口号被其他应用程序已经占用,则会导致web服务无法启动;另外,实际应用还可能会在运行程序的同时再指定使用的框架。

那么如何实现在使用python3命令启动web服务器的同时向web_server.py文件中传递期望的端口号并指定使用的框架呢?

2. 方法

在Python的sys模块中有一个argv属性,该属性为一个列表,列表中元素依次为:使用python3命令运行一个Python脚本时,命令后以空格分隔紧随的待执行.py文件名和参数。

例如,一个简单的test.py文件,其内容如下:

import sys

print(sys.argv)

则如果以python3 test.py 8888 mini_web_framework运行上述文件后,输出结果为:

[‘test.py’, ‘8888’, ‘mini_web_framework’]

需要注意的是,属性argv的元素均为字符串,则使用时需要注意类型转换。

3. 案例

针对Python中web服务器、web框架以及WSGI的作用和关系浅析中的服务器程序:

扫描二维码关注公众号,回复: 11482316 查看本文章
  • 第4行显式指定了要导入的框架模块mini_web_frame
  • 第16行将端口写死为7899;
  • 第61行显式指定了要调用mini_web_frame模块中的application()函数。

现在希望上述3处都需要通过python3运行web_server.py时跟着的参数来指定,即:希望使用命令python3 web_server.py 8888 mini_web_frame:application,实现指定web服务器运行时使用8888端口,并在请求动态资源时,调用mini_web_frame中的application函数。

3.1 端口参数化指定

由于服务器程序运行是从main()开始,于是需要在main()中成功创建服务器对象前提取出端口信息,并在实例化服务器对象是以参数形式传输服务器类的初始化方法,则可以将其做如下的修改:

def main():
    """控制整体,创建一个web服务器对象,然后调用这个对象的run_forever()方法运行"""

    if len(sys.argv) == 2:
        try:
            port = int(sys.argv[1])
        except Exception as ret:
            print("请输入正确格式的端口...")
            return 
    else:
        print("请按照以下方式运行服务器程序:")
        print("python3 web_server.py 8888")
        return 
    
    wsgi_server = WSGIServer(port)
    wsgi_server.run_forever()

且修改WSGIServer的__init__()方法,使之接收一个参数port,而后在绑定服务器套接字端口时传入参数port。

3.2 框架名称参数化指定

进一步地,想要在使用python3命令启动web服务器程序时,将实际使用的框架(此处以名为mini_web_frame的框架为例,且其中符合WSGI规范的函数名为application)导入,则同样地,需要在web服务器程序的main()中创建服务器对象前先提取出框架信息,然后完成框架的导入以及application()函数引用的获取,则对上述已修改的main()函数再作如下修改:

def main():
    """控制整体,创建一个web服务器对象,然后调用这个对象的run_forever()方法运行"""

    if len(sys.argv) == 3:
        try:
        	# 获取以字符串形式指定的端口信息,并转换成int类型
            port = int(sys.argv[1])
			# 获取以字符串形式指定的web框架和符合WSGI规范的application函数信息
            frame_application_str = sys.argv[2]
        except Exception as ret:
            print("请输入正确格式的端口...")
            return
    else:
        print("请按照以下方式运行服务器程序:")
        print("python3 web_server.py 8888 mini_web_frame:application")
        return

    # 使用正则表达式分别提取出web框架和符合WSGI规范的函数引用
    match_obj = re.match(r"([^:]+):(.*)", frame_application_str)
    if match_obj:
        frame_name = match_obj.group(1)
        application_name = match_obj.group(2)
    else:
        print("请按照以下方式运行服务器程序:")
        print("python3 web_server.py 8888 mini_web_frame:application")
        return

    # 通过变量frame_name导入mini_web_framework模块
    web_frame = __import__(frame_name)

    # 使用getattr函数获取mini_web_framework模块中application函数引用
    app = getattr(web_frame, application_name)

    print(app)

mini_web_frame模块和上述代码段放置在同一路径下,并通过python3 web_server.py 8888 mini_web_framework:application命令运行,则上述程序的运行结果为:

<function application at 0x7f924fb38b70>

表明模块mini_web_frame成功导入,且成功获取了其中application函数的引用。

几点说明:

(1)使用__import__而非import来导入模块:

  • import:在上述程序中,通过正则提取出了web框架的字符串格式信息为"mini_web_frame",并将其存入变量frame_name中,如果直接使用import frame_name,则解释器会尝试导入名为frame_name的模块,这是一定会报错的,因为不存在此名称的模块;
  • __import__:內置函数__import__可接收一个字符串形式module名称,并返回导入的模块对象。

(2)使用getattr函数获取mini_web_frame模块中的application函数:

  • 上述获取了字符串形式的application函数名被包保存于变量application_name中,也通过內置函数__import__导入了mini_web_framework模块,并将模块对象保存于web_frame变量中;
  • web_frame模块对象中的函数application引用可通过getattr()內置函数获取,该函数用于返回对象中指定名称的属性,且属性名称通过字符串形式给出;
  • 在获取application函数引用并将其保存于变量app中后,可在创建web服务器对象的同时将其作为实参传入,并相应修改初始化方法和web服务器中需要通过web框架请求动态资源的位置,最终web服务器程序如下所示:
import socket
import re
import multiprocessing
import sys


class WSGIServer(object):
    def __init__(self, port, app):
        # 1.创建套接字
        self.tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

        # 通过设定套接字选项解决[Errno 98]错误
        self.tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

        # 2.绑定端口
        self.tcp_server_socket.bind(("", port))

        # 3.变为监听套接字
        self.tcp_server_socket.listen(128)

        # 定义两个实例属性,用于存储response_header信息
        self.status = None
        self.headers = None
        self.app = app

    def set_response_header(self, status, headers):
        self.status = status
        # 在服务器程序中指定web服务器的信息
        self.headers = [("Server", "user-defined web server v1.0")]
        self.headers += headers

    def serve_client(self, new_client_socket):
        """为这个客户端返回数据"""
        # 6.接收浏览器发送过来的http请求
        request = new_client_socket.recv(1024).decode("utf-8")

        # 7.将请求报文分割成字符串列表
        request_lines = request.splitlines()

        print(request_lines)

        # 8.通过正则表达式提取浏览器请求的文件名
        file_name = None
        ret = re.match(r"^[^/]+(/[^ ]*)", request_lines[0])
        if ret:
            file_name = ret.group(1)
            print("file_name:", file_name)
            if file_name == "/":
                file_name = "/index.html"

        # 9.返回http格式的应答数据给浏览器
        # 9.1 如果请求的资源不是以.py结尾,那么就认为浏览器请求的是静态资源(HTML、CSS、JPG、PNG等)
        if not file_name.endswith(".py"):
            # 静态资源处理逻辑
            # 9.返回http格式的应答数据给浏览器
            try:
                f = open("./Charisma" + file_name, "rb")
            except Exception:
                response = "HTTP/1.1 404 NOT FOUND\r\n"
                response += "\r\n"
                response += "-----file not found-----"
                new_client_socket.send(response.encode("utf-8"))
            else:
                # 9.1 读取发送给浏览器的数据-->body
                html_content = f.read()
                f.close()

                # 9.2 准备发送给浏览器的数据-->header
                response = "HTTP/1.1 200 OK\r\n"
                response += "\r\n"

                # 将response header发送给浏览器--先以utf-8格式编码
                new_client_socket.send(response.encode("utf-8"))
                # 将response body发送给浏览器--直接是以字节形式发送
                new_client_socket.send(html_content)
        else:
            # 9.2 如果浏览器请求的资源是以.py结尾,则表示浏览器请求的是动态资源
            env = dict()

            env["PATH_INFO"] = file_name

            body = self.app(env, self.set_response_header)

            header = "HTTP/1.1 %s\r\n" % self.status

            for each in self.headers:
                header += "%s:%s\r\n" % (each[0], each[1])

            header += "\r\n"

            response = header + body
            new_client_socket.send(response.encode("utf-8"))

        # 10. 关闭此次服务的套接字
        new_client_socket.close()

    def run_forever(self):
        """用来完成程序整体控制"""
        while True:
            # 4.等待新客户端连接
            new_client_socket, client_addr = self.tcp_server_socket.accept()

            # 5.为连接上的客户端服务
            process = multiprocessing.Process(target=self.serve_client, args=(new_client_socket,))
            process.start()

            new_client_socket.close()

        # 关闭监听套接字
        self.tcp_server_socket.close()


def main():
    """控制整体,创建一个web服务器对象,然后调用这个对象的run_forever()方法运行"""

    if len(sys.argv) == 3:
        try:
            port = int(sys.argv[1])
            frame_application_str = sys.argv[2]
        except Exception as ret:
            print("请输入正确格式的端口...")
            return
    else:
        print("请按照以下方式运行服务器程序:")
        print("python3 web_server.py 8888 mini_web_frame:application")
        return

    # 使用正则表达式分别提取出web框架和符合WSGI规范的函数名称
    match_obj = re.match(r"([^:]+):(.*)", frame_application_str)
    if match_obj:
        frame_name = match_obj.group(1)
        application_name = match_obj.group(2)
    else:
        print("请按照以下方式运行服务器程序:")
        print("python3 web_server.py 8888 mini_web_framework:application")
        return

    # 通过变量frame_name导入mini_web_framework模块
    web_frame = __import__(frame_name)
    print(web_frame)

    # 使用getattr函数获取mini_web_framework模块中application函数引用
    app = getattr(web_frame, application_name)

    print(app)

    wsgi_server = WSGIServer(port, app)
    wsgi_server.run_forever()


if __name__ == "__main__":
    main()

4. 总结

实际上,上述web服务器程序主要又实现了两个功能:

  • 可以让用户在运行web服务器程序的同时再指定端口号,避免了可能发生的端口冲突;
  • 与web框架程序之间一定程度上的解耦,如:web框架的名称和其中符合WSGI规范的函数application可以是任何名称。

猜你喜欢

转载自blog.csdn.net/weixin_37780776/article/details/106129819