Delete converter.py
Browse files- converter.py +0 -492
converter.py
DELETED
|
@@ -1,492 +0,0 @@
|
|
| 1 |
-
import base64
|
| 2 |
-
import json
|
| 3 |
-
import re
|
| 4 |
-
import yaml
|
| 5 |
-
import requests
|
| 6 |
-
from ruamel.yaml import YAML
|
| 7 |
-
from urllib.parse import urlparse, parse_qs
|
| 8 |
-
|
| 9 |
-
# 用于生成美观YAML的实例
|
| 10 |
-
ruamel_yaml = YAML()
|
| 11 |
-
ruamel_yaml.indent(mapping=2, sequence=4, offset=2)
|
| 12 |
-
ruamel_yaml.width = 80
|
| 13 |
-
ruamel_yaml.allow_unicode = True
|
| 14 |
-
|
| 15 |
-
class SubscriptionConverter:
|
| 16 |
-
def __init__(self):
|
| 17 |
-
self.supported_types = ['clash', 'clashr', 'surge', 'quan', 'quanx', 'loon', 'ss', 'ssr', 'v2ray']
|
| 18 |
-
|
| 19 |
-
def convert(self, subscription_url, target_type, config_url=None):
|
| 20 |
-
"""
|
| 21 |
-
转换订阅链接到目标格式
|
| 22 |
-
"""
|
| 23 |
-
if target_type not in self.supported_types:
|
| 24 |
-
return {"status": "error", "message": f"不支持的目标类型: {target_type}"}
|
| 25 |
-
|
| 26 |
-
try:
|
| 27 |
-
# 获取原始订阅内容
|
| 28 |
-
nodes = self._fetch_subscription(subscription_url)
|
| 29 |
-
if not nodes:
|
| 30 |
-
return {"status": "error", "message": "无法获取订阅内容或订阅内容为空"}
|
| 31 |
-
|
| 32 |
-
# 获取配置文件 (如果提供)
|
| 33 |
-
config = {}
|
| 34 |
-
if config_url:
|
| 35 |
-
config = self._fetch_config(config_url)
|
| 36 |
-
|
| 37 |
-
# 根据目标类型进行转换
|
| 38 |
-
if target_type == 'clash':
|
| 39 |
-
result = self._convert_to_clash(nodes, config)
|
| 40 |
-
elif target_type == 'v2ray':
|
| 41 |
-
result = self._convert_to_v2ray(nodes)
|
| 42 |
-
else:
|
| 43 |
-
# 其他格式转换逻辑
|
| 44 |
-
return {"status": "error", "message": f"目标类型 {target_type} 暂未实现具体转换逻辑"}
|
| 45 |
-
|
| 46 |
-
return {"status": "success", "result": result}
|
| 47 |
-
|
| 48 |
-
except Exception as e:
|
| 49 |
-
return {"status": "error", "message": f"转换过程出错: {str(e)}"}
|
| 50 |
-
|
| 51 |
-
def _fetch_subscription(self, url):
|
| 52 |
-
"""获取订阅内容并解析节点"""
|
| 53 |
-
try:
|
| 54 |
-
response = requests.get(url, timeout=15)
|
| 55 |
-
if response.status_code != 200:
|
| 56 |
-
return None
|
| 57 |
-
|
| 58 |
-
content = response.text
|
| 59 |
-
# 尝试Base64解码(大多数订阅是Base64编码的)
|
| 60 |
-
try:
|
| 61 |
-
decoded = base64.b64decode(content).decode('utf-8')
|
| 62 |
-
content = decoded
|
| 63 |
-
except:
|
| 64 |
-
# 非Base64编码或已经解码,使用原始内容
|
| 65 |
-
pass
|
| 66 |
-
|
| 67 |
-
# 解析节点信息(根据订阅内容类型)
|
| 68 |
-
if content.startswith('{'): # JSON格式
|
| 69 |
-
try:
|
| 70 |
-
data = json.loads(content)
|
| 71 |
-
return self._parse_json_subscription(data)
|
| 72 |
-
except:
|
| 73 |
-
pass
|
| 74 |
-
|
| 75 |
-
elif 'proxies:' in content: # YAML (Clash)格式
|
| 76 |
-
try:
|
| 77 |
-
data = yaml.safe_load(content)
|
| 78 |
-
return self._parse_yaml_subscription(data)
|
| 79 |
-
except:
|
| 80 |
-
pass
|
| 81 |
-
|
| 82 |
-
else: # 文本格式(每行一个节点链接)
|
| 83 |
-
return self._parse_text_subscription(content)
|
| 84 |
-
|
| 85 |
-
return []
|
| 86 |
-
except Exception as e:
|
| 87 |
-
print(f"获取订阅内容失败: {str(e)}")
|
| 88 |
-
return []
|
| 89 |
-
|
| 90 |
-
def _parse_json_subscription(self, data):
|
| 91 |
-
"""解析JSON格式的订阅内容"""
|
| 92 |
-
nodes = []
|
| 93 |
-
|
| 94 |
-
# 支持常见JSON订阅格式
|
| 95 |
-
if 'proxies' in data:
|
| 96 |
-
return data['proxies'] # Clash格式
|
| 97 |
-
elif 'servers' in data:
|
| 98 |
-
# SS格式
|
| 99 |
-
for server in data['servers']:
|
| 100 |
-
nodes.append({
|
| 101 |
-
'type': 'ss',
|
| 102 |
-
'name': server.get('remarks', f"Server {len(nodes) + 1}"),
|
| 103 |
-
'server': server.get('server', ''),
|
| 104 |
-
'port': server.get('server_port', 0),
|
| 105 |
-
'password': server.get('password', ''),
|
| 106 |
-
'cipher': server.get('method', 'aes-256-gcm')
|
| 107 |
-
})
|
| 108 |
-
|
| 109 |
-
return nodes
|
| 110 |
-
|
| 111 |
-
def _parse_yaml_subscription(self, data):
|
| 112 |
-
"""解析YAML格式的订阅内容"""
|
| 113 |
-
if 'proxies' in data and isinstance(data['proxies'], list):
|
| 114 |
-
return data['proxies']
|
| 115 |
-
return []
|
| 116 |
-
|
| 117 |
-
def _parse_text_subscription(self, content):
|
| 118 |
-
"""解析文本格式的订阅内容(每行一个URI)"""
|
| 119 |
-
nodes = []
|
| 120 |
-
for line in content.splitlines():
|
| 121 |
-
line = line.strip()
|
| 122 |
-
if not line:
|
| 123 |
-
continue
|
| 124 |
-
|
| 125 |
-
# 尝试解析不同协议的URI
|
| 126 |
-
if line.startswith('ss://'):
|
| 127 |
-
node = self._parse_ss_uri(line)
|
| 128 |
-
if node:
|
| 129 |
-
nodes.append(node)
|
| 130 |
-
elif line.startswith('ssr://'):
|
| 131 |
-
node = self._parse_ssr_uri(line)
|
| 132 |
-
if node:
|
| 133 |
-
nodes.append(node)
|
| 134 |
-
elif line.startswith('vmess://'):
|
| 135 |
-
node = self._parse_vmess_uri(line)
|
| 136 |
-
if node:
|
| 137 |
-
nodes.append(node)
|
| 138 |
-
elif line.startswith('trojan://'):
|
| 139 |
-
node = self._parse_trojan_uri(line)
|
| 140 |
-
if node:
|
| 141 |
-
nodes.append(node)
|
| 142 |
-
|
| 143 |
-
return nodes
|
| 144 |
-
|
| 145 |
-
def _parse_ss_uri(self, uri):
|
| 146 |
-
"""解析 SS 协议URI"""
|
| 147 |
-
try:
|
| 148 |
-
if '#' in uri:
|
| 149 |
-
encoded_part, name = uri.split('#', 1)
|
| 150 |
-
name = name.strip()
|
| 151 |
-
else:
|
| 152 |
-
encoded_part = uri
|
| 153 |
-
name = f"SS {uri[:8]}"
|
| 154 |
-
|
| 155 |
-
encoded_part = encoded_part.replace('ss://', '')
|
| 156 |
-
|
| 157 |
-
# 处理不同格式的SS链接
|
| 158 |
-
if '@' in encoded_part:
|
| 159 |
-
# ss://method:password@server:port
|
| 160 |
-
auth_part, server_part = encoded_part.split('@', 1)
|
| 161 |
-
|
| 162 |
-
# 处理method:password部分 (可能需要解码)
|
| 163 |
-
if ':' not in auth_part:
|
| 164 |
-
try:
|
| 165 |
-
auth_part = base64.b64decode(auth_part).decode('utf-8')
|
| 166 |
-
except:
|
| 167 |
-
pass
|
| 168 |
-
|
| 169 |
-
if ':' in auth_part:
|
| 170 |
-
method, password = auth_part.split(':', 1)
|
| 171 |
-
else:
|
| 172 |
-
return None
|
| 173 |
-
|
| 174 |
-
# 处理server:port部分
|
| 175 |
-
server, port = server_part.split(':', 1)
|
| 176 |
-
port = int(port)
|
| 177 |
-
else:
|
| 178 |
-
# ss://BASE64(method:password@server:port)
|
| 179 |
-
try:
|
| 180 |
-
decoded = base64.b64decode(encoded_part).decode('utf-8')
|
| 181 |
-
if '@' in decoded:
|
| 182 |
-
auth_part, server_part = decoded.split('@', 1)
|
| 183 |
-
method, password = auth_part.split(':', 1)
|
| 184 |
-
server, port_str = server_part.split(':', 1)
|
| 185 |
-
port = int(port_str)
|
| 186 |
-
else:
|
| 187 |
-
return None
|
| 188 |
-
except:
|
| 189 |
-
return None
|
| 190 |
-
|
| 191 |
-
return {
|
| 192 |
-
'type': 'ss',
|
| 193 |
-
'name': name,
|
| 194 |
-
'server': server,
|
| 195 |
-
'port': port,
|
| 196 |
-
'password': password,
|
| 197 |
-
'cipher': method
|
| 198 |
-
}
|
| 199 |
-
except:
|
| 200 |
-
return None
|
| 201 |
-
|
| 202 |
-
def _parse_ssr_uri(self, uri):
|
| 203 |
-
"""解析 SSR 协议URI"""
|
| 204 |
-
try:
|
| 205 |
-
encoded_part = uri.replace('ssr://', '')
|
| 206 |
-
|
| 207 |
-
# SSR链接格式: ssr://BASE64(server:port:protocol:method:obfs:BASE64(password)/?params)
|
| 208 |
-
try:
|
| 209 |
-
decoded = base64.b64decode(encoded_part).decode('utf-8')
|
| 210 |
-
except:
|
| 211 |
-
return None
|
| 212 |
-
|
| 213 |
-
# 分离主要部分和参数部分
|
| 214 |
-
if '/' in decoded:
|
| 215 |
-
main_part, params_part = decoded.split('/', 1)
|
| 216 |
-
params = parse_qs(params_part.lstrip('?'))
|
| 217 |
-
else:
|
| 218 |
-
main_part = decoded
|
| 219 |
-
params = {}
|
| 220 |
-
|
| 221 |
-
# 解析主要部分
|
| 222 |
-
parts = main_part.split(':')
|
| 223 |
-
if len(parts) < 6:
|
| 224 |
-
return None
|
| 225 |
-
|
| 226 |
-
server, port, protocol, method, obfs = parts[:5]
|
| 227 |
-
password_base64 = parts[5]
|
| 228 |
-
try:
|
| 229 |
-
password = base64.b64decode(password_base64).decode('utf-8')
|
| 230 |
-
except:
|
| 231 |
-
password = password_base64
|
| 232 |
-
|
| 233 |
-
# 获取参数
|
| 234 |
-
obfs_param = base64.b64decode(params.get('obfsparam', [''])[0]).decode('utf-8') if 'obfsparam' in params else ''
|
| 235 |
-
protocol_param = base64.b64decode(params.get('protoparam', [''])[0]).decode('utf-8') if 'protoparam' in params else ''
|
| 236 |
-
remarks = base64.b64decode(params.get('remarks', [''])[0]).decode('utf-8') if 'remarks' in params else f"SSR {server[:8]}"
|
| 237 |
-
|
| 238 |
-
return {
|
| 239 |
-
'type': 'ssr',
|
| 240 |
-
'name': remarks,
|
| 241 |
-
'server': server,
|
| 242 |
-
'port': int(port),
|
| 243 |
-
'password': password,
|
| 244 |
-
'cipher': method,
|
| 245 |
-
'protocol': protocol,
|
| 246 |
-
'protocol-param': protocol_param,
|
| 247 |
-
'obfs': obfs,
|
| 248 |
-
'obfs-param': obfs_param
|
| 249 |
-
}
|
| 250 |
-
except:
|
| 251 |
-
return None
|
| 252 |
-
|
| 253 |
-
def _parse_vmess_uri(self, uri):
|
| 254 |
-
"""解析 VMess 协议URI"""
|
| 255 |
-
try:
|
| 256 |
-
encoded_part = uri.replace('vmess://', '')
|
| 257 |
-
|
| 258 |
-
# VMess链接通常是Base64编码的JSON
|
| 259 |
-
try:
|
| 260 |
-
decoded = base64.b64decode(encoded_part).decode('utf-8')
|
| 261 |
-
config = json.loads(decoded)
|
| 262 |
-
except:
|
| 263 |
-
return None
|
| 264 |
-
|
| 265 |
-
# 标准格式包含v,ps,add,port,id,aid,net等字段
|
| 266 |
-
return {
|
| 267 |
-
'type': 'vmess',
|
| 268 |
-
'name': config.get('ps', f"VMess Server"),
|
| 269 |
-
'server': config.get('add', ''),
|
| 270 |
-
'port': int(config.get('port', 0)),
|
| 271 |
-
'uuid': config.get('id', ''),
|
| 272 |
-
'alterId': int(config.get('aid', 0)),
|
| 273 |
-
'cipher': 'auto',
|
| 274 |
-
'network': config.get('net', 'tcp'),
|
| 275 |
-
'tls': True if config.get('tls') == 'tls' else False,
|
| 276 |
-
'ws-path': config.get('path', ''),
|
| 277 |
-
'ws-headers': {'Host': config.get('host', '')} if config.get('host') else {}
|
| 278 |
-
}
|
| 279 |
-
except:
|
| 280 |
-
return None
|
| 281 |
-
|
| 282 |
-
def _parse_trojan_uri(self, uri):
|
| 283 |
-
"""解析 Trojan 协议URI"""
|
| 284 |
-
try:
|
| 285 |
-
uri = uri.replace('trojan://', '')
|
| 286 |
-
|
| 287 |
-
# trojan://password@server:port?allowInsecure=1&peer=example.com#name
|
| 288 |
-
match = re.match(r'^([^@]+)@([^:]+):(\d+)(.*)$', uri)
|
| 289 |
-
if not match:
|
| 290 |
-
return None
|
| 291 |
-
|
| 292 |
-
password, server, port, params_part = match.groups()
|
| 293 |
-
|
| 294 |
-
# 解析参数
|
| 295 |
-
name = ''
|
| 296 |
-
sni = ''
|
| 297 |
-
allow_insecure = False
|
| 298 |
-
|
| 299 |
-
if '#' in params_part:
|
| 300 |
-
params_part, name = params_part.split('#', 1)
|
| 301 |
-
|
| 302 |
-
if '?' in params_part:
|
| 303 |
-
params_str = params_part.lstrip('?')
|
| 304 |
-
params = parse_qs(params_str)
|
| 305 |
-
sni = params.get('peer', [''])[0] or params.get('sni', [''])[0]
|
| 306 |
-
allow_insecure = params.get('allowInsecure', ['0'])[0] == '1'
|
| 307 |
-
|
| 308 |
-
return {
|
| 309 |
-
'type': 'trojan',
|
| 310 |
-
'name': name or f"Trojan {server[:8]}",
|
| 311 |
-
'server': server,
|
| 312 |
-
'port': int(port),
|
| 313 |
-
'password': password,
|
| 314 |
-
'sni': sni,
|
| 315 |
-
'skip-cert-verify': allow_insecure
|
| 316 |
-
}
|
| 317 |
-
except:
|
| 318 |
-
return None
|
| 319 |
-
|
| 320 |
-
def _fetch_config(self, config_url):
|
| 321 |
-
"""获取配置文件内容"""
|
| 322 |
-
try:
|
| 323 |
-
response = requests.get(config_url, timeout=15)
|
| 324 |
-
if response.status_code != 200:
|
| 325 |
-
return {}
|
| 326 |
-
|
| 327 |
-
content = response.text
|
| 328 |
-
|
| 329 |
-
# 检测配置文件格式并解析
|
| 330 |
-
if content.startswith('{'): # JSON
|
| 331 |
-
return json.loads(content)
|
| 332 |
-
elif '[' in content and ']' in content: # INI
|
| 333 |
-
return self._parse_ini_config(content)
|
| 334 |
-
else: # 尝试作为YAML解析
|
| 335 |
-
try:
|
| 336 |
-
return yaml.safe_load(content) or {}
|
| 337 |
-
except:
|
| 338 |
-
return {}
|
| 339 |
-
except:
|
| 340 |
-
return {}
|
| 341 |
-
|
| 342 |
-
def _parse_ini_config(self, content):
|
| 343 |
-
"""解析INI格式的配置文件"""
|
| 344 |
-
config = {
|
| 345 |
-
'rules': [],
|
| 346 |
-
'groups': []
|
| 347 |
-
}
|
| 348 |
-
|
| 349 |
-
current_section = None
|
| 350 |
-
|
| 351 |
-
for line in content.splitlines():
|
| 352 |
-
line = line.strip()
|
| 353 |
-
if not line or line.startswith(';') or line.startswith('#'):
|
| 354 |
-
continue
|
| 355 |
-
|
| 356 |
-
# 检测节
|
| 357 |
-
if line.startswith('[') and line.endswith(']'):
|
| 358 |
-
current_section = line[1:-1].strip()
|
| 359 |
-
continue
|
| 360 |
-
|
| 361 |
-
# 处理Rule节
|
| 362 |
-
if current_section == 'Rule':
|
| 363 |
-
config['rules'].append(line)
|
| 364 |
-
# 处理Proxy Group节
|
| 365 |
-
elif current_section and 'Group' in current_section:
|
| 366 |
-
config['groups'].append(line)
|
| 367 |
-
|
| 368 |
-
return config
|
| 369 |
-
|
| 370 |
-
def _convert_to_clash(self, nodes, config):
|
| 371 |
-
"""转换为Clash配置格式"""
|
| 372 |
-
# 创建基本结构
|
| 373 |
-
clash_config = {
|
| 374 |
-
'port': 7890,
|
| 375 |
-
'socks-port': 7891,
|
| 376 |
-
'allow-lan': True,
|
| 377 |
-
'mode': 'Rule',
|
| 378 |
-
'log-level': 'info',
|
| 379 |
-
'external-controller': '127.0.0.1:9090',
|
| 380 |
-
'proxies': nodes,
|
| 381 |
-
'proxy-groups': [],
|
| 382 |
-
'rules': []
|
| 383 |
-
}
|
| 384 |
-
|
| 385 |
-
# 根据配置创建代理组
|
| 386 |
-
if config.get('groups'):
|
| 387 |
-
default_groups = [
|
| 388 |
-
{
|
| 389 |
-
'name': '🚀 节点选择',
|
| 390 |
-
'type': 'select',
|
| 391 |
-
'proxies': ['DIRECT'] + [node['name'] for node in nodes]
|
| 392 |
-
},
|
| 393 |
-
{
|
| 394 |
-
'name': '🌍 国外媒体',
|
| 395 |
-
'type': 'select',
|
| 396 |
-
'proxies': ['🚀 节点选择'] + [node['name'] for node in nodes]
|
| 397 |
-
},
|
| 398 |
-
{
|
| 399 |
-
'name': '📲 电报信息',
|
| 400 |
-
'type': 'select',
|
| 401 |
-
'proxies': ['🚀 节点选择'] + [node['name'] for node in nodes]
|
| 402 |
-
},
|
| 403 |
-
{
|
| 404 |
-
'name': '🍎 苹果服务',
|
| 405 |
-
'type': 'select',
|
| 406 |
-
'proxies': ['DIRECT', '🚀 节点选择']
|
| 407 |
-
},
|
| 408 |
-
{
|
| 409 |
-
'name': '🎯 全球直连',
|
| 410 |
-
'type': 'select',
|
| 411 |
-
'proxies': ['DIRECT', '🚀 节点选择']
|
| 412 |
-
},
|
| 413 |
-
{
|
| 414 |
-
'name': '🛑 全球拦截',
|
| 415 |
-
'type': 'select',
|
| 416 |
-
'proxies': ['REJECT', 'DIRECT']
|
| 417 |
-
},
|
| 418 |
-
{
|
| 419 |
-
'name': '⚓ 漏网之鱼',
|
| 420 |
-
'type': 'select',
|
| 421 |
-
'proxies': ['🚀 节点选择', 'DIRECT']
|
| 422 |
-
}
|
| 423 |
-
]
|
| 424 |
-
clash_config['proxy-groups'] = default_groups
|
| 425 |
-
|
| 426 |
-
# 添加规则
|
| 427 |
-
if config.get('rules'):
|
| 428 |
-
clash_config['rules'] = config.get('rules', [])
|
| 429 |
-
else:
|
| 430 |
-
# 默认规则
|
| 431 |
-
clash_config['rules'] = [
|
| 432 |
-
'DOMAIN-SUFFIX,google.com,🚀 节点选择',
|
| 433 |
-
'DOMAIN-KEYWORD,google,🚀 节点选择',
|
| 434 |
-
'DOMAIN-SUFFIX,ad.com,🛑 全球拦截',
|
| 435 |
-
'DOMAIN-SUFFIX,apple.com,🍎 苹果服务',
|
| 436 |
-
'DOMAIN-SUFFIX,telegram.org,📲 电报信息',
|
| 437 |
-
'DOMAIN-SUFFIX,youtube.com,🌍 国外媒体',
|
| 438 |
-
'DOMAIN-SUFFIX,netflix.com,🌍 国外媒体',
|
| 439 |
-
'GEOIP,CN,🎯 全球直连',
|
| 440 |
-
'MATCH,⚓ 漏网之鱼'
|
| 441 |
-
]
|
| 442 |
-
|
| 443 |
-
# 转换成YAML格式文本
|
| 444 |
-
yaml_str = yaml.dump(clash_config, allow_unicode=True, sort_keys=False)
|
| 445 |
-
return yaml_str
|
| 446 |
-
|
| 447 |
-
def _convert_to_v2ray(self, nodes):
|
| 448 |
-
"""转换为V2Ray格式"""
|
| 449 |
-
# 实际情况下,可能需要更复杂的处理逻辑
|
| 450 |
-
v2ray_config = {
|
| 451 |
-
"outbounds": []
|
| 452 |
-
}
|
| 453 |
-
|
| 454 |
-
for node in nodes:
|
| 455 |
-
if node['type'] == 'vmess':
|
| 456 |
-
outbound = {
|
| 457 |
-
"protocol": "vmess",
|
| 458 |
-
"settings": {
|
| 459 |
-
"vnext": [
|
| 460 |
-
{
|
| 461 |
-
"address": node['server'],
|
| 462 |
-
"port": node['port'],
|
| 463 |
-
"users": [
|
| 464 |
-
{
|
| 465 |
-
"id": node['uuid'],
|
| 466 |
-
"alterId": node.get('alterId', 0),
|
| 467 |
-
"security": node.get('cipher', 'auto')
|
| 468 |
-
}
|
| 469 |
-
]
|
| 470 |
-
}
|
| 471 |
-
]
|
| 472 |
-
},
|
| 473 |
-
"tag": node['name']
|
| 474 |
-
}
|
| 475 |
-
|
| 476 |
-
# 添加额外设置
|
| 477 |
-
if node.get('network') == 'ws':
|
| 478 |
-
outbound["streamSettings"] = {
|
| 479 |
-
"network": "ws",
|
| 480 |
-
"security": "tls" if node.get('tls') else "none",
|
| 481 |
-
"wsSettings": {
|
| 482 |
-
"path": node.get('ws-path', ''),
|
| 483 |
-
"headers": node.get('ws-headers', {})
|
| 484 |
-
}
|
| 485 |
-
}
|
| 486 |
-
|
| 487 |
-
v2ray_config["outbounds"].append(outbound)
|
| 488 |
-
|
| 489 |
-
return json.dumps(v2ray_config, ensure_ascii=False, indent=2)
|
| 490 |
-
|
| 491 |
-
# 实例化供使用
|
| 492 |
-
converter = SubscriptionConverter()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|