'commit'
This commit is contained in:
888
Json/Ref/T6_PaChongGaoDe.py
Normal file
888
Json/Ref/T6_PaChongGaoDe.py
Normal file
@@ -0,0 +1,888 @@
|
||||
"""
|
||||
高德地图高精度坐标获取工具
|
||||
|
||||
使用说明:
|
||||
1、注册用户,实名认证
|
||||
2、手动登录,选择微信登录,生成cookie
|
||||
3、复用这个cookie,打开浏览器,传入单位名称,查询坐标,得到小数点6位的坐标数据
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
import weakref
|
||||
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.chrome.options import Options
|
||||
from selenium.webdriver.chrome.service import Service
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
|
||||
# 添加SQLAlchemy同步操作
|
||||
from sqlalchemy import create_engine, text
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
# 数据库连接配置
|
||||
from Config.Config import POSTGRES_HOST, POSTGRES_PORT, POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DATABASE
|
||||
|
||||
# 创建同步数据库连接URL
|
||||
db_url = f"postgresql://{POSTGRES_USER}:{POSTGRES_PASSWORD}@{POSTGRES_HOST}:{POSTGRES_PORT}/{POSTGRES_DATABASE}"
|
||||
|
||||
# 创建数据库引擎和会话工厂
|
||||
engine = create_engine(db_url)
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
# 数据库操作函数
|
||||
def find(sql, params=None):
|
||||
"""
|
||||
执行SQL查询,返回所有结果
|
||||
|
||||
Args:
|
||||
sql: SQL查询语句
|
||||
params: 参数字典
|
||||
|
||||
Returns:
|
||||
list: 查询结果的字典列表
|
||||
"""
|
||||
with SessionLocal() as session:
|
||||
result = session.execute(text(sql), params)
|
||||
rows = result.fetchall()
|
||||
return [dict(zip(result.keys(), row)) for row in rows]
|
||||
|
||||
|
||||
def findFirst(sql, params=None):
|
||||
"""
|
||||
执行SQL查询,返回第一条结果
|
||||
|
||||
Args:
|
||||
sql: SQL查询语句
|
||||
params: 参数字典
|
||||
|
||||
Returns:
|
||||
dict or None: 第一条查询结果的字典,或None(如果没有结果)
|
||||
"""
|
||||
with SessionLocal() as session:
|
||||
result = session.execute(text(sql), params)
|
||||
row = result.fetchone()
|
||||
if row:
|
||||
return dict(zip(result.keys(), row))
|
||||
return None
|
||||
|
||||
|
||||
def execute_update(sql, params=None):
|
||||
"""
|
||||
执行SQL更新操作
|
||||
|
||||
Args:
|
||||
sql: SQL语句
|
||||
params: 参数字典
|
||||
|
||||
Returns:
|
||||
int: 受影响的行数
|
||||
"""
|
||||
with SessionLocal() as session:
|
||||
with session.begin():
|
||||
result = session.execute(text(sql), params)
|
||||
return result.rowcount
|
||||
|
||||
|
||||
# 是否使用无头模式运行(True=无头模式,False=有界面模式)
|
||||
HEADLESS = False
|
||||
|
||||
# 最大重试次数
|
||||
MAX_RETRIES = 1
|
||||
|
||||
# WebDriver超时设置(秒)
|
||||
TIMEOUT_SHORT = 5
|
||||
TIMEOUT_MEDIUM = 10
|
||||
TIMEOUT_LONG = 15
|
||||
|
||||
|
||||
def parse_coordinates_info(text):
|
||||
"""解析坐标信息,提取第一个搜索结果的坐标"""
|
||||
# 检查输入参数
|
||||
if not text:
|
||||
return None
|
||||
|
||||
try:
|
||||
# 清理文本,移除可能的干扰字符
|
||||
clean_text = re.sub(r'[\u3000\t\n\r ]+', ',', text) # 替换空格和制表符为逗号
|
||||
clean_text = re.sub(r'[,,]+', ',', clean_text) # 统一使用英文逗号
|
||||
clean_text = clean_text.strip(',') # 移除首尾逗号
|
||||
|
||||
# 匹配高德地图坐标格式 - 中国地区的坐标范围,放宽小数位数要求
|
||||
pattern = r'([7-9]?[0-9]\.[0-9]{1,}|1[0-3][0-9]\.[0-9]{1,})[,,]\s*([1-8]?[0-9]\.[0-9]{1,}|90\.[0-9]{1,})'
|
||||
matches = re.finditer(pattern, clean_text)
|
||||
|
||||
valid_coordinates = []
|
||||
|
||||
for match in matches:
|
||||
lng = match.group(1).strip()
|
||||
lat = match.group(2).strip()
|
||||
|
||||
# 转换为浮点数进行范围验证
|
||||
try:
|
||||
lng_float = float(lng)
|
||||
lat_float = float(lat)
|
||||
|
||||
# 检查坐标是否在中国地区合理范围内
|
||||
if 70 <= lng_float <= 140 and 0 <= lat_float <= 60:
|
||||
# 计算坐标的精度(小数位数)
|
||||
lng_precision = len(lng.split('.')[1]) if '.' in lng else 0
|
||||
lat_precision = len(lat.split('.')[1]) if '.' in lat else 0
|
||||
|
||||
# 接受至少有1位小数的坐标,不再严格要求4位小数
|
||||
valid_coordinates.append((lng_float, lat_float, lng, lat))
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
if valid_coordinates:
|
||||
# 如果找到多个有效坐标,选择小数位数最多的
|
||||
valid_coordinates.sort(
|
||||
key=lambda x: len(x[2].split('.')[1]) + len(x[3].split('.')[1]) if '.' in x[2] and '.' in x[3] else 0,
|
||||
reverse=True)
|
||||
_, _, lng, lat = valid_coordinates[0]
|
||||
|
||||
return {
|
||||
'lng': lng,
|
||||
'lat': lat
|
||||
}
|
||||
else:
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
return None
|
||||
|
||||
|
||||
def export_cookies_from_browser(driver, output_file='amap_cookies.json'):
|
||||
"""
|
||||
导出浏览器中的Cookie到文件
|
||||
|
||||
Args:
|
||||
driver: WebDriver实例
|
||||
output_file: Cookie导出文件路径
|
||||
|
||||
Returns:
|
||||
bool: 导出是否成功
|
||||
"""
|
||||
# 检查输入参数
|
||||
if not driver:
|
||||
print("❌ WebDriver实例无效")
|
||||
return False
|
||||
|
||||
if not output_file:
|
||||
print("❌ Cookie输出文件路径无效")
|
||||
return False
|
||||
|
||||
try:
|
||||
print(f"正在导出Cookie到文件: {output_file}")
|
||||
|
||||
# 确保目录存在
|
||||
output_dir = os.path.dirname(output_file)
|
||||
if output_dir and not os.path.exists(output_dir):
|
||||
try:
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
except Exception as e:
|
||||
print(f"创建输出目录失败: {str(e)}")
|
||||
return False
|
||||
|
||||
# 获取Cookie
|
||||
cookies = driver.get_cookies()
|
||||
|
||||
if not cookies:
|
||||
print("⚠️ 未获取到任何Cookie")
|
||||
return False
|
||||
|
||||
# 保存Cookie到文件
|
||||
with open(output_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(cookies, f, ensure_ascii=False, indent=2)
|
||||
|
||||
print(f"成功导出 {len(cookies)} 个Cookie")
|
||||
print(f"Cookie已成功导出到: {os.path.abspath(output_file)}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"导出Cookie失败: {str(e)}")
|
||||
return False
|
||||
|
||||
|
||||
def load_cookies_to_browser(driver, cookie_file='amap_cookies.json'):
|
||||
"""
|
||||
从文件加载Cookie到浏览器
|
||||
|
||||
Args:
|
||||
driver: WebDriver实例
|
||||
cookie_file: Cookie文件路径
|
||||
|
||||
Returns:
|
||||
bool: 加载是否成功
|
||||
"""
|
||||
try:
|
||||
# 检查输入参数
|
||||
if not driver:
|
||||
print("❌ WebDriver实例无效")
|
||||
return False
|
||||
|
||||
if not cookie_file:
|
||||
print("❌ Cookie文件路径无效")
|
||||
return False
|
||||
|
||||
if not os.path.exists(cookie_file):
|
||||
return False
|
||||
|
||||
# 先访问高德域名,确保Cookie可以被正确设置
|
||||
import ssl
|
||||
ssl._create_default_https_context = ssl._create_unverified_context
|
||||
driver.get('https://www.amap.com')
|
||||
|
||||
time.sleep(1) # 等待页面加载
|
||||
|
||||
# 加载Cookie
|
||||
with open(cookie_file, 'r', encoding='utf-8') as f:
|
||||
cookies = json.load(f)
|
||||
|
||||
# 添加每个Cookie
|
||||
success_count = 0
|
||||
for cookie in cookies:
|
||||
# 移除可能导致问题的属性
|
||||
if 'expiry' in cookie:
|
||||
cookie['expiry'] = int(cookie['expiry'])
|
||||
if 'sameSite' in cookie and cookie['sameSite'] not in ['Strict', 'Lax', 'None']:
|
||||
del cookie['sameSite']
|
||||
|
||||
try:
|
||||
driver.add_cookie(cookie)
|
||||
success_count += 1
|
||||
except Exception as e:
|
||||
#print(f"添加Cookie失败 (可能是安全策略限制): {cookie.get('name')} - {str(e)}")
|
||||
pass
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"加载Cookie失败: {str(e)}")
|
||||
return False
|
||||
|
||||
|
||||
def get_location_coordinates(location_name, headless=False, cookie_file='amap_cookies.json', driver=None,
|
||||
reuse_driver=False):
|
||||
"""
|
||||
获取指定名称地点的坐标信息
|
||||
|
||||
Args:
|
||||
location_name (str): 要搜索的地点名称
|
||||
headless (bool): 是否使用无头模式,默认False(有界面)
|
||||
cookie_file (str): Cookie文件路径
|
||||
driver: 可选的WebDriver实例(用于复用)
|
||||
reuse_driver: 是否复用WebDriver实例
|
||||
|
||||
Returns:
|
||||
dict or None: 包含名称、经度和纬度的字典,失败时返回None
|
||||
"""
|
||||
# 检查输入参数
|
||||
if not location_name or not location_name.strip():
|
||||
print("❌ 地点名称无效")
|
||||
return None
|
||||
|
||||
if not cookie_file or not os.path.exists(cookie_file):
|
||||
print(f"❌ Cookie文件不存在或无效: {cookie_file}")
|
||||
return None
|
||||
|
||||
# 浏览器管理标志
|
||||
need_quit_driver = False
|
||||
|
||||
try:
|
||||
# 如果没有提供driver,创建新的
|
||||
if not driver:
|
||||
# 设置Chrome浏览器参数
|
||||
chrome_options = Options()
|
||||
if headless:
|
||||
chrome_options.add_argument('--headless')
|
||||
|
||||
# 添加必要的配置参数
|
||||
# 我有GPU,可以放开
|
||||
# chrome_options.add_argument('--disable-gpu')
|
||||
chrome_options.add_argument('--no-sandbox')
|
||||
chrome_options.add_argument('--disable-dev-shm-usage')
|
||||
chrome_options.add_argument('--window-size=1920,1080')
|
||||
chrome_options.add_argument('--lang=zh-CN')
|
||||
|
||||
# 禁用自动化控制特征
|
||||
chrome_options.add_experimental_option('useAutomationExtension', False)
|
||||
chrome_options.add_argument("--disable-blink-features=AutomationControlled") # 防止自动化检测,关闭浏览器控制显示
|
||||
chrome_options.add_argument('-ignore-certificate-errors') # 忽略证书错误
|
||||
chrome_options.add_argument('-ignore-ssl-errors') # 忽略相关错误
|
||||
chrome_options.add_experimental_option('excludeSwitches', ['enable-automation']) # 防止自动化检测
|
||||
chrome_options.add_experimental_option('excludeSwitches', ['enable-logging']) # 防止自动化日志输出检测
|
||||
|
||||
# 添加随机User-Agent
|
||||
user_agent = f'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{random.randint(80, 99)}.0.{random.randint(4000, 5000)}.{random.randint(100, 200)} Safari/537.36'
|
||||
chrome_options.add_argument(f'user-agent={user_agent}')
|
||||
|
||||
# 尝试初始化WebDriver
|
||||
service = Service()
|
||||
try:
|
||||
from webdriver_manager.chrome import ChromeDriverManager
|
||||
driver_path = ChromeDriverManager().install()
|
||||
service = Service(driver_path)
|
||||
except:
|
||||
pass
|
||||
|
||||
# 初始化WebDriver
|
||||
driver = webdriver.Chrome(service=service, options=chrome_options)
|
||||
need_quit_driver = True
|
||||
|
||||
# 最大化窗口(如果不是无头模式)
|
||||
if not headless:
|
||||
driver.maximize_window()
|
||||
|
||||
# 禁用自动化特征
|
||||
driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
|
||||
"source": "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"
|
||||
})
|
||||
|
||||
# 使用Cookie方式认证
|
||||
cookies_loaded = load_cookies_to_browser(driver, cookie_file)
|
||||
if not cookies_loaded:
|
||||
print("Cookie认证失败,无法继续获取高精度坐标")
|
||||
return None
|
||||
|
||||
# 访问高德地图坐标拾取工具
|
||||
url = 'https://lbs.amap.com/tools/picker'
|
||||
driver.get(url)
|
||||
|
||||
time.sleep(2) # 等待页面完全加载
|
||||
|
||||
# 定位搜索输入框并输入查询内容
|
||||
try:
|
||||
# 使用ID定位搜索框
|
||||
search_input = WebDriverWait(driver, TIMEOUT_MEDIUM).until(
|
||||
EC.presence_of_element_located((By.ID, 'txtSearch'))
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"定位搜索输入框失败: {e}")
|
||||
return None
|
||||
|
||||
# 输入搜索内容
|
||||
search_input.clear()
|
||||
search_input.send_keys(location_name)
|
||||
|
||||
# 定位搜索按钮并点击
|
||||
try:
|
||||
# 尝试使用class选择器定位搜索按钮
|
||||
search_button = WebDriverWait(driver, TIMEOUT_SHORT).until(
|
||||
EC.presence_of_element_located((By.CLASS_NAME, 'btn-search'))
|
||||
)
|
||||
search_button.click()
|
||||
except Exception as e:
|
||||
print(f"定位搜索按钮失败: {e}")
|
||||
# 尝试按回车键搜索
|
||||
try:
|
||||
search_input.send_keys(Keys.ENTER)
|
||||
except Exception as e2:
|
||||
print(f"回车键搜索也失败: {e2}")
|
||||
return None
|
||||
|
||||
# 等待搜索结果加载完成并显示坐标
|
||||
time.sleep(3) # 等待时间,确保页面完全加载并显示搜索结果和坐标
|
||||
|
||||
# 获取坐标信息 - 只使用成功的方式(从txtCoordinate元素的value属性获取)
|
||||
coordinate_text = None
|
||||
|
||||
# 查找ID为txtCoordinate的元素(这是高德地图坐标拾取器的核心坐标显示元素)
|
||||
try:
|
||||
coordinate_element = WebDriverWait(driver, TIMEOUT_MEDIUM).until(
|
||||
EC.presence_of_element_located((By.ID, 'txtCoordinate'))
|
||||
)
|
||||
|
||||
# 获取value属性(这是input元素存储值的标准方式)
|
||||
coordinate_value = coordinate_element.get_attribute('value')
|
||||
if coordinate_value and coordinate_value.strip():
|
||||
coordinate_text = coordinate_value.strip()
|
||||
else:
|
||||
print("警告: txtCoordinate元素的value属性为空")
|
||||
|
||||
# 作为备选,尝试获取text内容
|
||||
coordinate_text_content = coordinate_element.text.strip()
|
||||
if coordinate_text_content:
|
||||
coordinate_text = coordinate_text_content
|
||||
print(f"从text内容获取到坐标文本: '{coordinate_text}'")
|
||||
except Exception as e:
|
||||
print(f"查找或获取坐标元素失败: {e}")
|
||||
return None
|
||||
|
||||
# 如果没有获取到坐标,返回失败
|
||||
if not coordinate_text or coordinate_text.strip() == '':
|
||||
print("未能获取到有效的坐标文本")
|
||||
return None
|
||||
|
||||
# 解析坐标信息
|
||||
print(f"解析坐标文本: '{coordinate_text}'")
|
||||
|
||||
coordinate_info = parse_coordinates_info(coordinate_text)
|
||||
if coordinate_info:
|
||||
# 进一步验证坐标的合理性(中国地区范围)
|
||||
try:
|
||||
lng = float(coordinate_info['lng'])
|
||||
lat = float(coordinate_info['lat'])
|
||||
|
||||
# 中国地区的合理坐标范围
|
||||
if 70 <= lng <= 140 and 0 <= lat <= 60:
|
||||
print(f"成功获取坐标: {coordinate_info['lng']},{coordinate_info['lat']}")
|
||||
return coordinate_info
|
||||
else:
|
||||
print(f"坐标超出中国地区合理范围 - 经度: {lng}, 纬度: {lat}")
|
||||
return None
|
||||
except (ValueError, TypeError):
|
||||
print(f"坐标数值验证失败: {coordinate_info}")
|
||||
return None
|
||||
else:
|
||||
print("坐标解析失败")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
print(f"获取坐标时发生错误: {e}")
|
||||
return None
|
||||
finally:
|
||||
# 关闭浏览器(如果不是复用的)
|
||||
if not reuse_driver and need_quit_driver and driver:
|
||||
driver.close()
|
||||
driver.quit()
|
||||
|
||||
|
||||
def get_multiple_locations_coordinates(location_names, headless=False, cookie_file='amap_cookies.json'):
|
||||
"""
|
||||
批量获取多个地点的坐标信息
|
||||
|
||||
Args:
|
||||
location_names (list): 地点名称列表
|
||||
headless (bool): 是否使用无头模式,默认False(有界面)
|
||||
cookie_file (str): Cookie文件路径
|
||||
|
||||
Returns:
|
||||
dict: 包含所有地点坐标信息的字典,键为地点名称,值为坐标信息字典或None(失败时)
|
||||
"""
|
||||
# 检查输入参数
|
||||
if not location_names:
|
||||
print("❌ 地点列表为空,无需获取坐标")
|
||||
return {}
|
||||
|
||||
print(f"开始批量获取坐标信息 - 地点数量: {len(location_names)}, 无头模式: {headless}")
|
||||
|
||||
# 存储所有地点的坐标信息
|
||||
all_coordinates = {}
|
||||
success_count = 0
|
||||
failure_count = 0
|
||||
skipped_count = 0
|
||||
invalid_coord_count = 0
|
||||
|
||||
# 创建并复用浏览器实例
|
||||
created_driver = None
|
||||
|
||||
# 记录是否已经退出,避免重复退出
|
||||
driver_closed = False
|
||||
|
||||
try:
|
||||
# 初始化Chrome浏览器
|
||||
chrome_options = Options()
|
||||
if headless:
|
||||
chrome_options.add_argument('--headless')
|
||||
|
||||
# 添加必要的配置参数
|
||||
# chrome_options.add_argument('--disable-gpu')
|
||||
chrome_options.add_argument('--no-sandbox')
|
||||
chrome_options.add_argument('--disable-dev-shm-usage')
|
||||
chrome_options.add_argument('--window-size=1920,1080')
|
||||
chrome_options.add_argument('--lang=zh-CN')
|
||||
chrome_options.add_argument("--disable-blink-features=AutomationControlled") # 防止自动化检测,关闭浏览器控制显示
|
||||
chrome_options.add_argument('-ignore-certificate-errors') # 忽略证书错误
|
||||
chrome_options.add_argument('-ignore-ssl-errors') # 忽略相关错误
|
||||
chrome_options.add_experimental_option('excludeSwitches', ['enable-automation']) # 防止自动化检测
|
||||
chrome_options.add_experimental_option('excludeSwitches', ['enable-logging']) # 防止自动化日志输出检测
|
||||
chrome_options.add_experimental_option('useAutomationExtension', False)
|
||||
|
||||
# 添加随机User-Agent
|
||||
user_agent = f'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{random.randint(80, 99)}.0.{random.randint(4000, 5000)}.{random.randint(100, 200)} Safari/537.36'
|
||||
chrome_options.add_argument(f'user-agent={user_agent}')
|
||||
|
||||
# 尝试使用webdriver_manager
|
||||
try:
|
||||
from webdriver_manager.chrome import ChromeDriverManager
|
||||
driver_path = ChromeDriverManager().install()
|
||||
service = Service(driver_path)
|
||||
print(f"成功使用webdriver_manager安装驱动: {driver_path}")
|
||||
except:
|
||||
print("使用系统默认的Chrome驱动")
|
||||
service = Service()
|
||||
|
||||
# 初始化WebDriver
|
||||
created_driver = webdriver.Chrome(service=service, options=chrome_options)
|
||||
|
||||
# 将创建的driver实例添加到全局跟踪集合中
|
||||
if 'driver_instances' in globals():
|
||||
driver_instances.add(created_driver)
|
||||
|
||||
# 最大化窗口(如果不是无头模式)
|
||||
if not headless:
|
||||
created_driver.maximize_window()
|
||||
|
||||
# 禁用自动化特征
|
||||
created_driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
|
||||
"source": "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"
|
||||
})
|
||||
|
||||
# 使用Cookie方式认证
|
||||
print("尝试使用Cookie文件进行认证")
|
||||
cookies_loaded = load_cookies_to_browser(created_driver, cookie_file)
|
||||
if not cookies_loaded:
|
||||
print("Cookie认证失败,无法继续获取高精度坐标")
|
||||
return all_coordinates
|
||||
|
||||
# 访问高德地图坐标拾取工具
|
||||
url = 'https://lbs.amap.com/tools/picker'
|
||||
created_driver.get(url)
|
||||
time.sleep(2) # 增加等待时间以确保页面完全加载
|
||||
|
||||
# 循环获取每个地点的坐标
|
||||
for i, location_name in enumerate(location_names, 1):
|
||||
print(f"处理地点 {i}/{len(location_names)}: '{location_name}'")
|
||||
|
||||
# 对每个地点查询
|
||||
try:
|
||||
# 调用get_location_coordinates获取坐标
|
||||
result = get_location_coordinates(location_name, headless=headless, cookie_file=cookie_file,
|
||||
driver=created_driver, reuse_driver=True)
|
||||
except Exception as e:
|
||||
print(f"❌ 地点: {location_name} - 查询出错: {str(e)},跳过该地点")
|
||||
result = None
|
||||
skipped_count += 1
|
||||
|
||||
# 存储结果
|
||||
all_coordinates[location_name] = result
|
||||
|
||||
# 坐标获取成功,进行有效性验证
|
||||
if result:
|
||||
# 验证坐标的地理合理性(中国地区范围)
|
||||
try:
|
||||
lng = float(result['lng'])
|
||||
lat = float(result['lat'])
|
||||
|
||||
# 中国地区的合理坐标范围
|
||||
if 70 <= lng <= 140 and 0 <= lat <= 60:
|
||||
# 检查坐标精度(至少4位小数)
|
||||
lng_precision = len(result['lng'].split('.')[1]) if '.' in result['lng'] else 0
|
||||
lat_precision = len(result['lat'].split('.')[1]) if '.' in result['lat'] else 0
|
||||
|
||||
if lng_precision >= 4 and lat_precision >= 4:
|
||||
print(f"✅ 地点: {location_name} - 坐标获取成功: {result['lng']},{result['lat']}")
|
||||
success_count += 1
|
||||
else:
|
||||
print(
|
||||
f"⚠️ 地点: {location_name} - 坐标精度不足: {result['lng']},{result['lat']} (经度{lng_precision}位, 纬度{lat_precision}位小数)")
|
||||
invalid_coord_count += 1
|
||||
else:
|
||||
print(f"⚠️ 地点: {location_name} - 坐标超出中国地区合理范围: {result['lng']},{result['lat']}")
|
||||
invalid_coord_count += 1
|
||||
except (ValueError, TypeError):
|
||||
print(f"⚠️ 地点: {location_name} - 坐标数值无效: {result}")
|
||||
invalid_coord_count += 1
|
||||
else:
|
||||
print(f"❌ 地点: {location_name} - 未能获取有效的坐标信息")
|
||||
failure_count += 1
|
||||
|
||||
# 如果不是最后一个地点,添加短暂延迟以避免过快请求
|
||||
if i < len(location_names):
|
||||
delay_time = random.uniform(0.5, 2) # 减少延迟时间到0.5-2秒
|
||||
print(f"添加延迟: {delay_time:.2f}秒")
|
||||
time.sleep(delay_time)
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 批量坐标获取过程中发生错误: {str(e)}")
|
||||
|
||||
finally:
|
||||
created_driver.close()
|
||||
|
||||
# 打印详细统计信息
|
||||
print(f"\n{'=' * 60}")
|
||||
print("批量坐标获取完成")
|
||||
print(f"{'=' * 60}")
|
||||
print(f"总查询地点数: {len(location_names)}")
|
||||
print(f"成功获取: {success_count}")
|
||||
print(f"获取失败: {failure_count}")
|
||||
print(f"超时跳过: {skipped_count}")
|
||||
print(f"无效坐标: {invalid_coord_count}")
|
||||
print(f"获取到坐标数据的地点数: {len([c for c in all_coordinates.values() if c is not None])}")
|
||||
|
||||
# 计算实际成功率(只考虑成功获取的有效坐标)
|
||||
actual_processed = success_count + failure_count + skipped_count + invalid_coord_count
|
||||
if actual_processed > 0:
|
||||
actual_success_rate = (success_count / actual_processed) * 100
|
||||
print(f"实际成功率: {actual_success_rate:.1f}%")
|
||||
else:
|
||||
print("实际成功率: 0%")
|
||||
|
||||
print(f"{'=' * 60}")
|
||||
|
||||
# 返回结果字典
|
||||
return all_coordinates
|
||||
|
||||
|
||||
def create_cookie_export_tool():
|
||||
"""
|
||||
创建一个Cookie导出工具,帮助用户手动登录后导出Cookie
|
||||
"""
|
||||
print("\n===== Cookie导出工具 =====")
|
||||
print("此工具将打开一个Chrome浏览器窗口,让您手动登录高德地图账号,")
|
||||
print("登录成功后,按Enter键导出Cookie到文件。")
|
||||
print("==========================")
|
||||
|
||||
# 设置Chrome浏览器参数
|
||||
chrome_options = Options()
|
||||
chrome_options.add_argument('--disable-gpu')
|
||||
chrome_options.add_argument('--window-size=1920,1080')
|
||||
chrome_options.add_experimental_option("excludeSwitches", ["enable-automation"])
|
||||
chrome_options.add_experimental_option('useAutomationExtension', False)
|
||||
|
||||
# 初始化WebDriver
|
||||
driver = None
|
||||
try:
|
||||
# 尝试使用webdriver_manager
|
||||
try:
|
||||
from webdriver_manager.chrome import ChromeDriverManager
|
||||
driver_path = ChromeDriverManager().install()
|
||||
service = Service(driver_path)
|
||||
except:
|
||||
service = Service()
|
||||
|
||||
driver = webdriver.Chrome(service=service, options=chrome_options)
|
||||
driver.maximize_window()
|
||||
|
||||
# 禁用自动化特征
|
||||
driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
|
||||
"source": "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"
|
||||
})
|
||||
|
||||
# 访问高德地图坐标拾取工具
|
||||
url = 'https://lbs.amap.com/tools/picker'
|
||||
print(f"\n正在打开高德地图坐标拾取工具: {url}")
|
||||
import ssl
|
||||
ssl._create_default_https_context = ssl._create_unverified_context
|
||||
driver.get(url)
|
||||
|
||||
print("\n请在浏览器中手动登录高德地图账号。")
|
||||
print("提示:建议使用微信扫码登录以避免验证码问题")
|
||||
print("登录成功后,请按Enter键继续...")
|
||||
input()
|
||||
|
||||
# 导出Cookie - 使用基于当前脚本所在目录的绝对路径
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
cookie_file = os.path.join(script_dir, '../Test/amap_cookies.json')
|
||||
export_success = export_cookies_from_browser(driver, cookie_file)
|
||||
|
||||
if export_success:
|
||||
print(f"\nCookie导出成功!文件保存为: {os.path.abspath(cookie_file)}")
|
||||
print("您现在可以使用这个Cookie文件获取高精度坐标了。")
|
||||
print("\n重要提示:")
|
||||
print("1. 请妥善保管您的Cookie文件,不要分享给他人")
|
||||
print("2. Cookie文件包含您的登录状态信息,请确保安全存储")
|
||||
print("3. 当Cookie过期后,您需要重新运行此工具生成新的Cookie文件")
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n发生错误: {str(e)}")
|
||||
finally:
|
||||
if driver:
|
||||
print("\n正在关闭浏览器...")
|
||||
driver.quit()
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
主函数,用于测试和运行高德地图坐标获取工具
|
||||
"""
|
||||
print("=== 高德地图高精度坐标获取工具 ====")
|
||||
|
||||
# 初始化参数
|
||||
# 使用基于当前脚本所在目录的相对路径,确保在不同环境下都能正确访问
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
cookie_file = os.path.join(script_dir, '../Test/amap_cookies.json')
|
||||
|
||||
# 询问是否清空所有数据的经纬度
|
||||
clear_coords = input("是否清空所有数据的经纬度,全新开始更新坐标?(y/n,默认n): ").strip().lower() == 'y'
|
||||
if clear_coords:
|
||||
print("正在清空所有学校的经纬度数据...")
|
||||
try:
|
||||
execute_update("UPDATE t_school SET longitude=0, latitude=0")
|
||||
print("✅ 经纬度数据已清空")
|
||||
except Exception as e:
|
||||
print(f"❌ 清空经纬度数据失败: {str(e)}")
|
||||
sys.exit(1)
|
||||
|
||||
# 从数据库查询需要更新坐标的学校(经纬度为0的学校)
|
||||
print("正在从数据库查询需要更新坐标的学校...")
|
||||
try:
|
||||
results = find("SELECT org_name FROM t_school WHERE longitude=0 AND latitude=0 order by region_level1,region_level2,region_level3,school_type")
|
||||
locations = [result['org_name'] for result in results]
|
||||
print(f"✅ 找到 {len(locations)} 所需要更新坐标的学校")
|
||||
|
||||
if not locations:
|
||||
print("所有学校都已有坐标数据,程序退出。")
|
||||
sys.exit(0)
|
||||
|
||||
# 显示前10个学校作为预览
|
||||
print("\n待处理学校列表预览:")
|
||||
for i, loc in enumerate(locations[:10], 1):
|
||||
print(f" {i}. {loc}")
|
||||
if len(locations) > 10:
|
||||
print(f" ... 等{len(locations) - 10}个学校")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 查询学校列表失败: {str(e)}")
|
||||
sys.exit(1)
|
||||
|
||||
# 检查Cookie文件
|
||||
if not os.path.exists(cookie_file):
|
||||
print(f"\nCookie文件 '{cookie_file}' 不存在。")
|
||||
if input("是否现在运行Cookie导出工具? (y/n): ").strip().lower() == 'y':
|
||||
create_cookie_export_tool()
|
||||
else:
|
||||
print("\n程序退出。没有有效的Cookie文件,无法获取高精度坐标。")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"\n运行参数: 无头模式{'启用' if HEADLESS else '禁用'}")
|
||||
|
||||
# 逐个获取坐标并立即更新数据库
|
||||
print("\n开始逐个获取坐标并更新数据库...")
|
||||
success_updates = 0
|
||||
total_locations = len(locations)
|
||||
|
||||
# 创建浏览器实例(复用)
|
||||
chrome_options = Options()
|
||||
if HEADLESS:
|
||||
chrome_options.add_argument('--headless')
|
||||
|
||||
# 添加必要的配置参数
|
||||
chrome_options.add_argument('--no-sandbox')
|
||||
chrome_options.add_argument('--disable-dev-shm-usage')
|
||||
chrome_options.add_argument('--window-size=1920,1080')
|
||||
chrome_options.add_argument('--lang=zh-CN')
|
||||
chrome_options.add_argument("--disable-blink-features=AutomationControlled")
|
||||
chrome_options.add_argument('-ignore-certificate-errors')
|
||||
chrome_options.add_argument('-ignore-ssl-errors')
|
||||
chrome_options.add_experimental_option('excludeSwitches', ['enable-automation'])
|
||||
chrome_options.add_experimental_option('excludeSwitches', ['enable-logging'])
|
||||
chrome_options.add_experimental_option('useAutomationExtension', False)
|
||||
|
||||
# 随机User-Agent
|
||||
user_agent = f'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{random.randint(80, 99)}.0.{random.randint(4000, 5000)}.{random.randint(100, 200)} Safari/537.36'
|
||||
chrome_options.add_argument(f'user-agent={user_agent}')
|
||||
|
||||
# 初始化WebDriver
|
||||
try:
|
||||
from webdriver_manager.chrome import ChromeDriverManager
|
||||
driver_path = ChromeDriverManager().install()
|
||||
service = Service(driver_path)
|
||||
print(f"成功使用webdriver_manager安装驱动: {driver_path}")
|
||||
except:
|
||||
print("使用系统默认的Chrome驱动")
|
||||
service = Service()
|
||||
|
||||
driver = webdriver.Chrome(service=service, options=chrome_options)
|
||||
|
||||
# 添加到全局跟踪集合
|
||||
if 'driver_instances' in globals():
|
||||
driver_instances.add(driver)
|
||||
|
||||
# 最大化窗口(如果不是无头模式)
|
||||
if not HEADLESS:
|
||||
driver.maximize_window()
|
||||
|
||||
# 禁用自动化特征
|
||||
driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
|
||||
"source": "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"
|
||||
})
|
||||
|
||||
# 使用Cookie方式认证
|
||||
cookies_loaded = load_cookies_to_browser(driver, cookie_file)
|
||||
if not cookies_loaded:
|
||||
print("Cookie认证失败,无法继续获取高精度坐标")
|
||||
driver.quit()
|
||||
return
|
||||
|
||||
# 访问高德地图坐标拾取工具
|
||||
url = 'https://lbs.amap.com/tools/picker'
|
||||
driver.get(url)
|
||||
time.sleep(2) # 等待页面完全加载
|
||||
|
||||
update_sql = "UPDATE t_school SET longitude = :lng, latitude = :lat WHERE org_name = :org_name"
|
||||
|
||||
try:
|
||||
# 逐个处理每个地点
|
||||
for i, location_name in enumerate(locations, 1):
|
||||
print(f"处理地点 {i}/{total_locations}: '{location_name}'")
|
||||
|
||||
# 获取单个地点的坐标
|
||||
try:
|
||||
coords = get_location_coordinates(
|
||||
location_name,
|
||||
headless=HEADLESS,
|
||||
cookie_file=cookie_file,
|
||||
driver=driver,
|
||||
reuse_driver=True
|
||||
)
|
||||
|
||||
# 如果成功获取到坐标,立即更新数据库
|
||||
if coords:
|
||||
try:
|
||||
execute_update(update_sql, {
|
||||
'lng': coords['lng'],
|
||||
'lat': coords['lat'],
|
||||
'org_name': location_name
|
||||
})
|
||||
success_updates += 1
|
||||
print(f"✅ 地点: {location_name} - 坐标获取成功并更新数据库: {coords['lng']},{coords['lat']}")
|
||||
except Exception as e:
|
||||
print(f" ❌ 更新地点'{location_name}'失败: {str(e)}")
|
||||
else:
|
||||
print(f"❌ 地点: {location_name} - 未能获取有效的坐标信息")
|
||||
|
||||
# 添加短暂延迟以避免过快请求
|
||||
if i < total_locations:
|
||||
delay_time = random.uniform(0.5, 2)
|
||||
print(f"添加延迟: {delay_time:.2f}秒")
|
||||
time.sleep(delay_time)
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 地点: {location_name} - 查询出错: {str(e)}")
|
||||
|
||||
# 添加延迟后继续处理下一个地点
|
||||
if i < total_locations:
|
||||
time.sleep(random.uniform(0.5, 2))
|
||||
|
||||
finally:
|
||||
# 关闭浏览器
|
||||
driver.close()
|
||||
driver.quit()
|
||||
|
||||
print(f"\n✅ 数据库更新完成,成功{success_updates}/{total_locations}条记录")
|
||||
|
||||
print("\n✅ 程序执行完成!")
|
||||
print("\n提示:如需重新批量更新所有学校坐标,下次运行程序时选择清空所有数据的经纬度即可。")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 全局变量,用于跟踪所有创建的WebDriver实例
|
||||
driver_instances = weakref.WeakSet()
|
||||
|
||||
try:
|
||||
print("=== 高德地图高精度坐标获取工具 ===")
|
||||
print("正在初始化...")
|
||||
# 直接运行主程序
|
||||
main()
|
||||
except KeyboardInterrupt:
|
||||
print("\n⚠️ 接收到键盘中断,退出程序")
|
||||
except Exception as main_error:
|
||||
print(f"\n❌ 主程序运行出错: {main_error}")
|
||||
|
||||
print("\n程序已退出")
|
||||
810
Json/Ref/T7_PaChongGaoDePatch.py
Normal file
810
Json/Ref/T7_PaChongGaoDePatch.py
Normal file
@@ -0,0 +1,810 @@
|
||||
import json
|
||||
import random
|
||||
import asyncio
|
||||
import math
|
||||
from typing import List, Tuple, Dict, Any
|
||||
|
||||
from DbKit.Db import Db
|
||||
|
||||
|
||||
def is_point_in_polygon(point: Tuple[float, float], polygon: List[Tuple[float, float]]) -> bool:
|
||||
"""
|
||||
射线法判断点是否在多边形内
|
||||
:param point: (经度, 纬度)
|
||||
:param polygon: 多边形顶点坐标列表
|
||||
:return: True表示在多边形内
|
||||
"""
|
||||
x, y = point
|
||||
n = len(polygon)
|
||||
inside = False
|
||||
|
||||
for i in range(n):
|
||||
x1, y1 = polygon[i]
|
||||
x2, y2 = polygon[(i + 1) % n]
|
||||
|
||||
# 检查点是否在边的y范围内
|
||||
if ((y1 > y) != (y2 > y)):
|
||||
# 计算交点x坐标
|
||||
x_intersect = (x2 - x1) * (y - y1) / (y2 - y1) + x1
|
||||
|
||||
if x_intersect > x:
|
||||
inside = not inside
|
||||
|
||||
return inside
|
||||
|
||||
|
||||
def load_geojson(json_path: str) -> List[List[Tuple[float, float]]]:
|
||||
"""
|
||||
从GeoJSON文件加载多边形数据(支持Polygon和MultiPolygon)
|
||||
返回: 多边形列表,每个多边形是(经度, 纬度)元组的列表
|
||||
"""
|
||||
with open(json_path, 'r', encoding='utf-8') as f:
|
||||
geo_data = json.load(f)
|
||||
|
||||
polygons = []
|
||||
geometry_type = geo_data['features'][0]['geometry']['type']
|
||||
coordinates = geo_data['features'][0]['geometry']['coordinates']
|
||||
|
||||
if geometry_type == 'Polygon':
|
||||
polygons.append(coordinates[0])
|
||||
elif geometry_type == 'MultiPolygon':
|
||||
for polygon_coords in coordinates:
|
||||
polygons.append(polygon_coords[0])
|
||||
|
||||
return polygons
|
||||
|
||||
|
||||
# 计算点集的分布统计信息
|
||||
def calculate_distribution_statistics(points):
|
||||
"""
|
||||
计算点集的分布统计信息
|
||||
|
||||
:param points: 点集 [(lon, lat), ...]
|
||||
:return: 包含min_lon, max_lon, min_lat, max_lat, center_lon, center_lat, std_lon, std_lat的字典
|
||||
"""
|
||||
if not points:
|
||||
return None
|
||||
|
||||
lons = [p[0] for p in points]
|
||||
lats = [p[1] for p in points]
|
||||
|
||||
min_lon, max_lon = min(lons), max(lons)
|
||||
min_lat, max_lat = min(lats), max(lats)
|
||||
|
||||
# 计算中心点
|
||||
center_lon = (min_lon + max_lon) / 2
|
||||
center_lat = (min_lat + max_lat) / 2
|
||||
|
||||
# 计算标准差(衡量分布的分散程度)
|
||||
mean_lon = sum(lons) / len(lons)
|
||||
mean_lat = sum(lats) / len(lats)
|
||||
|
||||
std_lon = math.sqrt(sum((lon - mean_lon) ** 2 for lon in lons) / len(lons))
|
||||
std_lat = math.sqrt(sum((lat - mean_lat) ** 2 for lat in lats) / len(lats))
|
||||
|
||||
return {
|
||||
"min_lon": min_lon,
|
||||
"max_lon": max_lon,
|
||||
"min_lat": min_lat,
|
||||
"max_lat": max_lat,
|
||||
"center_lon": center_lon,
|
||||
"center_lat": center_lat,
|
||||
"std_lon": std_lon,
|
||||
"std_lat": std_lat
|
||||
}
|
||||
|
||||
# 线性映射函数
|
||||
def linear_map(value, from_min, from_max, to_min, to_max):
|
||||
"""
|
||||
将值从一个范围线性映射到另一个范围
|
||||
|
||||
:param value: 输入值
|
||||
:param from_min: 原始范围最小值
|
||||
:param from_max: 原始范围最大值
|
||||
:param to_min: 目标范围最小值
|
||||
:param to_max: 目标范围最大值
|
||||
:return: 映射后的值
|
||||
"""
|
||||
# 处理除零情况
|
||||
if from_max == from_min:
|
||||
return (to_min + to_max) / 2
|
||||
# 计算映射值
|
||||
return (value - from_min) * (to_max - to_min) / (from_max - from_min) + to_min
|
||||
|
||||
# 计算点到线段的最近点
|
||||
def closest_point_on_segment(point, line_start, line_end):
|
||||
"""
|
||||
计算点到线段的最近点
|
||||
|
||||
:param point: 点 [lon, lat]
|
||||
:param line_start: 线段起点 [lon, lat]
|
||||
:param line_end: 线段终点 [lon, lat]
|
||||
:return: 线段上的最近点 [lon, lat]
|
||||
"""
|
||||
# 将经纬度转换为向量
|
||||
a = [point[0] - line_start[0], point[1] - line_start[1]]
|
||||
b = [line_end[0] - line_start[0], line_end[1] - line_start[1]]
|
||||
|
||||
# 计算投影参数t
|
||||
t = max(0, min(1, (a[0] * b[0] + a[1] * b[1]) / (b[0] ** 2 + b[1] ** 2) if (b[0] ** 2 + b[1] ** 2) > 0 else 0))
|
||||
|
||||
# 计算投影点
|
||||
return [line_start[0] + t * b[0], line_start[1] + t * b[1]]
|
||||
|
||||
# 计算两点之间的距离
|
||||
def calculate_distance(point1, point2):
|
||||
"""
|
||||
计算两点之间的欧几里得距离
|
||||
|
||||
:param point1: 第一个点 [lon, lat]
|
||||
:param point2: 第二个点 [lon, lat]
|
||||
:return: 两点之间的距离
|
||||
"""
|
||||
return math.sqrt((point1[0] - point2[0]) ** 2 + (point1[1] - point2[1]) ** 2)
|
||||
|
||||
# 调整点到多边形边界
|
||||
def adjust_to_polygon_boundary(point, polygons):
|
||||
"""
|
||||
调整点到最近的多边形边界
|
||||
|
||||
:param point: 需要调整的点 [lon, lat]
|
||||
:param polygons: 多边形列表
|
||||
:return: 调整后的点 [lon, lat]
|
||||
"""
|
||||
min_distance = float('inf')
|
||||
closest_point = point
|
||||
|
||||
for polygon in polygons:
|
||||
for i in range(len(polygon)):
|
||||
p1 = polygon[i]
|
||||
p2 = polygon[(i + 1) % len(polygon)]
|
||||
|
||||
# 计算点到线段的最近点
|
||||
proj_point = closest_point_on_segment(point, p1, p2)
|
||||
distance = calculate_distance(point, proj_point)
|
||||
|
||||
if distance < min_distance:
|
||||
min_distance = distance
|
||||
closest_point = proj_point
|
||||
|
||||
return closest_point
|
||||
|
||||
# 将原始坐标映射到目标区域,保持相对位置关系
|
||||
def map_coordinates(original_points, polygons):
|
||||
"""
|
||||
将原始坐标映射到目标区域,保持相对位置关系
|
||||
|
||||
:param original_points: 原始坐标列表 [(original_lon, original_lat), ...]
|
||||
:param polygons: 目标区域的多边形列表
|
||||
:return: 映射后的坐标列表 [(mapped_lon, mapped_lat), ...]
|
||||
"""
|
||||
if not original_points or not polygons:
|
||||
return []
|
||||
|
||||
# 计算原始点集的分布统计
|
||||
original_stats = calculate_distribution_statistics(original_points)
|
||||
if not original_stats:
|
||||
return []
|
||||
|
||||
# 获取目标区域的边界信息
|
||||
all_target_points = []
|
||||
for polygon in polygons:
|
||||
all_target_points.extend(polygon)
|
||||
|
||||
if not all_target_points:
|
||||
return []
|
||||
|
||||
target_lons = [p[0] for p in all_target_points]
|
||||
target_lats = [p[1] for p in all_target_points]
|
||||
|
||||
target_min_lon, target_max_lon = min(target_lons), max(target_lons)
|
||||
target_min_lat, target_max_lat = min(target_lats), max(target_lats)
|
||||
|
||||
# 收缩目标边界框,使生成的坐标更靠近县区中心
|
||||
shrink_factor = 0.25 # 收缩25%,增大收缩比例使坐标更靠近中心
|
||||
delta_lon = (target_max_lon - target_min_lon) * shrink_factor
|
||||
delta_lat = (target_max_lat - target_min_lat) * shrink_factor
|
||||
target_min_lon += delta_lon
|
||||
target_max_lon -= delta_lon
|
||||
target_min_lat += delta_lat
|
||||
target_max_lat -= delta_lat
|
||||
|
||||
# 计算原始点集的边界
|
||||
original_min_lon = original_stats["min_lon"]
|
||||
original_max_lon = original_stats["max_lon"]
|
||||
original_min_lat = original_stats["min_lat"]
|
||||
original_max_lat = original_stats["max_lat"]
|
||||
|
||||
mapped_points = []
|
||||
for original_lon, original_lat in original_points:
|
||||
# 线性映射经度和纬度
|
||||
mapped_lon = linear_map(original_lon, original_min_lon, original_max_lon, target_min_lon, target_max_lon)
|
||||
mapped_lat = linear_map(original_lat, original_min_lat, original_max_lat, target_min_lat, target_max_lat)
|
||||
|
||||
# 检查映射后的点是否在目标区域内
|
||||
if any(is_point_in_polygon((mapped_lon, mapped_lat), polygon) for polygon in polygons):
|
||||
mapped_points.append((mapped_lon, mapped_lat))
|
||||
else:
|
||||
# 如果映射后的点不在目标区域内,调整到最近的边界
|
||||
adjusted_lon, adjusted_lat = adjust_to_polygon_boundary([mapped_lon, mapped_lat], polygons)
|
||||
mapped_points.append((adjusted_lon, adjusted_lat))
|
||||
|
||||
return mapped_points
|
||||
|
||||
# 传统的均匀分布点生成算法(作为回退方案)
|
||||
def generate_uniform_points_fallback(polygons, n_points):
|
||||
"""
|
||||
在指定的多边形区域内生成均匀分布的点(回退方案)
|
||||
|
||||
:param polygons: 多边形列表,每个多边形是坐标点的列表
|
||||
:param n_points: 需要生成的点数
|
||||
:return: 生成的点列表 [(lon, lat), ...]
|
||||
"""
|
||||
if not polygons:
|
||||
return []
|
||||
|
||||
# 计算整体边界框
|
||||
all_lons = [p[0] for polygon in polygons for p in polygon]
|
||||
all_lats = [p[1] for polygon in polygons for p in polygon]
|
||||
min_lon, max_lon = min(all_lons), max(all_lons)
|
||||
min_lat, max_lat = min(all_lats), max(all_lats)
|
||||
|
||||
# 收缩边界框,使生成的坐标更靠近县区中心
|
||||
lon_margin = (max_lon - min_lon) * 0.25 # 收缩25%,增大收缩比例使坐标更靠近中心
|
||||
lat_margin = (max_lat - min_lat) * 0.25
|
||||
min_lon = min_lon + lon_margin
|
||||
max_lon = max_lon - lon_margin
|
||||
min_lat = min_lat + lat_margin
|
||||
max_lat = max_lat - lat_margin
|
||||
|
||||
# 计算每个多边形的面积,用于按比例分配点数
|
||||
def calculate_polygon_area(polygon):
|
||||
# 使用Shoelace公式计算多边形面积
|
||||
area = 0.0
|
||||
n = len(polygon)
|
||||
for i in range(n):
|
||||
j = (i + 1) % n
|
||||
area += polygon[i][0] * polygon[j][1]
|
||||
area -= polygon[j][0] * polygon[i][1]
|
||||
return abs(area) / 2.0
|
||||
|
||||
# 计算每个多边形的面积
|
||||
polygon_areas = [calculate_polygon_area(polygon) for polygon in polygons]
|
||||
total_area = sum(polygon_areas)
|
||||
|
||||
# 根据面积比例分配每个多边形需要生成的点数
|
||||
if total_area > 0:
|
||||
points_per_polygon = [max(1, int(area / total_area * n_points)) for area in polygon_areas]
|
||||
# 调整点数,确保总和等于n_points
|
||||
points_per_polygon[-1] += n_points - sum(points_per_polygon)
|
||||
else:
|
||||
points_per_polygon = [n_points // len(polygons)] * len(polygons)
|
||||
points_per_polygon[-1] += n_points - sum(points_per_polygon)
|
||||
|
||||
points = []
|
||||
|
||||
# 为每个多边形单独生成点
|
||||
for polygon_index, (polygon, points_needed) in enumerate(zip(polygons, points_per_polygon)):
|
||||
if points_needed <= 0:
|
||||
continue
|
||||
|
||||
# 计算当前多边形的边界框
|
||||
polygon_lons = [p[0] for p in polygon]
|
||||
polygon_lats = [p[1] for p in polygon]
|
||||
poly_min_lon, poly_max_lon = min(polygon_lons), max(polygon_lons)
|
||||
poly_min_lat, poly_max_lat = min(polygon_lats), max(polygon_lats)
|
||||
|
||||
# 收缩当前多边形的边界框,使其更靠近中心
|
||||
poly_lon_margin = (poly_max_lon - poly_min_lon) * 0.25 # 收缩25%,与全局收缩比例保持一致
|
||||
poly_lat_margin = (poly_max_lat - poly_min_lat) * 0.25
|
||||
poly_min_lon += poly_lon_margin
|
||||
poly_max_lon -= poly_lon_margin
|
||||
poly_min_lat += poly_lat_margin
|
||||
poly_max_lat -= poly_lat_margin
|
||||
|
||||
# 为当前多边形生成足够的网格
|
||||
cells_needed = max(points_needed * 2, 10) # 网格数量是点数的2倍,确保分布均匀
|
||||
cols = math.ceil(math.sqrt(cells_needed))
|
||||
rows = math.ceil(cells_needed / cols)
|
||||
|
||||
cell_width = (poly_max_lon - poly_min_lon) / cols
|
||||
cell_height = (poly_max_lat - poly_min_lat) / rows
|
||||
|
||||
# 记录哪些网格已经生成了点
|
||||
filled_cells = []
|
||||
max_attempts_per_cell = 50
|
||||
|
||||
# 按网格生成点
|
||||
for attempt in range(rows * cols * 2): # 增加尝试次数
|
||||
if len(points) >= n_points:
|
||||
break
|
||||
|
||||
# 随机选择一个网格
|
||||
i = random.randint(0, rows - 1)
|
||||
j = random.randint(0, cols - 1)
|
||||
cell_key = (i, j)
|
||||
|
||||
# 如果这个网格已经生成了足够的点,跳过
|
||||
if filled_cells.count(cell_key) >= math.ceil(points_needed / (rows * cols)):
|
||||
continue
|
||||
|
||||
# 当前网格范围
|
||||
cell_min_lon = poly_min_lon + j * cell_width
|
||||
cell_max_lon = poly_min_lon + (j + 1) * cell_width
|
||||
cell_min_lat = poly_min_lat + i * cell_height
|
||||
cell_max_lat = poly_min_lat + (i + 1) * cell_height
|
||||
|
||||
# 在网格内尝试生成有效点
|
||||
for _ in range(max_attempts_per_cell):
|
||||
lon = random.uniform(cell_min_lon, cell_max_lon)
|
||||
lat = random.uniform(cell_min_lat, cell_max_lat)
|
||||
|
||||
# 检查点是否在当前多边形内
|
||||
if is_point_in_polygon((lon, lat), polygon):
|
||||
points.append((lon, lat))
|
||||
filled_cells.append(cell_key)
|
||||
break
|
||||
|
||||
# 如果当前多边形的点数不足,使用均匀分布补充
|
||||
while len(points) < sum(points_per_polygon[:polygon_index+1]):
|
||||
lon = random.uniform(poly_min_lon, poly_max_lon)
|
||||
lat = random.uniform(poly_min_lat, poly_max_lat)
|
||||
|
||||
if is_point_in_polygon((lon, lat), polygon):
|
||||
points.append((lon, lat))
|
||||
|
||||
# 如果总点数还是不足,使用全局均匀分布补充
|
||||
while len(points) < n_points:
|
||||
# 随机选择一个多边形
|
||||
polygon = random.choice(polygons)
|
||||
|
||||
# 计算该多边形的边界框
|
||||
polygon_lons = [p[0] for p in polygon]
|
||||
polygon_lats = [p[1] for p in polygon]
|
||||
poly_min_lon, poly_max_lon = min(polygon_lons), max(polygon_lons)
|
||||
poly_min_lat, poly_max_lat = min(polygon_lats), max(polygon_lats)
|
||||
|
||||
# 在边界框内随机生成点
|
||||
for _ in range(100): # 最多尝试100次
|
||||
lon = random.uniform(poly_min_lon, poly_max_lon)
|
||||
lat = random.uniform(poly_min_lat, poly_max_lat)
|
||||
|
||||
if is_point_in_polygon((lon, lat), polygon):
|
||||
points.append((lon, lat))
|
||||
break
|
||||
|
||||
return points[:n_points]
|
||||
|
||||
# 在指定的多边形区域内生成均匀分布的点
|
||||
async def generate_uniform_points(polygons: List[List[Tuple[float, float]]], num_points: int, original_coords=None) -> List[Tuple[float, float]]:
|
||||
"""
|
||||
在指定的多边形区域内生成均匀分布的点
|
||||
|
||||
:param polygons: 多边形列表,每个多边形是坐标点的列表
|
||||
:param num_points: 需要生成的点数
|
||||
:param original_coords: 原始坐标列表,用于参考相对位置关系
|
||||
:return: 生成的点列表 [(lon, lat), ...]
|
||||
"""
|
||||
if not polygons:
|
||||
return []
|
||||
|
||||
# 如果提供了原始坐标,使用坐标映射算法
|
||||
if original_coords and len(original_coords) > 1:
|
||||
# 过滤掉None值的原始坐标
|
||||
valid_original_coords = [(coord["original_longitude"], coord["original_latitude"])
|
||||
for coord in original_coords
|
||||
if coord["original_longitude"] is not None
|
||||
and coord["original_latitude"] is not None]
|
||||
|
||||
if len(valid_original_coords) >= num_points:
|
||||
# 如果原始坐标足够,直接使用映射算法
|
||||
return map_coordinates(valid_original_coords[:num_points], polygons)
|
||||
elif valid_original_coords: # 如果有部分原始坐标,先映射这些,再补充生成
|
||||
mapped_points = map_coordinates(valid_original_coords, polygons)
|
||||
# 计算还需要生成的点数
|
||||
remaining_points = num_points - len(mapped_points)
|
||||
if remaining_points > 0:
|
||||
# 补充生成均匀分布的点
|
||||
fallback_points = generate_uniform_points_fallback(polygons, remaining_points)
|
||||
mapped_points.extend(fallback_points)
|
||||
return mapped_points[:num_points]
|
||||
|
||||
# 如果没有原始坐标或原始坐标不足,使用传统的均匀分布点生成算法
|
||||
return generate_uniform_points_fallback(polygons, num_points)
|
||||
|
||||
|
||||
# 导入数据库操作类
|
||||
db = Db()
|
||||
|
||||
# 一共有哪些县区
|
||||
async def getAllArea():
|
||||
sql = "select area_code,area_name from t_dm_area where area_level=3 order by area_code"
|
||||
return await db.find(sql)
|
||||
|
||||
|
||||
# 根据名称获取县区代码
|
||||
async def getAreaCode(areaName):
|
||||
sql = "select area_code from t_dm_area where area_level=3 and area_name=:area_name"
|
||||
res = await db.findFirst(sql, {"area_name": areaName})
|
||||
# 需要去掉最后的三个0
|
||||
return str(res["area_code"])[:-3]
|
||||
|
||||
# 从t_school_xy表获取指定县区和学校类型的原始坐标数据
|
||||
async def get_original_school_coordinates(region_level3: str, school_types: List[str]) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
从t_school_xy表获取指定县区和学校类型的原始坐标数据
|
||||
|
||||
:param region_level3: 县区名称
|
||||
:param school_types: 学校类型列表
|
||||
:return: 包含学校信息和原始坐标的列表
|
||||
"""
|
||||
if not school_types:
|
||||
return []
|
||||
|
||||
# 构建SQL查询,获取原始坐标数据
|
||||
placeholders = ",".join([f":type{i}" for i in range(len(school_types))])
|
||||
sql = f"""
|
||||
SELECT org_name, school_type, longitude AS original_longitude, latitude AS original_latitude,
|
||||
region_level1, region_level2, region_level3, region_level4, region_level5
|
||||
FROM t_school_xy
|
||||
WHERE region_level3 = :region_level3 AND school_type IN ({placeholders})
|
||||
"""
|
||||
|
||||
params = {"region_level3": region_level3}
|
||||
# 添加每个学校类型的参数
|
||||
for i, st in enumerate(school_types):
|
||||
params[f"type{i}"] = st
|
||||
|
||||
# 获取原始坐标数据
|
||||
original_coords = await db.find(sql, params)
|
||||
|
||||
# 转换坐标数据类型,确保longitude和latitude是float类型
|
||||
# 这可以避免与代码中其他float类型进行数学运算时出现类型不兼容错误
|
||||
from decimal import Decimal
|
||||
for coord in original_coords:
|
||||
if isinstance(coord.get("original_longitude"), Decimal):
|
||||
coord["original_longitude"] = float(coord["original_longitude"])
|
||||
if isinstance(coord.get("original_latitude"), Decimal):
|
||||
coord["original_latitude"] = float(coord["original_latitude"])
|
||||
|
||||
return original_coords
|
||||
|
||||
|
||||
def get_school_types_by_education_level(education_level: str) -> List[str]:
|
||||
"""
|
||||
根据学段获取对应的学校类型列表
|
||||
|
||||
:param education_level: 学段名称,可选值:学前、小学、初中、高中
|
||||
:return: 对应学校类型的列表,用于SQL查询中的in子句
|
||||
"""
|
||||
# 定义学段到学校类型的映射关系
|
||||
school_type_mapping = {
|
||||
"学前": ["附设幼儿班", "幼儿园"],
|
||||
"小学": ["小学教学点", "小学", "附设小学班"],
|
||||
"初中": ["初级中学", "专门学校", "附设普通初中班", "附设职业初中班", "九年一贯制学校"],
|
||||
"高中": ["附设普通高中班", "高级中学", "十二年一贯制学校", "完全中学"]
|
||||
}
|
||||
|
||||
# 返回对应的学校类型列表,如果输入无效则返回空列表
|
||||
return school_type_mapping.get(education_level, [])
|
||||
|
||||
|
||||
# 获取指定县区+指定学校类型的学校,同时包含原始坐标信息
|
||||
async def getSchoolList(region_level3, school_type):
|
||||
# region_level3 是指县区的名称
|
||||
# 检查是否传入的是学段名称
|
||||
if school_type in ["学前", "小学", "初中", "高中"]:
|
||||
school_types = get_school_types_by_education_level(school_type)
|
||||
if not school_types:
|
||||
return []
|
||||
# 使用in子句查询多个学校类型
|
||||
placeholders = ",".join([f":type{i}" for i in range(len(school_types))])
|
||||
|
||||
# 1. 从t_school表获取需要更新的学校列表
|
||||
sql_school = f"""
|
||||
select region_level1,region_level2,region_level3,region_level4,region_level5,school_type,org_name
|
||||
from t_school
|
||||
where region_level3=:region_level3 and school_type in ({placeholders})
|
||||
order by region_level1,region_level2,region_level3,region_level4,region_level5,school_type
|
||||
"""
|
||||
params = {"region_level3": region_level3}
|
||||
# 添加每个学校类型的参数
|
||||
for i, st in enumerate(school_types):
|
||||
params[f"type{i}"] = st
|
||||
|
||||
schools = await db.find(sql_school, params)
|
||||
|
||||
if not schools:
|
||||
return []
|
||||
|
||||
# 2. 从t_school_xy表获取原始坐标数据
|
||||
original_coords = await get_original_school_coordinates(region_level3, school_types)
|
||||
|
||||
# 3. 创建原始坐标字典,方便查找
|
||||
original_coords_dict = {}
|
||||
for coord in original_coords:
|
||||
original_coords_dict[coord["org_name"]] = {
|
||||
"original_longitude": coord["original_longitude"],
|
||||
"original_latitude": coord["original_latitude"]
|
||||
}
|
||||
|
||||
# 4. 合并原始坐标信息到学校列表
|
||||
for school in schools:
|
||||
org_name = school["org_name"]
|
||||
if org_name in original_coords_dict:
|
||||
school["original_longitude"] = original_coords_dict[org_name]["original_longitude"]
|
||||
school["original_latitude"] = original_coords_dict[org_name]["original_latitude"]
|
||||
else:
|
||||
# 如果没有原始坐标,设置为None
|
||||
school["original_longitude"] = None
|
||||
school["original_latitude"] = None
|
||||
|
||||
return schools
|
||||
else:
|
||||
# 原有的单个学校类型查询逻辑(简化版,不包含原始坐标合并)
|
||||
sql = "select region_level1,region_level2,region_level3,region_level4,region_level5,school_type,org_name from t_school where region_level3=:region_level3 and school_type=:school_type order by region_level1,region_level2,region_level3,region_level4,region_level5,school_type"
|
||||
schools = await db.find(sql, {"region_level3": region_level3, "school_type": school_type})
|
||||
|
||||
# 获取原始坐标并合并
|
||||
original_coords = await get_original_school_coordinates(region_level3, [school_type])
|
||||
original_coords_dict = {
|
||||
coord["org_name"]: {
|
||||
"original_longitude": coord["original_longitude"],
|
||||
"original_latitude": coord["original_latitude"]
|
||||
}
|
||||
for coord in original_coords
|
||||
}
|
||||
|
||||
for school in schools:
|
||||
org_name = school["org_name"]
|
||||
if org_name in original_coords_dict:
|
||||
school["original_longitude"] = original_coords_dict[org_name]["original_longitude"]
|
||||
school["original_latitude"] = original_coords_dict[org_name]["original_latitude"]
|
||||
else:
|
||||
school["original_longitude"] = None
|
||||
school["original_latitude"] = None
|
||||
|
||||
return schools
|
||||
|
||||
|
||||
# 获取指定县区下+指定学段的学校数量
|
||||
async def getSchoolCount(region_level3, school_type):
|
||||
# 检查是否传入的是学段名称
|
||||
if school_type in ["学前", "小学", "初中", "高中"]:
|
||||
school_types = get_school_types_by_education_level(school_type)
|
||||
if not school_types:
|
||||
return {"c": 0}
|
||||
# 使用in子句查询多个学校类型
|
||||
placeholders = ",".join([f":type{i}" for i in range(len(school_types))])
|
||||
sql = f"select count(1) as c from t_school where region_level3=:region_level3 and school_type in ({placeholders})"
|
||||
params = {"region_level3": region_level3}
|
||||
# 添加每个学校类型的参数
|
||||
for i, st in enumerate(school_types):
|
||||
params[f"type{i}"] = st
|
||||
return await db.findFirst(sql, params)
|
||||
else:
|
||||
# 原有的单个学校类型查询逻辑
|
||||
sql = "select count(1) as c from t_school where region_level3=:region_level3 and school_type=:school_type"
|
||||
return await db.findFirst(sql, {"region_level3": region_level3, "school_type": school_type})
|
||||
|
||||
|
||||
# 更新学校经纬度
|
||||
async def updateSchool(org_name, longitude, latitude):
|
||||
sql = "update t_school set longitude=:longitude,latitude=:latitude where org_name=:org_name"
|
||||
return await db.execute_update(sql, {"org_name": org_name, "longitude": longitude, "latitude": latitude})
|
||||
|
||||
# 重置所有学校坐标为0
|
||||
async def reset_all_school_coordinates():
|
||||
"""
|
||||
将所有学校的经纬度坐标重置为0
|
||||
"""
|
||||
print("\n正在重置所有学校坐标...")
|
||||
sql = "update t_school set longitude=0,latitude=0"
|
||||
try:
|
||||
result = await db.execute_update(sql, {})
|
||||
print(f"✅ 成功重置 {result} 条学校记录的坐标")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"❌ 重置坐标失败: {str(e)}")
|
||||
return False
|
||||
|
||||
async def get_schools_by_county_and_level(county_name, education_level):
|
||||
"""
|
||||
获取指定县区和学段的学校列表,包含当前坐标信息
|
||||
只从t_school表获取数据,不依赖t_school_xy表
|
||||
"""
|
||||
# 获取对应的学校类型列表
|
||||
school_types = get_school_types_by_education_level(education_level)
|
||||
if not school_types:
|
||||
return []
|
||||
|
||||
# 使用in子句查询多个学校类型
|
||||
placeholders = ",".join([f":type{i}" for i in range(len(school_types))])
|
||||
|
||||
# 从t_school表获取学校数据和当前坐标
|
||||
sql = f"""
|
||||
select region_level1, region_level2, region_level3, region_level4, region_level5,
|
||||
school_type, org_name, longitude, latitude
|
||||
from t_school
|
||||
where region_level3=:county_name and school_type in ({placeholders})
|
||||
order by region_level1, region_level2, region_level3, region_level4, region_level5, school_type
|
||||
"""
|
||||
|
||||
params = {"county_name": county_name}
|
||||
# 添加每个学校类型的参数
|
||||
for i, st in enumerate(school_types):
|
||||
params[f"type{i}"] = st
|
||||
|
||||
schools = await db.find(sql, params)
|
||||
|
||||
# 转换坐标数据类型,确保longitude和latitude是float类型
|
||||
from decimal import Decimal
|
||||
for school in schools:
|
||||
if isinstance(school.get("longitude"), Decimal):
|
||||
school["longitude"] = float(school["longitude"])
|
||||
if isinstance(school.get("latitude"), Decimal):
|
||||
school["latitude"] = float(school["latitude"])
|
||||
|
||||
return schools
|
||||
|
||||
def optimize_school_distribution(schools, polygons):
|
||||
"""
|
||||
优化学校坐标分布
|
||||
- 对于已经在多边形内部的坐标,保持不变
|
||||
- 对于不在多边形内部的坐标,将其调整到多边形中
|
||||
"""
|
||||
if not schools:
|
||||
return []
|
||||
|
||||
# 获取目标区域的边界信息
|
||||
all_target_points = []
|
||||
for polygon in polygons:
|
||||
all_target_points.extend(polygon)
|
||||
|
||||
target_lons = [p[0] for p in all_target_points]
|
||||
target_lats = [p[1] for p in all_target_points]
|
||||
|
||||
target_min_lon, target_max_lon = min(target_lons), max(target_lons)
|
||||
target_min_lat, target_max_lat = min(target_lats), max(target_lats)
|
||||
|
||||
# 处理每个学校的坐标
|
||||
optimized_coords = []
|
||||
for school in schools:
|
||||
lon, lat = school["longitude"], school["latitude"]
|
||||
|
||||
# 检查坐标是否已经在多边形内
|
||||
if any(is_point_in_polygon((lon, lat), polygon) for polygon in polygons):
|
||||
# 如果在多边形内,保持原坐标不变
|
||||
optimized_coords.append((lon, lat))
|
||||
else:
|
||||
# 如果不在多边形内,将其调整到最近的多边形边界
|
||||
adjusted_lon, adjusted_lat = adjust_to_polygon_boundary([lon, lat], polygons)
|
||||
optimized_coords.append((adjusted_lon, adjusted_lat))
|
||||
|
||||
return optimized_coords
|
||||
|
||||
async def process_school_coordinates():
|
||||
"""
|
||||
优化t_school表中现有学校坐标分布
|
||||
按县区和学段分组处理,美化坐标分布
|
||||
注意:这是一个补丁程序,只修改现有坐标,不重置为0
|
||||
"""
|
||||
print("\n===== 学校坐标分布优化补丁 =====")
|
||||
print("正在开始优化现有学校坐标分布...")
|
||||
|
||||
try:
|
||||
# 1. 获取所有县区信息
|
||||
areas = await getAllArea()
|
||||
print(f"\n✅ 成功获取 {len(areas)} 个县区信息")
|
||||
|
||||
# 2. 定义需要处理的学段
|
||||
education_levels = ["学前", "小学", "初中", "高中"]
|
||||
|
||||
# 3. 统计信息
|
||||
total_schools = 0
|
||||
processed_schools = 0
|
||||
failed_counties = 0
|
||||
|
||||
# 4. 遍历所有县区
|
||||
for area in areas:
|
||||
county_name = area["area_name"]
|
||||
area_code = str(area["area_code"])[:-3] # 去掉最后三位0
|
||||
|
||||
print(f"\n🔄 正在处理: {county_name} (代码: {area_code})")
|
||||
|
||||
# 5. 构建GeoJSON文件路径
|
||||
geojson_path = f"d:\\dsWork\\YunNanProject\\Init\\Json\\{area_code}.json"
|
||||
|
||||
try:
|
||||
# 6. 加载县区边界数据
|
||||
polygons = load_geojson(geojson_path)
|
||||
print(f"✅ 成功加载 {county_name} 边界数据")
|
||||
|
||||
# 7. 遍历所有学段
|
||||
for education_level in education_levels:
|
||||
print(f" 📚 学段: {education_level}")
|
||||
|
||||
try:
|
||||
# 8. 获取该县区该学段的学校列表及其现有坐标
|
||||
schools = await get_schools_by_county_and_level(county_name, education_level)
|
||||
|
||||
if not schools:
|
||||
print(f" ⏭️ 该学段无待处理学校")
|
||||
continue
|
||||
|
||||
# 过滤掉没有坐标的学校
|
||||
schools_with_coords = [s for s in schools if s["longitude"] is not None and s["latitude"] is not None]
|
||||
|
||||
if not schools_with_coords:
|
||||
print(f" ⏭️ 该学段没有有效坐标的学校")
|
||||
continue
|
||||
|
||||
print(f" 📋 找到 {len(schools_with_coords)} 所需要优化坐标的学校")
|
||||
total_schools += len(schools_with_coords)
|
||||
|
||||
# 9. 优化学校坐标分布
|
||||
optimized_coords = optimize_school_distribution(schools_with_coords, polygons)
|
||||
|
||||
# 10. 更新学校坐标
|
||||
for i, (school, (lon, lat)) in enumerate(zip(schools_with_coords, optimized_coords), 1):
|
||||
org_name = school["org_name"]
|
||||
result = await updateSchool(org_name, lon, lat)
|
||||
if result:
|
||||
processed_schools += 1
|
||||
print(f" ✅ {i}/{len(schools_with_coords)} 已优化: {org_name}")
|
||||
else:
|
||||
print(f" ❌ {i}/{len(schools_with_coords)} 优化失败: {org_name}")
|
||||
|
||||
except Exception as e:
|
||||
print(f" ⚠️ 处理{education_level}学段时出错: {str(e)}")
|
||||
continue
|
||||
|
||||
except FileNotFoundError:
|
||||
print(f"❌ 未找到 {county_name} 的GeoJSON文件: {geojson_path}")
|
||||
failed_counties += 1
|
||||
except Exception as e:
|
||||
print(f"❌ 处理{county_name}时出错: {str(e)}")
|
||||
failed_counties += 1
|
||||
continue
|
||||
|
||||
# 12. 输出统计结果
|
||||
print("\n" + "="*50)
|
||||
print("🎯 批量处理完成!")
|
||||
print(f"📊 统计信息:")
|
||||
print(f" - 总县区数: {len(areas)}")
|
||||
print(f" - 成功处理县区数: {len(areas) - failed_counties}")
|
||||
print(f" - 失败县区数: {failed_counties}")
|
||||
print(f" - 待处理学校总数: {total_schools}")
|
||||
print(f" - 成功更新学校数: {processed_schools}")
|
||||
print("="*50)
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ 批量处理过程中发生错误: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
raise
|
||||
finally:
|
||||
# 确保在正常执行和异常情况下都关闭数据库连接
|
||||
if 'db' in globals() and hasattr(db, 'engine') and db.engine:
|
||||
try:
|
||||
await db.engine.dispose()
|
||||
print("\n✅ 数据库连接已成功关闭")
|
||||
except Exception as dispose_error:
|
||||
print(f"\n❌ 关闭数据库连接时出错: {str(dispose_error)}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 直接开始批量处理 - 为所有学校生成坐标
|
||||
try:
|
||||
asyncio.run(process_school_coordinates())
|
||||
except KeyboardInterrupt:
|
||||
print("\n⚠️ 程序被用户中断")
|
||||
except Exception as e:
|
||||
# 捕获并处理所有异常,包括asyncio和SSL相关错误
|
||||
print(f"\n❌ 程序执行出错: {str(e)}")
|
||||
print(" 错误类型:", type(e).__name__)
|
||||
# 仅在需要详细调试时显示完整堆栈
|
||||
print("\n详细错误信息:")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
finally:
|
||||
print("\n程序已退出")
|
||||
Reference in New Issue
Block a user