nonebot-plugin-hibank 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nonebot_plugin_hibank/__init__.py +43 -0
- nonebot_plugin_hibank/assets//321/205/320/236/320/257/321/207/320/265/320/256/321/205/320/275/320/247/321/204/342/225/234/320/243.ttf +0 -0
- nonebot_plugin_hibank/cache.py +77 -0
- nonebot_plugin_hibank/client.py +388 -0
- nonebot_plugin_hibank/commands.py +609 -0
- nonebot_plugin_hibank/config.py +10 -0
- nonebot_plugin_hibank/marks.py +83 -0
- nonebot_plugin_hibank/models.py +71 -0
- nonebot_plugin_hibank/render.py +347 -0
- nonebot_plugin_hibank-0.2.0.dist-info/METADATA +184 -0
- nonebot_plugin_hibank-0.2.0.dist-info/RECORD +14 -0
- nonebot_plugin_hibank-0.2.0.dist-info/WHEEL +5 -0
- nonebot_plugin_hibank-0.2.0.dist-info/licenses/LICENSE +674 -0
- nonebot_plugin_hibank-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from nonebot import require
|
|
4
|
+
from nonebot.plugin import PluginMetadata
|
|
5
|
+
|
|
6
|
+
require("nonebot_plugin_localstore")
|
|
7
|
+
|
|
8
|
+
from .config import HibankConfig # noqa: E402
|
|
9
|
+
|
|
10
|
+
__plugin_meta__ = PluginMetadata(
|
|
11
|
+
name="HiBank 城市银行查询",
|
|
12
|
+
description="查询 HiBank 城市银行分布与银行网点信息的 NoneBot2 插件。",
|
|
13
|
+
usage=(
|
|
14
|
+
"/bank 城市 <城市名>\n"
|
|
15
|
+
"/bank 网点 <城市名> <银行名> [页码]\n"
|
|
16
|
+
"/银行 <城市名>\n"
|
|
17
|
+
"/网点 <城市名> <银行名> [页码]\n"
|
|
18
|
+
"/bank 搜城市 <关键词>\n"
|
|
19
|
+
"/bank 搜银行 <关键词>\n"
|
|
20
|
+
"/bank 缓存\n"
|
|
21
|
+
"/bank 清缓存\n"
|
|
22
|
+
"/标记 <银行名...> / /mark <银行名...>\n"
|
|
23
|
+
"/取消标记 <银行名...> / /unmark <银行名...>\n"
|
|
24
|
+
"/批量标记 <城市名> <分类>\n"
|
|
25
|
+
"/批量取消标记 <城市名> <分类>\n"
|
|
26
|
+
"/复制标记 <@用户/QQ号>\n"
|
|
27
|
+
"/标记列表\n"
|
|
28
|
+
"/关注 <银行名...> / /follow <银行名...>\n"
|
|
29
|
+
"/取消关注 <银行名...> / /unfollow <银行名...>\n"
|
|
30
|
+
"/批量关注 <城市名> <分类>\n"
|
|
31
|
+
"/批量取消关注 <城市名> <分类>\n"
|
|
32
|
+
"/复制关注 <@用户/QQ号>\n"
|
|
33
|
+
"/关注列表\n"
|
|
34
|
+
"/搜城市 <关键词>\n"
|
|
35
|
+
"/搜银行 <关键词>"
|
|
36
|
+
),
|
|
37
|
+
type="application",
|
|
38
|
+
homepage="https://github.com/WhyPilotXia/nonebot-plugin-hibank",
|
|
39
|
+
config=HibankConfig,
|
|
40
|
+
supported_adapters={"~onebot.v11"},
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
from . import commands as commands # noqa: E402
|
|
Binary file
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import shutil
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from nonebot import require
|
|
9
|
+
|
|
10
|
+
require("nonebot_plugin_localstore")
|
|
11
|
+
|
|
12
|
+
import nonebot_plugin_localstore as localstore # noqa: E402
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
DATA_DIR: Path = localstore.get_plugin_data_dir()
|
|
16
|
+
INDEXES_FILE = DATA_DIR / "indexes.json"
|
|
17
|
+
CITY_DIR = DATA_DIR / "cities"
|
|
18
|
+
BRANCH_DIR = DATA_DIR / "branches"
|
|
19
|
+
USER_MARKS_FILE = DATA_DIR / "user_marks.json"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def ensure_dirs() -> None:
|
|
23
|
+
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
|
24
|
+
CITY_DIR.mkdir(parents=True, exist_ok=True)
|
|
25
|
+
BRANCH_DIR.mkdir(parents=True, exist_ok=True)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def safe_name(value: str) -> str:
|
|
29
|
+
result = []
|
|
30
|
+
for char in value:
|
|
31
|
+
if char.isalnum() or char in {"-", "_"}:
|
|
32
|
+
result.append(char)
|
|
33
|
+
else:
|
|
34
|
+
result.append("_")
|
|
35
|
+
return "".join(result).strip("_") or "cache"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def read_json(path: Path) -> Any | None:
|
|
39
|
+
try:
|
|
40
|
+
if not path.exists():
|
|
41
|
+
return None
|
|
42
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
43
|
+
except (OSError, json.JSONDecodeError):
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def write_json(path: Path, data: Any) -> None:
|
|
48
|
+
ensure_dirs()
|
|
49
|
+
path.write_text(
|
|
50
|
+
json.dumps(data, ensure_ascii=False, indent=2),
|
|
51
|
+
encoding="utf-8",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def city_cache_path(city_key: str) -> Path:
|
|
56
|
+
return CITY_DIR / f"{safe_name(city_key)}.json"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def branch_cache_path(city_key: str, bank_name: str) -> Path:
|
|
60
|
+
return BRANCH_DIR / f"{safe_name(city_key)}__{safe_name(bank_name)}.json"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def clear_cache() -> None:
|
|
64
|
+
if INDEXES_FILE.exists():
|
|
65
|
+
INDEXES_FILE.unlink()
|
|
66
|
+
if CITY_DIR.exists():
|
|
67
|
+
shutil.rmtree(CITY_DIR)
|
|
68
|
+
if BRANCH_DIR.exists():
|
|
69
|
+
shutil.rmtree(BRANCH_DIR)
|
|
70
|
+
ensure_dirs()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def cache_counts() -> tuple[int, int]:
|
|
74
|
+
ensure_dirs()
|
|
75
|
+
city_count = len(list(CITY_DIR.glob("*.json")))
|
|
76
|
+
branch_count = len(list(BRANCH_DIR.glob("*.json")))
|
|
77
|
+
return city_count, branch_count
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import html
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
import warnings
|
|
8
|
+
from typing import Any
|
|
9
|
+
from urllib.parse import quote
|
|
10
|
+
|
|
11
|
+
import requests
|
|
12
|
+
from bs4 import BeautifulSoup
|
|
13
|
+
from urllib3.exceptions import InsecureRequestWarning
|
|
14
|
+
from nonebot import get_plugin_config
|
|
15
|
+
|
|
16
|
+
from . import cache
|
|
17
|
+
from .config import HibankConfig
|
|
18
|
+
from .models import BankRef, BranchDetail, CacheStats, CityDetail, CityRef
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
STATE_RE = re.compile(
|
|
22
|
+
r"window\.__HIBANK_PINIA_STATE__\s*=\s*(\{.*?\})</script>",
|
|
23
|
+
re.S,
|
|
24
|
+
)
|
|
25
|
+
BRANCH_HREF_RE = re.compile(r"^/branches/([^/]+)/([^/]+)/(.+)$")
|
|
26
|
+
SUFFIXES = (
|
|
27
|
+
"省",
|
|
28
|
+
"市",
|
|
29
|
+
"自治区",
|
|
30
|
+
"特别行政区",
|
|
31
|
+
"壮族自治区",
|
|
32
|
+
"回族自治区",
|
|
33
|
+
"维吾尔自治区",
|
|
34
|
+
"自治州",
|
|
35
|
+
"地区",
|
|
36
|
+
"盟",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class HibankError(RuntimeError):
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class HibankClient:
|
|
45
|
+
def __init__(self) -> None:
|
|
46
|
+
self.config = get_plugin_config(HibankConfig)
|
|
47
|
+
self._indexes: dict[str, Any] | None = None
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def base_url(self) -> str:
|
|
51
|
+
return self.config.hibank_base_url.rstrip("/")
|
|
52
|
+
|
|
53
|
+
async def ensure_indexes(self) -> dict[str, Any]:
|
|
54
|
+
if self._indexes is not None:
|
|
55
|
+
return self._indexes
|
|
56
|
+
cached = cache.read_json(cache.INDEXES_FILE)
|
|
57
|
+
if self._indexes_valid(cached):
|
|
58
|
+
self._indexes = cached
|
|
59
|
+
return cached
|
|
60
|
+
html_text = await self._fetch_text("/cities")
|
|
61
|
+
state = self._parse_state(html_text)
|
|
62
|
+
indexes = state.get("data", {}).get("indexes", {})
|
|
63
|
+
if not self._indexes_valid(indexes):
|
|
64
|
+
raise HibankError("城市索引解析失败。")
|
|
65
|
+
cache.write_json(cache.INDEXES_FILE, indexes)
|
|
66
|
+
self._indexes = indexes
|
|
67
|
+
return indexes
|
|
68
|
+
|
|
69
|
+
async def search_cities(self, keyword: str, limit: int = 20) -> list[CityRef]:
|
|
70
|
+
keyword_norm = normalize(keyword)
|
|
71
|
+
if not keyword_norm:
|
|
72
|
+
return []
|
|
73
|
+
results: list[tuple[int, CityRef]] = []
|
|
74
|
+
for city in await self.iter_cities():
|
|
75
|
+
city_norm = normalize(city.city)
|
|
76
|
+
province_norm = normalize(city.province)
|
|
77
|
+
text = province_norm + city_norm
|
|
78
|
+
if keyword_norm == city_norm:
|
|
79
|
+
score = 100
|
|
80
|
+
elif city_norm.startswith(keyword_norm):
|
|
81
|
+
score = 80
|
|
82
|
+
elif keyword_norm in city_norm:
|
|
83
|
+
score = 60
|
|
84
|
+
elif keyword_norm in text:
|
|
85
|
+
score = 40
|
|
86
|
+
else:
|
|
87
|
+
continue
|
|
88
|
+
results.append((score, city))
|
|
89
|
+
results.sort(key=lambda item: (-item[0], item[1].province_code, item[1].city_slug))
|
|
90
|
+
return [city for _, city in results[:limit]]
|
|
91
|
+
|
|
92
|
+
async def search_banks(self, keyword: str, limit: int = 30) -> list[str]:
|
|
93
|
+
keyword_norm = normalize(keyword)
|
|
94
|
+
if not keyword_norm:
|
|
95
|
+
return []
|
|
96
|
+
bank_names = await self.all_bank_names()
|
|
97
|
+
results = [
|
|
98
|
+
name
|
|
99
|
+
for name in bank_names
|
|
100
|
+
if keyword_norm in normalize(name)
|
|
101
|
+
]
|
|
102
|
+
results.sort(key=lambda name: (normalize(name).find(keyword_norm), len(name), name))
|
|
103
|
+
return results[:limit]
|
|
104
|
+
|
|
105
|
+
async def all_bank_names(self) -> set[str]:
|
|
106
|
+
indexes = await self.ensure_indexes()
|
|
107
|
+
bank_names: set[str] = set()
|
|
108
|
+
banks = indexes.get("banks", [])
|
|
109
|
+
if isinstance(banks, list):
|
|
110
|
+
for group in banks:
|
|
111
|
+
if not isinstance(group, dict):
|
|
112
|
+
continue
|
|
113
|
+
value = group.get("value", [])
|
|
114
|
+
if not isinstance(value, list):
|
|
115
|
+
continue
|
|
116
|
+
for item in value:
|
|
117
|
+
if not isinstance(item, dict):
|
|
118
|
+
continue
|
|
119
|
+
name = item.get("name")
|
|
120
|
+
if isinstance(name, str) and name.strip():
|
|
121
|
+
bank_names.add(name.strip())
|
|
122
|
+
return bank_names
|
|
123
|
+
|
|
124
|
+
async def split_known_banks(self, banks: list[str]) -> tuple[list[str], list[str]]:
|
|
125
|
+
known_names = await self.all_bank_names()
|
|
126
|
+
known_by_norm = {normalize(name) for name in known_names}
|
|
127
|
+
known: list[str] = []
|
|
128
|
+
unknown: list[str] = []
|
|
129
|
+
for bank in banks:
|
|
130
|
+
target = bank.strip()
|
|
131
|
+
if not target:
|
|
132
|
+
continue
|
|
133
|
+
if normalize(target) in known_by_norm:
|
|
134
|
+
known.append(target)
|
|
135
|
+
else:
|
|
136
|
+
unknown.append(target)
|
|
137
|
+
return known, unknown
|
|
138
|
+
|
|
139
|
+
async def iter_cities(self) -> list[CityRef]:
|
|
140
|
+
indexes = await self.ensure_indexes()
|
|
141
|
+
cities = indexes.get("cities", {})
|
|
142
|
+
refs: list[CityRef] = []
|
|
143
|
+
for province, payload in cities.items():
|
|
144
|
+
if not isinstance(payload, dict):
|
|
145
|
+
continue
|
|
146
|
+
code = payload.get("code")
|
|
147
|
+
city_map = payload.get("cities", {})
|
|
148
|
+
if not isinstance(code, str) or not isinstance(city_map, dict):
|
|
149
|
+
continue
|
|
150
|
+
for city, slug in city_map.items():
|
|
151
|
+
if isinstance(city, str) and isinstance(slug, str):
|
|
152
|
+
refs.append(CityRef(province, code, city, slug))
|
|
153
|
+
return refs
|
|
154
|
+
|
|
155
|
+
async def resolve_city(self, query: str) -> CityRef:
|
|
156
|
+
parts = [part for part in query.strip().split() if part]
|
|
157
|
+
province_kw = ""
|
|
158
|
+
city_kw = query
|
|
159
|
+
if len(parts) >= 2:
|
|
160
|
+
province_kw = parts[0]
|
|
161
|
+
city_kw = "".join(parts[1:])
|
|
162
|
+
city_norm = normalize(city_kw)
|
|
163
|
+
province_norm = normalize(province_kw)
|
|
164
|
+
if not city_norm:
|
|
165
|
+
raise HibankError("请提供城市名。")
|
|
166
|
+
|
|
167
|
+
matches: list[tuple[int, CityRef]] = []
|
|
168
|
+
for city in await self.iter_cities():
|
|
169
|
+
current_city = normalize(city.city)
|
|
170
|
+
current_province = normalize(city.province)
|
|
171
|
+
if province_norm and province_norm not in current_province:
|
|
172
|
+
continue
|
|
173
|
+
if city_norm == current_city:
|
|
174
|
+
score = 100
|
|
175
|
+
elif current_city.startswith(city_norm):
|
|
176
|
+
score = 80
|
|
177
|
+
elif city_norm in current_city:
|
|
178
|
+
score = 60
|
|
179
|
+
elif city_norm in current_province + current_city:
|
|
180
|
+
score = 40
|
|
181
|
+
else:
|
|
182
|
+
continue
|
|
183
|
+
matches.append((score, city))
|
|
184
|
+
if not matches:
|
|
185
|
+
raise HibankError(f"未找到城市:{query}")
|
|
186
|
+
matches.sort(key=lambda item: (-item[0], item[1].province_code, item[1].city_slug))
|
|
187
|
+
return matches[0][1]
|
|
188
|
+
|
|
189
|
+
async def get_city_detail(self, city: CityRef) -> CityDetail:
|
|
190
|
+
cached = cache.read_json(cache.city_cache_path(city.key))
|
|
191
|
+
if isinstance(cached, dict) and cached.get("groups"):
|
|
192
|
+
return CityDetail(
|
|
193
|
+
city=city,
|
|
194
|
+
groups=cached["groups"],
|
|
195
|
+
bank_paths=cached.get("bank_paths", {}),
|
|
196
|
+
from_cache=True,
|
|
197
|
+
)
|
|
198
|
+
html_text = await self._fetch_text(f"/cities/{city.province_code}/{city.city_slug}")
|
|
199
|
+
state = self._parse_state(html_text)
|
|
200
|
+
city_cache = state.get("data", {}).get("cache", {}).get("cities", {})
|
|
201
|
+
groups = city_cache.get(city.key)
|
|
202
|
+
if not isinstance(groups, dict):
|
|
203
|
+
if len(city_cache) == 1:
|
|
204
|
+
groups = next(iter(city_cache.values()))
|
|
205
|
+
if not isinstance(groups, dict):
|
|
206
|
+
raise HibankError(f"{city.city} 银行列表解析失败。")
|
|
207
|
+
normalized_groups = {
|
|
208
|
+
str(category): [str(item) for item in items]
|
|
209
|
+
for category, items in groups.items()
|
|
210
|
+
if isinstance(items, list)
|
|
211
|
+
}
|
|
212
|
+
bank_paths = self._extract_bank_paths(html_text, city)
|
|
213
|
+
cache.write_json(
|
|
214
|
+
cache.city_cache_path(city.key),
|
|
215
|
+
{"city": city.__dict__, "groups": normalized_groups, "bank_paths": bank_paths},
|
|
216
|
+
)
|
|
217
|
+
return CityDetail(city=city, groups=normalized_groups, bank_paths=bank_paths)
|
|
218
|
+
|
|
219
|
+
async def get_branch_detail(
|
|
220
|
+
self,
|
|
221
|
+
city: CityRef,
|
|
222
|
+
bank_query: str,
|
|
223
|
+
page: int = 1,
|
|
224
|
+
) -> BranchDetail:
|
|
225
|
+
city_detail = await self.get_city_detail(city)
|
|
226
|
+
bank = self.resolve_bank(city_detail, bank_query)
|
|
227
|
+
cached = cache.read_json(cache.branch_cache_path(city.key, bank.name))
|
|
228
|
+
if isinstance(cached, list):
|
|
229
|
+
return BranchDetail(
|
|
230
|
+
city=city,
|
|
231
|
+
bank=bank,
|
|
232
|
+
branches=cached,
|
|
233
|
+
page=page,
|
|
234
|
+
page_size=self.config.hibank_branch_page_size,
|
|
235
|
+
from_cache=True,
|
|
236
|
+
)
|
|
237
|
+
bank_path = bank.path
|
|
238
|
+
if not bank_path.startswith("%"):
|
|
239
|
+
bank_path = quote(bank_path, safe="")
|
|
240
|
+
html_text = await self._fetch_text(
|
|
241
|
+
f"/branches/{city.province_code}/{city.city_slug}/{bank_path}"
|
|
242
|
+
)
|
|
243
|
+
state = self._parse_state(html_text)
|
|
244
|
+
branch_cache = state.get("data", {}).get("cache", {}).get("branches", {})
|
|
245
|
+
branches: Any = branch_cache.get(f"{city.key}_{bank.name}")
|
|
246
|
+
if not isinstance(branches, list) and len(branch_cache) == 1:
|
|
247
|
+
branches = next(iter(branch_cache.values()))
|
|
248
|
+
if not isinstance(branches, list):
|
|
249
|
+
raise HibankError(f"{city.city} {bank.name} 网点列表解析失败。")
|
|
250
|
+
normalized = [
|
|
251
|
+
item
|
|
252
|
+
for item in branches
|
|
253
|
+
if isinstance(item, dict)
|
|
254
|
+
]
|
|
255
|
+
cache.write_json(cache.branch_cache_path(city.key, bank.name), normalized)
|
|
256
|
+
return BranchDetail(
|
|
257
|
+
city=city,
|
|
258
|
+
bank=bank,
|
|
259
|
+
branches=normalized,
|
|
260
|
+
page=page,
|
|
261
|
+
page_size=self.config.hibank_branch_page_size,
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
def resolve_bank(self, city_detail: CityDetail, query: str) -> BankRef:
|
|
265
|
+
query_norm = normalize(query)
|
|
266
|
+
if not query_norm:
|
|
267
|
+
raise HibankError("请提供银行名。")
|
|
268
|
+
candidates = city_detail.bank_paths
|
|
269
|
+
if not candidates:
|
|
270
|
+
candidates = {
|
|
271
|
+
bank: bank
|
|
272
|
+
for banks in city_detail.groups.values()
|
|
273
|
+
for bank in banks
|
|
274
|
+
}
|
|
275
|
+
matches: list[tuple[int, str, str]] = []
|
|
276
|
+
for name, path in candidates.items():
|
|
277
|
+
name_norm = normalize(name)
|
|
278
|
+
base_norm = normalize(name.split("(", 1)[0])
|
|
279
|
+
if query_norm == name_norm or query_norm == base_norm:
|
|
280
|
+
score = 100
|
|
281
|
+
elif name_norm.startswith(query_norm) or base_norm.startswith(query_norm):
|
|
282
|
+
score = 80
|
|
283
|
+
elif query_norm in name_norm:
|
|
284
|
+
score = 60
|
|
285
|
+
else:
|
|
286
|
+
continue
|
|
287
|
+
matches.append((score, name, path))
|
|
288
|
+
if not matches:
|
|
289
|
+
raise HibankError(f"{city_detail.city.city} 未找到银行:{query}")
|
|
290
|
+
matches.sort(key=lambda item: (-item[0], len(item[1]), item[1]))
|
|
291
|
+
_, name, path = matches[0]
|
|
292
|
+
return BankRef(name=name, path=path)
|
|
293
|
+
|
|
294
|
+
def get_cache_stats(self) -> CacheStats:
|
|
295
|
+
city_count, branch_count = cache.cache_counts()
|
|
296
|
+
return CacheStats(
|
|
297
|
+
indexes_cached=cache.INDEXES_FILE.exists(),
|
|
298
|
+
city_cache_count=city_count,
|
|
299
|
+
branch_cache_count=branch_count,
|
|
300
|
+
cache_dir=str(cache.DATA_DIR),
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
def _indexes_valid(self, indexes: Any) -> bool:
|
|
304
|
+
if not isinstance(indexes, dict):
|
|
305
|
+
return False
|
|
306
|
+
cities = indexes.get("cities")
|
|
307
|
+
banks = indexes.get("banks")
|
|
308
|
+
if not isinstance(cities, dict) or not isinstance(banks, list):
|
|
309
|
+
return False
|
|
310
|
+
sichuan = cities.get("四川省")
|
|
311
|
+
if not isinstance(sichuan, dict):
|
|
312
|
+
return False
|
|
313
|
+
city_map = sichuan.get("cities")
|
|
314
|
+
if not isinstance(city_map, dict):
|
|
315
|
+
return False
|
|
316
|
+
return city_map.get("成都市") == "chengdu"
|
|
317
|
+
|
|
318
|
+
async def clear_cache(self) -> None:
|
|
319
|
+
await asyncio.to_thread(cache.clear_cache)
|
|
320
|
+
self._indexes = None
|
|
321
|
+
|
|
322
|
+
async def _fetch_text(self, path: str) -> str:
|
|
323
|
+
url = self.base_url + path
|
|
324
|
+
return await asyncio.to_thread(self._fetch_text_sync, url)
|
|
325
|
+
|
|
326
|
+
def _fetch_text_sync(self, url: str) -> str:
|
|
327
|
+
headers = {
|
|
328
|
+
"User-Agent": (
|
|
329
|
+
"Mozilla/5.0 AppleWebKit/537.36 "
|
|
330
|
+
"(KHTML, like Gecko) Chrome/126.0 Safari/537.36"
|
|
331
|
+
)
|
|
332
|
+
}
|
|
333
|
+
try:
|
|
334
|
+
with warnings.catch_warnings():
|
|
335
|
+
if not self.config.hibank_verify_ssl:
|
|
336
|
+
warnings.simplefilter("ignore", InsecureRequestWarning)
|
|
337
|
+
response = requests.get(
|
|
338
|
+
url,
|
|
339
|
+
headers=headers,
|
|
340
|
+
timeout=self.config.hibank_timeout,
|
|
341
|
+
verify=self.config.hibank_verify_ssl,
|
|
342
|
+
)
|
|
343
|
+
response.raise_for_status()
|
|
344
|
+
except requests.RequestException as exc:
|
|
345
|
+
raise HibankError(f"请求 HiBank 失败:{exc}") from exc
|
|
346
|
+
response.encoding = "utf-8"
|
|
347
|
+
return response.text
|
|
348
|
+
|
|
349
|
+
def _parse_state(self, html_text: str) -> dict[str, Any]:
|
|
350
|
+
match = STATE_RE.search(html_text)
|
|
351
|
+
if not match:
|
|
352
|
+
raise HibankError("页面状态数据解析失败。")
|
|
353
|
+
try:
|
|
354
|
+
return json.loads(match.group(1))
|
|
355
|
+
except json.JSONDecodeError as exc:
|
|
356
|
+
raise HibankError("页面状态 JSON 解析失败。") from exc
|
|
357
|
+
|
|
358
|
+
def _extract_bank_paths(self, html_text: str, city: CityRef) -> dict[str, str]:
|
|
359
|
+
soup = BeautifulSoup(html_text, "html.parser")
|
|
360
|
+
paths: dict[str, str] = {}
|
|
361
|
+
prefix = f"/branches/{city.province_code}/{city.city_slug}/"
|
|
362
|
+
for anchor in soup.find_all("a", href=True):
|
|
363
|
+
href = str(anchor.get("href"))
|
|
364
|
+
if not href.startswith(prefix):
|
|
365
|
+
continue
|
|
366
|
+
match = BRANCH_HREF_RE.match(href)
|
|
367
|
+
if not match:
|
|
368
|
+
continue
|
|
369
|
+
name = " ".join(anchor.get_text(" ", strip=True).split())
|
|
370
|
+
if not name:
|
|
371
|
+
image = anchor.find("img", alt=True)
|
|
372
|
+
if image is not None:
|
|
373
|
+
name = str(image.get("alt", "")).strip()
|
|
374
|
+
if name:
|
|
375
|
+
paths[html.unescape(name)] = match.group(3)
|
|
376
|
+
return paths
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def normalize(value: str) -> str:
|
|
380
|
+
text = str(value).strip().lower()
|
|
381
|
+
for char in (" ", "\t", "\n", "\r", " ", "-", "_"):
|
|
382
|
+
text = text.replace(char, "")
|
|
383
|
+
for suffix in SUFFIXES:
|
|
384
|
+
text = text.replace(suffix, "")
|
|
385
|
+
return text
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
client = HibankClient()
|