python HTTP Server 文件上传与下载--uploadHttp2.py
python HTTP Server 文件上传与下载
实现在局域网(同一WIFI下) 文件上传与下载
该模块通过实现标准GET在BaseHTTPServer上构建
和HEAD请求。(将所有代码粘贴到同一个py文件中,即可使用)
所需包
基于python3版本实现,python2版本无涉猎
import os
import sys
import argparse
import posixpath
try:
from html import escape
except ImportError:
from cgi import escape
import shutil
import mimetypes
import re
import signal
from io import StringIO, BytesIO
if sys.version_info.major == 3:
# Python3
from urllib.parse import quote
from urllib.parse import unquote
from http.server import HTTPServer
from http.server import BaseHTTPRequestHandler
基本类 简单HTTP服务类
带有GET/HEAD/POST命令的简单HTTP请求处理程序。
这将提供当前目录中的文件及其子目录。
文件的MIME类型由调用.gues_type()方法。
并且可以接收上传的文件由客户提供。
GET/HEAD/POST请求是相同的,除了HEAD请求忽略文件的实际内容。
class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):
server_version = "simple_http_server/" + __version__
def do_GET(self):
"""Serve a GET request."""
fd = self.send_head()
if fd:
shutil.copyfileobj(fd, self.wfile)
fd.close()
def do_HEAD(self):
"""Serve a HEAD request."""
fd = self.send_head()
if fd:
fd.close()
def do_POST(self):
"""Serve a POST request."""
r, info = self.deal_post_data()
print(r, info, "by: ", self.client_address)
f = BytesIO()
f.write(b'<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">')
f.write(b"<html>\n<title>Upload Result Page</title>\n")
f.write(b"<body>\n<h2>Upload Result Page</h2>\n")
f.write(b"<hr>\n")
if r:
f.write(b"<strong>Success:</strong>")
else:
f.write(b"<strong>Failed:</strong>")
f.write(info.encode('utf-8'))
f.write(b"<br><a href=\".\">back</a>")
f.write(b"<hr><small>Powered By: freelamb, check new version at ")
# 原始代码地址 可以参考
f.write(b"<a href=\"https://github.com/freelamb/simple_http_server\">")
f.write(b"here</a>.</small></body>\n</html>\n")
length = f.tell()
f.seek(0)
self.send_response(200)
self.send_header("Content-type", "text/html;charset=utf-8")
self.send_header("Content-Length", str(length))
self.end_headers()
if f:
shutil.copyfileobj(f, self.wfile)
f.close()
def deal_post_data(self):
boundary = self.headers["Content-Type"].split("=")[1].encode('utf-8')
remain_bytes = int(self.headers['content-length'])
line = self.rfile.readline()
remain_bytes -= len(line)
if boundary not in line:
return False, "Content NOT begin with boundary"
line = self.rfile.readline()
remain_bytes -= len(line)
fn = re.findall(r'Content-Disposition.*name="file"; filename="(.*)"', line.decode('utf-8'))
if not fn:
return False, "Can't find out file name..."
path = translate_path(self.path)
fn = os.path.join(path, fn[0])
while os.path.exists(fn):
fn += "_"
line = self.rfile.readline()
remain_bytes -= len(line)
line = self.rfile.readline()
remain_bytes -= len(line)
try:
out = open(fn, 'wb')
except IOError:
return False, "Can't create file to write, do you have permission to write?"
pre_line = self.rfile.readline()
remain_bytes -= len(pre_line)
while remain_bytes > 0:
line = self.rfile.readline()
remain_bytes -= len(line)
if boundary in line:
pre_line = pre_line[0:-1]
if pre_line.endswith(b'\r'):
pre_line = pre_line[0:-1]
out.write(pre_line)
out.close()
return True, "File '%s' upload success!" % fn
else:
out.write(pre_line)
pre_line = line
return False, "Unexpect Ends of data."
def send_head(self):
"""
GET和HEAD命令的通用代码。
这将发送响应代码和MIME标头。
返回值要么是文件对象
(除非命令是HEAD,否则调用方必须将其复制到输出文件中,
并且在任何情况下都必须由调用方关闭),
要么是None,在这种情况下,调用方无需进一步操作。
"""
path = translate_path(self.path)
if os.path.isdir(path):
if not self.path.endswith('/'):
# redirect browser - doing basically what apache does
self.send_response(301)
self.send_header("Location", self.path + "/")
self.end_headers()
return None
for index in "index.html", "index.htm":
index = os.path.join(path, index)
if os.path.exists(index):
path = index
break
else:
return self.list_directory(path)
content_type = self.guess_type(path)
try:
#始终以二进制模式读取。以文本模式打开文件可能会导致
#换行翻译,使内容的实际大小
#传输*小于*内容长度!
f = open(path, 'rb')
except IOError:
self.send_error(404, "File not found")
return None
self.send_response(200)
self.send_header("Content-type", content_type)
fs = os.fstat(f.fileno())
self.send_header("Content-Length", str(fs[6]))
self.send_header("Last-Modified", self.date_time_string(fs.st_mtime))
self.end_headers()
return f
def list_directory(self, path):
"""
帮助程序生成目录列表(缺少index.html)。
返回值为file对象或None(表示错误)。
无论哪种情况,都会发送标头接口与send_head()相同。
"""
try:
list_dir = os.listdir(path)
except os.error:
self.send_error(404, "No permission to list directory")
return None
list_dir.sort(key=lambda a: a.lower())
f = BytesIO()
display_path = escape(unquote(self.path))
f.write(b'<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">')
f.write(b"<html>\n<title>Directory listing for %s</title>\n" % display_path.encode('utf-8'))
f.write(b"<body>\n<h2>Directory listing for %s</h2>\n" % display_path.encode('utf-8'))
f.write(b"<hr>\n")
f.write(b"<form ENCTYPE=\"multipart/form-data\" method=\"post\">")
f.write(b"<input name=\"file\" type=\"file\"/>")
f.write(b"<input type=\"submit\" value=\"upload\"/></form>\n")
f.write(b"<hr>\n<ul>\n")
for name in list_dir:
fullname = os.path.join(path, name)
display_name = linkname = name
# Append / for directories or @ for symbolic links
if os.path.isdir(fullname):
display_name = name + "/"
linkname = name + "/"
if os.path.islink(fullname):
display_name = name + "@"
# Note: a link to a directory displays with @ and links with /
f.write(
b'<li><a href="%s">%s</a>\n' % (quote(linkname).encode('utf-8'), escape(display_name).encode('utf-8')))
f.write(b"</ul>\n<hr>\n</body>\n</html>\n")
length = f.tell()
f.seek(0)
self.send_response(200)
self.send_header("Content-type", "text/html;charset=utf-8")
self.send_header("Content-Length", str(length))
self.end_headers()
return f
def guess_type(self, path):
"""
参数是PATH(文件名)。
返回值是表单类型/子类型的字符串,
可用于MIME内容类型标头。
默认实现在self.extensions_map表中查找文件的扩展名,默认使用application/octet流;
"""
base, ext = posixpath.splitext(path)
if ext in self.extensions_map:
return self.extensions_map[ext]
ext = ext.lower()
if ext in self.extensions_map:
return self.extensions_map[ext]
else:
return self.extensions_map['']
if not mimetypes.inited:
mimetypes.init() # try to read system mime.types
extensions_map = mimetypes.types_map.copy()
extensions_map.update({
'': 'application/octet-stream', # Default
'.py': 'text/plain',
'.c': 'text/plain',
'.h': 'text/plain',
})
文件路径处理
将/分隔的PATH转换为本地文件名语法。
对本地文件系统有特殊意义的组件(例如驱动器或目录名),那么可能会被阻止或诊断。
def translate_path(path):
# abandon query parameters
path = path.split('?', 1)[0]
path = path.split('#', 1)[0]
path = posixpath.normpath(unquote(path))
words = path.split('/')
words = filter(None, words)
# 获取你的py文件存放的路径
path = os.getcwd()
# 可在此自定义路径(如果有其路径)
path = path+"/file_xxx/xxx"
for word in words:
drive, word = os.path.splitdrive(word)
head, word = os.path.split(word)
if word in (os.curdir, os.pardir):
continue
path = os.path.join(path, word)
return path
信息提醒
如果HTTP Server被关闭
def signal_handler(signal, frame):
print("You choose to stop me.")
exit()
HTTP Server 初始化
设置HTTP Server初始数值,基于自己电脑设置。
对于IP来说,双方ip设置应一样。
# 版本设置 自定义
__version__ = "0.3.1"
def _argparse():
parser = argparse.ArgumentParser()
# 一般用户电脑用做服务器,每次开机后,ip4地址可能会发生变化
ip = input("请输入IP地址:")
parser.add_argument('--bind', '-b', metavar='ADDRESS', default=ip,
help='Specify alternate bind address [default: all interfaces]')
parser.add_argument('--version', '-v', action='version', version=__version__)
parser.add_argument('port', action='store', default=8000, type=int, nargs='?',
help='Specify alternate port [default: 8000]')
return parser.parse_args()
启用HTTP Server
py程序启动后,会输出网址,点击后,会自动进入HTTP服务,可以进行文件传输操作。
def main():
args = _argparse()
# print(args)
server_address = (args.bind, args.port)
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
httpd = HTTPServer(server_address, SimpleHTTPRequestHandler)
server = httpd.socket.getsockname()
print("server_version: " + SimpleHTTPRequestHandler.server_version + ", python_version: " + SimpleHTTPRequestHandler.sys_version)
print("sys encoding: " + sys.getdefaultencoding())
print("Serving http on: " + str(server[0]) + ", port: " + str(server[1]) + " ... (http://" + server[0] + ":" + str(server[1]) + "/)")
httpd.serve_forever()
if __name__ == '__main__':
main()
完整代码
import os import sys import argparse import posixpath try: from html import escape except ImportError: from cgi import escape import shutil import mimetypes import re import signal from io import StringIO, BytesIO if sys.version_info.major == 3: # Python3 from urllib.parse import quote from urllib.parse import unquote from http.server import HTTPServer from http.server import BaseHTTPRequestHandler # 版本设置 自定义 __version__ = "0.3.1" def _argparse(): parser = argparse.ArgumentParser() # 一般用户电脑用做服务器,每次开机后,ip4地址可能会发生变化 #ip = input("请输入IP地址:") parser.add_argument('--bind', '--b', metavar='ADDRESS', default="172.21.137.235", help='Specify alternate bind address [default: all interfaces]') parser.add_argument('--version', '-v', action='version', version=__version__) parser.add_argument('port', action='store', default=8000, type=int, nargs='?', help='Specify alternate port [default: 8000]') return parser.parse_args() class SimpleHTTPRequestHandler(BaseHTTPRequestHandler): server_version = "simple_http_server/" + __version__ def do_GET(self): """Serve a GET request.""" fd = self.send_head() if fd: shutil.copyfileobj(fd, self.wfile) fd.close() def do_HEAD(self): """Serve a HEAD request.""" fd = self.send_head() if fd: fd.close() def do_POST(self): """Serve a POST request.""" r, info = self.deal_post_data() print(r, info, "by: ", self.client_address) f = BytesIO() f.write(b'<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">') f.write(b"<html>\n<title>Upload Result Page</title>\n") f.write(b"<body>\n<h2>Upload Result Page</h2>\n") f.write(b"<hr>\n") if r: f.write(b"<strong>Success:</strong>") else: f.write(b"<strong>Failed:</strong>") f.write(info.encode('utf-8')) f.write(b"<br><a href=\".\">back</a>") f.write(b"<hr><small>Powered By: freelamb, check new version at ") # 原始代码地址 可以参考 f.write(b"<a href=\"https://github.com/freelamb/simple_http_server\">") f.write(b"here</a>.</small></body>\n</html>\n") length = f.tell() f.seek(0) self.send_response(200) self.send_header("Content-type", "text/html;charset=utf-8") self.send_header("Content-Length", str(length)) self.end_headers() if f: shutil.copyfileobj(f, self.wfile) f.close() def deal_post_data(self): boundary = self.headers["Content-Type"].split("=")[1].encode('utf-8') remain_bytes = int(self.headers['content-length']) line = self.rfile.readline() remain_bytes -= len(line) if boundary not in line: return False, "Content NOT begin with boundary" line = self.rfile.readline() remain_bytes -= len(line) fn = re.findall(r'Content-Disposition.*name="file"; filename="(.*)"', line.decode('utf-8')) if not fn: return False, "Can't find out file name..." path = translate_path(self.path) fn = os.path.join(path, fn[0]) while os.path.exists(fn): fn += "_" line = self.rfile.readline() remain_bytes -= len(line) line = self.rfile.readline() remain_bytes -= len(line) try: out = open(fn, 'wb') except IOError: return False, "Can't create file to write, do you have permission to write?" pre_line = self.rfile.readline() remain_bytes -= len(pre_line) while remain_bytes > 0: line = self.rfile.readline() remain_bytes -= len(line) if boundary in line: pre_line = pre_line[0:-1] if pre_line.endswith(b'\r'): pre_line = pre_line[0:-1] out.write(pre_line) out.close() return True, "File '%s' upload success!" % fn else: out.write(pre_line) pre_line = line return False, "Unexpect Ends of data." def send_head(self): """ GET和HEAD命令的通用代码。 这将发送响应代码和MIME标头。 返回值要么是文件对象 (除非命令是HEAD,否则调用方必须将其复制到输出文件中, 并且在任何情况下都必须由调用方关闭), 要么是None,在这种情况下,调用方无需进一步操作。 """ path = translate_path(self.path) if os.path.isdir(path): if not self.path.endswith('/'): # redirect browser - doing basically what apache does self.send_response(301) self.send_header("Location", self.path + "/") self.end_headers() return None for index in "index.html", "index.htm": index = os.path.join(path, index) if os.path.exists(index): path = index break else: return self.list_directory(path) content_type = self.guess_type(path) try: #始终以二进制模式读取。以文本模式打开文件可能会导致 #换行翻译,使内容的实际大小 #传输*小于*内容长度! f = open(path, 'rb') except IOError: self.send_error(404, "File not found") return None self.send_response(200) self.send_header("Content-type", content_type) fs = os.fstat(f.fileno()) self.send_header("Content-Length", str(fs[6])) self.send_header("Last-Modified", self.date_time_string(fs.st_mtime)) self.end_headers() return f def list_directory(self, path): """ 帮助程序生成目录列表(缺少index.html)。 返回值为file对象或None(表示错误)。 无论哪种情况,都会发送标头接口与send_head()相同。 """ try: list_dir = os.listdir(path) except os.error: self.send_error(404, "No permission to list directory") return None list_dir.sort(key=lambda a: a.lower()) f = BytesIO() display_path = escape(unquote(self.path)) f.write(b'<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">') f.write(b"<html>\n<title>Directory listing for %s</title>\n" % display_path.encode('utf-8')) f.write(b"<body>\n<h2>Directory listing for %s</h2>\n" % display_path.encode('utf-8')) f.write(b"<hr>\n") f.write(b"<form ENCTYPE=\"multipart/form-data\" method=\"post\">") f.write(b"<input name=\"file\" type=\"file\"/>") f.write(b"<input type=\"submit\" value=\"upload\"/></form>\n") f.write(b"<hr>\n<ul>\n") for name in list_dir: fullname = os.path.join(path, name) display_name = linkname = name # Append / for directories or @ for symbolic links if os.path.isdir(fullname): display_name = name + "/" linkname = name + "/" if os.path.islink(fullname): display_name = name + "@" # Note: a link to a directory displays with @ and links with / f.write( b'<li><a href="%s">%s</a>\n' % (quote(linkname).encode('utf-8'), escape(display_name).encode('utf-8'))) f.write(b"</ul>\n<hr>\n</body>\n</html>\n") length = f.tell() f.seek(0) self.send_response(200) self.send_header("Content-type", "text/html;charset=utf-8") self.send_header("Content-Length", str(length)) self.end_headers() return f def guess_type(self, path): """ 参数是PATH(文件名)。 返回值是表单类型/子类型的字符串, 可用于MIME内容类型标头。 默认实现在self.extensions_map表中查找文件的扩展名,默认使用application/octet流; """ base, ext = posixpath.splitext(path) if ext in self.extensions_map: return self.extensions_map[ext] ext = ext.lower() if ext in self.extensions_map: return self.extensions_map[ext] else: return self.extensions_map[''] if not mimetypes.inited: mimetypes.init() # try to read system mime.types extensions_map = mimetypes.types_map.copy() extensions_map.update({ '': 'application/octet-stream', # Default '.py': 'text/plain', '.c': 'text/plain', '.h': 'text/plain', }) ########################################### def translate_path(path): # abandon query parameters path = path.split('?', 1)[0] path = path.split('#', 1)[0] path = posixpath.normpath(unquote(path)) words = path.split('/') words = filter(None, words) # 获取你的py文件存放的路径 path = os.getcwd() # 可在此自定义路径(如果有其路径) #path = path+"/file_xxx/xxx" for word in words: drive, word = os.path.splitdrive(word) head, word = os.path.split(word) if word in (os.curdir, os.pardir): continue path = os.path.join(path, word) return path def signal_handler(signal, frame): print("You choose to stop me.") exit() def main(): args = _argparse() # print(args) server_address = (args.bind, args.port) signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGTERM, signal_handler) httpd = HTTPServer(server_address, SimpleHTTPRequestHandler) server = httpd.socket.getsockname() print("python_version: " + SimpleHTTPRequestHandler.sys_version) print("server_version: " + SimpleHTTPRequestHandler.server_version) print("sys encoding: " + sys.getdefaultencoding()) print("Serving http on: " + str(server[0]) + ", port: " + str(server[1]) + " ... (http://" + server[0] + ":" + str(server[1]) + "/)") httpd.serve_forever() if __name__ == '__main__': main()
【出处】:https://blog.csdn.net/love_wgll/article/details/129155712
=======================================================================================
个人使用
针对以上代码,后续将需要优化:
1)上传大文件时,需要禁用upload按钮,防止多次提交(已完成)
2)使用配置项,优化参数项和帮助
3)translate_path应该放到类内,自动创建文件夹--(已完成)
4)下载文件列表,过滤当前py程序--(已完成)
5)自定义输出日志,打印:日期,调用堆栈信息--(已完成)
6)优化基类的BaseHTTPRequestHandler的send_response方法的服务端打印信息
7)空文件上传时的bug,会上传一个下划线的空文件(已完成,前端不做校验,实现太麻烦了)
8)最小化运行,双击状态栏图标显示界面
版本1
优化说明:
1)路径转换:translate_path方法,作为对象方法,放到类内
2)下载文件列表,过滤当前py程序
import os import sys import argparse import posixpath try: from html import escape except ImportError: from cgi import escape import shutil import mimetypes import re import signal from io import StringIO, BytesIO if sys.version_info.major == 3: # Python3 from urllib.parse import quote from urllib.parse import unquote from http.server import HTTPServer from http.server import BaseHTTPRequestHandler # 版本设置 自定义 __version__ = "0.3.1" def _argparse(): parser = argparse.ArgumentParser() # 一般用户电脑用做服务器,每次开机后,ip4地址可能会发生变化 #ip = input("请输入IP地址:") parser.add_argument('--bind', '--b', metavar='ADDRESS', default="172.21.137.235", help='Specify alternate bind address [default: all interfaces]') parser.add_argument('--version', '-v', action='version', version=__version__) parser.add_argument('port', action='store', default=8000, type=int, nargs='?', help='Specify alternate port [default: 8000]') return parser.parse_args() class SimpleHTTPRequestHandler(BaseHTTPRequestHandler): server_version = "simple_http_server/" + __version__ def do_GET(self): """Serve a GET request.""" fd = self.send_head() if fd: shutil.copyfileobj(fd, self.wfile) fd.close() def do_HEAD(self): """Serve a HEAD request.""" fd = self.send_head() if fd: fd.close() def do_POST(self): """Serve a POST request.""" r, info = self.deal_post_data() print(r, info, "by: ", self.client_address) f = BytesIO() f.write(b'<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">') f.write(b"<html>\n<title>Upload Result Page</title>\n") f.write(b"<body>\n<h2>Upload Result Page</h2>\n") f.write(b"<hr>\n") if r: f.write(b"<strong>Success:</strong>") else: f.write(b"<strong>Failed:</strong>") f.write(info.encode('utf-8')) f.write(b"<br><a href=\".\">back</a>") f.write(b"<hr><small>Powered By: freelamb, check new version at ") # 原始代码地址 可以参考 f.write(b"<a href=\"https://github.com/freelamb/simple_http_server\">") f.write(b"here</a>.</small></body>\n</html>\n") length = f.tell() f.seek(0) self.send_response(200) self.send_header("Content-type", "text/html;charset=utf-8") self.send_header("Content-Length", str(length)) self.end_headers() if f: shutil.copyfileobj(f, self.wfile) f.close() def deal_post_data(self): boundary = self.headers["Content-Type"].split("=")[1].encode('utf-8') remain_bytes = int(self.headers['content-length']) line = self.rfile.readline() remain_bytes -= len(line) if boundary not in line: return False, "Content NOT begin with boundary" line = self.rfile.readline() remain_bytes -= len(line) fn = re.findall(r'Content-Disposition.*name="file"; filename="(.*)"', line.decode('utf-8')) if not fn: return False, "Can't find out file name..." path = self.translate_path(self.path) fn = os.path.join(path, fn[0]) while os.path.exists(fn): fn += "_" line = self.rfile.readline() remain_bytes -= len(line) line = self.rfile.readline() remain_bytes -= len(line) try: out = open(fn, 'wb') except IOError: return False, "Can't create file to write, do you have permission to write?" pre_line = self.rfile.readline() remain_bytes -= len(pre_line) while remain_bytes > 0: line = self.rfile.readline() remain_bytes -= len(line) if boundary in line: pre_line = pre_line[0:-1] if pre_line.endswith(b'\r'): pre_line = pre_line[0:-1] out.write(pre_line) out.close() return True, "File '%s' upload success!" % fn else: out.write(pre_line) pre_line = line return False, "Unexpect Ends of data." def send_head(self): """ GET和HEAD命令的通用代码。 这将发送响应代码和MIME标头。 返回值要么是文件对象 (除非命令是HEAD,否则调用方必须将其复制到输出文件中, 并且在任何情况下都必须由调用方关闭), 要么是None,在这种情况下,调用方无需进一步操作。 """ path = self.translate_path(self.path) if os.path.isdir(path): if not self.path.endswith('/'): # redirect browser - doing basically what apache does self.send_response(301) self.send_header("Location", self.path + "/") self.end_headers() return None for index in "index.html", "index.htm": index = os.path.join(path, index) if os.path.exists(index): path = index break else: return self.list_directory(path) content_type = self.guess_type(path) try: #始终以二进制模式读取。以文本模式打开文件可能会导致 #换行翻译,使内容的实际大小 #传输*小于*内容长度! f = open(path, 'rb') except IOError: self.send_error(404, "File not found") return None self.send_response(200) self.send_header("Content-type", content_type) fs = os.fstat(f.fileno()) self.send_header("Content-Length", str(fs[6])) self.send_header("Last-Modified", self.date_time_string(fs.st_mtime)) self.end_headers() return f def list_directory(self, path): """ 帮助程序生成目录列表(缺少index.html)。 返回值为file对象或None(表示错误)。 无论哪种情况,都会发送标头接口与send_head()相同。 """ try: list_dir = os.listdir(path) except os.error: self.send_error(404, "No permission to list directory") return None list_dir.sort(key=lambda a: a.lower()) f = BytesIO() display_path = escape(unquote(self.path)) f.write(b'<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">') f.write(b"<html>\n<title>Directory listing for %s</title>\n" % display_path.encode('utf-8')) f.write(b"<body>\n<h2>Directory listing for %s</h2>\n" % display_path.encode('utf-8')) f.write(b"<hr>\n") f.write(b"<form ENCTYPE=\"multipart/form-data\" method=\"post\">") f.write(b"<input name=\"file\" type=\"file\"/>") f.write(b"<input type=\"submit\" value=\"upload\" onclick=\"this.disabled=false\"/></form>\n") f.write(b"<hr>\n<ul>\n") for name in list_dir: if name==sys.argv[0]:continue fullname = os.path.join(path, name) display_name = linkname = name # Append / for directories or @ for symbolic links if os.path.isdir(fullname): display_name = name + "/" linkname = name + "/" if os.path.islink(fullname): display_name = name + "@" # Note: a link to a directory displays with @ and links with / f.write( b'<li><a href="%s">%s</a>\n' % (quote(linkname).encode('utf-8'), escape(display_name).encode('utf-8'))) f.write(b"</ul>\n<hr>\n</body>\n</html>\n") length = f.tell() f.seek(0) self.send_response(200) self.send_header("Content-type", "text/html;charset=utf-8") self.send_header("Content-Length", str(length)) self.end_headers() return f def guess_type(self, path): """ 参数是PATH(文件名)。 返回值是表单类型/子类型的字符串, 可用于MIME内容类型标头。 默认实现在self.extensions_map表中查找文件的扩展名,默认使用application/octet流; """ base, ext = posixpath.splitext(path) if ext in self.extensions_map: return self.extensions_map[ext] ext = ext.lower() if ext in self.extensions_map: return self.extensions_map[ext] else: return self.extensions_map[''] if not mimetypes.inited: mimetypes.init() # try to read system mime.types extensions_map = mimetypes.types_map.copy() extensions_map.update({ '': 'application/octet-stream', # Default '.py': 'text/plain', '.c': 'text/plain', '.h': 'text/plain', }) ########################################### def translate_path(self,path): # abandon query parameters path = path.split('?', 1)[0] path = path.split('#', 1)[0] path = posixpath.normpath(unquote(path)) words = path.split('/') words = filter(None, words) # 获取你的py文件存放的路径 path = os.getcwd() # 可在此自定义路径(如果有其路径) #path = path+"/file_xxx/xxx" for word in words: drive, word = os.path.splitdrive(word) head, word = os.path.split(word) if word in (os.curdir, os.pardir): continue path = os.path.join(path, word) return path def signal_handler(signal, frame): print("You choose to stop me.") exit() def main(): args = _argparse() # print(args) server_address = (args.bind, args.port) signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGTERM, signal_handler) httpd = HTTPServer(server_address, SimpleHTTPRequestHandler) server = httpd.socket.getsockname() print("python_version: " + SimpleHTTPRequestHandler.sys_version) print("server_version: " + SimpleHTTPRequestHandler.server_version) print("sys encoding: " + sys.getdefaultencoding()) print("Serving http on: " + str(server[0]) + ", port: " + str(server[1]) + " ... (http://" + server[0] + ":" + str(server[1]) + "/)") httpd.serve_forever() if __name__ == '__main__': main()
版本2
优化说明:
1)自定义输出日志,打印:日期,调用堆栈信息
2)自动创建文件夹
import os import sys import argparse import posixpath from datetime import datetime import traceback try: from html import escape except ImportError: from cgi import escape import shutil import mimetypes import re import signal from io import StringIO, BytesIO if sys.version_info.major == 3: # Python3 from urllib.parse import quote from urllib.parse import unquote from http.server import HTTPServer from http.server import BaseHTTPRequestHandler # 版本设置 自定义 __version__ = "0.3.1" def _argparse(): parser = argparse.ArgumentParser() # 一般用户电脑用做服务器,每次开机后,ip4地址可能会发生变化 #ip = input("请输入IP地址:") parser.add_argument('--bind', '-b', metavar='ADDRESS', default="127.0.0.1", help='Specify alternate bind address [default: all interfaces]') parser.add_argument('--version', '-v', action='version', version=__version__) parser.add_argument('port', action='store', default=8000, type=int, nargs='?', help='Specify alternate port [default: 8000]') return parser.parse_args() class SimpleHTTPRequestHandler(BaseHTTPRequestHandler): server_version = "simple_http_server/" + __version__ def do_GET(self): """Serve a GET request.""" fd = self.send_head() if fd: shutil.copyfileobj(fd, self.wfile) fd.close() def do_HEAD(self): """Serve a HEAD request.""" fd = self.send_head() if fd: fd.close() def do_POST(self): """Serve a POST request.""" r, info = self.deal_post_data() logInfo(r, info, "by: ", self.client_address) f = BytesIO() f.write(b'<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">') f.write(b"<html>\n<title>Upload Result Page</title>\n") f.write(b"<body>\n<h2>Upload Result Page</h2>\n") f.write(b"<hr>\n") if r: f.write(b"<strong>Success:</strong>") else: f.write(b"<strong>Failed:</strong>") f.write(info.encode('utf-8')) f.write(b"<br><a href=\".\">back</a>") f.write(b"<hr><small>Powered By: freelamb, check new version at ") # 原始代码地址 可以参考 f.write(b"<a href=\"https://github.com/freelamb/simple_http_server\">") f.write(b"here</a>.</small></body>\n</html>\n") length = f.tell() f.seek(0) self.send_response(200) self.send_header("Content-type", "text/html;charset=utf-8") self.send_header("Content-Length", str(length)) self.end_headers() if f: shutil.copyfileobj(f, self.wfile) f.close() def deal_post_data(self): boundary = self.headers["Content-Type"].split("=")[1].encode('utf-8') remain_bytes = int(self.headers['content-length']) line = self.rfile.readline() remain_bytes -= len(line) if boundary not in line: return False, "Content NOT begin with boundary" line = self.rfile.readline() remain_bytes -= len(line) fn = re.findall(r'Content-Disposition.*name="file"; filename="(.*)"', line.decode('utf-8')) if not fn: return False, "Can't find out file name..." path = self.translate_path(self.path) fn = os.path.join(path, fn[0]) while os.path.exists(fn): fn += "_" line = self.rfile.readline() remain_bytes -= len(line) line = self.rfile.readline() remain_bytes -= len(line) try: out = open(fn, 'wb') except IOError: return False, "Can't create file to write, do you have permission to write?" pre_line = self.rfile.readline() remain_bytes -= len(pre_line) while remain_bytes > 0: line = self.rfile.readline() remain_bytes -= len(line) if boundary in line: pre_line = pre_line[0:-1] if pre_line.endswith(b'\r'): pre_line = pre_line[0:-1] out.write(pre_line) out.close() return True, "File '%s' upload success!" % fn else: out.write(pre_line) pre_line = line return False, "Unexpect Ends of data." def send_head(self): """ GET和HEAD命令的通用代码。 这将发送响应代码和MIME标头。 返回值要么是文件对象 (除非命令是HEAD,否则调用方必须将其复制到输出文件中, 并且在任何情况下都必须由调用方关闭), 要么是None,在这种情况下,调用方无需进一步操作。 """ path = self.translate_path(self.path) if os.path.isdir(path): if not self.path.endswith('/'): # redirect browser - doing basically what apache does self.send_response(301) self.send_header("Location", self.path + "/") self.end_headers() return None for index in "index.html", "index.htm": index = os.path.join(path, index) if os.path.exists(index): path = index break else: return self.list_directory(path) content_type = self.guess_type(path) try: #始终以二进制模式读取。以文本模式打开文件可能会导致 #换行翻译,使内容的实际大小 #传输*小于*内容长度! f = open(path, 'rb') except IOError: self.send_error(404, "File not found") return None print("-----------------------",self) self.send_response(200) self.send_header("Content-type", content_type) fs = os.fstat(f.fileno()) self.send_header("Content-Length", str(fs[6])) self.send_header("Last-Modified", self.date_time_string(fs.st_mtime)) self.end_headers() return f def list_directory(self, path): """ 帮助程序生成目录列表(缺少index.html)。 返回值为file对象或None(表示错误)。 无论哪种情况,都会发送标头接口与send_head()相同。 """ try: list_dir = os.listdir(path) except os.error: self.send_error(404, "No permission to list directory") return None list_dir.sort(key=lambda a: a.lower()) f = BytesIO() display_path = escape(unquote(self.path)) f.write(b'<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">') f.write(b"<html>\n<title>Directory listing for %s</title>\n" % display_path.encode('utf-8')) f.write(b"<body>\n<h2>Directory listing for %s</h2>\n" % display_path.encode('utf-8')) f.write(b"<hr>\n") f.write(b"<form ENCTYPE=\"multipart/form-data\" method=\"post\">") f.write(b"<input name=\"file\" type=\"file\"/>") f.write(b"<input type=\"submit\" value=\"upload\" onclick=\"this.disabled=false\"/></form>\n") f.write(b"<hr>\n<ul>\n") for name in list_dir: if name==sys.argv[0]:continue fullname = os.path.join(path, name) display_name = linkname = name # Append / for directories or @ for symbolic links if os.path.isdir(fullname): display_name = name + "/" linkname = name + "/" if os.path.islink(fullname): display_name = name + "@" # Note: a link to a directory displays with @ and links with / f.write( b'<li><a href="%s">%s</a>\n' % (quote(linkname).encode('utf-8'), escape(display_name).encode('utf-8'))) f.write(b"</ul>\n<hr>\n</body>\n</html>\n") length = f.tell() f.seek(0) self.send_response(200) self.send_header("Content-type", "text/html;charset=utf-8") self.send_header("Content-Length", str(length)) self.end_headers() return f def guess_type(self, path): """ 参数是PATH(文件名)。 返回值是表单类型/子类型的字符串, 可用于MIME内容类型标头。 默认实现在self.extensions_map表中查找文件的扩展名,默认使用application/octet流; """ base, ext = posixpath.splitext(path) if ext in self.extensions_map: return self.extensions_map[ext] ext = ext.lower() if ext in self.extensions_map: return self.extensions_map[ext] else: return self.extensions_map[''] if not mimetypes.inited: mimetypes.init() # try to read system mime.types extensions_map = mimetypes.types_map.copy() extensions_map.update({ '': 'application/octet-stream', # Default '.py': 'text/plain', '.c': 'text/plain', '.h': 'text/plain', }) ########################################### def translate_path(self,path): # abandon query parameters path = path.split('?', 1)[0] path = path.split('#', 1)[0] path = posixpath.normpath(unquote(path)) words = path.split('/') words = filter(None, words) # 获取你的py文件存放的路径 path = os.getcwd() # 可在此自定义路径(如果有其路径) #path = path+"/file_xxx/xxx" # 获取文件所在的文件夹路径 #folder_path = os.path.dirname(path) # 判断文件夹是否存在,不存在则创建 if not os.path.exists(path): os.makedirs(path) for word in words: drive, word = os.path.splitdrive(word) head, word = os.path.split(word) if word in (os.curdir, os.pardir): continue path = os.path.join(path, word) return path def signal_handler(signal, frame): logInfo("You choose to stop me.") exit() def main(): args = _argparse() # logInfo(args) server_address = (args.bind, args.port) signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGTERM, signal_handler) httpd = HTTPServer(server_address, SimpleHTTPRequestHandler) server = httpd.socket.getsockname() logInfo("server: ", server) logInfo("python_version: " + SimpleHTTPRequestHandler.sys_version) logInfo("server_version: " + SimpleHTTPRequestHandler.server_version) logInfo("sys encoding: " + sys.getdefaultencoding()) logInfo("Serving http on: http://" + server[0] + ":" + str(server[1]) + "/") httpd.serve_forever() def test(): print("test") def logInfo(*msg): print("----------") print_callstack() print("【%s】"%datetime.now(),*msg) #print("Result: %s" % msg) def print_callstack(): # 获取当前的调用栈信息 stack = traceback.extract_stack()[:-1] # 去除最后一行(print_callstack函数本身) for frame in stack: filename, line_number, function_name, code = frame print("==",frame) if code is not None and len(code) > 0: code = f"{filename}, line:{line_number} - {function_name} -- {code}" else: code = f"{filename}, {line_number} - {function_name}" print(code) if __name__ == '__main__': print("============================================================================") #test() main()
版本3
优化说明:
1)加入debug开关,打印堆栈信息
2)前端防止上传按钮多次提交
3)后台判断上传空文件的问题
import os import sys import argparse import posixpath from datetime import datetime import traceback try: from html import escape except ImportError: from cgi import escape import shutil import mimetypes import re import signal from io import StringIO, BytesIO if sys.version_info.major == 3: # Python3 from urllib.parse import quote from urllib.parse import unquote from http.server import HTTPServer from http.server import BaseHTTPRequestHandler __debug=False # 版本设置 自定义 __version__ = "0.3.1" def _argparse(): parser = argparse.ArgumentParser() # 一般用户电脑用做服务器,每次开机后,ip4地址可能会发生变化 #ip = input("请输入IP地址:") parser.add_argument('--bind', '-b', metavar='ADDRESS', default="127.0.0.1", help='Specify alternate bind address [default: all interfaces]') parser.add_argument('--version', '-v', action='version', version=__version__) parser.add_argument('port', action='store', default=8000, type=int, nargs='?', help='Specify alternate port [default: 8000]') return parser.parse_args() class SimpleHTTPRequestHandler(BaseHTTPRequestHandler): server_version = "simple_http_server/" + __version__ def do_GET(self): """Serve a GET request.""" fd = self.send_head() if fd: shutil.copyfileobj(fd, self.wfile) fd.close() def do_HEAD(self): """Serve a HEAD request.""" fd = self.send_head() if fd: fd.close() def do_POST(self): """Serve a POST request.""" r, info = self.deal_post_data() logInfo(r, info, "by: ", self.client_address) f = BytesIO() f.write(b'<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">') f.write(b"<html>\n<title>Upload Result Page</title>\n") f.write(b"<body>\n<h2>Upload Result Page</h2>\n") f.write(b"<hr>\n") if r: f.write(b"<strong>Success:</strong>") else: f.write(b"<strong>Failed:</strong>") f.write(info.encode('utf-8')) f.write(b"<br><a href=\".\">back</a>") f.write(b"<hr><small>Powered By: freelamb, check new version at ") # 原始代码地址 可以参考 f.write(b"<a href=\"https://github.com/freelamb/simple_http_server\">") f.write(b"here</a>.</small></body>\n</html>\n") length = f.tell() f.seek(0) self.send_response(200) self.send_header("Content-type", "text/html;charset=utf-8") self.send_header("Content-Length", str(length)) self.end_headers() if f: shutil.copyfileobj(f, self.wfile) f.close() def deal_post_data(self): boundary = self.headers["Content-Type"].split("=")[1].encode('utf-8') remain_bytes = int(self.headers['content-length']) line = self.rfile.readline() logInfo("=======",line) logInfo("=======",remain_bytes) remain_bytes -= len(line) logInfo("boundary=======",boundary) if boundary not in line: return False, "Content NOT begin with boundary" line = self.rfile.readline() logInfo("line=======",line) remain_bytes -= len(line) fn = re.findall(r'Content-Disposition.*name="file"; filename="(.*)"', line.decode('utf-8')) fn = [i for i in fn if i] if not fn: return False, "Can't find upload file ......" path = self.translate_path(self.path) fn = os.path.join(path, fn[0]) while os.path.exists(fn): fn += "_" line = self.rfile.readline() remain_bytes -= len(line) line = self.rfile.readline() remain_bytes -= len(line) try: out = open(fn, 'wb') except IOError: return False, "Can't create file to write, do you have permission to write?" pre_line = self.rfile.readline() remain_bytes -= len(pre_line) while remain_bytes > 0: line = self.rfile.readline() remain_bytes -= len(line) if boundary in line: pre_line = pre_line[0:-1] if pre_line.endswith(b'\r'): pre_line = pre_line[0:-1] out.write(pre_line) out.close() return True, "File '%s' upload success!" % fn else: out.write(pre_line) pre_line = line return False, "Unexpect Ends of data." def send_head(self): """ GET和HEAD命令的通用代码。 这将发送响应代码和MIME标头。 返回值要么是文件对象 (除非命令是HEAD,否则调用方必须将其复制到输出文件中, 并且在任何情况下都必须由调用方关闭), 要么是None,在这种情况下,调用方无需进一步操作。 """ path = self.translate_path(self.path) if os.path.isdir(path): if not self.path.endswith('/'): # redirect browser - doing basically what apache does self.send_response(301) self.send_header("Location", self.path + "/") self.end_headers() return None for index in "index.html", "index.htm": index = os.path.join(path, index) if os.path.exists(index): path = index break else: return self.list_directory(path) content_type = self.guess_type(path) try: #始终以二进制模式读取。以文本模式打开文件可能会导致 #换行翻译,使内容的实际大小 #传输*小于*内容长度! f = open(path, 'rb') except IOError: self.send_error(404, "File not found") return None print("-----------------------",self) self.send_response(200) self.send_header("Content-type", content_type) fs = os.fstat(f.fileno()) self.send_header("Content-Length", str(fs[6])) self.send_header("Last-Modified", self.date_time_string(fs.st_mtime)) self.end_headers() return f def list_directory(self, path): """ 帮助程序生成目录列表(缺少index.html)。 返回值为file对象或None(表示错误)。 无论哪种情况,都会发送标头接口与send_head()相同。 """ try: list_dir = os.listdir(path) except os.error: self.send_error(404, "No permission to list directory") return None list_dir.sort(key=lambda a: a.lower()) f = BytesIO() display_path = escape(unquote(self.path)) f.write(b'<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">') f.write(b"<html>\n<title>Directory listing for %s</title>\n" % display_path.encode('utf-8')) f.write(b"<body>\n<h2>Directory listing for %s</h2>\n" % display_path.encode('utf-8')) f.write(b"<hr>\n") f.write(b"<form ENCTYPE=\"multipart/form-data\" method=\"post\">") f.write(b"<input name=\"file\" type=\"file\"/>") #$("input[name=file]").files.length f.write(b"<input type=\"submit\" value=\"upload\" onclick=\"this.form.submit();this.disabled=true;\"/></form>\n") f.write(b"<hr>\n<ul>\n") for name in list_dir: if name==sys.argv[0]:continue fullname = os.path.join(path, name) display_name = linkname = name # Append / for directories or @ for symbolic links if os.path.isdir(fullname): display_name = name + "/" linkname = name + "/" if os.path.islink(fullname): display_name = name + "@" # Note: a link to a directory displays with @ and links with / f.write( b'<li><a href="%s">%s</a>\n' % (quote(linkname).encode('utf-8'), escape(display_name).encode('utf-8'))) f.write(b"</ul>\n<hr>\n</body>\n</html>\n") length = f.tell() f.seek(0) self.send_response(200) self.send_header("Content-type", "text/html;charset=utf-8") self.send_header("Content-Length", str(length)) self.end_headers() return f def guess_type(self, path): """ 参数是PATH(文件名)。 返回值是表单类型/子类型的字符串, 可用于MIME内容类型标头。 默认实现在self.extensions_map表中查找文件的扩展名,默认使用application/octet流; """ base, ext = posixpath.splitext(path) if ext in self.extensions_map: return self.extensions_map[ext] ext = ext.lower() if ext in self.extensions_map: return self.extensions_map[ext] else: return self.extensions_map[''] if not mimetypes.inited: mimetypes.init() # try to read system mime.types extensions_map = mimetypes.types_map.copy() extensions_map.update({ '': 'application/octet-stream', # Default '.py': 'text/plain', '.c': 'text/plain', '.h': 'text/plain', }) ########################################### def translate_path(self,path): # abandon query parameters path = path.split('?', 1)[0] path = path.split('#', 1)[0] path = posixpath.normpath(unquote(path)) words = path.split('/') words = filter(None, words) # 获取你的py文件存放的路径 path = os.getcwd() # 可在此自定义路径(如果有其路径) #path = path+"/file_xxx/xxx" # 获取文件所在的文件夹路径 #folder_path = os.path.dirname(path) # 判断文件夹是否存在,不存在则创建 if not os.path.exists(path): os.makedirs(path) for word in words: drive, word = os.path.splitdrive(word) head, word = os.path.split(word) if word in (os.curdir, os.pardir): continue path = os.path.join(path, word) return path def signal_handler(signal, frame): logInfo("You choose to stop me.") exit() def main(): args = _argparse() # logInfo(args) server_address = (args.bind, args.port) signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGTERM, signal_handler) httpd = HTTPServer(server_address, SimpleHTTPRequestHandler) server = httpd.socket.getsockname() logInfo("server: ", server) logInfo("python_version: " + SimpleHTTPRequestHandler.sys_version) logInfo("server_version: " + SimpleHTTPRequestHandler.server_version) logInfo("sys encoding: " + sys.getdefaultencoding()) logInfo("Serving http on: http://" + server[0] + ":" + str(server[1]) + "/") httpd.serve_forever() def test(): print("test") def is_empty(val): # 这个函数检查值是否为空(None,空字符串,空列表,空元组,空字典,空集合) return val is None or val == '' or val == [] or val == () or val == {} or val == set() def logInfo(*msg): print("----------") if __debug: print_callstack() print("【%s】"%datetime.now(),*msg) #print("Result: %s" % msg) def print_callstack(): # 获取当前的调用栈信息 stack = traceback.extract_stack()[:-1] # 去除最后一行(print_callstack函数本身) for frame in stack: filename, line_number, function_name, code = frame print("==",frame) if code is not None and len(code) > 0: code = f"{filename}, line:{line_number} - {function_name} -- {code}" else: code = f"{filename}, {line_number} - {function_name}" print(code) if __name__ == '__main__': print("============================================================================") #test() main()
版本4
优化说明:
测试前端上传空文件提醒
import os import sys import argparse import posixpath from datetime import datetime import traceback try: from html import escape except ImportError: from cgi import escape import shutil import mimetypes import re import signal from io import StringIO, BytesIO if sys.version_info.major == 3: # Python3 from urllib.parse import quote from urllib.parse import unquote from http.server import HTTPServer from http.server import BaseHTTPRequestHandler __debug=False # 版本设置 自定义 __version__ = "0.3.1" def _argparse(): parser = argparse.ArgumentParser() # 一般用户电脑用做服务器,每次开机后,ip4地址可能会发生变化 #ip = input("请输入IP地址:") parser.add_argument('--bind', '-b', metavar='ADDRESS', default="127.0.0.1", help='Specify alternate bind address [default: all interfaces]') parser.add_argument('--version', '-v', action='version', version=__version__) parser.add_argument('port', action='store', default=8000, type=int, nargs='?', help='Specify alternate port [default: 8000]') return parser.parse_args() class SimpleHTTPRequestHandler(BaseHTTPRequestHandler): server_version = "simple_http_server/" + __version__ def do_GET(self): """Serve a GET request.""" fd = self.send_head() if fd: shutil.copyfileobj(fd, self.wfile) fd.close() def do_HEAD(self): """Serve a HEAD request.""" fd = self.send_head() if fd: fd.close() def do_POST(self): """Serve a POST request.""" r, info = self.deal_post_data() logInfo(r, info, "by: ", self.client_address) f = BytesIO() f.write(b'<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">') f.write(b"<html>\n<title>Upload Result Page</title>\n") f.write(b"<body>\n<h2>Upload Result Page</h2>\n") f.write(b"<hr>\n") if r: f.write(b"<strong>Success:</strong>") else: f.write(b"<strong>Failed:</strong>") f.write(info.encode('utf-8')) f.write(b"<br><a href=\".\">back</a>") f.write(b"<hr><small>Powered By: freelamb, check new version at ") # 原始代码地址 可以参考 f.write(b"<a href=\"https://github.com/freelamb/simple_http_server\">") f.write(b"here</a>.</small></body>\n</html>\n") length = f.tell() f.seek(0) self.send_response(200) self.send_header("Content-type", "text/html;charset=utf-8") self.send_header("Content-Length", str(length)) self.end_headers() if f: shutil.copyfileobj(f, self.wfile) f.close() def deal_post_data(self): boundary = self.headers["Content-Type"].split("=")[1].encode('utf-8') remain_bytes = int(self.headers['content-length']) line = self.rfile.readline() logInfo("=======",line) logInfo("=======",remain_bytes) remain_bytes -= len(line) logInfo("boundary=======",boundary) if boundary not in line: return False, "Content NOT begin with boundary" line = self.rfile.readline() logInfo("line=======",line) remain_bytes -= len(line) fn = re.findall(r'Content-Disposition.*name="file"; filename="(.*)"', line.decode('utf-8')) fn = [i for i in fn if i] if not fn: return False, "Can't find upload file ......" path = self.translate_path(self.path) fn = os.path.join(path, fn[0]) while os.path.exists(fn): fn += "_" line = self.rfile.readline() remain_bytes -= len(line) line = self.rfile.readline() remain_bytes -= len(line) try: out = open(fn, 'wb') except IOError: return False, "Can't create file to write, do you have permission to write?" pre_line = self.rfile.readline() remain_bytes -= len(pre_line) while remain_bytes > 0: line = self.rfile.readline() remain_bytes -= len(line) if boundary in line: pre_line = pre_line[0:-1] if pre_line.endswith(b'\r'): pre_line = pre_line[0:-1] out.write(pre_line) out.close() return True, "File '%s' upload success!" % fn else: out.write(pre_line) pre_line = line return False, "Unexpect Ends of data." def send_head(self): """ GET和HEAD命令的通用代码。 这将发送响应代码和MIME标头。 返回值要么是文件对象 (除非命令是HEAD,否则调用方必须将其复制到输出文件中, 并且在任何情况下都必须由调用方关闭), 要么是None,在这种情况下,调用方无需进一步操作。 """ path = self.translate_path(self.path) if os.path.isdir(path): if not self.path.endswith('/'): # redirect browser - doing basically what apache does self.send_response(301) self.send_header("Location", self.path + "/") self.end_headers() return None for index in "index.html", "index.htm": index = os.path.join(path, index) if os.path.exists(index): path = index break else: return self.list_directory(path) content_type = self.guess_type(path) try: #始终以二进制模式读取。以文本模式打开文件可能会导致 #换行翻译,使内容的实际大小 #传输*小于*内容长度! f = open(path, 'rb') except IOError: self.send_error(404, "File not found") return None print("-----------------------",self) self.send_response(200) self.send_header("Content-type", content_type) fs = os.fstat(f.fileno()) self.send_header("Content-Length", str(fs[6])) self.send_header("Last-Modified", self.date_time_string(fs.st_mtime)) self.end_headers() return f def list_directory(self, path): """ 帮助程序生成目录列表(缺少index.html)。 返回值为file对象或None(表示错误)。 无论哪种情况,都会发送标头接口与send_head()相同。 """ try: list_dir = os.listdir(path) except os.error: self.send_error(404, "No permission to list directory") return None list_dir.sort(key=lambda a: a.lower()) f = BytesIO() display_path = escape(unquote(self.path)) f.write(b'<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">') f.write(b"<html lang='zh-cn'>\n<title>Directory listing for %s</title>\n" % display_path.encode('utf-8')) fun = """<script type='text/javascript'> function postSubmit(){ console.log("=="); alert(event.target); var fileInput = document.getElementsByName('file')[0]; if (fileInput.files.length > 0) { //if ($("input[name=file]").files.length > 0) { event.target.disabled=true; #event.target.form.submit(); console.log('='); } else { console.log('-'); event.preventDefault(); return false; } }\n</script>\n""" #f.write(fun.encode('utf-8')) f.write(b"<body>\n<h2>Directory listing for %s</h2>\n" % display_path.encode('utf-8')) f.write(b"<hr>\n") f.write(b"<form ENCTYPE=\"multipart/form-data\" method=\"post\">") f.write(b"<input name=\"file\" type=\"file\"/>") f.write(b"<input type=\"submit\" value=\"upload\" onclick=\"this.form.submit();this.disabled=true;\"/></form>\n") #f.write(b"<input type=\"submit\" value=\"upload\" onclick=\"postSubmit()\"/></form>\n") f.write(b"<hr>\n<ul>\n") for name in list_dir: if name==sys.argv[0]:continue fullname = os.path.join(path, name) display_name = linkname = name # Append / for directories or @ for symbolic links if os.path.isdir(fullname): display_name = name + "/" linkname = name + "/" if os.path.islink(fullname): display_name = name + "@" # Note: a link to a directory displays with @ and links with / f.write(b'<li><a href="%s">%s</a>\n' % (quote(linkname).encode('utf-8'), escape(display_name).encode('utf-8'))) f.write(b"</ul>\n<hr>\n</body>\n</html>\n") length = f.tell() f.seek(0) self.send_response(200) self.send_header("Content-type", "text/html;charset=utf-8") self.send_header("Content-Length", str(length)) self.end_headers() return f def guess_type(self, path): """ 参数是PATH(文件名)。 返回值是表单类型/子类型的字符串, 可用于MIME内容类型标头。 默认实现在self.extensions_map表中查找文件的扩展名,默认使用application/octet流; """ base, ext = posixpath.splitext(path) if ext in self.extensions_map: return self.extensions_map[ext] ext = ext.lower() if ext in self.extensions_map: return self.extensions_map[ext] else: return self.extensions_map[''] if not mimetypes.inited: mimetypes.init() # try to read system mime.types extensions_map = mimetypes.types_map.copy() extensions_map.update({ '': 'application/octet-stream', # Default '.py': 'text/plain', '.c': 'text/plain', '.h': 'text/plain', }) ########################################### def translate_path(self,path): # abandon query parameters path = path.split('?', 1)[0] path = path.split('#', 1)[0] path = posixpath.normpath(unquote(path)) words = path.split('/') words = filter(None, words) # 获取你的py文件存放的路径 path = os.getcwd() # 可在此自定义路径(如果有其路径) #path = path+"/file_xxx/xxx" # 获取文件所在的文件夹路径 #folder_path = os.path.dirname(path) # 判断文件夹是否存在,不存在则创建 if not os.path.exists(path): os.makedirs(path) for word in words: drive, word = os.path.splitdrive(word) head, word = os.path.split(word) if word in (os.curdir, os.pardir): continue path = os.path.join(path, word) return path def signal_handler(signal, frame): logInfo("You choose to stop me.") exit() def main(): args = _argparse() # logInfo(args) server_address = (args.bind, args.port) signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGTERM, signal_handler) httpd = HTTPServer(server_address, SimpleHTTPRequestHandler) server = httpd.socket.getsockname() logInfo("server: ", server) logInfo("python_version: " + SimpleHTTPRequestHandler.sys_version) logInfo("server_version: " + SimpleHTTPRequestHandler.server_version) logInfo("sys encoding: " + sys.getdefaultencoding()) logInfo("Serving http on: http://" + server[0] + ":" + str(server[1]) + "/") httpd.serve_forever() def test(): print("test") def is_empty(val): # 这个函数检查值是否为空(None,空字符串,空列表,空元组,空字典,空集合) return val is None or val == '' or val == [] or val == () or val == {} or val == set() def logInfo(*msg): print("----------") if __debug: print_callstack() print("【%s】"%datetime.now(),*msg) #print("Result: %s" % msg) def print_callstack(): # 获取当前的调用栈信息 stack = traceback.extract_stack()[:-1] # 去除最后一行(print_callstack函数本身) for frame in stack: filename, line_number, function_name, code = frame print("==",frame) if code is not None and len(code) > 0: code = f"{filename}, line:{line_number} - {function_name} -- {code}" else: code = f"{filename}, {line_number} - {function_name}" print(code) if __name__ == '__main__': print("============================================================================") #test() main()
版本5
优化说明:使用隐藏窗口的功能
#!/usr/bin/env python3 import signal import threading import pystray # 导入 PyStray 库 from PIL import Image # 导入 Python Imaging Library 的 Image 类 import win32gui from collections import namedtuple import time class icoManage: MenuItemObj = namedtuple('MenuItemObj', ['text','action',"default",'visible','enabled']) # exitCallback = None def __init__(self, imgFilePath, menuArr=None): self.icon = None self.tip = None signal.signal(signal.SIGINT, self.signal_handler) self.hwnd = win32gui.GetForegroundWindow() self.imgPath=imgFilePath self.img = Image.open(self.imgPath) self.menuList=tuple() if menuArr is None: menuArr=[('显示', self.Show),('隐藏', self.Hid)] for i, val in enumerate(menuArr): self.menuList += (pystray.MenuItem(val[0],val[1],default=True),) if i == 0 else (pystray.MenuItem(val[0],val[1]),) #self.menuList += (pystray.MenuItem(val[0],val[1]),) for item in menuArr def Show(self): win32gui.ShowWindow(self.hwnd, 1) def Hid(self): win32gui.ShowWindow(self.hwnd, 0) # 定义退出菜单项的回调函数 def exit(self,icon, item): win32gui.ShowWindow(self.hwnd, 1) self.icon.stop() if icon is None else icon.stop() self.hwnd = 0 # if self.exitCallback is not None and callable(self.exitCallback): # self.exitCallback(True) return True # def SetExitCallback(self, func=None): # if callable(func): # self.exitCallback = func # 定义点击菜单项的回调函数 def click_menu(self,icon, item): print("点击了", item) # 定义通知内容的回调函数 def notify(self,icon: pystray.Icon): icon.notify(title="通知标题", message="通知内容") #@property def CurrWindHand(self)->int: return self.hwnd def Tip(self,msg:str): self.tip=msg def Stop(self)->bool: res = self.exit(None, None) signal.signal(signal.SIGINT, signal.SIG_DFL) # if fun is not None and callable(fun): # fun(res) return res # 定义 def signal_handler(self, signum, frame): print('You pressed Ctrl+C! Exiting icoManage.') self.Stop() def addMenu(self,mentText,handFun,visible=True,default=False): self.menuList += (pystray.MenuItem(mentText, handFun, visible=visible, default=default),) #self.menuList += (pystray.MenuItem(text=mentText, action=handFun,visible=visible,default=default),) def run(self, showExit=False): if showExit: self.menuList += (pystray.MenuItem(text='退出', action=self.exit),) self.icon = pystray.Icon('Double-click Example', self.img, self.tip, self.menuList) #此处的图标对象不支持字符串路径格式 print(" taskBar is running...") # self.icon.run() #已使用守护线程模式,主线程结束则子线程自动退出 threading.Thread(target=self.icon.run, daemon=True).start() def runTest(self): # 创建菜单项 menu = ( pystray.MenuItem('显示', self.Show), # 第一个菜单项 pystray.MenuItem('隐藏', self.Hid), # 第二个菜单项 pystray.MenuItem(text='菜单C', action=self.click_menu), # 第三个菜单项 pystray.MenuItem(text='发送通知', action=self.notify, enabled=False), # 第四个菜单项 pystray.MenuItem(text='点击托盘', action=self.click_menu, default=True,visible=False), # 第五个菜单项,验证visible pystray.MenuItem(text='退出', action=self.exit), # 最后一个菜单项 ) # 创建图标对象 imgPath="pythonx50.png" #imgPath="pyProject.ico" img = Image.open(self.imgPath) # 打开并读取图片文件 icon = pystray.Icon("name", img, "鼠标移动到\n托盘图标上\n展示内容", menu) # 创建图标对象并绑定菜单项 #icon = pystray.Icon('Double-click Example', img,"鼠标移动到\n托盘图标上\n展示内容", self.menuList) #此处的图标对象不支持字符串路径格式 # 显示图标并等待用户操作 icon.run() if __name__ == '__main__': imgPath="pythonx50.png" #imgPath="pyProject.ico" #menuItems=[('显示1', ui.on_Show),('隐藏1', ui.on_Hid)] #可提前指定菜单和处理函数 icoMng = icoManage(imgPath) #必须指定图标文件 # icoMng.addMenu('显示2', icoMng.Show,default=True) # icoMng.addMenu('隐藏2', icoMng.Hid) icoMng.addMenu(mentText='点击测试', handFun=icoMng.click_menu) icoMng.run() for i in range(10): time.sleep(1) print("Running...")
#!/usr/bin/env python3 import os import sys import argparse import posixpath from datetime import datetime import traceback # import win32gui from taskBarTest import icoManage try: from html import escape except ImportError: from cgi import escape import shutil import mimetypes import re import signal from io import StringIO, BytesIO if sys.version_info.major == 3: # Python3 from urllib.parse import quote from urllib.parse import unquote from http.server import HTTPServer from http.server import BaseHTTPRequestHandler """ 说明: 1)安装打包环境:pip install pyinstaller 2)打包:pyinstaller --onefile uploadHttp2.py """ # 定义模块全局变量 _args = None icoMng = None # 定义模块私有变量 __debug=False __hidRun=False # 版本设置 自定义 __version__ = "0.4.1" def _argparse(): parser = argparse.ArgumentParser() # 一般用户电脑用做服务器,每次开机后,ip4地址可能会发生变化 #ip = input("请输入IP地址:") #parser.add_argument('-s',dest='--sex',type=str,default='无',help='性别') parser.add_argument('-d','--debug',action='store_true', default=False, help='Use Debug mode to track programs [default: False]') parser.add_argument('--hid',action='store_true', default=False, help='Hide running this program [default: False]') parser.add_argument("-p",'--path', type=str, default=os.getcwd(), help='Web server path [default: os.getcwd()]') parser.add_argument('-b', '--bind', metavar='ADDRESS', default="127.0.0.1", help='Specify alternate bind address [default: all interfaces]') #必选参数,已定义默认值 parser.add_argument('port', metavar='port', default=8000, type=int, nargs='?', help='Specify alternate port [default: 8000]') #parser.add_argument('-v', '--version', action='version', version=__version__) parser.add_argument('-v','--version', action='version', version='Version '+__version__, help='Show version information') return parser.parse_args() def logInfo(*msg): if __debug: print_callstack() print("【%s】"%datetime.now(),*msg) #print("Result: %s" % msg) #print("----------") def print_callstack(): # 获取当前的调用栈信息 stack = traceback.extract_stack()[:-1] # 去除最后一行(print_callstack函数本身) for frame in stack: filename, line_number, function_name, code = frame if code is not None and len(code) > 0: code = f"{filename}, line:{line_number} - {function_name} -- {code}" else: code = f"{filename}, {line_number} - {function_name}" #print("【%s】"%datetime.now(),"[stack]%s"%frame) #print("【%s】"%datetime.now(),"[code]%s"%code) print("【%s】"%datetime.now(),"[stack]%s"%frame,"===%s"%code) class SimpleHTTPRequestHandler(BaseHTTPRequestHandler): server_version = "simple_http_server/" + __version__ def do_GET(self): """Serve a GET request.""" fd = self.send_head() if fd: shutil.copyfileobj(fd, self.wfile) fd.close() def do_HEAD(self): """Serve a HEAD request.""" fd = self.send_head() if fd: fd.close() def do_POST(self): """Serve a POST request.""" r, info = self.deal_post_data() logInfo(r, info, "by: ", self.client_address) f = BytesIO() f.write(b'<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">') f.write(b"<html>\n<title>Upload Result Page</title>\n") f.write(b"<body>\n<h2>Upload Result Page</h2>\n") f.write(b"<hr>\n") if r: f.write(b"<strong>Success:</strong>") else: f.write(b"<strong>Failed:</strong>") f.write(info.encode('utf-8')) f.write(b"<br><a href=\".\">back</a>") f.write(b"<hr><small>Powered By: freelamb, check new version at ") # 原始代码地址 可以参考 f.write(b"<a href=\"https://github.com/freelamb/simple_http_server\">") f.write(b"here</a>.</small></body>\n</html>\n") length = f.tell() f.seek(0) self.send_response(200) self.send_header("Content-type", "text/html;charset=utf-8") self.send_header("Content-Length", str(length)) self.end_headers() if f: shutil.copyfileobj(f, self.wfile) f.close() def deal_post_data(self): boundary = self.headers["Content-Type"].split("=")[1].encode('utf-8') remain_bytes = int(self.headers['content-length']) line = self.rfile.readline() logInfo("=======",line) logInfo("=======",remain_bytes) remain_bytes -= len(line) logInfo("boundary=======",boundary) if boundary not in line: return False, "Content NOT begin with boundary" line = self.rfile.readline() logInfo("line=======",line) remain_bytes -= len(line) fn = re.findall(r'Content-Disposition.*name="file"; filename="(.*)"', line.decode('utf-8')) fn = [i for i in fn if i] if not fn: return False, "Can't find upload file ......" path = self.translate_path(self.path) fn = os.path.join(path, fn[0]) while os.path.exists(fn): fn += "_" line = self.rfile.readline() remain_bytes -= len(line) line = self.rfile.readline() remain_bytes -= len(line) try: out = open(fn, 'wb') except IOError: return False, "Can't create file to write, do you have permission to write?" pre_line = self.rfile.readline() remain_bytes -= len(pre_line) while remain_bytes > 0: line = self.rfile.readline() remain_bytes -= len(line) if boundary in line: pre_line = pre_line[0:-1] if pre_line.endswith(b'\r'): pre_line = pre_line[0:-1] out.write(pre_line) out.close() return True, "File '%s' upload success!" % fn else: out.write(pre_line) pre_line = line return False, "Unexpect Ends of data." def send_head(self): """ GET和HEAD命令的通用代码。 这将发送响应代码和MIME标头。 返回值要么是文件对象 (除非命令是HEAD,否则调用方必须将其复制到输出文件中, 并且在任何情况下都必须由调用方关闭), 要么是None,在这种情况下,调用方无需进一步操作。 """ path = self.translate_path(self.path) if os.path.isdir(path): if not self.path.endswith('/'): # redirect browser - doing basically what apache does self.send_response(301) self.send_header("Location", self.path + "/") self.end_headers() return None for index in "index.html", "index.htm": index = os.path.join(path, index) if os.path.exists(index): path = index break else: return self.list_directory(path) content_type = self.guess_type(path) logInfo("content_type=",content_type) try: #始终以二进制模式读取。以文本模式打开文件可能会导致 #换行翻译,使内容的实际大小 #传输*小于*内容长度! f = open(path, 'rb') except IOError: self.send_error(404, "File not found") return None print("-----------------------",self) self.send_response(200) self.send_header("Content-type", content_type) fs = os.fstat(f.fileno()) self.send_header("Content-Length", str(fs[6])) self.send_header("Last-Modified", self.date_time_string(fs.st_mtime)) self.end_headers() return f def list_directory(self, path): """ 帮助程序生成目录列表(缺少index.html)。 返回值为file对象或None(表示错误)。 无论哪种情况,都会发送标头接口与send_head()相同。 """ try: list_dir = os.listdir(path) except os.error: self.send_error(404, "No permission to list directory") return None list_dir.sort(key=lambda a: a.lower()) f = BytesIO() display_path = escape(unquote(self.path)) f.write(b'<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">') f.write(b"<html lang='zh-cn'>\n<title>Directory listing for %s</title>\n" % display_path.encode('utf-8')) fun = """<script type='text/javascript'> function postSubmit(){ console.log("=="); alert(event.target); var fileInput = document.getElementsByName('file')[0]; if (fileInput.files.length > 0) { //if ($("input[name=file]").files.length > 0) { event.target.disabled=true; #event.target.form.submit(); console.log('='); } else { console.log('-'); event.preventDefault(); return false; } }\n</script>\n""" #f.write(fun.encode('utf-8')) f.write(b"<body>\n<h2><a href='javascript:' onclick='window.history.go(-1)'>Back</a> Directory listing for %s</h2>\n" % display_path.encode('utf-8')) f.write(b"<hr>\n") f.write(b"<form ENCTYPE=\"multipart/form-data\" method=\"post\">") f.write(b"<input name=\"file\" type=\"file\"/>") f.write(b"<input type=\"submit\" value=\"upload\" onclick=\"this.form.submit();this.disabled=true;\"/></form>\n") #f.write(b"<input type=\"submit\" value=\"upload\" onclick=\"postSubmit()\"/></form>\n") f.write(b"<hr>\n<ul>\n") for name in list_dir: if name==os.path.basename(sys.argv[0]):continue fullname = os.path.join(path, name) display_name = linkname = name # Append / for directories or @ for symbolic links if os.path.isdir(fullname): display_name = name + "/" linkname = name + "/" if os.path.islink(fullname): display_name = name + "@" # Note: a link to a directory displays with @ and links with / f.write(b'<li><a href="%s">%s</a>\n' % (quote(linkname).encode('utf-8'), escape(display_name).encode('utf-8'))) f.write(b"</ul>\n<hr>\n</body>\n</html>\n") length = f.tell() f.seek(0) self.send_response(200) self.send_header("Content-type", "text/html;charset=utf-8") self.send_header("Content-Length", str(length)) self.end_headers() return f def guess_type(self, path): """ 参数是PATH(文件名)。 返回值是表单类型/子类型的字符串, 可用于MIME内容类型标头。 默认实现在self.extensions_map表中查找文件的扩展名,默认使用application/octet流; """ base, ext = posixpath.splitext(path) if ext in self.extensions_map: return self.extensions_map[ext] ext = ext.lower() if ext in self.extensions_map: return self.extensions_map[ext] else: return self.extensions_map[''] # 初始化Http传输文件类型 if not mimetypes.inited: mimetypes.init() # try to read system mime.types extensions_map = mimetypes.types_map.copy() extensions_map.update({ '': 'application/octet-stream', # Default '.py': 'text/plain', '.c': 'text/plain', '.h': 'text/plain', '.mp4': 'application/octet-stream', }) #删除字典中指定类型,以使用默认流处理 del extensions_map['.mp4'] ########################################### def translate_path(self,path): # abandon query parameters path = path.split('?', 1)[0] path = path.split('#', 1)[0] path = posixpath.normpath(unquote(path)) words = path.split('/') words = filter(None, words) # 获取你的py文件存放的路径 path = os.getcwd() path = _args.path # 可在此自定义路径(如果有其路径) # 获取文件所在的文件夹路径 #folder_path = os.path.dirname(path) # 判断文件夹是否存在,不存在则创建 if not os.path.exists(path): os.makedirs(path) for word in words: drive, word = os.path.splitdrive(word) head, word = os.path.split(word) if word in (os.curdir, os.pardir): continue path = os.path.join(path, word) return path def signal_handler(signal, frame): #logInfo("You choose to stop me.") # todo : Stop后需要返回处理结果,根据结果显示提示信息 # None if icoMng.hwnd==0 else icoMng.Stop() # if icoMng.hwnd != 0: # logInfo("正在停止任务栏,请等待...") # isIcoMngStop = icoMng.Stop() # if not isIcoMngStop: # logInfo("任务栏停止失败,请手动右键任务栏图标->退出") logInfo("Web程序已停止运行") sys.exit() # os.kill(os.getpid(), signal.SIGTERM) # os._exit() def InitArgs(): global _args,__debug,__hidRun _args = _argparse() logInfo("args info :",_args) __debug=_args.debug __hidRun=_args.hid def main(): logInfo("current version: ",__version__) server_address = (_args.bind, _args.port) signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGTERM, signal_handler) httpd = HTTPServer(server_address, SimpleHTTPRequestHandler) server = httpd.socket.getsockname() logInfo("server IP, port: ", server) logInfo("python_version: " + SimpleHTTPRequestHandler.sys_version) logInfo("server_version: " + SimpleHTTPRequestHandler.server_version) logInfo("sys encoding: " + sys.getdefaultencoding()) logInfo("web server work path: ",_args.path) logInfo("Serving http on: http://" + server[0] + ":" + str(server[1]) + "/") httpd.serve_forever() def test(): print("test") def is_empty(val): # 这个函数检查值是否为空(None,空字符串,空列表,空元组,空字典,空集合) return val is None or val == '' or val == [] or val == () or val == {} or val == set() def IcoManage(): global icoMng imgPath="E:/PyWork/taskBarTest/pyProject.ico" #menuItems=[('显示1', ui.on_Show),('隐藏1', ui.on_Hid)] icoMng = icoManage.icoManage(imgPath) #必须指定图标文件 # icoMng.addMenu(mentText='点击测试', handFun=icoMng.click_menu) icoMng.Hid() if __hidRun else icoMng.Show() icoMng.run() if __name__ == '__main__': print("============================================================================") InitArgs() #test() IcoManage() main()
版本6
#!/usr/bin/env python3 import os import sys import argparse import posixpath from datetime import datetime import traceback # import win32gui from taskBarTest import icoManage try: from html import escape except ImportError: from cgi import escape import shutil import mimetypes import re import signal from io import StringIO, BytesIO if sys.version_info.major == 3: # Python3 from urllib.parse import quote from urllib.parse import unquote from http.server import HTTPServer from http.server import BaseHTTPRequestHandler """ 说明: 1)安装打包环境:pip install pyinstaller 2)打包:pyinstaller --onefile uploadHttp2.py """ # 定义模块全局变量 _args = None icoMng = None # 定义模块私有变量 __debug=False __hidRun=False # 版本设置 自定义 __version__ = "0.4.2" def _argparse(): parser = argparse.ArgumentParser() # 一般用户电脑用做服务器,每次开机后,ip4地址可能会发生变化 #ip = input("请输入IP地址:") #parser.add_argument('-s',dest='--sex',type=str,default='无',help='性别') parser.add_argument('-d','--debug',action='store_true', default=False, help='Use Debug mode to track programs [default: False]') parser.add_argument('--hid',action='store_true', default=False, help='Hide running this program [default: False]') parser.add_argument("-p",'--path', type=str, default=os.getcwd(), help='Web server path [default: os.getcwd()]') parser.add_argument('-b', '--bind', metavar='ADDRESS', default="127.0.0.1", help='Specify alternate bind address [default: all interfaces]') #必选参数,已定义默认值 parser.add_argument('port', metavar='port', default=8000, type=int, nargs='?', help='Specify alternate port [default: 8000]') #parser.add_argument('-v', '--version', action='version', version=__version__) parser.add_argument('-v','--version', action='version', version='Version '+__version__, help='Show version information') return parser.parse_args() def logInfo(*msg): if __debug: print_callstack() print("【%s】"%datetime.now(),*msg) #print("Result: %s" % msg) #print("----------") def print_callstack(): # 获取当前的调用栈信息 stack = traceback.extract_stack()[:-1] # 去除最后一行(print_callstack函数本身) for frame in stack: filename, line_number, function_name, code = frame if code is not None and len(code) > 0: code = f"{filename}, line:{line_number} - {function_name} -- {code}" else: code = f"{filename}, {line_number} - {function_name}" #print("【%s】"%datetime.now(),"[stack]%s"%frame) #print("【%s】"%datetime.now(),"[code]%s"%code) print("【%s】"%datetime.now(),"[stack]%s"%frame,"===%s"%code) class SimpleHTTPRequestHandler(BaseHTTPRequestHandler): server_version = "simple_http_server/" + __version__ def do_GET(self): """Serve a GET request.""" fd = self.send_head() if fd: shutil.copyfileobj(fd, self.wfile) fd.close() def do_HEAD(self): """Serve a HEAD request.""" fd = self.send_head() if fd: fd.close() def do_POST(self): """Serve a POST request.""" r, info = self.deal_post_data() logInfo(r, info, "by: ", self.client_address) f = BytesIO() f.write(b'<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">') f.write(b"<html>\n<title>Upload Result Page</title>\n") f.write(b"<body>\n<h2>Upload Result Page</h2>\n") f.write(b"<hr>\n") if r: f.write(b"<strong>Success:</strong>") else: f.write(b"<strong>Failed:</strong>") f.write(info.encode('utf-8')) f.write(b"<br><a href=\".\">back</a>") f.write(b"<hr><small>Powered By: freelamb, check new version at ") # 原始代码地址 可以参考 f.write(b"<a href=\"https://github.com/freelamb/simple_http_server\">") f.write(b"here</a>.</small></body>\n</html>\n") length = f.tell() f.seek(0) self.send_response(200) self.send_header("Content-type", "text/html;charset=utf-8") self.send_header("Content-Length", str(length)) self.end_headers() if f: shutil.copyfileobj(f, self.wfile) f.close() def deal_post_data(self): boundary = self.headers["Content-Type"].split("=")[1].encode('utf-8') remain_bytes = int(self.headers['content-length']) line = self.rfile.readline() logInfo("=======",line) logInfo("=======",remain_bytes) remain_bytes -= len(line) logInfo("boundary=======",boundary) if boundary not in line: return False, "Content NOT begin with boundary" line = self.rfile.readline() logInfo("line=======",line) remain_bytes -= len(line) fn = re.findall(r'Content-Disposition.*name="file"; filename="(.*)"', line.decode('utf-8')) fn = [i for i in fn if i] if not fn: return False, "Can't find upload file ......" path = self.translate_path(self.path) fn = os.path.join(path, fn[0]) while os.path.exists(fn): fn += "_" line = self.rfile.readline() remain_bytes -= len(line) line = self.rfile.readline() remain_bytes -= len(line) try: out = open(fn, 'wb') except IOError: return False, "Can't create file to write, do you have permission to write?" pre_line = self.rfile.readline() remain_bytes -= len(pre_line) while remain_bytes > 0: line = self.rfile.readline() remain_bytes -= len(line) if boundary in line: pre_line = pre_line[0:-1] if pre_line.endswith(b'\r'): pre_line = pre_line[0:-1] out.write(pre_line) out.close() return True, "File '%s' upload success!" % fn else: out.write(pre_line) pre_line = line return False, "Unexpect Ends of data." def send_head(self): """ GET和HEAD命令的通用代码。 这将发送响应代码和MIME标头。 返回值要么是文件对象 (除非命令是HEAD,否则调用方必须将其复制到输出文件中, 并且在任何情况下都必须由调用方关闭), 要么是None,在这种情况下,调用方无需进一步操作。 """ path = self.translate_path(self.path) if os.path.isdir(path): if not self.path.endswith('/'): # redirect browser - doing basically what apache does self.send_response(301) self.send_header("Location", self.path + "/") self.end_headers() return None for index in "index.html", "index.htm": index = os.path.join(path, index) if os.path.exists(index): path = index break else: return self.list_directory(path) content_type = self.guess_type(path) logInfo("content_type=",content_type) try: #始终以二进制模式读取。以文本模式打开文件可能会导致 #换行翻译,使内容的实际大小 #传输*小于*内容长度! f = open(path, 'rb') except IOError: self.send_error(404, "File not found") return None print("-----------------------",self) self.send_response(200) self.send_header("Content-type", content_type) fs = os.fstat(f.fileno()) self.send_header("Content-Length", str(fs[6])) self.send_header("Last-Modified", self.date_time_string(fs.st_mtime)) self.end_headers() return f def list_directory(self, path): """ 帮助程序生成目录列表(缺少index.html)。 返回值为file对象或None(表示错误)。 无论哪种情况,都会发送标头接口与send_head()相同。 """ try: list_dir = os.listdir(path) except os.error: self.send_error(404, "No permission to list directory") return None list_dir.sort(key=lambda a: a.lower()) f = BytesIO() display_path = escape(unquote(self.path)) f.write(b'<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">') f.write(b"<html lang='zh-cn'>\n<title>Directory listing for %s</title>\n" % display_path.encode('utf-8')) fun = """<script type='text/javascript'> function postSubmit(){ console.log("=="); alert(event.target); var fileInput = document.getElementsByName('file')[0]; if (fileInput.files.length > 0) { //if ($("input[name=file]").files.length > 0) { event.target.disabled=true; #event.target.form.submit(); console.log('='); } else { console.log('-'); event.preventDefault(); return false; } }\n</script>\n""" #f.write(fun.encode('utf-8')) f.write(b"<body>\n<h2><a href='javascript:' onclick='window.history.go(-1)'>Back</a> Directory listing for %s</h2>\n" % display_path.encode('utf-8')) f.write(b"<hr>\n") f.write(b"<form ENCTYPE=\"multipart/form-data\" method=\"post\">") f.write(b"<input name=\"file\" type=\"file\"/>") f.write(b"<input type=\"submit\" value=\"upload\" onclick=\"this.form.submit();this.disabled=true;\"/></form>\n") #f.write(b"<input type=\"submit\" value=\"upload\" onclick=\"postSubmit()\"/></form>\n") f.write(b"<hr>\n<ul>\n") for name in list_dir: if name==os.path.basename(sys.argv[0]):continue fullname = os.path.join(path, name) display_name = linkname = name # Append / for directories or @ for symbolic links if os.path.isdir(fullname): display_name = name + "/" linkname = name + "/" if os.path.islink(fullname): display_name = name + "@" # Note: a link to a directory displays with @ and links with / f.write(b'<li><a href="%s">%s</a>\n' % (quote(linkname).encode('utf-8'), escape(display_name).encode('utf-8'))) f.write(b"</ul>\n<hr>\n</body>\n</html>\n") length = f.tell() f.seek(0) self.send_response(200) self.send_header("Content-type", "text/html;charset=utf-8") self.send_header("Content-Length", str(length)) self.end_headers() return f def guess_type(self, path): """ 参数是PATH(文件名)。 返回值是表单类型/子类型的字符串, 可用于MIME内容类型标头。 默认实现在self.extensions_map表中查找文件的扩展名,默认使用application/octet流; """ base, ext = posixpath.splitext(path) if ext in self.extensions_map: return self.extensions_map[ext] ext = ext.lower() if ext in self.extensions_map: return self.extensions_map[ext] else: return self.extensions_map[''] # 初始化Http传输文件类型 if not mimetypes.inited: mimetypes.init() # try to read system mime.types extensions_map = mimetypes.types_map.copy() extensions_map.update({ '': 'application/octet-stream', # Default '.py': 'text/plain', '.c': 'text/plain', '.h': 'text/plain', '.mp4': 'application/octet-stream', }) #删除字典中指定类型,以使用默认流处理 del extensions_map['.mp4'] ########################################### def translate_path(self,path): # abandon query parameters path = path.split('?', 1)[0] path = path.split('#', 1)[0] path = posixpath.normpath(unquote(path)) words = path.split('/') words = filter(None, words) # 获取你的py文件存放的路径 path = os.getcwd() path = _args.path # 可在此自定义路径(如果有其路径) # 获取文件所在的文件夹路径 #folder_path = os.path.dirname(path) # 判断文件夹是否存在,不存在则创建 if not os.path.exists(path): os.makedirs(path) for word in words: drive, word = os.path.splitdrive(word) head, word = os.path.split(word) if word in (os.curdir, os.pardir): continue path = os.path.join(path, word) return path def signal_handler(signal, frame): #logInfo("You choose to stop me.") # todo : Stop后需要返回处理结果,根据结果显示提示信息 # None if icoMng.hwnd==0 else icoMng.Stop() # if icoMng.hwnd != 0: # logInfo("正在停止任务栏,请等待...") # isIcoMngStop = icoMng.Stop() # if not isIcoMngStop: # logInfo("任务栏停止失败,请手动右键任务栏图标->退出") logInfo("Web程序已停止运行") sys.exit() # os.kill(os.getpid(), signal.SIGTERM) # os._exit() def InitArgs(): global _args,__debug,__hidRun _args = _argparse() logInfo("args info :",_args) __debug=_args.debug __hidRun=_args.hid def setCmdTitle(title): if sys.platform == "win32": # 发送 ANSI 转义序列来修改 CMD 窗口标题 sys.stdout.write(f"\033]0;{title}\007") sys.stdout.flush() def main(): print("=======================================================================") logInfo("current version: ",__version__) server_address = (_args.bind, _args.port) signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGTERM, signal_handler) httpd = HTTPServer(server_address, SimpleHTTPRequestHandler) server = httpd.socket.getsockname() logInfo("server IP, port: ", server) logInfo("python_version: " + SimpleHTTPRequestHandler.sys_version) logInfo("server_version: " + SimpleHTTPRequestHandler.server_version) logInfo("sys encoding: " + sys.getdefaultencoding()) logInfo("Serving http on: http://" + server[0] + ":" + str(server[1]) + "/") logInfo("web root path: ",_args.path) icoMng.setCmdTitle(f"{sys.argv[0]}||{server[0]}:{server[1]}||{_args.path}") print("=======================================================================") httpd.serve_forever() def test(): print("test") def is_empty(val): # 这个函数检查值是否为空(None,空字符串,空列表,空元组,空字典,空集合) return val is None or val == '' or val == [] or val == () or val == {} or val == set() def IcoManage(): global icoMng imgPath="E:/PyWork/taskBarTest/pyProject.ico" #menuItems=[('显示1', ui.on_Show),('隐藏1', ui.on_Hid)] # icoMng = icoManage.icoManage(imgPath) icoMng = icoManage.icoManage() # icoMng.addMenu(mentText='点击测试', handFun=icoMng.click_menu) logInfo("__hidRun: ",__hidRun) icoMng.Hid() if __hidRun else icoMng.Show() icoMng.run() if __name__ == '__main__': InitArgs() #test() IcoManage() main()
#!/usr/bin/env python3 #状态栏图标使用系统dll或exe中的图标 import signal import threading import pystray # 导入 PyStray 库 from PIL import Image # 导入 Python Imaging Library 的 Image 类 import win32gui import win32ui from collections import namedtuple import time import sys class icoManage: MenuItemObj = namedtuple('MenuItemObj', ['text','action',"default",'visible','enabled']) # exitCallback = None def __init__(self, imgFilePath=None, menuArr=None): self.icon = None self.tip = None signal.signal(signal.SIGINT, self.signal_handler) self.hwnd = win32gui.GetForegroundWindow() if imgFilePath is None: self._getExeIcon() else: self.imgPath=imgFilePath self.img = Image.open(self.imgPath) self.menuList=tuple() if menuArr is None: menuArr=[('显示', self.Show),('隐藏', self.Hid)] for i, val in enumerate(menuArr): self.menuList += (pystray.MenuItem(val[0],val[1],default=True),) if i == 0 else (pystray.MenuItem(val[0],val[1]),) #self.menuList += (pystray.MenuItem(val[0],val[1]),) for item in menuArr def _getExeIcon(self): large, small = win32gui.ExtractIconEx(sys.argv[0], 0) if sys.platform != "win32" or len(small) == 0: img = Image.new('RGBA',(32,32),'#000000') #颜色可以使用字符串,如'red','#FF0000' 或rgb数组如(200,100,100) else: win32gui.DestroyIcon(small[0]) hdc = win32ui.CreateDCFromHandle(win32gui.GetDC(0)) hbmp = win32ui.CreateBitmap() hbmp.CreateCompatibleBitmap(hdc, 32, 32) hdc = hdc.CreateCompatibleDC() hdc.SelectObject(hbmp) hdc.DrawIcon((0, 0), large[0]) # 保存图标到本地文件 bmpinfo = hbmp.GetInfo() bmpstr = hbmp.GetBitmapBits(True) img = Image.frombuffer( 'RGB', (bmpinfo['bmWidth'], bmpinfo['bmHeight']), bmpstr, 'raw', 'BGRX', 0, 1) self.img=img # 激活窗口并将其最大化。nCmdShow=3 # SW_SHOWMINIMIZED:激活窗口并将其最小化。nCmdShow=2。 # SW_SHOWMINNOACTIVE:窗口最小化,激活窗口仍然维持激活状态。nCmdShow=7。 # SW_SHOWNA:以窗口原来的状态显示窗口。激活窗口仍然维持激活状态。nCmdShow=8。 # SW_SHOWNOACTIVATE:以窗口最近一次的大小和状态显示窗口。激活窗口仍然维持激活状态。nCmdShow=4。 # SW_SHOWNORMAL:激活并显示一个窗口。如果窗口被最小化或最大化,系统将其恢复到原来的尺寸和大小。应用程序在第一次显示窗口的时候应该指定此标志。nCmdShow=1。 def Show(self): win32gui.ShowWindow(self.hwnd, 8) #原值:1 def Hid(self): win32gui.ShowWindow(self.hwnd, 0) # 定义退出菜单项的回调函数 def exit(self,icon, item): win32gui.ShowWindow(self.hwnd, 1) self.icon.stop() if icon is None else icon.stop() self.hwnd = 0 # if self.exitCallback is not None and callable(self.exitCallback): # self.exitCallback(True) return True # def SetExitCallback(self, func=None): # if callable(func): # self.exitCallback = func # 定义点击菜单项的回调函数 def click_menu(self,icon, item): print("点击了", item) # 定义通知内容的回调函数 def notify(self,icon: pystray.Icon): icon.notify(title="通知标题", message="通知内容") #@property def CurrWindHand(self)->int: return self.hwnd def Tip(self,msg:str): self.tip=msg def Stop(self)->bool: res = self.exit(None, None) signal.signal(signal.SIGINT, signal.SIG_DFL) # if fun is not None and callable(fun): # fun(res) return res # 定义 def signal_handler(self, signum, frame): print('You pressed Ctrl+C! Exiting icoManage.') self.Stop() def addMenu(self,mentText,handFun,visible=True,default=False): self.menuList += (pystray.MenuItem(mentText, handFun, visible=visible, default=default),) #self.menuList += (pystray.MenuItem(text=mentText, action=handFun,visible=visible,default=default),) def run(self, showExit=False): if showExit: self.menuList += (pystray.MenuItem(text='退出', action=self.exit),) self.icon = pystray.Icon('Double-click Example', self.img, self.tip, self.menuList) #此处的图标对象不支持字符串路径格式 print(" taskBar is running...") # self.icon.run() #已使用守护线程模式,主线程结束则子线程自动退出 threading.Thread(target=self.icon.run, daemon=True).start() # 修改窗口标题 def setCmdTitle(self,strTitle): win32gui.SetWindowText(self.hwnd, strTitle) def runTest(self): # 创建菜单项 menu = ( pystray.MenuItem('显示', self.Show), # 第一个菜单项 pystray.MenuItem('隐藏', self.Hid), # 第二个菜单项 pystray.MenuItem(text='菜单C', action=self.click_menu), # 第三个菜单项 pystray.MenuItem(text='发送通知', action=self.notify, enabled=False), # 第四个菜单项 pystray.MenuItem(text='点击托盘', action=self.click_menu, default=True,visible=False), # 第五个菜单项,验证visible pystray.MenuItem(text='退出', action=self.exit), # 最后一个菜单项 ) # 创建图标对象 imgPath="pythonx50.png" #imgPath="pyProject.ico" img = Image.open(self.imgPath) # 打开并读取图片文件 icon = pystray.Icon("name", img, "鼠标移动到\n托盘图标上\n展示内容", menu) # 创建图标对象并绑定菜单项 #icon = pystray.Icon('Double-click Example', img,"鼠标移动到\n托盘图标上\n展示内容", self.menuList) #此处的图标对象不支持字符串路径格式 # 显示图标并等待用户操作 icon.run() if __name__ == '__main__': imgPath="pythonx50.png" #imgPath="pyProject.ico" #menuItems=[('显示1', ui.on_Show),('隐藏1', ui.on_Hid)] #可提前指定菜单和处理函数 icoMng = icoManage(imgPath) #必须指定图标文件 # icoMng.addMenu('显示2', icoMng.Show,default=True) # icoMng.addMenu('隐藏2', icoMng.Hid) icoMng.addMenu(mentText='点击测试', handFun=icoMng.click_menu) icoMng.run() for i in range(10): time.sleep(1) print("Running...")
版本7
优化:打包时在exe文件中包含版本信息等
=======================================================================================
python上传下载文件
场景:
- 点击上传文件按钮,选择需要上传的文件后上传
- 文件上传成功后,会将文件保存到指定目录下
- 限制上传文件的格式
- 在前端点击文件后下载
- 基于上述上传并保存到指定目录下的文件
上手
文件上传
app.py
upload.html
文件下载
app.py
download.html
运行
- 文件上传和下载的视图函数代码都完成后,开始运行项目
app.py(在编写好试图代码的最下方编写运行代码)
- 打开调试模式后,运行项目建议在Pycharm中的Terminal命令行中输入以下运行
整个app.py代码
模板文件中代码已完整
注意
- 代码编写好后,需要在项目根目录先创建一个路径用于存放上传的文件
- 即创建upload文件夹
- 运行项目后,需要在浏览器地址后补充输入
/upload
,即可进入上传页面
- 即
http://127.0.0.1:5000/upload
,具体端口号需根据个人项目运行修改,可以在运行时看控制台
- 下载页面
项目文件目录
出处:https://blog.51cto.com/u_14273/7758108
=======================================================================================
python-上传下载文件
一、服务端接口
import flask, os,sys,time from flask import request, send_from_directory interface_path = os.path.dirname(__file__) sys.path.insert(0, interface_path) #将当前文件的父目录加入临时系统变量 server = flask.Flask(__name__) #get方法:指定目录下载文件 @server.route('/download', methods=['get']) def download(): fpath = request.values.get('path', '') #获取文件路径 fname = request.values.get('filename', '') #获取文件名 if fname.strip() and fpath.strip(): print(fname, fpath) if os.path.isfile(os.path.join(fpath,fname)) and os.path.isdir(fpath): return send_from_directory(fpath, fname, as_attachment=True) #返回要下载的文件内容给客户端 else: return '{"msg":"参数不正确"}' else: return '{"msg":"请输入参数"}' # get方法:查询当前路径下的所有文件 @server.route('/getfiles', methods=['get']) def getfiles(): fpath = request.values.get('fpath', '') #获取用户输入的目录 print(fpath) if os.path.isdir(fpath): filelist = os.listdir(fpath) files = [file for file in filelist if os.path.isfile(os.path.join(fpath, file))] return '{"files":"%s"}' % files # post方法:上传文件的 @server.route('/upload', methods=['post']) def upload(): fname = request.files.get('file') #获取上传的文件 if fname: t = time.strftime('%Y%m%d%H%M%S') new_fname = r'upload/' + t + fname.filename fname.save(new_fname) #保存文件到指定路径 return '{"code": "ok"}' else: return '{"msg": "请上传文件!"}' server.run(port=8000, debug=True)
二、客户端发送请求
import requests import os #上传文件到服务器 file = {'file': open('hello.txt','rb')} r = requests.post('http://127.0.0.1:8000/upload', files=file) print(r.text) #查询fpath下的所有文件 r1 = requests.get('http://127.0.0.1:8000/getfiles',data={'fpath': r'download/'}) print(r1.text) #下载服务器download目录下的指定文件 r2 = requests.get('http://127.0.0.1:8000/download',data={'filename':'hello_upload.txt', 'path': r'upload/'}) file = r2.text #获取文件内容 basepath = os.path.join(os.path.dirname(__file__), r'download/') with open(os.path.join(basepath, 'hello_download.txt'),'w',encoding='utf-8') as f: #保存文件 f.write(file)
【出处】:https://www.cnblogs.com/jessicaxu/p/7891372.html
=======================================================================================
python-文件上传下载,解决粘包问题
一、数据粘包
【1】客户端两次发送请求,但是可能被服务端的同个recv收到,不能区分,会造成数据粘包(实际上需要服务端将两次请求区分接受)
二、服务器
# -*- coding:utf-8 -*-
# __author__:pansy
# 2022/5/14
import socket
# 创建socket对象
sk = socket.socket()
# 给服务器绑定ip和端口
sk.bind(('127.0.0.1',8889))
# 创建监听,监听客户端是否有发请求过来
sk.listen()
def get_file(sk_obj):
'''
接收文件
:param sk_obj: 文件对象
:return:
'''
# 从服务端会发送1个请求,用来传输文件大小,文件大小是整形,需要将string类型强转成int类型
file_size = int(sk_obj.recv(1024).decode('utf8'))
# 为了避免粘包,当执行完接收file_size语句后,需要告知post_file,文件大小已经接收成功
sk_obj.sendall(b'ok')
# 从服务端会发送1个请求,用来传输文件名称
file_name = sk_obj.recv(1024).decode('utf8')
sk_obj.sendall(b'ok')
# 接收文件内容
with open('./%s' %file_name,'wb') as f:
while file_size > 0:
f.write(sk_obj.recv(1024))
file_size -= 1024
# 阻塞状态,若接收到数据,则阻塞解除
# accept返回一个套接字和客户端的ip端口
conn ,addr = sk.accept()
# 调用接收文件方法,conn是专门用来处理客户端业务的套接字
get_file(conn)
conn.close()
sk.close()
三、客户端
# -*- coding:utf-8 -*-
# __author__:pansy
# 2022/5/14
import os
import socket
# 创建socket对象
sk = socket.socket()
# 连接服务器,连接的是服务器绑定的ip和端口
sk.connect(('127.0.0.1',8889))
def post_file(sk_obj,file_path):
'''
发送文件,需要和接收文件一一对应
:param sk_obj:文件对象
:param file_path:文件路径
:return:
'''
# 发送文件大小,用os.stat方法可以获取文件属性
file_size = os.stat(file_path).st_size
# 获取到的file_size是整形,不能直接编码,所以需要先强转成字符串
sk_obj.sendall(str(file_size).encode('utf8'))
# 为了避免粘包,需要用recv接收下参数,直到接收到ok后,才会继续下面的代码
sk_obj.recv(1024)
# 发送文件名称,用os.path.split方法,可以将文件路径切割成路径和文件名,返回这两个字段
file_name = os.path.split(file_path)[1]
sk_obj.sendall(file_name.encode('utf8'))
sk_obj.recv(1024)
# 发送文件内容,循环发送,1次发送1024个字节
# 1、先读取文件,用rb二进制读取
with open(file_path,'rb') as f:
# 每发送1次,file_size会减少1024,不满足1024的,全部发送
while file_size > 0:
sk_obj.sendall(f.read(1024))
file_size -= 1024
# 调用发送文件方法
path = '/Users/panshaoying/Desktop/database/data/img1.png'
post_file(sk,path)
sk.close()
【出处】:https://www.cnblogs.com/flowers-pansy/p/16269797.html
=======================================================================================
关注我】。(●'◡'●)
如果,您希望更容易地发现我的新博客,不妨点击一下绿色通道的【因为,我的写作热情也离不开您的肯定与支持,感谢您的阅读,我是【Jack_孟】!
本文来自博客园,作者:jack_Meng,转载请注明原文链接:https://www.cnblogs.com/mq0036/p/17869271.html
【免责声明】本文来自源于网络,如涉及版权或侵权问题,请及时联系我们,我们将第一时间删除或更改!
posted on 2023-12-01 11:05 jack_Meng 阅读(1256) 评论(0) 编辑 收藏 举报