├── README.md └── main.py /README.md: -------------------------------------------------------------------------------- 1 | # 用另外一个,更通用更完善:[domain-scanner-4-nodeseeker](https://github.com/stx-x/domain-scanner-4-nodeseeker) 2 | 3 | # .li 域名可用性扫描工具 4 | 5 | 这是一个灵活的 .li 域名可用性扫描工具,可以根据用户定义的条件组合扫描可注册的 .li 域名。 6 | 7 | ## 功能特点 8 | 9 | - **灵活的扫描配置**:支持指定域名长度、字符集和生成方法 10 | - **多种生成方法**: 11 | - 所有组合:生成指定长度和字符集的所有可能组合 12 | - 字典匹配:从字典文件中查找符合长度和字符集要求的单词 13 | - 连续重复模式:生成包含连续重复字符的域名 14 | - 拼音匹配:从拼音字典中查找符合条件的拼音 15 | - **实时结果输出**:扫描过程中实时将可用域名写入文件,无需等待扫描完成 16 | - **错误处理**:健壮的网络错误处理和自动重试机制 17 | - **详细模式**:可选择输出每个查询域名的详细状态 18 | 19 | ## 安装 20 | 21 | 只需克隆此仓库: 22 | 23 | ```bash 24 | git clone https://github.com/stx-x/net-cup-domain-checker 25 | cd net-cup-domain-checker 26 | ``` 27 | ## 查看帮助 这个比下面的准 28 | 29 | ```bash 30 | python3 main.py -h 31 | ``` 32 | 33 | ## 使用方法 34 | 35 | ```bash 36 | python3 main.py -l 长度 -c 字符集 -m 方法1 [方法2 ...] -o results.txt 37 | ``` 38 | 39 | ### 必需参数 40 | 41 | - `-l, --length`:要扫描的域名主体长度(不含 .li 后缀) 42 | - `-m, --methods`:域名生成方法,可选择多个: 43 | - `all`:生成所有可能的组合 44 | - `dict`:从字典文件中查找 45 | - `repeats`:生成包含连续重复字符的域名 46 | - `pinyin`:从拼音字典文件中查找 47 | 48 | ### 可选参数 49 | 50 | - `-c, --chars`:字符集类型 (默认: `alnum`) 51 | - `letters`:仅小写字母 (a-z) 52 | - `digits`:仅数字 (0-9) 53 | - `alnum`:字母和数字 54 | - `letters-hyphen`:字母和连字符 55 | - `digits-hyphen`:数字和连字符 56 | - `alnum-hyphen`:字母、数字和连字符 57 | - `-o, --output`:可选:将找到的可用域名写入指定的文件路径。 (default: None) 58 | - `--live-log`: 指定实时日志文件,可用域名会被立即写入此文件 (可在扫描过程中查看) (default: None) 59 | - `--min-repeats`:指定最少连续重复字符数量 (默认: 2) 60 | - `--dict-file`:字典文件的路径 (默认: /usr/share/dict/words) 61 | - `--pinyin-dict-file`:拼音字典文件的路径 62 | - `--delay`:每次查询之间的延迟秒数 (默认: 1.0) 63 | - `--max-retries`:连接失败时的最大重试次数 (默认: 2) 64 | - `-v, --verbose`:显示每个查询域名的详细状态 65 | 66 | ## 示例 67 | 68 | ### 扫描所有2位名 69 | ```bash 70 | python3 main.py -l 2 -c alnum-hyphen -m all -v --live-log log.txt 71 | ``` 72 | 73 | ### 扫描所有3位数字域名 74 | ```bash 75 | python3 main.py -l 3 -c digits -m all --live-log log.txt 76 | ``` 77 | 78 | ### 扫描包含至少3个连续相同字母的5位域名 79 | ```bash 80 | python3 main.py -l 5 -c letters -m repeats --min-repeats 3 --live-log log.txt 81 | ``` 82 | 83 | ### 从字典和拼音两个来源扫描4位域名 84 | ```bash 85 | python3 main.py -l 4 -c alnum -m dict pinyin --pinyin-dict-file pinyin.txt --live-log log.txt 86 | ``` 87 | 88 | ### 使用详细模式并指定输出文件 89 | ```bash 90 | python3 main.py -l 3 -c alnum-hyphen -m all -v -o results.txt 91 | ``` 92 | 93 | ## 注意事项 94 | 95 | 1. 使用 `all` 方法生成较长域名时会产生大量组合,请谨慎设置参数 96 | 2. 设置合理的查询延迟(`--delay`)以避免触发频率限制 97 | 3. 结果会实时写入输出文件,可在扫描过程中查看 98 | 4. 对于拼音方法,需要提供拼音字典文件(每行一个拼音) 99 | 100 | ## 错误代码说明 101 | 102 | - 代码 1: 域名可以注册 103 | - 代码 0: 域名已被注册 104 | - 代码 -1: 无效查询 105 | - 代码 -95: 访问受限 (稍后重试) 106 | - 代码 -99: 服务器临时错误 (稍后重试) 107 | 108 | ## 许可 109 | 110 | MIT 许可证 111 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | li_domain_scanner.py 6 | 7 | 一个灵活的 .li 域名可用性扫描工具。 8 | 允许用户根据长度、字符集、生成方法(所有组合、字典、重复模式、拼音) 9 | 等条件组合来扫描域名。 10 | """ 11 | 12 | import socket 13 | import time 14 | import string 15 | import itertools 16 | import argparse 17 | import sys 18 | import re 19 | from typing import Generator, Dict, Any, Optional, Set, List, Iterable 20 | 21 | # --- 常量定义 --- 22 | LI_WHOIS_HOST = "whois.nic.ch" # .li 域名的 WHOIS 服务器地址 23 | LI_WHOIS_PORT = 4343 # .li 域名的 WHOIS 服务器端口 24 | SOCKET_TIMEOUT = 10 # Socket 连接和读取超时时间 (秒) 25 | DEFAULT_DELAY = 1.0 # 默认两次查询之间的延迟时间 (秒) 26 | MAX_RETRIES = 2 # 连接失败时的最大重试次数 27 | 28 | # 基础字符集 29 | LETTERS = string.ascii_lowercase 30 | DIGITS = string.digits 31 | ALNUM = LETTERS + DIGITS 32 | 33 | # 预编译正则表达式 34 | REPEAT_PATTERN_CACHE = {} # 重复模式正则表达式缓存 35 | 36 | # --- 核心 WHOIS 查询函数 --- 37 | 38 | def check_li_domain(domain_base: str, retry_count: int = 0) -> Dict[str, Any]: 39 | """ 40 | 检查单个 .li 域名是否可用。 41 | 42 | 通过连接到官方 WHOIS 服务器的特定端口来查询。 43 | 解析服务器返回的状态码,并提供详细的错误处理。 44 | 45 | Args: 46 | domain_base: 不包含 '.li' 的域名主体部分 (例如: 'example')。 47 | retry_count: 内部重试计数器。 48 | 49 | Returns: 50 | 一个包含查询结果的字典: 51 | { 52 | 'domain': 查询的完整域名 (例如: 'example.li'), 53 | 'status': 英文状态 ('available', 'unavailable', etc.), 54 | 'status_cn': 中文状态描述, 55 | 'raw_code': 原始整数状态码 (-99 到 1) 或 None, 56 | 'raw_response': 服务器原始响应或错误信息。 57 | } 58 | """ 59 | domain_full = f"{domain_base}.li" 60 | result = { 61 | 'domain': domain_full, 62 | 'status': 'unknown_error', 63 | 'status_cn': '未知错误', 64 | 'raw_code': None, 65 | 'raw_response': '' 66 | } 67 | 68 | sock = None 69 | try: 70 | # 建立 TCP 连接 71 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 72 | sock.settimeout(SOCKET_TIMEOUT) # 设置连接超时 73 | sock.connect((LI_WHOIS_HOST, LI_WHOIS_PORT)) 74 | 75 | # 发送查询 (UTF-8 编码, 以 \r\n 结尾) 76 | query = f"{domain_full}\r\n".encode('utf-8') 77 | sock.sendall(query) 78 | 79 | # 接收响应 80 | response_bytes = bytearray() 81 | while True: 82 | try: 83 | chunk = sock.recv(4096) 84 | if not chunk: 85 | break 86 | response_bytes.extend(chunk) 87 | 88 | # 如果响应包含完整内容可以提前退出 89 | if b'\n\n' in response_bytes or len(response_bytes) > 8192: 90 | break 91 | except socket.timeout: 92 | # 接收超时但已有数据,可能已完成 93 | if response_bytes: 94 | break 95 | raise 96 | 97 | # 解码响应 (UTF-8, 忽略错误) 并清理 98 | response_text = response_bytes.decode('utf-8', errors='ignore').strip() 99 | result['raw_response'] = response_text 100 | 101 | # --- 解析响应 --- 102 | if not response_text: 103 | result['status'] = 'network_error' 104 | result['status_cn'] = '网络错误 (空响应)' 105 | result['raw_response'] = "从服务器收到空的响应。" 106 | return result 107 | 108 | # 提取第一行并查找状态码 109 | first_line = response_text.split('\n', 1)[0].strip() 110 | if ':' not in first_line: 111 | result['status'] = 'unknown_error' 112 | result['status_cn'] = '未知错误 (无法解析代码)' 113 | result['raw_response'] += "\n错误: 无法解析响应代码 (未找到冒号)." 114 | return result 115 | 116 | code_str = first_line.split(':', 1)[0].strip() 117 | try: 118 | code = int(code_str) 119 | result['raw_code'] = code 120 | 121 | # 映射状态码到状态 122 | if code == 1: 123 | result['status'] = 'available' 124 | result['status_cn'] = '可以注册' 125 | elif code == 0: 126 | result['status'] = 'unavailable' 127 | result['status_cn'] = '已被注册' 128 | elif code == -1: 129 | result['status'] = 'invalid_query' 130 | result['status_cn'] = '无效查询' 131 | elif code == -95: 132 | result['status'] = 'rate_limited' 133 | result['status_cn'] = '访问受限 (稍后重试)' 134 | elif code == -99: 135 | result['status'] = 'server_error' 136 | result['status_cn'] = '服务器临时错误 (稍后重试)' 137 | else: 138 | result['status'] = 'unknown_error' 139 | result['status_cn'] = f'未知错误 (代码: {code})' 140 | 141 | except ValueError: 142 | result['status'] = 'unknown_error' 143 | result['status_cn'] = '未知错误 (代码解析失败)' 144 | result['raw_response'] += f"\n错误: 无法将响应代码 '{code_str}' 解析为整数。" 145 | 146 | # --- 异常处理 --- 147 | except socket.timeout: 148 | result['status'] = 'network_error' 149 | result['status_cn'] = '网络错误 (超时)' 150 | result['raw_response'] = f"连接或读取超时 ({SOCKET_TIMEOUT} 秒)." 151 | 152 | # 尝试重试 153 | if retry_count < MAX_RETRIES: 154 | time.sleep(1) # 重试前等待一秒 155 | return check_li_domain(domain_base, retry_count + 1) 156 | 157 | except (socket.gaierror, ConnectionRefusedError, OSError) as e: 158 | result['status'] = 'network_error' 159 | result['status_cn'] = '网络错误 (连接失败)' 160 | result['raw_response'] = f"连接到 {LI_WHOIS_HOST}:{LI_WHOIS_PORT} 时发生网络错误 - {e}" 161 | 162 | # 尝试重试 163 | if retry_count < MAX_RETRIES: 164 | time.sleep(1) # 重试前等待一秒 165 | return check_li_domain(domain_base, retry_count + 1) 166 | 167 | except Exception as e: 168 | # 捕获任何其他意外错误 169 | result['status'] = 'unknown_error' 170 | result['status_cn'] = '未知错误 (程序异常)' 171 | result['raw_response'] = f"发生意外错误: {e}" 172 | finally: 173 | # 确保 socket 被关闭 174 | if sock: 175 | try: 176 | sock.shutdown(socket.SHUT_RDWR) 177 | except Exception: 178 | pass 179 | finally: 180 | sock.close() 181 | 182 | return result 183 | 184 | # --- 域名规则和字符集处理 --- 185 | 186 | def get_charset(chars_arg: str) -> str: 187 | """根据命令行参数 (--chars) 返回实际的字符集字符串。""" 188 | if chars_arg == 'letters': return LETTERS 189 | elif chars_arg == 'digits': return DIGITS 190 | elif chars_arg == 'alnum': return ALNUM 191 | elif chars_arg == 'letters-hyphen': return LETTERS + '-' 192 | elif chars_arg == 'digits-hyphen': return DIGITS + '-' 193 | elif chars_arg == 'alnum-hyphen': return ALNUM + '-' 194 | else: 195 | # argparse 的 choices 应该阻止这种情况,但作为后备 196 | raise ValueError(f"内部错误:未知的字符集参数: {chars_arg}") 197 | 198 | def is_valid_domain_base(domain_base: str, allow_hyphen: bool) -> bool: 199 | """ 200 | 检查域名主体部分是否符合基本规则。 201 | - 非空 202 | - 不包含点 '.' 203 | - 如果允许连字符:不能在开头或结尾 204 | """ 205 | if not domain_base: return False 206 | if '.' in domain_base: return False 207 | if allow_hyphen and '-' in domain_base: 208 | if domain_base.startswith('-') or domain_base.endswith('-'): 209 | return False 210 | elif not allow_hyphen and '-' in domain_base: 211 | return False 212 | return True 213 | 214 | def contains_only_allowed_chars(text: str, allowed_chars: str) -> bool: 215 | """检查字符串是否只包含允许的字符集中的字符。""" 216 | # 使用集合提高查找效率 217 | allowed_set = set(allowed_chars) 218 | return all(char in allowed_set for char in text) 219 | 220 | def get_repeat_pattern(min_repeat_count: int): 221 | """获取预编译的重复模式正则表达式""" 222 | if min_repeat_count not in REPEAT_PATTERN_CACHE: 223 | pattern_str = r"([a-zA-Z0-9])\1{" + str(min_repeat_count - 1) + r",}" 224 | REPEAT_PATTERN_CACHE[min_repeat_count] = re.compile(pattern_str) 225 | return REPEAT_PATTERN_CACHE[min_repeat_count] 226 | 227 | def has_min_repeats(text: str, min_repeat_count: int) -> bool: 228 | """ 229 | 检查字符串是否包含至少 N 个连续相同的字母或数字。 230 | 使用缓存的正则表达式提高效率。 231 | """ 232 | if min_repeat_count < 2: return True # 小于2个不算重复 233 | pattern = get_repeat_pattern(min_repeat_count) 234 | return bool(pattern.search(text)) 235 | 236 | # --- 域名生成器 --- 237 | 238 | def generate_all_combinations(length: int, charset: str) -> Generator[str, None, None]: 239 | """生成器:生成指定长度和字符集的所有组合。""" 240 | if length <= 0: return 241 | for item in itertools.product(charset, repeat=length): 242 | yield "".join(item) 243 | 244 | def generate_from_file(filepath: str, length: int, allowed_charset: str) -> Generator[str, None, None]: 245 | """ 246 | 生成器:从文件读取单词/拼音,按长度和允许的字符集过滤。 247 | 内部使用 set 去除文件中可能存在的重复行。 248 | """ 249 | try: 250 | seen_in_file = set() # 用于文件内去重 251 | with open(filepath, 'r', encoding='utf-8', errors='ignore') as f: 252 | for line in f: 253 | word = line.strip().lower() 254 | # 检查长度、是否已见过、是否只包含允许字符 255 | if (len(word) == length and 256 | word not in seen_in_file and 257 | contains_only_allowed_chars(word, allowed_charset)): 258 | yield word 259 | seen_in_file.add(word) 260 | except FileNotFoundError: 261 | print(f"警告: 文件未找到 '{filepath}',将跳过此生成方法。", file=sys.stderr) 262 | except Exception as e: 263 | print(f"警告: 读取文件 '{filepath}' 时出错: {e},将跳过此生成方法。", file=sys.stderr) 264 | 265 | def generate_repeating_patterns(length: int, charset: str, min_repeats: int) -> Generator[str, None, None]: 266 | """ 267 | 生成器:生成所有组合,并筛选出包含指定最少重复字符的。 268 | """ 269 | if length <= 0 or min_repeats < 2: return 270 | 271 | # 预编译正则表达式 272 | pattern = get_repeat_pattern(min_repeats) 273 | 274 | # 遍历所有可能组合 275 | for item in itertools.product(charset, repeat=length): 276 | candidate = "".join(item) 277 | # 检查是否包含足够的重复 278 | if pattern.search(candidate): 279 | yield candidate 280 | 281 | # --- 主域名生成协调函数 --- 282 | 283 | def generate_domains( 284 | length: int, 285 | chars_arg: str, 286 | methods: List[str], 287 | min_repeats: Optional[int], 288 | dict_file: Optional[str], 289 | pinyin_dict_file: Optional[str] 290 | ) -> Generator[str, None, None]: 291 | """ 292 | 主生成器函数,根据用户参数协调不同的生成方法。 293 | 294 | 1. 获取实际使用的字符集。 295 | 2. 根据选择的 `methods` 准备相应的底层生成器列表。 296 | 3. 使用 `itertools.chain` 连接所有选中的生成器。 297 | 4. 迭代组合后的生成器,进行最终的域名格式校验。 298 | 5. 使用集合 `final_unique_domains` 去重,确保每个域名只 `yield` 一次。 299 | 300 | Args: 301 | (参数与 main 函数中解析的 args 对应) 302 | 303 | Yields: 304 | 唯一的、符合所有指定条件的域名主体字符串。 305 | """ 306 | # 步骤 1: 获取字符集和是否允许连字符 307 | actual_charset = get_charset(chars_arg) 308 | allow_hyphen = '-' in actual_charset 309 | 310 | # 步骤 2: 准备生成器列表 311 | generators_to_run: List[Iterable[str]] = [] 312 | 313 | if 'all' in methods: 314 | print(f" [生成器] 添加: 所有组合 (长度 {length}, 字符集 '{actual_charset}')") 315 | generators_to_run.append(generate_all_combinations(length, actual_charset)) 316 | 317 | if 'dict' in methods: 318 | if dict_file: 319 | print(f" [生成器] 添加: 字典文件 '{dict_file}' (长度 {length}, 字符集 '{actual_charset}')") 320 | generators_to_run.append(generate_from_file(dict_file, length, actual_charset)) 321 | else: 322 | # argparse 的校验应该阻止这种情况,但以防万一 323 | print("警告: 请求使用 'dict' 方法但未提供 --dict-file,已跳过。", file=sys.stderr) 324 | 325 | if 'pinyin' in methods: 326 | if pinyin_dict_file: 327 | print(f" [生成器] 添加: 拼音文件 '{pinyin_dict_file}' (长度 {length}, 字符集 '{actual_charset}')") 328 | generators_to_run.append(generate_from_file(pinyin_dict_file, length, actual_charset)) 329 | else: 330 | print("警告: 请求使用 'pinyin' 方法但未提供 --pinyin-dict-file,已跳过。", file=sys.stderr) 331 | 332 | if 'repeats' in methods: 333 | if min_repeats is not None and min_repeats >= 2: 334 | print(f" [生成器] 添加: 重复模式 (长度 {length}, 字符集 '{actual_charset}', 最少重复 {min_repeats})") 335 | generators_to_run.append(generate_repeating_patterns(length, actual_charset, min_repeats)) 336 | else: 337 | print(f"警告: 请求使用 'repeats' 方法但 --min-repeats ({min_repeats}) 无效,已跳过。", file=sys.stderr) 338 | 339 | # 步骤 3: 连接所有选择的生成器 340 | if not generators_to_run: 341 | print("警告: 没有有效的生成器被选中,扫描将不会产生任何域名。", file=sys.stderr) 342 | return # 返回一个空的生成器 343 | 344 | combined_generator = itertools.chain.from_iterable(generators_to_run) 345 | 346 | # 步骤 4 & 5: 迭代、过滤、去重并 Yield 347 | final_unique_domains: Set[str] = set() # 用于最终去重 348 | processed_count = 0 # 跟踪生成器产生的数量(用于调试/感知进度) 349 | 350 | print(" [生成器] 开始处理...") 351 | for domain_base in combined_generator: 352 | processed_count += 1 353 | # a. 检查基本域名格式 (包括连字符规则) 354 | if is_valid_domain_base(domain_base, allow_hyphen): 355 | # b. 检查是否已生成过 (去重) 356 | if domain_base not in final_unique_domains: 357 | final_unique_domains.add(domain_base) 358 | yield domain_base # 产生一个唯一的、有效的域名主体 359 | 360 | # 每处理10000个候选项打印一次进度 361 | if processed_count % 10000 == 0: 362 | print(f" [生成器] 已处理 {processed_count} 个候选项,找到 {len(final_unique_domains)} 个有效候选项...") 363 | 364 | print(f" [生成器] 处理完成,共找到 {len(final_unique_domains)} 个唯一的有效候选项。") 365 | 366 | 367 | # --- 主程序入口 --- 368 | 369 | def main(): 370 | """解析命令行参数并执行域名扫描。""" 371 | global MAX_RETRIES # 移到函数开头 372 | 373 | parser = argparse.ArgumentParser( 374 | description="灵活扫描 .li 域名可用性。", 375 | formatter_class=argparse.ArgumentDefaultsHelpFormatter # 在 help 中显示默认值 376 | ) 377 | 378 | # --- 定义命令行参数 --- 379 | parser.add_argument("-l", "--length", type=int, required=True, 380 | help="要扫描的域名主体的长度。") 381 | parser.add_argument("-c", "--chars", 382 | choices=['letters', 'digits', 'alnum', 383 | 'letters-hyphen', 'digits-hyphen', 'alnum-hyphen'], 384 | default='alnum', 385 | help="允许的字符集类型:\n" 386 | " letters: 仅小写字母 (a-z)\n" 387 | " digits: 仅数字 (0-9)\n" 388 | " alnum: 字母和数字\n" 389 | " letters-hyphen: 仅字母和连字符 '-'\n" 390 | " digits-hyphen: 仅数字和连字符 '-'\n" 391 | " alnum-hyphen: 字母、数字和连字符 '-' (自动应用连字符规则)") 392 | parser.add_argument("-m", "--methods", choices=['all', 'dict', 'repeats', 'pinyin'], 393 | nargs='+', # 允许选择一个或多个方法 394 | required=True, 395 | help="生成域名的方法 (可多选):\n" 396 | " all: 生成所选长度和字符集的所有可能组合。\n" 397 | " dict: 从字典文件中查找符合长度和字符集要求的单词。\n" 398 | " repeats: 生成包含连续重复字符(字母或数字)的组合。\n" 399 | " pinyin: 从拼音字典文件中查找符合条件的拼音。") 400 | parser.add_argument("--min-repeats", type=int, default=2, 401 | help="当方法包含 'repeats' 时,指定最少连续重复字符(字母或数字)的数量 (>=2)。") 402 | parser.add_argument("--dict-file", type=str, default="/usr/share/dict/words", 403 | help="当方法包含 'dict' 时,指定字典文件的路径。") 404 | parser.add_argument("--pinyin-dict-file", type=str, 405 | help="当方法包含 'pinyin' 时,指定拼音字典文件的路径 (例如,每行一个拼音,如 'taobao')。") 406 | parser.add_argument("--delay", type=float, default=DEFAULT_DELAY, 407 | help=f"每次 WHOIS 查询之间的延迟时间(秒)。默认: {DEFAULT_DELAY}") 408 | parser.add_argument("-o", "--output", type=str, 409 | help="可选:将找到的可用域名写入指定的文件路径。") 410 | parser.add_argument("-v", "--verbose", action="store_true", 411 | help="详细模式:打印每个正在检查的域名的状态。") 412 | parser.add_argument("--max-retries", type=int, default=MAX_RETRIES, 413 | help=f"连接失败时的最大重试次数。默认: {MAX_RETRIES}") 414 | 415 | parser.add_argument("--live-log", type=str, 416 | help="指定实时日志文件,可用域名会被立即写入此文件 (可在扫描过程中查看)") 417 | 418 | # --- 解析参数 --- 419 | args = parser.parse_args() 420 | 421 | # --- 参数依赖性校验 --- 422 | if 'repeats' in args.methods and (args.min_repeats is None or args.min_repeats < 2): 423 | parser.error("使用 'repeats' 方法时,--min-repeats 必须指定且 >= 2。") 424 | if 'dict' in args.methods and not args.dict_file: 425 | # 即使有默认值,也检查一下以防用户错误地设置为空字符串等 426 | parser.error("使用 'dict' 方法时,必须提供有效的 --dict-file。") 427 | if 'pinyin' in args.methods and not args.pinyin_dict_file: 428 | parser.error("使用 'pinyin' 方法时,必须通过 --pinyin-dict-file 指定文件。") 429 | if args.length <= 0: 430 | parser.error("--length 必须是正整数。") 431 | 432 | # 更新全局最大重试次数 433 | MAX_RETRIES = args.max_retries 434 | 435 | # --- 准备输出文件 (如果指定) --- 436 | output_file = None 437 | if args.output: 438 | try: 439 | output_file = open(args.output, 'w', encoding='utf-8') 440 | print(f"结果将写入文件: {args.output}") 441 | except IOError as e: 442 | print(f"错误: 无法打开输出文件 '{args.output}': {e}", file=sys.stderr) 443 | sys.exit(1) # 决定退出 444 | 445 | # 准备实时日志文件 (如果指定) 446 | live_log_file = None 447 | if args.live_log: 448 | try: 449 | live_log_file = open(args.live_log, 'w', encoding='utf-8') 450 | print(f"实时可用域名将写入: {args.live_log}") 451 | except IOError as e: 452 | print(f"警告: 无法打开实时日志文件 '{args.live_log}': {e}", file=sys.stderr) 453 | # 这里只是警告,不终止程序 454 | 455 | # --- 打印扫描配置 --- 456 | print("\n--- 扫描配置 ---") 457 | print(f"域名长度: {args.length}") 458 | actual_charset = get_charset(args.chars) # 获取实际字符集用于显示 459 | print(f"字符集类型: {args.chars} (实际使用: '{actual_charset}')") 460 | print(f"生成方法: {', '.join(args.methods)}") 461 | if 'repeats' in args.methods: print(f"最小重复数: {args.min_repeats}") 462 | if 'dict' in args.methods: print(f"字典文件: {args.dict_file}") 463 | if 'pinyin' in args.methods: print(f"拼音字典: {args.pinyin_dict_file}") 464 | if args.delay > 0: print(f"查询延迟: {args.delay} 秒") 465 | else: print("警告: 查询延迟为 0,极易触发频率限制!") 466 | print(f"最大重试次数: {MAX_RETRIES}") 467 | print("-" * 18) 468 | 469 | # --- 获取域名生成器 --- 470 | try: 471 | print("初始化域名生成器...") 472 | domain_generator = generate_domains( 473 | length=args.length, 474 | chars_arg=args.chars, 475 | methods=args.methods, 476 | min_repeats=args.min_repeats, 477 | dict_file=args.dict_file, 478 | pinyin_dict_file=args.pinyin_dict_file 479 | ) 480 | except ValueError as e: # 捕获 get_charset 可能的错误 481 | print(f"参数错误: {e}", file=sys.stderr) 482 | if output_file: output_file.close() # 关闭已打开的文件 483 | sys.exit(1) 484 | 485 | # --- 执行扫描循环 --- 486 | print("\n--- 开始 WHOIS 查询 ---") 487 | count = 0 488 | available_count = 0 489 | rate_limit_hits = 0 490 | error_hits = 0 # 记录其他错误次数 491 | start_time = time.time() 492 | 493 | try: 494 | for domain_base in domain_generator: 495 | count += 1 496 | # 调用核心 WHOIS 查询函数 497 | result = check_li_domain(domain_base) 498 | 499 | # --- 处理并打印结果 --- 500 | status = result['status'] 501 | status_cn = result['status_cn'] 502 | domain_full = result['domain'] 503 | 504 | # 详细模式 或 非"不可用"状态时,打印详细信息 505 | if args.verbose or status != 'unavailable': 506 | print(f"[{count}] 查询 {domain_full:<25} ... 状态: {status_cn}") 507 | # 对特定错误状态打印更详细的原始响应(截断) 508 | if status in ['rate_limited', 'server_error', 'network_error', 509 | 'unknown_error', 'invalid_query']: 510 | error_hits += 1 511 | raw_resp_preview = result['raw_response'].split('\n', 1)[0][:100] 512 | if len(result['raw_response']) > 100: raw_resp_preview += '...' 513 | print(f" 原始响应/错误: {raw_resp_preview}") 514 | 515 | # --- 特殊状态处理 --- 516 | if status == 'available': 517 | available_count += 1 518 | # 突出显示可用域名 519 | print(f" \033[92m-> 找到可用域名: {domain_full}\033[0m") # 绿色高亮 520 | # 写入结果文件 521 | if output_file: 522 | output_file.write(f"{domain_full}\n") 523 | output_file.flush() # 立即写入 524 | 525 | # 写入实时日志(带时间戳) 526 | if live_log_file: 527 | timestamp = time.strftime("%Y-%m-%d %H:%M:%S") 528 | live_log_file.write(f"[{timestamp}] {domain_full}\n") 529 | live_log_file.flush() # 确保实时写入磁盘 530 | 531 | elif status == 'rate_limited': 532 | rate_limit_hits += 1 533 | # 增加暂停时间 534 | pause_time = args.delay * 5 if args.delay > 0 else 5 535 | print(f" \033[93m-> 触发访问频率限制!暂停 {pause_time:.1f} 秒...\033[0m") # 黄色高亮 536 | time.sleep(pause_time) 537 | 538 | elif status == 'server_error': 539 | # 短暂暂停 540 | pause_time = args.delay * 2 if args.delay > 0 else 2 541 | print(f" \033[93m-> 服务器临时错误。暂停 {pause_time:.1f} 秒...\033[0m") 542 | time.sleep(pause_time) 543 | 544 | # --- 查询延迟 --- 545 | if args.delay > 0: 546 | # 在每次查询后(无论结果如何)都应用延迟 547 | time.sleep(args.delay) 548 | 549 | except KeyboardInterrupt: 550 | print("\n\033[91m扫描被用户中断 (Ctrl+C)。\033[0m") # 红色提示 551 | except Exception as e: 552 | print(f"\n\033[91m扫描过程中发生意外错误: {e}\033[0m") 553 | finally: 554 | # --- 打印总结信息 --- 555 | end_time = time.time() 556 | duration = end_time - start_time 557 | print("\n--- 扫描结果统计 ---") 558 | print(f"扫描配置:") 559 | print(f" 长度={args.length}, 字符集={args.chars}, 方法={','.join(args.methods)}") 560 | print("-" * 20) 561 | print(f"已检查域名总数: {count}") 562 | print(f"找到可用域名数量: \033[92m{available_count}\033[0m") # 绿色 563 | print(f"遇到频率限制次数: \033[93m{rate_limit_hits}\033[0m") # 黄色 564 | print(f"遇到其他错误次数: \033[91m{error_hits}\033[0m") # 红色 565 | print(f"总耗时: {duration:.2f} 秒") 566 | 567 | # 计算平均速度(避免除零错误) 568 | if count > 0 and duration > 0.01: # 避免耗时过短导致速度异常大 569 | speed = count / duration 570 | print(f"平均检查速度: {speed:.2f} 个域名/秒 (包含延迟)") 571 | elif count > 0: 572 | print(f"平均检查速度: N/A (耗时过短)") 573 | 574 | 575 | if output_file: 576 | print(f"可用域名已保存至: {args.output}") 577 | output_file.close() 578 | 579 | if live_log_file: 580 | print(f"实时日志已保存至: {args.live_log}") 581 | live_log_file.close() 582 | 583 | # 输出成功率信息 584 | if count > 0: 585 | success_rate = (count - error_hits - rate_limit_hits) / count * 100 586 | print(f"查询成功率: {success_rate:.2f}%") 587 | 588 | if __name__ == "__main__": 589 | main() 590 | --------------------------------------------------------------------------------