'commit'
8
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
14
.idea/Crawler.iml
generated
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/venv" />
|
||||
</content>
|
||||
<orderEntry type="jdk" jdkName="D:\anaconda3\envs\py310" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
<component name="PyDocumentationSettings">
|
||||
<option name="format" value="PLAIN" />
|
||||
<option name="myDocStringFormat" value="Plain" />
|
||||
</component>
|
||||
</module>
|
||||
196
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@@ -0,0 +1,196 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="ignoredPackages">
|
||||
<value>
|
||||
<list size="182">
|
||||
<item index="0" class="java.lang.String" itemvalue="httpx" />
|
||||
<item index="1" class="java.lang.String" itemvalue="gensim" />
|
||||
<item index="2" class="java.lang.String" itemvalue="numba" />
|
||||
<item index="3" class="java.lang.String" itemvalue="tzlocal" />
|
||||
<item index="4" class="java.lang.String" itemvalue="greenlet" />
|
||||
<item index="5" class="java.lang.String" itemvalue="PyYAML" />
|
||||
<item index="6" class="java.lang.String" itemvalue="psycopg2" />
|
||||
<item index="7" class="java.lang.String" itemvalue="py" />
|
||||
<item index="8" class="java.lang.String" itemvalue="pycparser" />
|
||||
<item index="9" class="java.lang.String" itemvalue="redis" />
|
||||
<item index="10" class="java.lang.String" itemvalue="elasticsearch" />
|
||||
<item index="11" class="java.lang.String" itemvalue="volcengine" />
|
||||
<item index="12" class="java.lang.String" itemvalue="aliyun-python-sdk-core" />
|
||||
<item index="13" class="java.lang.String" itemvalue="pycryptodomex" />
|
||||
<item index="14" class="java.lang.String" itemvalue="starlette" />
|
||||
<item index="15" class="java.lang.String" itemvalue="bleach" />
|
||||
<item index="16" class="java.lang.String" itemvalue="lxml" />
|
||||
<item index="17" class="java.lang.String" itemvalue="pyreadline3" />
|
||||
<item index="18" class="java.lang.String" itemvalue="soupsieve" />
|
||||
<item index="19" class="java.lang.String" itemvalue="et_xmlfile" />
|
||||
<item index="20" class="java.lang.String" itemvalue="esdk-obs-python" />
|
||||
<item index="21" class="java.lang.String" itemvalue="json_repair" />
|
||||
<item index="22" class="java.lang.String" itemvalue="nano-vectordb" />
|
||||
<item index="23" class="java.lang.String" itemvalue="fuzzywuzzy" />
|
||||
<item index="24" class="java.lang.String" itemvalue="jsonschema" />
|
||||
<item index="25" class="java.lang.String" itemvalue="passlib" />
|
||||
<item index="26" class="java.lang.String" itemvalue="xlrd" />
|
||||
<item index="27" class="java.lang.String" itemvalue="darabonba-core" />
|
||||
<item index="28" class="java.lang.String" itemvalue="pywin32" />
|
||||
<item index="29" class="java.lang.String" itemvalue="pydantic" />
|
||||
<item index="30" class="java.lang.String" itemvalue="zai" />
|
||||
<item index="31" class="java.lang.String" itemvalue="alibabacloud_tea_util" />
|
||||
<item index="32" class="java.lang.String" itemvalue="lightrag-hku" />
|
||||
<item index="33" class="java.lang.String" itemvalue="rembg" />
|
||||
<item index="34" class="java.lang.String" itemvalue="google" />
|
||||
<item index="35" class="java.lang.String" itemvalue="pipmaster" />
|
||||
<item index="36" class="java.lang.String" itemvalue="click" />
|
||||
<item index="37" class="java.lang.String" itemvalue="attrs" />
|
||||
<item index="38" class="java.lang.String" itemvalue="contourpy" />
|
||||
<item index="39" class="java.lang.String" itemvalue="openai" />
|
||||
<item index="40" class="java.lang.String" itemvalue="flatbuffers" />
|
||||
<item index="41" class="java.lang.String" itemvalue="pdfminer.six" />
|
||||
<item index="42" class="java.lang.String" itemvalue="regex" />
|
||||
<item index="43" class="java.lang.String" itemvalue="WMI" />
|
||||
<item index="44" class="java.lang.String" itemvalue="pydantic_core" />
|
||||
<item index="45" class="java.lang.String" itemvalue="aioredis" />
|
||||
<item index="46" class="java.lang.String" itemvalue="imageio" />
|
||||
<item index="47" class="java.lang.String" itemvalue="platformdirs" />
|
||||
<item index="48" class="java.lang.String" itemvalue="propcache" />
|
||||
<item index="49" class="java.lang.String" itemvalue="matplotlib" />
|
||||
<item index="50" class="java.lang.String" itemvalue="oss2" />
|
||||
<item index="51" class="java.lang.String" itemvalue="PyMySQL" />
|
||||
<item index="52" class="java.lang.String" itemvalue="httpcore" />
|
||||
<item index="53" class="java.lang.String" itemvalue="idna" />
|
||||
<item index="54" class="java.lang.String" itemvalue="referencing" />
|
||||
<item index="55" class="java.lang.String" itemvalue="rsa" />
|
||||
<item index="56" class="java.lang.String" itemvalue="alibabacloud_tea_fileform" />
|
||||
<item index="57" class="java.lang.String" itemvalue="decorator" />
|
||||
<item index="58" class="java.lang.String" itemvalue="langchain-openai" />
|
||||
<item index="59" class="java.lang.String" itemvalue="networkx" />
|
||||
<item index="60" class="java.lang.String" itemvalue="xfyunsdkspeech" />
|
||||
<item index="61" class="java.lang.String" itemvalue="zhipuai" />
|
||||
<item index="62" class="java.lang.String" itemvalue="Levenshtein" />
|
||||
<item index="63" class="java.lang.String" itemvalue="numpy" />
|
||||
<item index="64" class="java.lang.String" itemvalue="pyasn1" />
|
||||
<item index="65" class="java.lang.String" itemvalue="requests" />
|
||||
<item index="66" class="java.lang.String" itemvalue="bitarray" />
|
||||
<item index="67" class="java.lang.String" itemvalue="opencv-python-headless" />
|
||||
<item index="68" class="java.lang.String" itemvalue="sniffio" />
|
||||
<item index="69" class="java.lang.String" itemvalue="exceptiongroup" />
|
||||
<item index="70" class="java.lang.String" itemvalue="alibabacloud-docmind-api20220711" />
|
||||
<item index="71" class="java.lang.String" itemvalue="tenacity" />
|
||||
<item index="72" class="java.lang.String" itemvalue="jsonpath" />
|
||||
<item index="73" class="java.lang.String" itemvalue="alibabacloud-credentials-api" />
|
||||
<item index="74" class="java.lang.String" itemvalue="smart_open" />
|
||||
<item index="75" class="java.lang.String" itemvalue="volcengine-python-sdk" />
|
||||
<item index="76" class="java.lang.String" itemvalue="websockets" />
|
||||
<item index="77" class="java.lang.String" itemvalue="annotated-types" />
|
||||
<item index="78" class="java.lang.String" itemvalue="lazy_loader" />
|
||||
<item index="79" class="java.lang.String" itemvalue="scipy" />
|
||||
<item index="80" class="java.lang.String" itemvalue="pyasn1_modules" />
|
||||
<item index="81" class="java.lang.String" itemvalue="aiofiles" />
|
||||
<item index="82" class="java.lang.String" itemvalue="jsonpointer" />
|
||||
<item index="83" class="java.lang.String" itemvalue="python-multipart" />
|
||||
<item index="84" class="java.lang.String" itemvalue="pandas" />
|
||||
<item index="85" class="java.lang.String" itemvalue="xfyunsdkcore" />
|
||||
<item index="86" class="java.lang.String" itemvalue="pdfplumber" />
|
||||
<item index="87" class="java.lang.String" itemvalue="future" />
|
||||
<item index="88" class="java.lang.String" itemvalue="dashscope" />
|
||||
<item index="89" class="java.lang.String" itemvalue="typing_extensions" />
|
||||
<item index="90" class="java.lang.String" itemvalue="cachetools" />
|
||||
<item index="91" class="java.lang.String" itemvalue="langchain-core" />
|
||||
<item index="92" class="java.lang.String" itemvalue="multidict" />
|
||||
<item index="93" class="java.lang.String" itemvalue="yarl" />
|
||||
<item index="94" class="java.lang.String" itemvalue="pycryptodome" />
|
||||
<item index="95" class="java.lang.String" itemvalue="pytz" />
|
||||
<item index="96" class="java.lang.String" itemvalue="tiktoken" />
|
||||
<item index="97" class="java.lang.String" itemvalue="protobuf" />
|
||||
<item index="98" class="java.lang.String" itemvalue="alibabacloud_openapi_util" />
|
||||
<item index="99" class="java.lang.String" itemvalue="jiter" />
|
||||
<item index="100" class="java.lang.String" itemvalue="alibabacloud_endpoint_util" />
|
||||
<item index="101" class="java.lang.String" itemvalue="python-dotenv" />
|
||||
<item index="102" class="java.lang.String" itemvalue="h11" />
|
||||
<item index="103" class="java.lang.String" itemvalue="pyuca" />
|
||||
<item index="104" class="java.lang.String" itemvalue="cycler" />
|
||||
<item index="105" class="java.lang.String" itemvalue="MarkupSafe" />
|
||||
<item index="106" class="java.lang.String" itemvalue="sse-starlette" />
|
||||
<item index="107" class="java.lang.String" itemvalue="RapidFuzz" />
|
||||
<item index="108" class="java.lang.String" itemvalue="tinycss2" />
|
||||
<item index="109" class="java.lang.String" itemvalue="frozenlist" />
|
||||
<item index="110" class="java.lang.String" itemvalue="ascii_colors" />
|
||||
<item index="111" class="java.lang.String" itemvalue="alibabacloud-credentials" />
|
||||
<item index="112" class="java.lang.String" itemvalue="alibabacloud-ocr-api20210707" />
|
||||
<item index="113" class="java.lang.String" itemvalue="captcha" />
|
||||
<item index="114" class="java.lang.String" itemvalue="anyio" />
|
||||
<item index="115" class="java.lang.String" itemvalue="google-genai" />
|
||||
<item index="116" class="java.lang.String" itemvalue="elastic-transport" />
|
||||
<item index="117" class="java.lang.String" itemvalue="pyparsing" />
|
||||
<item index="118" class="java.lang.String" itemvalue="pypinyin" />
|
||||
<item index="119" class="java.lang.String" itemvalue="sox" />
|
||||
<item index="120" class="java.lang.String" itemvalue="sympy" />
|
||||
<item index="121" class="java.lang.String" itemvalue="beautifulsoup4" />
|
||||
<item index="122" class="java.lang.String" itemvalue="tifffile" />
|
||||
<item index="123" class="java.lang.String" itemvalue="wrapt" />
|
||||
<item index="124" class="java.lang.String" itemvalue="aiohappyeyeballs" />
|
||||
<item index="125" class="java.lang.String" itemvalue="cryptography" />
|
||||
<item index="126" class="java.lang.String" itemvalue="kiwisolver" />
|
||||
<item index="127" class="java.lang.String" itemvalue="orjson" />
|
||||
<item index="128" class="java.lang.String" itemvalue="APScheduler" />
|
||||
<item index="129" class="java.lang.String" itemvalue="coloredlogs" />
|
||||
<item index="130" class="java.lang.String" itemvalue="alibabacloud-tea" />
|
||||
<item index="131" class="java.lang.String" itemvalue="fonttools" />
|
||||
<item index="132" class="java.lang.String" itemvalue="alibabacloud_tea_xml" />
|
||||
<item index="133" class="java.lang.String" itemvalue="alibabacloud_oss_sdk" />
|
||||
<item index="134" class="java.lang.String" itemvalue="configparser" />
|
||||
<item index="135" class="java.lang.String" itemvalue="charset-normalizer" />
|
||||
<item index="136" class="java.lang.String" itemvalue="gmssl" />
|
||||
<item index="137" class="java.lang.String" itemvalue="langsmith" />
|
||||
<item index="138" class="java.lang.String" itemvalue="scikit-image" />
|
||||
<item index="139" class="java.lang.String" itemvalue="jieba" />
|
||||
<item index="140" class="java.lang.String" itemvalue="async-timeout" />
|
||||
<item index="141" class="java.lang.String" itemvalue="python-Levenshtein" />
|
||||
<item index="142" class="java.lang.String" itemvalue="alibabacloud-openplatform20191219" />
|
||||
<item index="143" class="java.lang.String" itemvalue="jwt" />
|
||||
<item index="144" class="java.lang.String" itemvalue="SQLAlchemy" />
|
||||
<item index="145" class="java.lang.String" itemvalue="wcwidth" />
|
||||
<item index="146" class="java.lang.String" itemvalue="crcmod" />
|
||||
<item index="147" class="java.lang.String" itemvalue="PyMatting" />
|
||||
<item index="148" class="java.lang.String" itemvalue="llvmlite" />
|
||||
<item index="149" class="java.lang.String" itemvalue="Jinja2" />
|
||||
<item index="150" class="java.lang.String" itemvalue="typing-inspection" />
|
||||
<item index="151" class="java.lang.String" itemvalue="jsonschema-specifications" />
|
||||
<item index="152" class="java.lang.String" itemvalue="onnxruntime" />
|
||||
<item index="153" class="java.lang.String" itemvalue="rpds-py" />
|
||||
<item index="154" class="java.lang.String" itemvalue="pypdfium2" />
|
||||
<item index="155" class="java.lang.String" itemvalue="urllib3" />
|
||||
<item index="156" class="java.lang.String" itemvalue="python-jose" />
|
||||
<item index="157" class="java.lang.String" itemvalue="retry" />
|
||||
<item index="158" class="java.lang.String" itemvalue="alibabacloud_oss_util" />
|
||||
<item index="159" class="java.lang.String" itemvalue="python-docx" />
|
||||
<item index="160" class="java.lang.String" itemvalue="six" />
|
||||
<item index="161" class="java.lang.String" itemvalue="bitstring" />
|
||||
<item index="162" class="java.lang.String" itemvalue="tzdata" />
|
||||
<item index="163" class="java.lang.String" itemvalue="ecdsa" />
|
||||
<item index="164" class="java.lang.String" itemvalue="jsonpath-rw" />
|
||||
<item index="165" class="java.lang.String" itemvalue="packaging" />
|
||||
<item index="166" class="java.lang.String" itemvalue="pooch" />
|
||||
<item index="167" class="java.lang.String" itemvalue="xlsxwriter" />
|
||||
<item index="168" class="java.lang.String" itemvalue="alibabacloud-tea-openapi" />
|
||||
<item index="169" class="java.lang.String" itemvalue="humanfriendly" />
|
||||
<item index="170" class="java.lang.String" itemvalue="zstandard" />
|
||||
<item index="171" class="java.lang.String" itemvalue="alibabacloud_gateway_spi" />
|
||||
<item index="172" class="java.lang.String" itemvalue="chardet" />
|
||||
<item index="173" class="java.lang.String" itemvalue="jmespath" />
|
||||
<item index="174" class="java.lang.String" itemvalue="tqdm" />
|
||||
<item index="175" class="java.lang.String" itemvalue="dotenv" />
|
||||
<item index="176" class="java.lang.String" itemvalue="aliyun-python-sdk-kms" />
|
||||
<item index="177" class="java.lang.String" itemvalue="pillow" />
|
||||
<item index="178" class="java.lang.String" itemvalue="aiohttp" />
|
||||
<item index="179" class="java.lang.String" itemvalue="langchain-text-splitters" />
|
||||
<item index="180" class="java.lang.String" itemvalue="aiosignal" />
|
||||
<item index="181" class="java.lang.String" itemvalue="google-auth" />
|
||||
</list>
|
||||
</value>
|
||||
</option>
|
||||
</inspection_tool>
|
||||
</profile>
|
||||
</component>
|
||||
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
||||
7
.idea/misc.xml
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Black">
|
||||
<option name="sdkName" value="Python 3.12" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="D:\anaconda3\envs\py310" project-jdk-type="Python SDK" />
|
||||
</project>
|
||||
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/Crawler.iml" filepath="$PROJECT_DIR$/.idea/Crawler.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
7
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$/../.." vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
888
Apps/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
Apps/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程序已退出")
|
||||
89
Apps/Json/amap_cookies.json
Normal file
@@ -0,0 +1,89 @@
|
||||
[
|
||||
{
|
||||
"domain": ".amap.com",
|
||||
"expiry": 1783436001,
|
||||
"httpOnly": false,
|
||||
"name": "tfstk",
|
||||
"path": "/",
|
||||
"sameSite": "None",
|
||||
"secure": true,
|
||||
"value": "gM_oFfTFRg-71NH4Sv874ohkZ_ZAwUTBK93ppepU0KJXJW9RTebhp6-FyUCRnBWVhe3JzeUHKTWM99fQNBJFL9vd2oUON_TB8RB3WPC5mpKEaTAE8sPe1BAUM3zSaLiH8ReTkUIGoi8Fw8wZHn52hBAEapWe0sRH3QReY9J20CRtUe8FLSv29Bkr8LuUumR93p8e8954iBp2ae8FLsPDObOaUL7FyZyL5IXlLBGRuQxkQ_JPw1bqo3o5OK0E8NSDqdupn2uFoQ5FNSuqSkpNfLK9ptzxref2tTpAQPuDuhj19BXur48R0i_WyZeip3YvehSem7zPiUvkjaKi1mLlYT_HVgcQ1s8PhG964owXiaQ9xdtm385AiL-GxTej8LIGgtxOkx39RtsFLBXqEgoq0coMm2O49ZoIA3RXidFP3WmOJLCWuSVmjptyGQJTiSmIA3RXidF0icYM4IOyB"
|
||||
},
|
||||
{
|
||||
"domain": ".amap.com",
|
||||
"httpOnly": true,
|
||||
"name": "oauth_state",
|
||||
"path": "/",
|
||||
"sameSite": "None",
|
||||
"secure": true,
|
||||
"value": "e88390e153019eb5fdc2a6a361b08407"
|
||||
},
|
||||
{
|
||||
"domain": ".amap.com",
|
||||
"expiry": 1775660027,
|
||||
"httpOnly": true,
|
||||
"name": "passport_login",
|
||||
"path": "/",
|
||||
"sameSite": "None",
|
||||
"secure": true,
|
||||
"value": "MTIyMDQwNjY0MSxhbWFwMjc1OWtPR0wsdGtsaXFqZGE0dDNnbnRrY3dpbHFtZG1nbXdncGE2ZjUsMTc2Nzg4NDAyNyxOVEJpT1dRMVlqaGlOemRsWVRZeE4yRTBNVGswWkRreU5tVTBNVFF5WW1FPQ%3D%3D"
|
||||
},
|
||||
{
|
||||
"domain": ".amap.com",
|
||||
"expiry": 1768143203,
|
||||
"httpOnly": false,
|
||||
"name": "xlly_s",
|
||||
"path": "/",
|
||||
"sameSite": "None",
|
||||
"secure": true,
|
||||
"value": "1"
|
||||
},
|
||||
{
|
||||
"domain": ".amap.com",
|
||||
"expiry": 1802444002,
|
||||
"httpOnly": false,
|
||||
"name": "cna",
|
||||
"path": "/",
|
||||
"sameSite": "None",
|
||||
"secure": true,
|
||||
"value": "4rLmIViEnl4CAXzrzjwC3UGc"
|
||||
},
|
||||
{
|
||||
"domain": "lbs.amap.com",
|
||||
"expiry": 1802444003,
|
||||
"httpOnly": false,
|
||||
"name": "_uab_collina",
|
||||
"path": "/tools",
|
||||
"sameSite": "Lax",
|
||||
"secure": false,
|
||||
"value": "176788400312910602225889"
|
||||
},
|
||||
{
|
||||
"domain": ".lbs.amap.com",
|
||||
"httpOnly": false,
|
||||
"name": "HMACCOUNT",
|
||||
"path": "/",
|
||||
"sameSite": "Lax",
|
||||
"secure": false,
|
||||
"value": "DAC02E19E8B24CB5"
|
||||
},
|
||||
{
|
||||
"domain": ".lbs.amap.com",
|
||||
"httpOnly": false,
|
||||
"name": "Hm_lpvt_c8ac07c199b1c09a848aaab761f9f909",
|
||||
"path": "/",
|
||||
"sameSite": "Lax",
|
||||
"secure": false,
|
||||
"value": "1767884002"
|
||||
},
|
||||
{
|
||||
"domain": ".lbs.amap.com",
|
||||
"expiry": 1799420002,
|
||||
"httpOnly": false,
|
||||
"name": "Hm_lvt_c8ac07c199b1c09a848aaab761f9f909",
|
||||
"path": "/",
|
||||
"sameSite": "Lax",
|
||||
"secure": false,
|
||||
"value": "1767884002"
|
||||
}
|
||||
]
|
||||
145
Apps/XinDianTu/Coord.py
Normal file
@@ -0,0 +1,145 @@
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
from sqlalchemy import text
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# python -m pip install webdriver-manager selenium
|
||||
# https://googlechromelabs.github.io/chrome-for-testing/
|
||||
# https://storage.googleapis.com/chrome-for-testing-public/143.0.7499.192/win64/chromedriver-win64.zip
|
||||
# update t_station_profile_scd set coord_x=null,coord_y=null where id='95783932-1d32-48e4-9a38-6a6696a6a423'
|
||||
# 将项目根目录添加到系统路径,确保可以导入 Util, DbKit 和 Config 模块
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from Util.PaChongGaoDeKit import PaChongGaoDeKit
|
||||
from DbKit.Db import Db
|
||||
from Config.Config import DB_URL
|
||||
|
||||
class CookieInvalidError(Exception):
|
||||
"""自定义异常:表示 Cookie 无效或过期"""
|
||||
pass
|
||||
|
||||
async def main(check_cookie_first=True):
|
||||
"""
|
||||
坐标回填任务:自动扫描数据库中缺失坐标的场站地址,并通过高德地图补全。
|
||||
:param check_cookie_first: 是否在开始前验证 Cookie 有效性
|
||||
"""
|
||||
logger.info("正在启动 T4_Coord: 高德地图坐标回填任务...")
|
||||
|
||||
# 初始化数据库连接
|
||||
db = Db(db_url=DB_URL)
|
||||
await db.init_db()
|
||||
|
||||
# 初始化高德地图工具类(使用无头模式)
|
||||
# 显式指定 Cookie 路径,确保跨目录运行时能找到文件
|
||||
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
cookie_path = os.path.join(project_root, 'Json', 'amap_cookies.json')
|
||||
|
||||
if not os.path.exists(cookie_path):
|
||||
logger.error(f"错误:未找到 Cookie 文件:{cookie_path}")
|
||||
# 如果要求检查且文件不存在,直接抛出异常以便上层处理
|
||||
if check_cookie_first:
|
||||
await db.close()
|
||||
raise CookieInvalidError("Cookie file not found")
|
||||
logger.error("请先运行 Tools/T3_GetAmapCookie.py 生成有效 Cookie。")
|
||||
return
|
||||
|
||||
kit = PaChongGaoDeKit(cookie_file=cookie_path, headless=False)
|
||||
|
||||
try:
|
||||
# 0. Cookie 有效性探针
|
||||
if check_cookie_first:
|
||||
logger.info("正在验证高德地图 Cookie 有效性...")
|
||||
loop = asyncio.get_event_loop()
|
||||
# 使用一个肯定存在的地址进行测试
|
||||
test_addr = "北京市天安门"
|
||||
t_lng, t_lat = await loop.run_in_executor(None, kit.get_coordinate, test_addr)
|
||||
|
||||
if not t_lng:
|
||||
logger.warning(f"Cookie 验证失败:无法获取测试地址 '{test_addr}' 的坐标。")
|
||||
logger.warning("判定 Cookie 已失效或过期。")
|
||||
raise CookieInvalidError("Cookie invalid or expired")
|
||||
else:
|
||||
logger.info(f"Cookie 验证通过 (测试坐标: {t_lng}, {t_lat})。")
|
||||
|
||||
async with await db.get_session() as session:
|
||||
# 1. 查询所有地址不为空但缺失坐标的唯一地址
|
||||
sql_missing = """
|
||||
SELECT DISTINCT address
|
||||
FROM t_station_profile_scd
|
||||
WHERE address IS NOT NULL
|
||||
AND address != ''
|
||||
AND (coord_x IS NULL OR coord_y IS NULL)
|
||||
"""
|
||||
result = await session.execute(text(sql_missing))
|
||||
missing_addresses = [row[0] for row in result.fetchall()]
|
||||
|
||||
logger.info(f"共发现 {len(missing_addresses)} 个缺失坐标的唯一地址。")
|
||||
|
||||
for address in missing_addresses:
|
||||
logger.info(f"\n正在处理地址: {address}")
|
||||
|
||||
# 2. 检查数据库中其他记录是否已经存有该地址的坐标(去重复查询)
|
||||
# 这样可以最大程度减少对高德地图接口的调用
|
||||
sql_check = """
|
||||
SELECT coord_x, coord_y
|
||||
FROM t_station_profile_scd
|
||||
WHERE address = :addr
|
||||
AND coord_x IS NOT NULL
|
||||
AND coord_y IS NOT NULL
|
||||
LIMIT 1
|
||||
"""
|
||||
existing = await session.execute(text(sql_check), {"addr": address})
|
||||
row = existing.fetchone()
|
||||
|
||||
lng, lat = None, None
|
||||
|
||||
if row:
|
||||
lng, lat = row[0], row[1]
|
||||
logger.info(f" -> 在数据库中找到现有坐标: {lng}, {lat}")
|
||||
else:
|
||||
# 3. 数据库中没有,调用高德地图进行查询
|
||||
logger.info(" -> 数据库中无记录,正在查询高德地图...")
|
||||
# 在线程池中运行同步的 Selenium 调用,避免阻塞异步循环
|
||||
loop = asyncio.get_event_loop()
|
||||
lng, lat = await loop.run_in_executor(None, kit.get_coordinate, address)
|
||||
|
||||
if lng and lat:
|
||||
logger.info(f" -> 高德地图返回坐标: {lng}, {lat}")
|
||||
else:
|
||||
logger.info(" -> 高德地图未返回有效坐标。")
|
||||
|
||||
# 4. 如果获取到了坐标,更新数据库中所有匹配该地址的记录
|
||||
if lng and lat:
|
||||
sql_update = """
|
||||
UPDATE t_station_profile_scd
|
||||
SET coord_x = :lng, coord_y = :lat
|
||||
WHERE address = :addr
|
||||
"""
|
||||
await session.execute(text(sql_update), {
|
||||
"lng": lng,
|
||||
"lat": lat,
|
||||
"addr": address
|
||||
})
|
||||
await session.commit()
|
||||
logger.info(f" -> 已成功更新数据库中地址为 '{address}' 的记录。")
|
||||
else:
|
||||
logger.info(f" -> 跳过更新 (未找到有效坐标)。")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"执行过程中出错: {e}")
|
||||
finally:
|
||||
# 释放资源
|
||||
kit.close()
|
||||
await db.close()
|
||||
logger.info("\n任务执行结束。")
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 针对 Windows 平台的异步策略设置
|
||||
if sys.platform == 'win32':
|
||||
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
||||
asyncio.run(main())
|
||||
581
Apps/XinDianTu/Crawler.py
Normal file
@@ -0,0 +1,581 @@
|
||||
# coding=utf-8
|
||||
import asyncio
|
||||
import logging
|
||||
import uuid
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import time
|
||||
import hashlib
|
||||
from PIL import Image
|
||||
|
||||
# 将项目根目录添加到 sys.path
|
||||
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
if project_root not in sys.path:
|
||||
sys.path.append(project_root)
|
||||
|
||||
import uiautomator2 as u2
|
||||
from Core.BaseCrawler import BaseCrawler
|
||||
from Util import Kit
|
||||
from Util.Kit import take_screenshot
|
||||
from Util.ObsUtil import ObsUploader
|
||||
from Util.RedisKit import RedisKit
|
||||
from Util.PaddleOCRKit import get_ocr_kit
|
||||
import cv2
|
||||
from Apps.XinDianTu.Service import XinDianTuService
|
||||
from Config.Config import (
|
||||
OBS_TMP_PREFIX, CDN_DOMAIN, SCROLL_DISTANCE_RATIO,
|
||||
MAX_SCROLLS, REDIS_STATION_EXPIRE,
|
||||
WAIT_DETAIL_PAGE_LOAD, WAIT_BACK_TO_LIST, WAIT_AFTER_SCROLL,
|
||||
MAX_CRAWL_DISTANCE
|
||||
)
|
||||
|
||||
# --- 用户配置区域 ---
|
||||
# 是否保留截图文件(True=保留备查,False=随用随删)
|
||||
KEEP_SCREENSHOTS = True
|
||||
|
||||
# [Testing] 是否在启动时清除 Redis 中的场站处理记录
|
||||
# True: 每次运行前清除记录,方便反复测试同一个场站
|
||||
# False: 生产模式,保留记录以避免重复爬取
|
||||
TEST_CLEAR_REDIS = True
|
||||
|
||||
# 配置说明:
|
||||
# SCROLL_DISTANCE_RATIO 控制翻页时的滑动距离(在 Config.py 中修改)。
|
||||
# 对于华为 Mate 20x 等分辨率较低或屏幕较小的手机,如果发现翻页时跳过了某些场站,
|
||||
# 请尝试减小 SCROLL_DISTANCE_RATIO(例如设置为 0.4 或 0.3)。
|
||||
# 这样每次滑动的距离变短,可以确保所有场站都能被完整显示并识别。
|
||||
|
||||
# 配置日志输出
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.StreamHandler(sys.stdout)
|
||||
]
|
||||
)
|
||||
logger = logging.getLogger("StationList")
|
||||
|
||||
# 强制设置所有相关模块的日志级别为 INFO,防止被第三方库干扰
|
||||
logging.getLogger("PaddleOCRKit").setLevel(logging.INFO)
|
||||
logging.getLogger("Util.Kit").setLevel(logging.INFO)
|
||||
logging.getLogger("OpenXinDianTu").setLevel(logging.INFO)
|
||||
logging.getLogger("FullProcess").setLevel(logging.INFO)
|
||||
|
||||
class XinDianTuCrawler(BaseCrawler):
|
||||
"""
|
||||
新电途小程序爬虫实现
|
||||
"""
|
||||
def __init__(self, service=None):
|
||||
super().__init__(service)
|
||||
# 初始化配置参数
|
||||
self.max_scrolls = MAX_SCROLLS
|
||||
self.uploader = ObsUploader()
|
||||
self.redis_kit = RedisKit()
|
||||
|
||||
async def start(self):
|
||||
"""
|
||||
实现 BaseCrawler 的启动入口
|
||||
"""
|
||||
# 兼容旧逻辑,直接调用 main
|
||||
await main(self.service)
|
||||
|
||||
async def open_app(self):
|
||||
# 实际逻辑在 Opener.py,此处可作为封装层
|
||||
pass
|
||||
|
||||
async def crawl_list(self):
|
||||
# 实际逻辑在 get_station_list,此处可作为封装层
|
||||
pass
|
||||
|
||||
async def crawl_detail(self, station_info):
|
||||
pass
|
||||
|
||||
async def get_station_list(d, service, uploader, max_scrolls=MAX_SCROLLS):
|
||||
"""
|
||||
获取场站列表并处理翻页
|
||||
"""
|
||||
all_stations = []
|
||||
seen_names = set()
|
||||
redis_kit = RedisKit()
|
||||
|
||||
# 确保在“找桩”或者首页列表界面
|
||||
window_size = d.window_size()
|
||||
w, h = window_size[0], window_size[1]
|
||||
|
||||
# 获取设备信息以用于坐标计算
|
||||
device_info = d.info
|
||||
device_info['width'] = w
|
||||
device_info['height'] = h
|
||||
|
||||
logger.info(f"设备信息: {device_info.get('productName')} | 窗口分辨率: {w}x{h}")
|
||||
|
||||
max_distance_reached = False
|
||||
stop_reason = "max_scrolls_reached" # 默认停止原因:达到最大翻页次数
|
||||
|
||||
# 存储后台异步任务
|
||||
background_tasks = []
|
||||
|
||||
for i in range(max_scrolls + 1):
|
||||
if max_distance_reached:
|
||||
logger.info("已达到最大抓取距离,停止翻页。")
|
||||
stop_reason = "max_distance_reached"
|
||||
break
|
||||
logger.info(f"正在处理第 {i + 1} 页...")
|
||||
|
||||
# 1. 拍摄截图
|
||||
logger.info(f"Step [1/6] 正在拍摄列表页截图...")
|
||||
t_shot = time.time()
|
||||
image_uuid = str(uuid.uuid4())
|
||||
# 使用相对路径: 基于当前脚本目录下的 Images 文件夹
|
||||
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
save_dir = os.path.join(base_dir, "./Images")
|
||||
screenshot_path = take_screenshot(d, image_uuid, save_dir=save_dir)
|
||||
logger.info(f"Step [1/6] 列表页截图已完成: {screenshot_path} (耗时: {time.time() - t_shot:.2f}s)")
|
||||
|
||||
# 校验截图实际尺寸
|
||||
img_w, img_h = 0, 0
|
||||
try:
|
||||
with Image.open(screenshot_path) as img:
|
||||
img_w, img_h = img.size
|
||||
logger.info(f"截图实际尺寸: {img_w}x{img_h}")
|
||||
# 如果截图尺寸与窗口尺寸不符,更新 device_info 以适配坐标换算
|
||||
if img_w != w or img_h != h:
|
||||
logger.warning(f"检测到截图尺寸 ({img_w}x{img_h}) 与窗口尺寸 ({w}x{h}) 不一致,将以截图尺寸为准进行坐标换算。")
|
||||
device_info['width'] = img_w
|
||||
device_info['height'] = img_h
|
||||
except Exception as e:
|
||||
logger.warning(f"无法获取截图尺寸信息: {e}")
|
||||
|
||||
# 2. 使用 Kit.crop_cards_from_image 进行切片和生成 _vl.jpg
|
||||
logger.info("Step [2/6] 正在执行本地图形学切片分析,识别场站卡片并生成绿色方框图...")
|
||||
t_crop = time.time()
|
||||
# 这会生成 _vl.jpg 和 .json
|
||||
Kit.crop_cards_from_image(screenshot_path)
|
||||
logger.info(f"Step [2/6] 图形学切片分析完成 (耗时: {time.time() - t_crop:.2f}s)")
|
||||
|
||||
# 读取生成的元数据
|
||||
json_path = screenshot_path.replace(".jpg", ".json")
|
||||
vl_img_path = screenshot_path.replace(".jpg", "_vl.jpg")
|
||||
|
||||
if not os.path.exists(json_path) or not os.path.exists(vl_img_path):
|
||||
logger.error(f"❌ Step [2/6] 失败: 未生成有效的 JSON 或 VL 图片 (JSON: {os.path.exists(json_path)}, VL: {os.path.exists(vl_img_path)})")
|
||||
logger.warning("可能未识别到任何卡片,跳过当前页")
|
||||
continue
|
||||
|
||||
logger.info(f"Step [3/6] 绿色方框图已生成: {vl_img_path}")
|
||||
|
||||
with open(json_path, 'r', encoding='utf-8') as f:
|
||||
json_metadata = json.load(f)
|
||||
|
||||
# --- New Logic: Local OCR via PaddleOCRKit ---
|
||||
logger.info("Step [4/6] 正在开始本地 OCR 识别流程,将识别出的卡片逐一交给 OCR...")
|
||||
t_ocr_total = time.time()
|
||||
|
||||
stations = []
|
||||
try:
|
||||
ocr_kit = get_ocr_kit()
|
||||
|
||||
# 读取原始截图进行裁剪 (比读取 _vl.jpg 更干净,没有绿框干扰)
|
||||
original_img = cv2.imread(screenshot_path)
|
||||
|
||||
if "cards" in json_metadata and original_img is not None:
|
||||
h_img, w_img = original_img.shape[:2]
|
||||
num_cards = len(json_metadata['cards'])
|
||||
logger.info(f"检测到 {num_cards} 个场站卡片,准备开始逐一 OCR 识别...")
|
||||
|
||||
for idx, card in enumerate(json_metadata["cards"]):
|
||||
# card: {"id": 1, "rect": [x1, y1, x2, y2], "click_point": [cx, cy]}
|
||||
rect = card.get("rect")
|
||||
if not rect: continue
|
||||
x1, y1, x2, y2 = rect
|
||||
|
||||
# 边界检查
|
||||
x1 = max(0, min(x1, w_img))
|
||||
x2 = max(0, min(x2, w_img))
|
||||
y1 = max(0, min(y1, h_img))
|
||||
y2 = max(0, min(y2, h_img))
|
||||
|
||||
if x2 <= x1 or y2 <= y1: continue
|
||||
|
||||
# 裁剪卡片
|
||||
cropped_card = original_img[y1:y2, x1:x2]
|
||||
|
||||
# 识别
|
||||
logger.info(f"正在识别第 {idx+1}/{num_cards} 个卡片 (区域: {rect})...")
|
||||
t_card_start = time.time()
|
||||
parsed_data = ocr_kit.recognize(cropped_card)
|
||||
t_card_end = time.time()
|
||||
|
||||
# Log detailed OCR result
|
||||
logger.info(f"卡片 {idx+1} OCR 识别耗时: {t_card_end - t_card_start:.2f}s")
|
||||
|
||||
if parsed_data and parsed_data.get("station_name"):
|
||||
# 格式化数据以匹配原有结构
|
||||
piles_list = parsed_data.get("piles", [])
|
||||
piles_str_parts = []
|
||||
for p in piles_list:
|
||||
p_type = p.get("type", "")
|
||||
p_free = p.get("free", 0)
|
||||
p_total = p.get("total", 0)
|
||||
piles_str_parts.append(f"{p_type}:{p_free}/{p_total}")
|
||||
|
||||
piles_str = " ".join(piles_str_parts)
|
||||
|
||||
station_info = {
|
||||
"station_name": parsed_data.get("station_name"),
|
||||
"price": str(parsed_data.get("price")) if parsed_data.get("price") is not None else "",
|
||||
"pro_price": "", # 暂未解析
|
||||
"piles": piles_str,
|
||||
"distance": parsed_data.get("distance", ""),
|
||||
"uia_center_x": card["click_point"][0],
|
||||
"uia_center_y": card["click_point"][1],
|
||||
"tags": parsed_data.get("tags", []),
|
||||
"parking_info": parsed_data.get("parking", "")
|
||||
}
|
||||
logger.info(f"✅ 成功解析场站: {station_info['station_name']} | 距离: {station_info['distance']}")
|
||||
|
||||
# 立即记录场站基础状态信息 (电桩、价格等)
|
||||
try:
|
||||
await service.record_station_status(station_info)
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 记录场站状态失败: {e}")
|
||||
|
||||
stations.append(station_info)
|
||||
else:
|
||||
logger.warning(f"⚠️ 卡片 {idx+1} 未能识别出有效场站名称")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Local OCR processing failed: {e}", exc_info=True)
|
||||
|
||||
logger.info(f"Step [5/6] 本地 OCR 解析完成 (总耗时: {time.time() - t_ocr_total:.2f}s) | 成功识别: {len(stations)}/{len(json_metadata.get('cards', []))}")
|
||||
|
||||
if not stations:
|
||||
logger.warning("当前页面未检测到任何有效的场站信息")
|
||||
else:
|
||||
logger.info(f"Step [6/6] 准备开始逐一点击场站进入详情页 (共 {len(stations)} 个)...")
|
||||
new_stations_found = False
|
||||
for s_idx, s in enumerate(stations):
|
||||
name = s.get("station_name")
|
||||
|
||||
# 过滤掉明显的非场站名称
|
||||
if not name:
|
||||
continue
|
||||
|
||||
# --- 短期去重:检查 Redis 缓存 ---
|
||||
# 2分钟内如果处理过该场站,则跳过
|
||||
redis_key = f"processed_station:{name}"
|
||||
if await redis_kit.exists(redis_key):
|
||||
logger.info(f"场站 {name} 在 2 分钟内已处理过,跳过")
|
||||
continue
|
||||
|
||||
if name in seen_names:
|
||||
continue
|
||||
|
||||
# 使用从 JSON 元数据中恢复的点击坐标
|
||||
x = s.get("uia_center_x")
|
||||
y = s.get("uia_center_y")
|
||||
|
||||
if x is None or y is None:
|
||||
logger.warning(f"场站 {name} 缺少坐标信息,无法进入详情页")
|
||||
continue
|
||||
|
||||
seen_names.add(name)
|
||||
all_stations.append(s)
|
||||
new_stations_found = True
|
||||
|
||||
# 打印基本信息
|
||||
price = s.get("price")
|
||||
pro_price = s.get("pro_price")
|
||||
piles_str = s.get("piles", "")
|
||||
distance_str = s.get("distance", "")
|
||||
|
||||
# 检查距离
|
||||
dist_log_part = ""
|
||||
if distance_str:
|
||||
try:
|
||||
import re
|
||||
dist_match = re.search(r"(\d+(\.\d+)?)", distance_str)
|
||||
if dist_match:
|
||||
dist_val = float(dist_match.group(1))
|
||||
is_km = "km" in distance_str.lower()
|
||||
dist_km = dist_val if is_km else dist_val / 1000.0
|
||||
|
||||
# 将距离解析结果和限制检查合并到日志中
|
||||
dist_log_part = f" (解析: {dist_km:.2f}km / 限: {MAX_CRAWL_DISTANCE}km)"
|
||||
|
||||
if dist_km >= MAX_CRAWL_DISTANCE:
|
||||
logger.info(f"当前场站 {name} 距离 {dist_km}km (原始: {distance_str}) 已达到或超过限制 {MAX_CRAWL_DISTANCE}km。")
|
||||
max_distance_reached = True
|
||||
except Exception as e:
|
||||
logger.warning(f"解析距离出错: {distance_str}, {e}")
|
||||
|
||||
pro_info = f" | PRO会员价: {pro_price}" if pro_price else ""
|
||||
# 确保距离始终显示,如果为空则显示 "未知"
|
||||
display_distance = distance_str if distance_str else "未知"
|
||||
logger.info(f"正在处理第 {s_idx+1} 个场站: {name} | 价格: {price}{pro_info} | 枪: {piles_str} | 距离: {display_distance}{dist_log_part}")
|
||||
|
||||
if max_distance_reached:
|
||||
break
|
||||
|
||||
# --- 点击进入详情页记录地址 ---
|
||||
# 使用相对坐标点击,以适应不同分辨率
|
||||
if img_w > 0 and img_h > 0:
|
||||
rel_x = max(0.0, min(1.0, x / img_w))
|
||||
rel_y = max(0.0, min(1.0, y / img_h))
|
||||
logger.info(f"👉 正在点击进入详情页: {name} (相对坐标: {rel_x:.3f}, {rel_y:.3f})")
|
||||
d.click(rel_x, rel_y)
|
||||
else:
|
||||
logger.info(f"👉 正在点击进入详情页: {name} (绝对坐标: {x}, {y})")
|
||||
d.click(x, y)
|
||||
|
||||
await asyncio.sleep(WAIT_DETAIL_PAGE_LOAD) # 等待详情页加载
|
||||
|
||||
# 尝试获取并保存地址
|
||||
address_recorded = False
|
||||
for retry in range(2):
|
||||
# 1. 拍摄详情页截图
|
||||
t_d_shot = time.time()
|
||||
detail_uuid = str(uuid.uuid4())
|
||||
detail_screenshot_path = take_screenshot(d, detail_uuid, save_dir=save_dir)
|
||||
logger.info(f"Step [详情页截图] 耗时: {time.time() - t_d_shot:.4f}s")
|
||||
|
||||
# 2. 上传至 OBS
|
||||
t_d_up = time.time()
|
||||
detail_object_key = f"{OBS_TMP_PREFIX}/{detail_uuid}.jpg"
|
||||
up_success, _ = uploader.upload_file(detail_object_key, detail_screenshot_path)
|
||||
logger.info(f"Step [上传详情页截图] 耗时: {time.time() - t_d_up:.4f}s")
|
||||
|
||||
if up_success:
|
||||
detail_url = f"https://{CDN_DOMAIN}/{detail_object_key}"
|
||||
# 3. 解析并保存地址 (异步后台处理)
|
||||
# 定义异步任务函数
|
||||
async def _process_address_task(task_name, task_url, task_dev_info, task_redis_key):
|
||||
t_addr_task = time.time()
|
||||
try:
|
||||
addr_res = await service.process_station_address(task_name, task_url, device_info=task_dev_info)
|
||||
logger.info(f"Step [解析并保存地址-后台] 耗时: {time.time() - t_addr_task:.4f}s")
|
||||
if addr_res.get("address"):
|
||||
logger.info(f"成功记录地址: {addr_res.get('address')}")
|
||||
# 记录到 Redis,设置过期时间
|
||||
await redis_kit.set_data(task_redis_key, "1", expire=REDIS_STATION_EXPIRE)
|
||||
except Exception as e:
|
||||
logger.error(f"后台处理地址失败 ({task_name}): {e}")
|
||||
|
||||
# 创建并启动任务
|
||||
addr_task = asyncio.create_task(_process_address_task(name, detail_url, device_info, redis_key))
|
||||
background_tasks.append(addr_task)
|
||||
logger.info(f"已提交地址解析任务到后台: {name}")
|
||||
|
||||
# 标记为已记录以跳出 retry 循环
|
||||
address_recorded = True
|
||||
|
||||
# 清理详情页临时截图
|
||||
if not KEEP_SCREENSHOTS and os.path.exists(detail_screenshot_path):
|
||||
os.remove(detail_screenshot_path)
|
||||
|
||||
break # 直接跳出 retry 循环
|
||||
|
||||
# --- 抓取价格时段信息 (New Feature) ---
|
||||
# 仅在测试模式下或特定需求下启用
|
||||
try:
|
||||
logger.info(f"开始抓取价格时段信息: {name}")
|
||||
|
||||
# 使用几何特征识别 "全部时段" 按钮
|
||||
# 临时截图
|
||||
temp_uuid = "temp_find_expand"
|
||||
screenshot_path = take_screenshot(d, temp_uuid, save_dir="Temp")
|
||||
|
||||
# 尝试识别,将调试图片保存到 Images 目录
|
||||
t_find = time.time()
|
||||
pos = Kit.find_expand_button_position(screenshot_path, debug_dir=save_dir, debug_filename_prefix=name)
|
||||
logger.info(f"Step [识别展开按钮] 耗时: {time.time() - t_find:.4f}s")
|
||||
|
||||
# 清理截图
|
||||
try:
|
||||
os.remove(screenshot_path)
|
||||
except:
|
||||
pass
|
||||
|
||||
if pos:
|
||||
x, y = pos
|
||||
logger.info(f"通过几何特征找到 '全部时段' 按钮: ({x}, {y})")
|
||||
d.click(x, y)
|
||||
await asyncio.sleep(1.5)
|
||||
|
||||
# 抓取3屏截图
|
||||
price_image_urls = []
|
||||
for p_idx in range(1, 4):
|
||||
# 使用场站名称的MD5值作为文件名前缀,避免中文和特殊字符导致的URL问题
|
||||
t_p_loop = time.time()
|
||||
name_md5 = hashlib.md5(name.encode('utf-8')).hexdigest()
|
||||
p_uuid = f"{name_md5}_price_{p_idx}"
|
||||
p_path = take_screenshot(d, p_uuid, save_dir=save_dir)
|
||||
logger.info(f"已保存价格时段截图 {p_idx} (Station: {name}, MD5: {name_md5}): {p_path}")
|
||||
|
||||
# 上传到 OBS
|
||||
p_object_key = f"{OBS_TMP_PREFIX}/{p_uuid}.jpg"
|
||||
success, _ = uploader.upload_file(p_object_key, p_path)
|
||||
if success:
|
||||
p_url = f"https://{CDN_DOMAIN}/{p_object_key}"
|
||||
price_image_urls.append(p_url)
|
||||
logger.info(f"截图上传成功: {p_url}")
|
||||
else:
|
||||
logger.warning(f"截图上传失败: {p_path}")
|
||||
|
||||
logger.info(f"Step [价格截图与上传-{p_idx}] 耗时: {time.time() - t_p_loop:.4f}s")
|
||||
|
||||
if p_idx < 3:
|
||||
d.swipe_ext("up", scale=0.7)
|
||||
await asyncio.sleep(1.0)
|
||||
|
||||
# 调用服务解析价格时段 (异步后台处理)
|
||||
if price_image_urls:
|
||||
logger.info(f"正在调用接口解析电费时段数据 ({len(price_image_urls)} 张图片) - 转入后台...")
|
||||
|
||||
async def _process_price_task(task_name, task_urls, task_dev_info):
|
||||
t_price_task = time.time()
|
||||
try:
|
||||
await service.process_price_schedule(task_name, task_urls, device_info=task_dev_info)
|
||||
logger.info(f"Step [解析电费时段数据-后台] 耗时: {time.time() - t_price_task:.4f}s")
|
||||
logger.info(f"电费时段数据解析完成: {task_name}")
|
||||
except Exception as e:
|
||||
logger.error(f"后台处理价格时段失败 ({task_name}): {e}")
|
||||
|
||||
price_task = asyncio.create_task(_process_price_task(name, price_image_urls, device_info))
|
||||
background_tasks.append(price_task)
|
||||
else:
|
||||
logger.warning("未能获取到有效的价格时段截图,跳过解析")
|
||||
else:
|
||||
logger.warning(f"未能通过几何特征识别 '全部时段' 按钮")
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"抓取价格时段信息时发生异常: {e}")
|
||||
|
||||
# 测试模式:仅处理第一个场站
|
||||
# logger.info("测试模式:仅处理第一个场站,即将退出 (Y3)")
|
||||
# return all_stations
|
||||
|
||||
# 返回列表页
|
||||
d.press("back")
|
||||
await asyncio.sleep(WAIT_BACK_TO_LIST) # 等待列表页重新稳定
|
||||
|
||||
if not new_stations_found and i > 0:
|
||||
logger.info("未发现更多新场站,停止下拉。")
|
||||
# 如果需要强行爬取所有,可以注释掉 break
|
||||
# break
|
||||
|
||||
# 5. 如果还没到最后一页,执行下拉
|
||||
if i < max_scrolls and not max_distance_reached:
|
||||
logger.info(f"执行下拉翻页 (距离比例: {SCROLL_DISTANCE_RATIO})...")
|
||||
# 根据配置的比例计算滑动起始和终点
|
||||
# 保证滑动在屏幕中心区域进行
|
||||
start_y = 0.5 + (SCROLL_DISTANCE_RATIO / 2)
|
||||
end_y = 0.5 - (SCROLL_DISTANCE_RATIO / 2)
|
||||
d.swipe(w * 0.5, h * start_y, w * 0.5, h * end_y, duration=0.5)
|
||||
await asyncio.sleep(WAIT_AFTER_SCROLL) # 等待页面加载和列表稳定
|
||||
|
||||
# 清理列表页截图
|
||||
if not KEEP_SCREENSHOTS:
|
||||
if os.path.exists(screenshot_path):
|
||||
os.remove(screenshot_path)
|
||||
if os.path.exists(vl_img_path):
|
||||
os.remove(vl_img_path)
|
||||
if os.path.exists(json_path):
|
||||
os.remove(json_path)
|
||||
|
||||
logger.info(f"爬取任务结束,正在等待 {len(background_tasks)} 个后台处理任务完成...")
|
||||
if background_tasks:
|
||||
await asyncio.gather(*background_tasks)
|
||||
logger.info("所有后台任务已完成。")
|
||||
|
||||
logger.info(f"任务结束,共采集到 {len(all_stations)} 个场站。停止原因: {stop_reason}")
|
||||
return all_stations
|
||||
|
||||
def clean_images_dir():
|
||||
"""清理 Images 目录下除保留文件外的所有文件"""
|
||||
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
images_dir = os.path.join(base_dir, "Images")
|
||||
|
||||
if not os.path.exists(images_dir):
|
||||
return
|
||||
|
||||
keep_files = {'1.jpg', '2.jpg', '3.jpg', '4.jpg'}
|
||||
logger.info(f"正在清理 Images 目录 (保留: {keep_files})...")
|
||||
|
||||
deleted_count = 0
|
||||
for filename in os.listdir(images_dir):
|
||||
if filename in keep_files:
|
||||
continue
|
||||
|
||||
file_path = os.path.join(images_dir, filename)
|
||||
try:
|
||||
if os.path.isfile(file_path):
|
||||
os.remove(file_path)
|
||||
deleted_count += 1
|
||||
except Exception as e:
|
||||
logger.warning(f"删除文件失败 {filename}: {e}")
|
||||
|
||||
logger.info(f"清理完成,共删除 {deleted_count} 个文件。")
|
||||
|
||||
async def clean_redis_data(redis_kit):
|
||||
"""清除 Redis 中的场站处理记录"""
|
||||
if TEST_CLEAR_REDIS:
|
||||
logger.info("测试模式开启:正在清除 Redis 中的场站处理记录 (processed_station:*)...")
|
||||
keys = await redis_kit.keys("processed_station:*")
|
||||
if keys:
|
||||
count = await redis_kit.delete(*keys)
|
||||
logger.info(f"已清除 {count} 条场站处理记录")
|
||||
else:
|
||||
logger.info("未发现旧的场站处理记录")
|
||||
|
||||
async def main(service=None, do_cleanup=True):
|
||||
# 连接设备
|
||||
d = u2.connect()
|
||||
|
||||
# 初始化服务
|
||||
should_close_service = False
|
||||
if service is None:
|
||||
service = XinDianTuService()
|
||||
should_close_service = True
|
||||
# 初始化数据库连接(XinDianTuService 需要)
|
||||
await service.init_db()
|
||||
|
||||
uploader = ObsUploader()
|
||||
redis_kit = RedisKit()
|
||||
|
||||
try:
|
||||
if do_cleanup:
|
||||
# 清理图片目录
|
||||
clean_images_dir()
|
||||
|
||||
# [Testing] 如果配置为测试模式,启动时清除 Redis 记录
|
||||
await clean_redis_data(redis_kit)
|
||||
|
||||
# 清理过期数据
|
||||
# await service.cleanup_old_data()
|
||||
|
||||
# 获取场站列表
|
||||
stations = await get_station_list(d, service, uploader, max_scrolls=MAX_SCROLLS)
|
||||
|
||||
if stations:
|
||||
logger.info("场站列表采集完成。")
|
||||
else:
|
||||
logger.warning("未采集到任何场站信息。")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f"运行过程中出现异常: {e}")
|
||||
return False
|
||||
finally:
|
||||
# 关闭数据库连接
|
||||
if should_close_service:
|
||||
await service.close_db()
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
asyncio.run(main())
|
||||
except KeyboardInterrupt:
|
||||
logger.info("程序被用户中断.")
|
||||
except Exception as e:
|
||||
logger.exception(f"程序崩溃: {e}")
|
||||
64
Apps/XinDianTu/GetCookie.py
Normal file
@@ -0,0 +1,64 @@
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# pip install selenium
|
||||
# 将项目根目录添加到系统路径,确保可以导入 Util 模块
|
||||
# 注意:PaChongGaoDeKit 位于 Util 目录下
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.append(os.path.join(current_dir, '..'))
|
||||
|
||||
from Util.PaChongGaoDeKit import PaChongGaoDeKit
|
||||
|
||||
def run_login_tool(force=False):
|
||||
"""
|
||||
运行高德地图 Cookie 获取工具。
|
||||
:param force: 如果为 True,则跳过 'Cookie 已存在' 的询问,直接开始流程。
|
||||
:return: Boolean, True 表示获取成功, False 表示失败或取消。
|
||||
"""
|
||||
logger.info("正在启动高德地图 Cookie 获取工具,使用手机号18686619970+验证码登录即可!")
|
||||
|
||||
# 默认将 Cookie 保存到项目根目录下的 Json/amap_cookies.json
|
||||
cookie_path = os.path.join(current_dir, '../Json/amap_cookies.json')
|
||||
|
||||
# 检查 Cookie 文件是否已存在
|
||||
if os.path.exists(cookie_path) and not force:
|
||||
logger.info(f"提示:检测到已存在 Cookie 文件: {os.path.abspath(cookie_path)}")
|
||||
logger.info("如果您本地的 Cookie 文件没有过期且可以正常使用,则不需要重新生成。")
|
||||
|
||||
user_choice = input("是否仍要继续运行并重新获取 Cookie?(y/n): ").strip().lower()
|
||||
if user_choice != 'y':
|
||||
logger.info("用户取消操作,程序退出。")
|
||||
return False
|
||||
|
||||
logger.info("\n" + "-"*60)
|
||||
logger.info("即将开始获取 Cookie,请按照以下步骤操作:")
|
||||
logger.info("1. 程序将自动打开一个 Chrome 浏览器窗口。")
|
||||
logger.info("2. 请在该窗口中完成高德地图账号登录(扫码或账号密码)。")
|
||||
logger.info("3. 登录成功后,保持页面处于控制台或个人中心状态。")
|
||||
logger.info("4. 回到此终端窗口,按下回车键,程序将保存 Cookie 并自动关闭浏览器。")
|
||||
logger.info("-"*60 + "\n")
|
||||
|
||||
# 初始化工具类,强制开启非无头模式(显示浏览器窗口)以便手动登录
|
||||
# 确保 Json 目录存在
|
||||
os.makedirs(os.path.dirname(cookie_path), exist_ok=True)
|
||||
|
||||
kit = PaChongGaoDeKit(headless=False, cookie_file=cookie_path)
|
||||
|
||||
try:
|
||||
# 执行登录并保存流程
|
||||
return kit.login_and_save_cookies()
|
||||
except Exception as e:
|
||||
logger.error(f"执行过程中发生意外错误: {e}")
|
||||
return False
|
||||
finally:
|
||||
# 关闭浏览器资源
|
||||
kit.close()
|
||||
logger.info("工具运行结束。")
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_login_tool()
|
||||
BIN
Apps/XinDianTu/Images/.OKUU_flag_expand.jpg
Normal file
|
After Width: | Height: | Size: 205 KiB |
BIN
Apps/XinDianTu/Images/000a46ca-21de-40fe-853b-7ab733e789dc.jpg
Normal file
|
After Width: | Height: | Size: 109 KiB |
BIN
Apps/XinDianTu/Images/00593070-d0f8-47b1-a5bc-274b2e41aed7.jpg
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
Apps/XinDianTu/Images/007da665-7d1b-40dd-8d28-93d376270a8e.jpg
Normal file
|
After Width: | Height: | Size: 125 KiB |
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 124 KiB |
|
After Width: | Height: | Size: 124 KiB |
|
After Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 117 KiB |
BIN
Apps/XinDianTu/Images/00c3c976-07e8-4039-aa27-523fa00208ef.jpg
Normal file
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 254 KiB |
|
After Width: | Height: | Size: 254 KiB |
BIN
Apps/XinDianTu/Images/00cdb168-4bb4-4a9d-ae7c-98e324ccfb51.jpg
Normal file
|
After Width: | Height: | Size: 111 KiB |
BIN
Apps/XinDianTu/Images/0177a88f-daf7-42d7-8b28-c9cbce8a76a6.jpg
Normal file
|
After Width: | Height: | Size: 153 KiB |
BIN
Apps/XinDianTu/Images/020e54a4-3d68-4612-9a0b-1c9976e8ec50.jpg
Normal file
|
After Width: | Height: | Size: 118 KiB |
BIN
Apps/XinDianTu/Images/02b78492-9d8a-4c47-a6ee-87def2890163.jpg
Normal file
|
After Width: | Height: | Size: 122 KiB |
BIN
Apps/XinDianTu/Images/0313040f-efb2-467b-ab22-8798ce322ac3.jpg
Normal file
|
After Width: | Height: | Size: 110 KiB |
BIN
Apps/XinDianTu/Images/032ceeb4-1f23-402b-bb56-a5d3dba395e6.jpg
Normal file
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 255 KiB |
|
After Width: | Height: | Size: 254 KiB |
|
After Width: | Height: | Size: 114 KiB |
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 114 KiB |
BIN
Apps/XinDianTu/Images/03ddecac-0f44-4ae9-80d1-fa6fb34cb1e9.jpg
Normal file
|
After Width: | Height: | Size: 115 KiB |
BIN
Apps/XinDianTu/Images/0455a9de-6a2e-4e36-a287-8ad9d7b2cb14.jpg
Normal file
|
After Width: | Height: | Size: 127 KiB |
|
After Width: | Height: | Size: 252 KiB |
|
After Width: | Height: | Size: 251 KiB |
|
After Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 112 KiB |
BIN
Apps/XinDianTu/Images/046f1b02-291f-46be-80a3-e7c0a90b3fe3.jpg
Normal file
|
After Width: | Height: | Size: 119 KiB |
|
After Width: | Height: | Size: 109 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 110 KiB |
BIN
Apps/XinDianTu/Images/04b5e613-8e21-408b-ae93-b878a6126844.jpg
Normal file
|
After Width: | Height: | Size: 123 KiB |
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"image": "04b5e613-8e21-408b-ae93-b878a6126844.jpg",
|
||||
"width": 720,
|
||||
"height": 202,
|
||||
"cards": [
|
||||
{
|
||||
"id": 1,
|
||||
"rect": [
|
||||
19,
|
||||
226,
|
||||
720,
|
||||
428
|
||||
],
|
||||
"click_point": [
|
||||
124,
|
||||
266
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 226 KiB |
|
After Width: | Height: | Size: 226 KiB |
BIN
Apps/XinDianTu/Images/04fb17c6-124c-4661-9c50-ead044d94ebc.jpg
Normal file
|
After Width: | Height: | Size: 126 KiB |
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"image": "04fb17c6-124c-4661-9c50-ead044d94ebc.jpg",
|
||||
"width": 720,
|
||||
"height": 259,
|
||||
"cards": [
|
||||
{
|
||||
"id": 1,
|
||||
"rect": [
|
||||
19,
|
||||
404,
|
||||
700,
|
||||
581
|
||||
],
|
||||
"click_point": [
|
||||
121,
|
||||
439
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"rect": [
|
||||
19,
|
||||
595,
|
||||
700,
|
||||
844
|
||||
],
|
||||
"click_point": [
|
||||
121,
|
||||
644
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"rect": [
|
||||
19,
|
||||
852,
|
||||
700,
|
||||
1101
|
||||
],
|
||||
"click_point": [
|
||||
121,
|
||||
901
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"rect": [
|
||||
19,
|
||||
1115,
|
||||
700,
|
||||
1374
|
||||
],
|
||||
"click_point": [
|
||||
121,
|
||||
1166
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 250 KiB |
|
After Width: | Height: | Size: 249 KiB |
BIN
Apps/XinDianTu/Images/051ffe99-ba1e-4363-8ec4-e7e3133a8c4b.jpg
Normal file
|
After Width: | Height: | Size: 123 KiB |
|
After Width: | Height: | Size: 114 KiB |
|
After Width: | Height: | Size: 115 KiB |
|
After Width: | Height: | Size: 116 KiB |
|
After Width: | Height: | Size: 104 KiB |
|
After Width: | Height: | Size: 102 KiB |
|
After Width: | Height: | Size: 104 KiB |
BIN
Apps/XinDianTu/Images/05915ddc-4f28-40a6-ae99-aa65fa382ef6.jpg
Normal file
|
After Width: | Height: | Size: 124 KiB |
BIN
Apps/XinDianTu/Images/05af72de-43b4-445f-9435-935bf4a3f9fb.jpg
Normal file
|
After Width: | Height: | Size: 123 KiB |
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"image": "05af72de-43b4-445f-9435-935bf4a3f9fb.jpg",
|
||||
"width": 720,
|
||||
"height": 140,
|
||||
"cards": [
|
||||
{
|
||||
"id": 1,
|
||||
"rect": [
|
||||
19,
|
||||
428,
|
||||
700,
|
||||
677
|
||||
],
|
||||
"click_point": [
|
||||
121,
|
||||
477
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"rect": [
|
||||
19,
|
||||
690,
|
||||
700,
|
||||
965
|
||||
],
|
||||
"click_point": [
|
||||
121,
|
||||
745
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"rect": [
|
||||
19,
|
||||
979,
|
||||
700,
|
||||
1228
|
||||
],
|
||||
"click_point": [
|
||||
121,
|
||||
1028
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 237 KiB |
|
After Width: | Height: | Size: 237 KiB |
BIN
Apps/XinDianTu/Images/05eaf8a3-8236-4b9e-bee1-3115217f49d5.jpg
Normal file
|
After Width: | Height: | Size: 112 KiB |
BIN
Apps/XinDianTu/Images/05ff2b26-72c7-4014-8657-d63c47c2d47f.jpg
Normal file
|
After Width: | Height: | Size: 153 KiB |
BIN
Apps/XinDianTu/Images/06091644-a1fc-480e-a2fd-7c6565a8aaed.jpg
Normal file
|
After Width: | Height: | Size: 125 KiB |
|
After Width: | Height: | Size: 250 KiB |
|
After Width: | Height: | Size: 249 KiB |
|
After Width: | Height: | Size: 109 KiB |
|
After Width: | Height: | Size: 107 KiB |
|
After Width: | Height: | Size: 109 KiB |
BIN
Apps/XinDianTu/Images/06757ffa-6f6f-4706-884a-d3d255474556.jpg
Normal file
|
After Width: | Height: | Size: 122 KiB |
|
After Width: | Height: | Size: 243 KiB |
|
After Width: | Height: | Size: 242 KiB |
BIN
Apps/XinDianTu/Images/06bf220a-f218-423f-88cf-44ad06545a99.jpg
Normal file
|
After Width: | Height: | Size: 104 KiB |
BIN
Apps/XinDianTu/Images/06ec294c-ce75-4a42-9c15-caaa2d57e60e.jpg
Normal file
|
After Width: | Height: | Size: 132 KiB |
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"image": "06ec294c-ce75-4a42-9c15-caaa2d57e60e.jpg",
|
||||
"width": 720,
|
||||
"height": 106,
|
||||
"cards": [
|
||||
{
|
||||
"id": 1,
|
||||
"rect": [
|
||||
19,
|
||||
491,
|
||||
700,
|
||||
740
|
||||
],
|
||||
"click_point": [
|
||||
121,
|
||||
540
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"rect": [
|
||||
19,
|
||||
748,
|
||||
700,
|
||||
997
|
||||
],
|
||||
"click_point": [
|
||||
121,
|
||||
797
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"rect": [
|
||||
19,
|
||||
1011,
|
||||
700,
|
||||
1260
|
||||
],
|
||||
"click_point": [
|
||||
121,
|
||||
1060
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 253 KiB |
|
After Width: | Height: | Size: 252 KiB |
BIN
Apps/XinDianTu/Images/07237cb5-4f55-4e7f-8ae6-3a927d046e06.jpg
Normal file
|
After Width: | Height: | Size: 127 KiB |
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"image": "07237cb5-4f55-4e7f-8ae6-3a927d046e06.jpg",
|
||||
"width": 720,
|
||||
"height": 243,
|
||||
"cards": [
|
||||
{
|
||||
"id": 1,
|
||||
"rect": [
|
||||
19,
|
||||
514,
|
||||
700,
|
||||
789
|
||||
],
|
||||
"click_point": [
|
||||
121,
|
||||
569
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"rect": [
|
||||
19,
|
||||
802,
|
||||
700,
|
||||
1117
|
||||
],
|
||||
"click_point": [
|
||||
121,
|
||||
865
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"rect": [
|
||||
19,
|
||||
1131,
|
||||
700,
|
||||
1374
|
||||
],
|
||||
"click_point": [
|
||||
121,
|
||||
1179
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 245 KiB |
|
After Width: | Height: | Size: 244 KiB |
BIN
Apps/XinDianTu/Images/0724d0ef-941c-4d3c-a911-4b3e84343e52.jpg
Normal file
|
After Width: | Height: | Size: 133 KiB |
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"image": "0724d0ef-941c-4d3c-a911-4b3e84343e52.jpg",
|
||||
"width": 720,
|
||||
"height": 249,
|
||||
"cards": [
|
||||
{
|
||||
"id": 1,
|
||||
"rect": [
|
||||
19,
|
||||
515,
|
||||
700,
|
||||
764
|
||||
],
|
||||
"click_point": [
|
||||
121,
|
||||
564
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"rect": [
|
||||
19,
|
||||
772,
|
||||
700,
|
||||
1021
|
||||
],
|
||||
"click_point": [
|
||||
121,
|
||||
821
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"rect": [
|
||||
19,
|
||||
1035,
|
||||
700,
|
||||
1284
|
||||
],
|
||||
"click_point": [
|
||||
121,
|
||||
1084
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 256 KiB |
|
After Width: | Height: | Size: 255 KiB |
BIN
Apps/XinDianTu/Images/072e1a31-67c9-427e-9979-67118ea0df45.jpg
Normal file
|
After Width: | Height: | Size: 118 KiB |
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"image": "072e1a31-67c9-427e-9979-67118ea0df45.jpg",
|
||||
"width": 720,
|
||||
"height": 202,
|
||||
"cards": [
|
||||
{
|
||||
"id": 1,
|
||||
"rect": [
|
||||
19,
|
||||
170,
|
||||
720,
|
||||
372
|
||||
],
|
||||
"click_point": [
|
||||
124,
|
||||
210
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 216 KiB |
|
After Width: | Height: | Size: 216 KiB |
BIN
Apps/XinDianTu/Images/0770ecef-1903-4684-9dac-a3fb01ee1d80.jpg
Normal file
|
After Width: | Height: | Size: 125 KiB |
BIN
Apps/XinDianTu/Images/0775d83c-8912-4fa0-9c07-d2ff5a577081.jpg
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
Apps/XinDianTu/Images/07aa2084-03b9-4777-aac4-a89e18ca25f5.jpg
Normal file
|
After Width: | Height: | Size: 112 KiB |
BIN
Apps/XinDianTu/Images/07afb5d0-c7a4-4bc5-ab3b-7c4421f204a9.jpg
Normal file
|
After Width: | Height: | Size: 82 KiB |