This commit is contained in:
HuangHai
2026-01-12 08:09:32 +08:00
parent 22596afaa4
commit ca23ebf606
44 changed files with 60 additions and 877 deletions

888
Json/Ref/T6_PaChongGaoDe.py Normal file
View 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程序已退出")

View 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程序已退出")