Python实战:构建双色球查询与随机生成工具,掌握网络请求与数据处理

发布时间:2026/7/29 7:04:42
Python实战:构建双色球查询与随机生成工具,掌握网络请求与数据处理 1. 项目概述一个实用工具的双重使命最近在整理自己的代码工具箱翻到了一个几年前写的“双色球彩票查询和随机号生成”脚本。当时写它纯粹是为了解决两个很实际的需求一是每次想查查历史开奖号码都得去各种彩票网站页面广告多、加载慢体验一言难尽二是偶尔和朋友闲聊时想随机生成几组号码玩玩又不想依赖那些不知底细的在线工具。于是就用Python搓了这么个小工具。没想到几年下来它成了我电脑里使用频率颇高的“摸鱼”神器之一也成了我向新手朋友介绍Python实用性的经典案例。这个工具的核心功能非常直白就是标题里的两件事查询和生成。查询指的是能方便地获取并展示历史开奖数据生成则是能按照双色球的规则随机产生符合规范的投注号码。你别看它功能简单但麻雀虽小五脏俱全里面涉及了Python里好几个非常核心且实用的知识点比如网络请求、数据解析、随机算法、数据持久化还有命令行交互设计。对于刚学完Python基础语法、想找个实际项目练手的朋友来说这是一个绝佳的“练级”小副本。它没有复杂的业务逻辑目标明确完成后成就感强还能立刻用起来。所以这篇文章我就把这个工具的完整实现思路、代码细节以及我在开发过程中踩过的坑、总结的技巧毫无保留地分享出来。无论你是想自己实现一个类似的工具还是单纯想学习这些Python技术的实战应用相信都能有所收获。我们不止于写出能跑的代码更要理解每行代码背后的“为什么”以及如何让它变得更健壮、更好用。2. 核心需求与设计思路拆解在动手写代码之前我们先得把需求掰开揉碎了看清楚。一个工具好不好用往往就取决于最初的设计思路是否清晰。2.1 功能需求细化首先“双色球查询”具体要查什么双色球每期开奖包含期号、开奖日期、红球号码6个从1-33中选、蓝球号码1个从1-16中选以及奖池金额、一等奖注数和中奖金额等信息。对于我们的工具核心是获取期号、日期和号码。查询方式可以设计为查询最新一期、查询指定期号、查询最近N期。其次“随机生成”必须严格遵守双色球规则生成一组6个不重复的红球1-33和1个蓝球1-16。此外实用性上可以扩展一次生成多组、是否排除近期已开奖号码虽然从数学上不影响概率但有些人心理上会觉得这样更“随机”。2.2 技术方案选型与考量明确了做什么接下来就是决定怎么做也就是技术选型。这里每一个选择都有背后的考量。1. 数据来源网络请求与解析历史开奖数据必须从网上获取。我们需要一个稳定、免费、易于解析的数据源。经过一番搜寻和测试我发现一些公共的彩票数据接口或特定的数据网站比较合适。这里就涉及到Python的requests库来发送HTTP请求。为什么不直接用urllib因为requests的API更加人性化代码更简洁对于处理HTTP连接、超时、重试等场景更友好是行业事实上的标准。拿到数据通常是JSON或HTML格式后需要解析。如果数据源返回的是结构清晰的JSON那直接用json库解析是最简单的。但很多网站的数据是嵌入在HTML中的这时就需要用到BeautifulSoup或lxml这样的HTML解析库。考虑到新手友好度和解析能力的平衡BeautifulSoup是更优的选择它的语法直观学习曲线平缓。2. 数据存储要不要本地缓存每次查询都去网上抓取不仅慢而且对数据源不友好容易因频繁请求被限制。因此引入一个轻量级的本地缓存机制是很有必要的。我们可以把查询到的数据保存到本地文件如JSON或CSV下次查询同一期数据时优先从本地读取。这又引出了两个问题存什么格式如何更新JSON格式非常适合存储这种结构化的数据Python原生支持读写人类也勉强可读。更新策略可以设计为如果本地有数据则使用同时在工具启动时可以尝试从网络获取最新一期数据与本地最新一期对比如果发现更新则提示用户或自动更新本地缓存。这既保证了速度又确保了数据的相对新鲜度。3. 随机算法如何保证“真随机”与合规生成随机号码核心是使用Python的random模块。但这里有个关键点random模块生成的是伪随机数对于彩票模拟这种场景完全足够。我们需要用到random.sample(population, k)函数它可以从一个序列中无放回地随机选取k个不重复的元素这正好用于生成6个不重复的红球。蓝球则直接用random.randint(1, 16)。注意这里必须强调这个工具生成的所有随机号码仅供娱乐、学习或模拟分析使用。彩票的本质是概率游戏任何号码的中奖概率在数学上都是完全均等的不存在“预测”或“提高中奖率”的算法。我们编写的只是一个规则模拟器。4. 用户交互命令行界面(CLI)的设计作为一个桌面小工具图形界面(GUI)并非必需一个清晰易用的命令行界面(CLI)反而更轻量、更快捷。Python的argparse库是构建CLI的标准工具它可以轻松定义命令、子命令、参数和帮助信息。我们可以将工具设计成类似这样使用python lottery_tool.py query --latest # 查询最新一期 python lottery_tool.py query --issue 2023001 # 查询指定期号 python lottery_tool.py generate --count 5 # 生成5组随机号 python lottery_tool.py generate --exclude-recent 10 # 生成号码并排除最近10期出现过的红球这样的设计功能一目了然也便于后续集成到脚本或其它工具中。3. 核心模块实现与代码精讲理清了思路我们就可以开始搭建项目的骨架并逐一实现核心模块了。我会采用“分模块”的构建方式这样代码结构清晰也便于理解和维护。3.1 项目结构与环境搭建首先创建一个项目目录比如double_color_ball。在里面我们初步规划几个核心文件double_color_ball/ ├── lottery_tool.py # 主程序入口CLI交互逻辑 ├── data_fetcher.py # 数据获取与解析模块 ├── number_generator.py # 随机号码生成模块 ├── cache_manager.py # 本地缓存管理模块 ├── config.py # 配置文件如数据源URL └── data/ # 目录用于存放本地缓存文件如history.json接下来是环境准备。你需要一个Python环境建议3.6以上。安装必要的库pip install requests beautifulsoup4如果速度慢可以使用国内镜像源例如pip install requests beautifulsoup4 -i https://pypi.tuna.tsinghua.edu.cn/simplerequests用于网络请求beautifulsoup4用于HTML解析。我们的项目就依赖这两个第三方库非常轻量。3.2 数据获取模块深度解析这是工具的“数据源头”必须保证其稳定性和健壮性。我们把它封装在data_fetcher.py中。第一步定义数据模型在开始抓取前我们先定义一个简单的类来表示一期开奖数据这会让后续的数据处理更加清晰。# data_fetcher.py class LotteryDraw: def __init__(self, issue, date, red_balls, blue_ball, prize_poolNone, first_prizeNone): self.issue issue # 期号如2023144 self.date date # 开奖日期如2023-12-14 self.red_balls red_balls # 红球列表如[1, 12, 23, 24, 30, 33] self.blue_ball blue_ball # 蓝球如16 self.prize_pool prize_pool # 奖池金额可选 self.first_prize first_prize # 一等奖信息可选 def __str__(self): # 提供一个友好的字符串表示便于打印 reds .join(f{num:02d} for num in self.red_balls) return f期号{self.issue} | 日期{self.date} | 红球{reds} | 蓝球{self.blue_ball:02d}这里用了f{num:02d}来格式化号码保证个位数前面补零显示如“01”这是彩票号码显示的常见格式。第二步实现网络请求与解析假设我们从一个固定的数据页面抓取。这里以示例为目的请注意实际开发中请务必寻找稳定、合法、公开的数据源并遵守其robots.txt协议和访问频率限制避免给对方服务器造成压力。# data_fetcher.py import requests from bs4 import BeautifulSoup import json from typing import List, Optional import time class DataFetcher: # 数据源URL示例需替换为真实可用的源 BASE_URL https://example.com/lottery/history # 请替换 def __init__(self): self.session requests.Session() # 设置一个合理的请求头模拟浏览器访问 self.session.headers.update({ User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 }) def fetch_latest(self) - Optional[LotteryDraw]: 获取最新一期开奖数据 try: # 增加超时和重试机制是生产环境必备 response self.session.get(self.BASE_URL, timeout10) response.raise_for_status() # 如果状态码不是200抛出HTTPError # 假设页面最新一期数据在某个id为latest-draw的div里 soup BeautifulSoup(response.text, html.parser) latest_div soup.find(div, idlatest-draw) # 这里需要根据实际网页结构编写具体的解析逻辑 # 以下是模拟解析过程 # issue latest_div.find(span, class_issue).text # reds [int(num) for num in latest_div.find(span, class_reds).text.split()] # blue int(latest_div.find(span, class_blue).text) # date latest_div.find(span, class_date).text # return LotteryDraw(issue, date, reds, blue) print(警告解析逻辑需要根据实际数据源实现) # 返回模拟数据供测试 return LotteryDraw(2023144, 2023-12-14, [6, 13, 18, 23, 28, 33], 16) except requests.exceptions.RequestException as e: print(f网络请求失败{e}) return None except (AttributeError, ValueError, KeyError) as e: print(f数据解析失败网页结构可能已更改{e}) return None def fetch_by_issue(self, issue: str) - Optional[LotteryDraw]: 根据指定期号查询数据 # 构造查询特定期号的URL例如 BASE_URL f?issue{issue} url f{self.BASE_URL}?issue{issue} try: response self.session.get(url, timeout10) response.raise_for_status() # ... 解析逻辑类似fetch_latest ... print(f正在查询期号 {issue}... (解析逻辑待实现)) return None except requests.exceptions.RequestException as e: print(f查询期号 {issue} 失败{e}) return None这里有几个关键点和避坑指南使用Sessionrequests.Session()可以复用TCP连接在需要多次请求时效率更高也能保持cookies等信息。设置User-Agent有些网站会屏蔽没有User-Agent的请求被认为是爬虫模拟一个常见的浏览器UA是基本操作。异常处理网络请求充满不确定性必须用try...except包裹。response.raise_for_status()能在HTTP错误码如404500时立即抛出异常便于我们捕获处理。超时设置timeout10意味着连接和读取超时均为10秒防止程序因网络问题无限挂起。解析的脆弱性基于HTML标签和属性的解析非常依赖网页结构。一旦网站改版解析代码很可能失效。这是此类工具最大的维护痛点。如果可能优先寻找提供JSON API的数据源。3.3 缓存管理模块的设计为了提升体验和减少网络请求缓存模块至关重要。我们设计一个CacheManager类来负责数据的本地读写。# cache_manager.py import json import os from typing import List, Dict, Any from .data_fetcher import LotteryDraw # 假设在同一包内 class CacheManager: CACHE_FILE os.path.join(data, history.json) def __init__(self): # 确保缓存目录存在 os.makedirs(os.path.dirname(self.CACHE_FILE), exist_okTrue) self._cache self._load_cache() def _load_cache(self) - Dict[str, Any]: 从文件加载缓存数据 if not os.path.exists(self.CACHE_FILE): return {draws: {}, latest_issue: None} try: with open(self.CACHE_FILE, r, encodingutf-8) as f: return json.load(f) except (json.JSONDecodeError, IOError) as e: print(f缓存文件损坏将创建新文件{e}) return {draws: {}, latest_issue: None} def _save_cache(self): 保存缓存数据到文件 try: with open(self.CACHE_FILE, w, encodingutf-8) as f: # indent参数让JSON文件更易读 json.dump(self._cache, f, ensure_asciiFalse, indent2, defaultself._serialize_draw) except IOError as e: print(f保存缓存失败{e}) staticmethod def _serialize_draw(obj): 自定义JSON序列化方法用于处理LotteryDraw对象 if isinstance(obj, LotteryDraw): return { issue: obj.issue, date: obj.date, red_balls: obj.red_balls, blue_ball: obj.blue_ball, prize_pool: obj.prize_pool, first_prize: obj.first_prize } raise TypeError(fObject of type {type(obj)} is not JSON serializable) def get_draw(self, issue: str) - Optional[LotteryDraw]: 根据期号从缓存获取数据 draw_data self._cache[draws].get(issue) if draw_data: # 将字典转换回LotteryDraw对象 return LotteryDraw(**draw_data) return None def save_draw(self, draw: LotteryDraw): 保存单期数据到缓存 self._cache[draws][draw.issue] draw # 更新最新期号 current_latest self._cache.get(latest_issue) if not current_latest or draw.issue current_latest: self._cache[latest_issue] draw.issue self._save_cache() def get_all_draws(self) - List[LotteryDraw]: 获取缓存中的所有数据按期号排序 draws [] for issue, data in self._cache[draws].items(): draws.append(LotteryDraw(**data)) # 按期号排序返回 draws.sort(keylambda x: x.issue) return draws def get_latest_cached_issue(self) - Optional[str]: 获取缓存中最新的期号 return self._cache.get(latest_issue)设计要点与心得数据结构缓存使用一个字典draws字段以期号为键存储每期数据latest_issue字段方便快速获取最新期号。这种结构查询效率高。错误处理加载缓存时文件不存在或内容损坏是常见情况代码必须能从容应对并初始化一个空的缓存结构。对象序列化LotteryDraw对象不能直接被json.dump序列化。我们通过default参数指定一个自定义函数_serialize_draw将对象转换为字典。对应的从缓存读取时再用字典参数重新构造对象。目录创建os.makedirs(..., exist_okTrue)能确保缓存目录存在避免因目录不存在而写入失败。3.4 随机号码生成器的实现这个模块相对独立和纯粹核心是正确使用random模块。# number_generator.py import random from typing import List, Tuple from .cache_manager import CacheManager # 为了获取历史数据 class NumberGenerator: RED_RANGE (1, 33) BLUE_RANGE (1, 16) RED_COUNT 6 BLUE_COUNT 1 def __init__(self, cache_manager: CacheManager None): self.cache_manager cache_manager # 初始化随机数生成器 # random.seed() # 通常不需要手动设置系统会以当前时间等作为种子 def generate_one_set(self) - Tuple[List[int], int]: 生成一组号码 reds random.sample(range(self.RED_RANGE[0], self.RED_RANGE[1] 1), self.RED_COUNT) reds.sort() # 彩票号码通常按升序排列显示 blue random.randint(self.BLUE_RANGE[0], self.BLUE_RANGE[1]) return reds, blue def generate_multiple_sets(self, count: int 5) - List[Tuple[List[int], int]]: 生成多组号码 if count 0: return [] return [self.generate_one_set() for _ in range(count)] def generate_excluding_recent(self, count: int 5, recent_issues: int 10) - List[Tuple[List[int], int]]: 生成多组号码并排除最近N期开奖中出现过的红球。 注意此功能仅为满足某种心理需求从数学概率上不影响结果。 if not self.cache_manager: print(未提供缓存管理器无法执行排除操作。) return self.generate_multiple_sets(count) # 1. 获取最近N期数据 all_draws self.cache_manager.get_all_draws() recent_draws all_draws[-recent_issues:] if len(all_draws) recent_issues else all_draws # 2. 提取所有出现过的红球号码 excluded_reds set() for draw in recent_draws: excluded_reds.update(draw.red_balls) # 3. 构建可选红球池从全部红球中排除近期出现过的 all_possible_reds list(range(self.RED_RANGE[0], self.RED_RANGE[1] 1)) available_reds [num for num in all_possible_reds if num not in excluded_reds] if len(available_reds) self.RED_COUNT: print(f警告最近{recent_issues}期已覆盖红球{len(excluded_reds)}个剩余可选号码不足{self.RED_COUNT}个将使用全部红球池。) available_reds all_possible_reds results [] for _ in range(count): reds random.sample(available_reds, self.RED_COUNT) reds.sort() blue random.randint(self.BLUE_RANGE[0], self.BLUE_RANGE[1]) results.append((reds, blue)) return results关键解析与技巧random.sample(population, k)这是生成不重复随机数的利器。population是一个序列如range(1, 34)k是抽取数量。它保证了抽取的k个元素各不相同完美符合红球规则。排序reds.sort()让生成的号码以升序显示这是标准的查看习惯。排除近期号码功能这个功能展示了如何将不同模块组合起来。它从缓存中读取历史数据构建一个“排除集合”然后从剩余号码中抽样。这里有一个边界情况处理如果近期开奖号码覆盖太多导致剩余可选红球不足6个则回退到使用全部红球池并给出明确警告。这是健壮性编程的体现。随机种子代码中注释掉了random.seed()。在测试阶段你可以设置一个固定的种子如random.seed(42)来让每次生成的“随机”序列相同便于调试。但在最终工具中应该让系统自动选择种子通常是基于时间以保证每次运行的随机性。4. 命令行界面整合与功能串联各个核心模块准备好后我们需要一个“指挥官”来把它们串联起来并提供一个友好的用户入口。这就是主程序lottery_tool.py的任务我们将使用argparse库。# lottery_tool.py #!/usr/bin/env python3 双色球彩票查询与随机号生成工具 import argparse import sys from data_fetcher import DataFetcher, LotteryDraw from cache_manager import CacheManager from number_generator import NumberGenerator def display_draw(draw: LotteryDraw): 美化打印一期开奖信息 if not draw: print(未找到相关开奖信息。) return print(*50) print(draw) if draw.prize_pool: print(f奖池{draw.prize_pool}) print(*50) def main(): # 初始化管理器 cache_mgr CacheManager() fetcher DataFetcher() generator NumberGenerator(cache_mgr) parser argparse.ArgumentParser(description双色球历史查询与随机号生成工具) subparsers parser.add_subparsers(destcommand, help可用命令) # 查询命令 query_parser subparsers.add_parser(query, help查询开奖信息) query_group query_parser.add_mutually_exclusive_group(requiredTrue) query_group.add_argument(--latest, actionstore_true, help查询最新一期) query_group.add_argument(--issue, typestr, help查询指定期号如2023144) query_group.add_argument(--recent, typeint, help查询最近N期, default5) # 生成命令 generate_parser subparsers.add_parser(generate, help生成随机号码) generate_parser.add_argument(-c, --count, typeint, default5, help生成组数默认5组) generate_parser.add_argument(--exclude-recent, typeint, metavarN, help生成时排除最近N期出现过的红球心理作用) args parser.parse_args() if not args.command: parser.print_help() sys.exit(1) if args.command query: if args.latest: print(正在获取最新开奖信息...) # 策略先查缓存最新再尝试网络更新 latest_issue_cached cache_mgr.get_latest_cached_issue() draw None if latest_issue_cached: draw cache_mgr.get_draw(latest_issue_cached) print(从缓存读取) display_draw(draw) print(尝试从网络更新...) latest_draw_online fetcher.fetch_latest() if latest_draw_online: if not draw or latest_draw_online.issue ! draw.issue: print(发现更新) cache_mgr.save_draw(latest_draw_online) display_draw(latest_draw_online) else: print(已是最新数据。) else: print(网络更新失败使用缓存数据。) elif args.issue: print(f正在查询期号 {args.issue} ...) # 先查缓存 draw cache_mgr.get_draw(args.issue) if draw: print(从缓存读取) display_draw(draw) else: # 缓存没有尝试从网络获取 draw fetcher.fetch_by_issue(args.issue) if draw: cache_mgr.save_draw(draw) display_draw(draw) else: print(f未找到期号为 {args.issue} 的开奖信息。) elif args.recent: n args.recent print(f查询最近 {n} 期开奖信息...) all_draws cache_mgr.get_all_draws() if not all_draws: print(缓存为空请先使用 query --latest 获取数据。) return recent_draws all_draws[-n:] if len(all_draws) n else all_draws for draw in recent_draws: display_draw(draw) elif args.command generate: count args.count if count 0 or count 100: # 做个简单限制 print(生成组数应在1-100之间。) return if args.exclude_recent: print(f生成 {count} 组号码排除最近{args.exclude_recent}期红球...) results generator.generate_excluding_recent(count, args.exclude_recent) else: print(f生成 {count} 组随机号码...) results generator.generate_multiple_sets(count) for i, (reds, blue) in enumerate(results, 1): red_str .join(f{num:02d} for num in reds) print(f第{i:2d}组红球 {red_str} | 蓝球 {blue:02d}) if __name__ __main__: main()CLI设计精讲子命令Subparsersargparse的add_subparsers让我们可以定义像git commit、git push这样的子命令结构使工具逻辑非常清晰。destcommand用于存储用户输入的子命令名。互斥组Mutually Exclusive Group在query子命令中--latest、--issue、--recent三个参数是互斥的一次只能用一个。add_mutually_exclusive_group(requiredTrue)确保了这一点并且requiredTrue意味着这个互斥组中必须提供一个参数。参数类型与默认值typeint确保输入被转换为整数default5提供了方便的默认值。清晰的帮助信息description和help参数让用户运行python lottery_tool.py -h或python lottery_tool.py generate -h时能获得清晰的指引。业务逻辑串联 在query --latest的实现中我采用了一个**“缓存优先网络更新”**的策略首先从缓存读出最新一期数据显示给用户快。同时尝试从网络获取最新数据可能慢。如果网络获取成功且期号更新则更新缓存并显示新数据。如果网络失败用户至少还能看到缓存的数据。 这种策略在桌面工具中非常实用兼顾了响应速度和数据新鲜度。5. 进阶优化与扩展思考一个基本可用的工具已经完成了。但作为一个有追求的开发者我们还可以让它更好。下面是一些进阶的优化方向和扩展思路。5.1 提升数据源的健壮性这是工具最脆弱的环节。除了之前提到的异常处理我们还可以多数据源备份不要只依赖一个网站。可以配置2-3个备用的数据源URL。当主源失败时按顺序尝试备用源。这能极大提高工具的可用性。更智能的解析对于HTML解析不要依赖单一的CSS选择器路径。可以尝试同时匹配多个可能的标签或类名或者使用更宽松的正则表达式来提取关键信息虽然正则解析HTML通常不推荐但在有限内容提取中可作为备用手段。设置请求间隔在循环抓取多期历史数据时务必在请求间添加time.sleep(1)之类的间隔避免触发网站的防爬机制。5.2 丰富查询与展示功能当前的查询展示比较基础可以增加更多实用功能号码出现频率统计遍历所有缓存的历史数据统计每个红球、蓝球历史上出现的次数并排序输出。这对于喜欢“数据分析”的用户是个有趣的功能。区间分布查询例如查询红球“1-11”这个区间小号区在最近100期里出现了多少次。连号查询查找历史中红球出现连号如12, 13的期数。数据导出将缓存的历史数据导出为Excel(.xlsx)或CSV文件方便用户用其他软件进行更复杂的分析。可以使用pandas库来轻松实现。5.3 生成算法的趣味性扩展除了基础的随机生成可以加入一些有趣的、基于简单统计的“策略”但务必注明其娱乐性质奇偶比/大小比生成允许用户指定生成号码的奇偶比例如4奇2偶或大小比例以17为界4大2小。算法上可以在生成随机组合后进行过滤直到满足条件。和值范围生成双色球红球和值6个红球相加历史范围大约在70-140之间。可以允许用户指定一个和值范围来生成号码。号码模式排除例如排除全奇、全偶、连号超过3个等极端形态的号码同样这纯属心理安慰。5.4 打包与分发为了让没有Python环境的朋友也能使用我们可以将脚本打包成独立的可执行文件.exe。使用PyInstaller这是最常用的工具。pip install pyinstaller pyinstaller --onefile --name双色球工具 lottery_tool.py--onefile参数将所有依赖打包成一个exe文件。打包后在dist目录下就能找到可执行文件。注意事项打包时如果代码中有相对路径如data/history.json在exe运行环境下可能会找不到。需要使用sys._MEIPASSPyInstaller创建的临时目录或确保路径正确。一个更健壮的方法是使用os.path.join(os.path.dirname(__file__), data, history.json)来定位文件但在打包时需通过--add-data参数将数据文件夹一起打包。6. 常见问题与实战调试记录在开发和后续使用中你肯定会遇到各种各样的问题。这里我记录了几个典型的问题和解决方法希望能帮你快速排雷。6.1 网络请求相关问题1请求数据时收到HTTP 403 Forbidden错误。原因这是最常见的反爬措施。网站检测到你的请求头不像浏览器或者来自同一个IP的请求过于频繁。解决完善请求头除了User-Agent有时还需要添加Referer、Accept-Language等。用浏览器开发者工具F12查看一次正常请求的Headers并模仿设置。使用Session并维持Cookies。最重要的降低请求频率在批量抓取时务必添加延时如time.sleep(random.uniform(1, 3))。考虑使用requests的proxies参数设置代理需自行寻找可靠代理源但这会引入复杂性和成本。问题2解析数据时BeautifulSoup找不到对应的标签返回None。原因网页结构改变了或者你的选择器写错了。也可能数据是通过JavaScript动态加载的初始HTML里没有。解决再次检查选择器用浏览器检查元素确认标签和类名是否正确。注意有些类名可能是动态生成的。打印响应内容将response.text的一部分保存到文件或者直接打印出来看看你要找的数据是否在HTML中。如果不在很可能是JS加载。处理JS渲染页面对于JS加载的数据简单的requestsBeautifulSoup就无能为力了。这时需要用到Selenium或Playwright这样的浏览器自动化工具来模拟浏览器获取完整页面但会重很多。如果网站有移动端页面或API接口通常是更好的选择。6.2 数据处理与缓存问题3JSON解码错误json.decoder.JSONDecodeError。原因缓存文件history.json可能被意外修改、损坏或者写入过程中程序被中断导致文件内容不是有效的JSON格式。解决在_load_cache方法中我们已经用try...except捕获了这个异常并返回一个空的缓存结构。这是一种“自我修复”机制。更严谨的做法是在捕获异常后可以将损坏的文件重命名备份如history.json.corrupted然后新建一个空文件并提示用户。问题4生成的随机号码有时红球会重复。原因绝对不应该发生如果发生了说明你没有使用random.sample()而是错误地使用了random.randint()或random.choice()在循环中重复选取。解决严格使用random.sample(range(1, 34), 6)来生成红球。这是唯一能保证不重复且等概率的方法。6.3 用户交互与打包问题5在命令行中中文显示为乱码。原因Windows命令行cmd默认编码是GBK而Python 3默认字符串是UTF-8。解决一种方法是改变cmd的代码页在命令行先执行chcp 65001切换到UTF-8编码。但这不是一劳永逸的。更好的方法是在代码中处理。对于输出可以尝试将字符串编码后再打印但这比较麻烦。更推荐的方法是如果你的用户主要是中文Windows环境可以考虑在打印时使用GBK编码或者引导用户使用支持UTF-8的终端如Windows Terminal、PowerShell 7。问题6使用PyInstaller打包后程序找不到数据文件如history.json。原因打包成单文件exe后脚本的运行时路径__file__是一个临时解压目录你的相对路径data/history.json自然不存在。解决需要使用PyInstaller提供的运行时钩子来定位资源。一个更简单通用的方法是import sys import os def get_resource_path(relative_path): 获取资源的绝对路径。在开发环境和打包后均有效 if hasattr(sys, _MEIPASS): # PyInstaller创建的临时文件夹 base_path sys._MEIPASS else: base_path os.path.abspath(.) return os.path.join(base_path, relative_path) # 使用方式 CACHE_FILE get_resource_path(os.path.join(data, history.json))并且在打包命令中指定附加文件pyinstaller --onefile --add-data data;data lottery_tool.py这样data文件夹及其内容会被打包进exe并在运行时解压到临时目录供程序访问。这个小工具从构思到实现再到不断打磨整个过程本身就是一个非常棒的Python学习项目。它覆盖了从网络爬虫、数据解析、本地存储、随机算法到命令行交互的完整链条。最重要的是它解决了一个真实存在的小需求。编程的乐趣很多时候就来自于这种用代码将想法变为现实并切实改善一点生活或工作效率的瞬间。希望这个详细的拆解不仅能让你做出这个工具更能理解其中每一步的设计权衡和代码背后的逻辑。