Spaces:
Running
Running
File size: 7,616 Bytes
b819d98 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 | """
统一服务器 - 合并H5页面 + OCR + 导出服务于一个端口
手机端通过内网穿透访问
"""
import json
import io
import os
import sys
import re
import base64
import tempfile
from http.server import HTTPServer, BaseHTTPRequestHandler
from urllib.parse import urlparse
# 确保能导入同目录模块
WORK_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, WORK_DIR)
# 导入导出服务核心函数
from export_server import generate_docx
# 导入OCR核心函数
import pytesseract
import platform
import shutil
from PIL import Image
# 跨平台 Tesseract 路径配置
if platform.system() == 'Windows':
# Windows 本地开发环境
pytesseract.pytesseract.tesseract_cmd = r'C:\Program Files\Tesseract-OCR\tesseract.exe'
os.environ['TESSDATA_PREFIX'] = os.path.expandvars(r'%USERPROFILE%\.tesseract\tessdata')
else:
# Linux/Docker 环境:tesseract 已通过 apt 安装到 PATH
tesseract_bin = shutil.which('tesseract')
if tesseract_bin:
pytesseract.pytesseract.tesseract_cmd = tesseract_bin
# 自动检测 tessdata 路径(兼容 Tesseract 4.x/5.x)
for _tessdata in ['/usr/share/tesseract-ocr/5/tessdata',
'/usr/share/tesseract-ocr/4.00/tessdata',
'/usr/share/tessdata']:
if os.path.isdir(_tessdata):
os.environ.setdefault('TESSDATA_PREFIX', _tessdata)
break
print(f'[Tesseract] cmd={pytesseract.pytesseract.tesseract_cmd}, '
f'TESSDATA_PREFIX={os.environ.get("TESSDATA_PREFIX", "(default)")}')
# H5页面路径
H5_FILE = os.path.join(WORK_DIR, 'test-h5.html')
PORT = int(os.environ.get('PORT', 7860))
def ocr_image(image_data):
"""对图片进行OCR识别"""
img = Image.open(io.BytesIO(image_data))
w, h = img.size
max_dim = 2000
if w > max_dim or h > max_dim:
ratio = min(max_dim / w, max_dim / h)
img = img.resize((int(w * ratio), int(h * ratio)), Image.LANCZOS)
text = pytesseract.image_to_string(img, lang='chi_sim', config='--psm 6')
return text.strip()
class UnifiedHandler(BaseHTTPRequestHandler):
def _add_cors_headers(self):
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
self.send_header('Access-Control-Allow-Headers', 'Content-Type')
def do_OPTIONS(self):
self.send_response(200)
self._add_cors_headers()
self.end_headers()
def do_GET(self):
"""处理GET请求 - 返回H5页面"""
path = urlparse(self.path).path
if path == '/' or path == '/test-h5.html' or path == '/index.html':
try:
with open(H5_FILE, 'r', encoding='utf-8') as f:
content = f.read()
self.send_response(200)
self.send_header('Content-Type', 'text/html; charset=utf-8')
self._add_cors_headers()
self.end_headers()
self.wfile.write(content.encode('utf-8'))
except Exception as e:
self.send_error(500, f'读取页面失败: {e}')
elif path == '/health':
# 健康检查
self.send_response(200)
self.send_header('Content-Type', 'application/json')
self._add_cors_headers()
self.end_headers()
self.wfile.write(json.dumps({'status': 'ok'}).encode('utf-8'))
else:
self.send_error(404, '页面不存在')
def do_POST(self):
"""处理POST请求 - OCR和导出"""
path = urlparse(self.path).path
if path == '/ocr':
self._handle_ocr()
elif path == '/export':
self._handle_export()
else:
self.send_error(404, '接口不存在')
def _handle_ocr(self):
"""处理OCR识别请求"""
content_length = int(self.headers.get('Content-Length', 0))
body = self.rfile.read(content_length)
content_type = self.headers.get('Content-Type', '')
if 'multipart/form-data' not in content_type:
self._send_json({'error': '需要 multipart/form-data'}, 400)
return
# 提取 boundary
boundary = content_type.split('boundary=')[1].strip()
if boundary.startswith('"') and boundary.endswith('"'):
boundary = boundary[1:-1]
# 手动解析 multipart
parts = body.split(('--' + boundary).encode())
image_data = None
for part in parts:
if b'Content-Disposition' in part and b'filename=' in part:
header_end = part.find(b'\r\n\r\n')
if header_end == -1:
continue
file_data = part[header_end + 4:]
if file_data.endswith(b'\r\n'):
file_data = file_data[:-2]
image_data = file_data
break
if not image_data:
self._send_json({'error': '未找到图片数据'}, 400)
return
try:
text = ocr_image(image_data)
self._send_json({'text': text, 'length': len(text)})
print(f'[OCR] 识别成功,提取 {len(text)} 个字符')
except Exception as e:
self._send_json({'error': f'OCR识别失败: {str(e)}'}, 500)
print(f'[OCR] 识别失败: {e}')
def _handle_export(self):
"""处理Word文档导出请求"""
content_length = int(self.headers.get('Content-Length', 0))
body = self.rfile.read(content_length)
try:
visit_data = json.loads(body)
buffer = generate_docx(visit_data)
self.send_response(200)
self.send_header('Content-Type',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document')
self._add_cors_headers()
filename = 'visit_%s_%sstores.docx' % (
visit_data.get('visitDateDisplay', '').replace('.', '-'),
visit_data.get('totalCount', 0)
)
self.send_header('Content-Disposition', f'attachment; filename="{filename}"')
self.end_headers()
self.wfile.write(buffer.getvalue())
print(f'[Export] 导出成功: {filename}')
except Exception as e:
self.send_response(500)
self.send_header('Content-Type', 'application/json')
self._add_cors_headers()
self.end_headers()
self.wfile.write(json.dumps({'error': str(e)}).encode('utf-8'))
print(f'[Export] 导出失败: {e}')
def _send_json(self, data, status=200):
self.send_response(status)
self.send_header('Content-Type', 'application/json; charset=utf-8')
self._add_cors_headers()
self.end_headers()
self.wfile.write(json.dumps(data, ensure_ascii=False).encode('utf-8'))
def log_message(self, format, *args):
print(f'[{self.log_date_time_string()}] {args[0]}')
if __name__ == '__main__':
print('=' * 50)
print(' 药店拜访助手 - 统一服务器')
print(f' 本机访问: http://127.0.0.1:{PORT}')
print(f' 局域网访问: http://<本机IP>:{PORT}')
print('=' * 50)
# 绑定 0.0.0.0 让局域网设备也能访问
server = HTTPServer(('0.0.0.0', PORT), UnifiedHandler)
try:
server.serve_forever()
except KeyboardInterrupt:
print('\n服务器已停止')
server.shutdown()
|