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()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|