农产品价格报告爬虫使用说明
农产品价格报告爬虫使用说明
# **************************************************************************
# * *
# * 农产品价格报告爬虫 *
# * *
# * 作者: xiaohai *
# * 版本: v1.0.0 *
# * 日期: 2024-12-05 *
# * *
# * 功能说明: *
# * 1. 日度报告 *
# * - 生成今日分析报告 *
# * - 生成指定日期报告 *
# * - 包含价格指数、分品类分析等 *
# * *
# * 2. 周度报告 *
# * - 生成本周分析报告 *
# * - 生成指定周报告 *
# * - 汇总周内价格变化 *
# * *
# * 3. 价格走势 *
# * - 农产品价格200指数走势 *
# * - 猪肉价格全国走势 *
# * - 猪肉价格区域走势 *
# * - 粮油价格指数走势 *
# * *
# * 4. 数据导出 *
# * - 支持Excel格式导出 *
# * - 包含多个数据分类 *
# * - 支持时间范围选择 *
# * *
# * : 农业农村部市场信息中心 *
# * 版权声明: 仅用于学习交流 *
# * *
# **************************************************************************import os
import json
import logging
import requests
from datetime import datetime, timedelta
import matplotlib.pyplot as plt
import pandas as pd
import warnings
import urllib3
import sys
import subprocess
import pkg_resources
from bs4 import BeautifulSoup
import re
import time# 禁用SSL警告
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
warnings.filterwarnings('ignore')# 配置常量
VERSION = 'v1.0.0'
AUTHOR = 'xiaohai'
DATA_SOURCE = '农业农村部市场信息中心'# API配置
API_BASE_URL = 'https://ncpscxx.moa.gov.cn'
API_ENDPOINTS = {'price_index': '/product/common-price-index/getIndexList','variety_list': '/product/sys-variety/selectList','price_trend': '/product/price-info/getPriceInfoList','market_list': '/product/sys-market/selectList','daily_price': '/product/price-info/getDailyPrice','analysis_report': '/product/analysis-report/pageList'
}# 输出目录配置
OUTPUT_DIRS = {'base': 'reports','daily': 'reports/daily','weekly': 'reports/weekly'
}# 图表样式配置
CHART_STYLE = {'figure': {'figsize': (12, 6),'facecolor': '#f8fcfa'},'grid': {'linestyle': '--','alpha': 0.3,'color': 'gray'},'line': {'marker': 'o','markersize': 4,'linewidth': 2},'colors': {'blue': '#40a9ff','green': '#73d13d','orange': '#ffa940','red': '#ff4d4f','purple': '#9254de','cyan': '#36cfc9'}
}def check_and_install_packages():"""检查并安装所需的包"""required_packages = {'requests': 'requests', # HTTP请求'pandas': 'pandas', # 数据处理'matplotlib': 'matplotlib', # 绘图支持'urllib3': 'urllib3', # HTTP客户端'openpyxl': 'openpyxl', # Excel支持'colorama': 'colorama' # 控制台颜色}print("\n" + "="*50)print("检查并安装依赖包...")print("="*50)try:import coloramacolorama.init()success_mark = colorama.Fore.GREEN + "✓" + colorama.Style.RESET_ALLerror_mark = colorama.Fore.RED + "✗" + colorama.Style.RESET_ALLexcept ImportError:success_mark = "✓"error_mark = "✗"all_success = Truefor package, import_name in required_packages.items():try:pkg_resources.require(package)print(f"{success_mark} {package:15} 已安装")except (pkg_resources.DistributionNotFound, pkg_resources.VersionConflict):print(f"{error_mark} {package:15} 未安装,正在安装...")try:subprocess.check_call([sys.executable, "-m", "pip", "install", "--disable-pip-version-check","--no-cache-dir",package], stdout=subprocess.DEVNULL)print(f"{success_mark} {package:15} 安装成功")except Exception as e:print(f"{error_mark} {package:15} 安装失败: {str(e)}")all_success = Falseprint("\n依赖包检查" + ("全部完成" if all_success else "存在问题"))print("="*50 + "\n")if not all_success:print("某些依赖包安装失败,程序能无法正常运行!")if input("是否继续运行?(y/n): ").lower() != 'y':sys.exit(1)class ReportCrawler:"""农产品价格报告爬虫"""def __init__(self):# 禁用SSL警告warnings.filterwarnings('ignore')# 基础配置self._setup_directories()self._setup_logger()self._setup_api()def _setup_directories(self):"""创建输出目录"""self.output_dir = "reports"self.daily_dir = os.path.join(self.output_dir, "daily")self.weekly_dir = os.path.join(self.output_dir, "weekly")for d in [self.output_dir, self.daily_dir, self.weekly_dir]:if not os.path.exists(d):os.makedirs(d)def _setup_logger(self):"""配置日志"""log_file = os.path.join("logs", f"crawler_{datetime.now().strftime('%Y%m%d')}.log")os.makedirs("logs", exist_ok=True)formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s',datefmt='%Y-%m-%d %H:%M:%S')# 文件处理器file_handler = logging.FileHandler(log_file, encoding='utf-8')file_handler.setFormatter(formatter)# 制台处理器console_handler = logging.StreamHandler()console_handler.setFormatter(formatter)# 配置日志器self.logger = logging.getLogger(__name__)self.logger.setLevel(logging.INFO)self.logger.addHandler(file_handler)self.logger.addHandler(console_handler)def _setup_api(self):"""配置API"""self.headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36','Origin': 'https://ncpscxx.moa.gov.cn','Referer': 'https://ncpscxx.moa.gov.cn/','Accept': 'application/json, text/plain, */*','Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8','Content-Type': 'application/json;charset=UTF-8'}def show_menu(self):"""显示功能菜单"""menu = """
农产品价格报告爬虫系统
====================
1. 成今日分析报告
2. 生成本周分报告
3. 生成指定日期报告
4. 生成指定周报告
5. 生成价格指数走势图
6. 生成猪肉价格走势图
7. 生成区域价格走势图
8. 生成粮油价格走势图
9. 导出Excel数据
0. 退出系统请输入���能编号(0-9): """print("\n" + "="*50) # 添加分隔线choice = input(menu)print("="*50 + "\n") # 添加分隔线return choicedef run(self):"""运行系统"""while True:choice = self.show_menu()if choice == "0":print("感谢使用,再见!")breakelif choice == "1":print("正在生成今日分析报告...")self.generate_daily_report(datetime.now())elif choice == "2":print("正在生成本周分析报告...")today = datetime.now()self.generate_weekly_report(today.year, int(today.strftime("%W")))elif choice == "3":date_str = input("请输入日期(格式:YYYY-MM-DD): ")try:date = datetime.strptime(date_str, "%Y-%m-%d")self.generate_daily_report(date)except:print("日期格式错误!")elif choice == "4":year = int(input("请输入年份: "))week = int(input("请输入周数(1-52): "))self.generate_weekly_report(year, week)elif choice == "5":days = int(input("请输入要查看的天数: "))end = datetime.now()start = end - timedelta(days=days)self.plot_index_trend(start, end)elif choice == "6":days = int(input("请输入要查看的天数: "))end = datetime.now()start = end - timedelta(days=days)self.plot_pig_price_trend(start, end)elif choice == "7":days = int(input("请输入要查看的天数: "))end = datetime.now()start = end - timedelta(days=days)self.plot_pig_price_region_trend(start, end)elif choice == "8":days = int(input("请输入要查看的天数: "))end = datetime.now()start = end - timedelta(days=days)self.plot_grain_price_trend(start, end)elif choice == "9":days = int(input("请输入要导天数: "))end = datetime.now()start = end - timedelta(days=days)self.export_data(start, end)else:print("无效的选择,请重试!")input("\n按回车键继续...")def _make_request(self, url, method='get', params=None, data=None):"""发送HTTP请求Args:url: 请求URLmethod: 请求方法,支持 'get'/'post'params: URL参数data: POST数据Returns:Response对象或None(请求失败)"""try:headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'}if method.lower() == 'get':response = requests.get(url,params=params,headers=headers,verify=False,timeout=10)else:response = requests.post(url,params=params,json=data, # 添加json参数支持headers=headers,verify=False,timeout=10)response.raise_for_status()return responseexcept requests.exceptions.RequestException as e:self.logger.error(f"请求失败: {str(e)}")return Nonedef fetch_daily_report(self, date):"""获取日度价格报告"""try:url = f"{API_BASE_URL}/api/FarmDaily/list"data = {"daylyDate": date.strftime("%Y-%m-%d")}response = self._make_request(url, method='post', data=data)if not response:return Nonedata = response.json()if data.get("code") == 200 and data.get("content",{}).get("list"):# 找到指定日期的报告target_date = date.strftime("%Y-%m-%d")for report in data["content"]["list"]:if report["daylyDate"].startswith(target_date):# 提取所需数据return {"conclusion": report["counclesion"],"indexConclusion": report["indexConclusion"],"animalConclusion": report["animalConclusion"],"aquaticConclusion": report["aquaticConclusion"],"vegetablesConclusion": report["vegetablesConclusion"],"fruitsConclusion": report["fruitsConclusion"],"content": report["countent"],"incOrReduRange": report["incOrReduRange"]}self.logger.warning(f"未找到{target_date}的报告")return Noneself.logger.warning(f"获取数据失败: {data.get('message', '未知错误')}")return Noneexcept Exception as e:self.logger.error(f"获取日度报告出错: {str(e)}")return Nonedef _extract_conclusions(self, report):"""从报告中提取各类结论"""try:return {"index": report.get("indexConclusion", ""),"animal": report.get("animalConclusion", ""),"aquatic": report.get("aquaticConclusion", ""),"vegetables": report.get("vegetablesConclusion", ""),"fruits": report.get("fruitsConclusion", ""),"range": report.get("incOrReduRange", "")}except Exception as e:self.logger.error(f"提取论出错: {str(e)}")return {}def fetch_index_data(self, start_date, end_date):"""获取价格指数数据"""try:url = "https://pfsc.agri.cn/price_portal/pi-info-day/getPortalPiInfoDay"response = requests.post(url, headers=self.headers, verify=False)data = response.json()if data["code"] == 200:result = []for item in data["content"]:pub_date = datetime.strptime(item["publishDate"], "%Y-%m-%d")if start_date <= pub_date <= end_date:result.append({"日期": item["publishDate"],"农产品批发价格200指数": item["agriculture"],"粮油指数": item["grainAndOil"],"篮子数": item["vegetableBasket"]})return resultreturn Noneexcept Exception as e:self.logger.error(f"获取指数数据失败: {str(e)}")return Nonedef fetch_pig_price_data(self, start_date, end_date):"""获取猪肉价格数据"""try:url = f"{API_BASE_URL}{API_ENDPOINTS['variety_list']}"params = {'pid': 'MH'} # 猪肉品类IDresponse = self._make_request(url, method='post', params=params)if not response:return Nonedata = response.json()if data.get("code") == 200 and data.get("data"):# 转换数据格式result = []for item in data["data"]:if start_date <= datetime.strptime(item["date"], "%Y-%m-%d") <= end_date:result.append({"日期": item["date"],"全国": float(item["national"]),"东北": float(item["northEast"]),"华北": float(item["northChina"]),"华东": float(item["eastChina"]),"华中": float(item["centralChina"]),"华南": float(item["southChina"]),"西南": float(item["southWest"])})return resultself.logger.warning(f"获取数据失败: {data.get('message', '未知错误')}")return Noneexcept Exception as e:self.logger.error(f"获取猪肉价格数据失败: {str(e)}")return Nonedef fetch_grain_price_data(self, start_date, end_date):"""获取粮油价格数据"""try:url = f"{API_BASE_URL}{API_ENDPOINTS['variety_list']}"params = {'pid': 'TL'} # 粮油品类IDresponse = self._make_request(url, method='post', params=params)if not response:return Nonedata = response.json()if data.get("code") == 200 and data.get("data"):# 转换数据格式result = []for item in data["data"]:if start_date <= datetime.strptime(item["date"], "%Y-%m-%d") <= end_date:result.append({"日期": item["date"],"通义粮价指数": float(item["grainPriceIndex"]),"通义粮市指数": float(item["grainMarketIndex"]),"通义粮市第1号": float(item["grainMarketNo1"]),"通义粮天指数": float(item["grainDayIndex"]),"通义���指": float(item["grainIndex"]),"通义粮天指数(干粮)": float(item["grainDayDryIndex"])})return resultself.logger.warning(f"获取数据失败: {data.get('message', '未知错误')}")return Noneexcept Exception as e:self.logger.error(f"获取粮油价格数据失败: {str(e)}")return Nonedef generate_daily_report(self, date):"""生成每日分析报告"""try:report_data = self.fetch_daily_report(date)if not report_data:self.logger.warning(f"未获取到 {date.strftime('%Y-%m-%d')} 的报告数据")returnreport_file = os.path.join(self.daily_dir,f"{date.strftime('%Y年%m月%d日')}_价格分析报告.md")# 使用更清晰模板格式content = f"""# {date.strftime('%Y年%m月%d日')} 农产品价格分析报告## 一、价格指数变化
{report_data["indexConclusion"]}## 二、分品类分析### 1. 畜禽产品
{report_data["animalConclusion"]}### 2. 水产品
{report_data["aquaticConclusion"]}### 3. 蔬菜
{report_data["vegetablesConclusion"]}### 4. 水果
{report_data["fruitsConclusion"]}## 三、价格波动情况
{report_data["incOrReduRange"]}## 四、数据说明
- 数据来源: {report_data["source"]}
- 生成时间: {datetime.now().strftime('%Y年%m月%d日 %H:%M:%S')}
- 价格单位: 元/斤
- 涨跌幅: 与上一交易日相比---
*注: 本报告由系统自动生成,仅供参考。*
"""with open(report_file, "w", encoding="utf-8") as f:f.write(content)self.logger.info(f"分析报告已生成: {report_file}")except Exception as e:self.logger.error(f"生成分析报告失败: {str(e)}")def generate_weekly_report(self, year, week):"""生成周度汇总报告"""try:start_date = datetime.strptime(f'{year}-W{week:02d}-1', '%Y-W%W-%w')end_date = start_date + timedelta(days=6)print(f"\n正在生成第{week}周报告...")print(f"时间范围: {start_date.strftime('%Y-%m-%d')} 至 {end_date.strftime('%Y-%m-%d')}")print("="*50)# 获周内所有报告reports = []current = start_datetotal_days = (end_date - start_date).days + 1for i in range(total_days):print(f"\r进度: {i+1}/{total_days} ", end="")report = self.fetch_daily_report(current)if report:reports.append(report)current += timedelta(days=1)if not reports:self.logger.warning("本周无可用数据")return# 计算周度汇总数据weekly_summary = self._calculate_weekly_summary(reports)report_file = os.path.join(self.weekly_dir,f"{year}年第{week:02d}周_{start_date.strftime('%m月%d日')}-{end_date.strftime('%m月%d日')}_价格分析报告.md")with open(report_file, "w", encoding="utf-8") as f:f.write(f"""# {year}年第{week:02d}周农产品价格分析报告
({start_date.strftime('%Y年%m月%d日')} 至 {end_date.strftime('%Y年%m月%d日')})## 一、本周价格概况
{weekly_summary['overview']}## 二、价格指数变化
- 周初: {weekly_summary['index_start']}
- 周末: {weekly_summary['index_end']}
- 度变化: {weekly_summary['index_change']}## 三、分品类周度分析
### 1. 畜禽产品
{weekly_summary['animal_summary']}### 2. 水产品
{weekly_summary['aquatic_summary']}### 3. 蔬菜
{weekly_summary['vegetables_summary']}### 4. 水果
{weekly_summary['fruits_summary']}## 四、日度价格详情
""")for report in reports:pub_date = datetime.strptime(report['daylyDate'][:10], '%Y-%m-%d')f.write(f"""### {pub_date.strftime('%Y年%m月%d日')}
1. 价格指数: {report.get('indexConclusion', '暂无数据')}
2. 畜禽产品: {report.get('animalConclusion', '暂无数据')}
3. 水产品: {report.get('aquaticConclusion', '暂无数据')}
4. 蔬菜: {report.get('vegetablesConclusion', '暂无数据')}
5. 水果: {report.get('fruitsConclusion', '暂无数据')}""")f.write(f"""## 五、数据说明
- 数据来源: {reports[0]["source"]}
- 生成时间: {datetime.now().strftime('%Y年%m月%d日 %H:%M:%S')}
- 价格单位: 元/公斤
- 跌幅: 与上期相比---
*注: 本报告由系统自动生成,仅供参考。*""")print("\n报告生成完成!")self.logger.info(f"周度报告已生成: {report_file}")except Exception as e:self.logger.error(f"生成周度报告失败: {str(e)}")def _calculate_weekly_summary(self, reports):"""计算周度汇总数据"""summary = {'overview': '','index_start': reports[0].get('indexConclusion', '暂无数据'),'index_end': reports[-1].get('indexConclusion', '暂无数据'),'index_change': '','animal_summary': '','aquatic_summary': '','vegetables_summary': '','fruits_summary': ''}# 计算价格指数变化try:start_index = float(reports[0]['indexConclusion'].split('为')[1].split(',')[0])end_index = float(reports[-1]['indexConclusion'].split('为')[1].split(',')[0])change = end_index - start_indexsummary['index_change'] = f"{'上升' if change >= 0 else '下降'}{abs(change):.2f}个点"except:summary['index_change'] = '数据异常'# 生成概述summary['overview'] = f"本周农产品批发价格200指数从{summary['index_start']},到{summary['index_end']},整体{summary['index_change']}。"# 其他品类汇总...return summarydef plot_index_trend(self, start_date, end_date):"""绘制价格指数走势图"""try:data = self.fetch_index_data(start_date, end_date)if not data:returnplt.figure(figsize=(12, 6), facecolor='#f8fcfa')ax = plt.gca()ax.set_facecolor('#f8fcfa')dates = [item["日期"] for item in data]indices = [("农品批发价格200指数", "#ffa940"),("菜篮子指", "#73d13d"),("粮油指数", "#40a9ff")]for name, color in indices:values = [item[name] for item in data]plt.plot(dates, values, color=color, marker='o',markersize=4, linewidth=2, label=name)plt.title('农业农村部"农产品批发价格200指数"日度走势图',pad=20, fontsize=12, loc='left')plt.grid(True, linestyle='--', alpha=0.3)plt.xticks(rotation=45)plt.legend(loc='upper right', frameon=False)plt.tight_layout()plt.savefig(os.path.join(self.output_dir, "价格指数走势图.png"),dpi=300,bbox_inches='tight')plt.close()self.logger.info("价格指数走势已生成")except Exception as e:self.logger.error(f"生成价格指数走势图失败: {str(e)}")def plot_pig_price_trend(self, start_date, end_date):"""绘制猪肉价格走势图"""try:data = self.fetch_pig_price_data(start_date, end_date)if not data:returnplt.figure(figsize=(12, 6), facecolor='#f8fcfa')ax = plt.gca()ax.set_facecolor('#f8fcfa')dates = [item["日期"] for item in data]values = [item["全国"] for item in data]plt.plot(dates, values, color='#40a9ff', marker='o',markersize=4, linewidth=2)plt.fill_between(dates, values, color='#e6f7ff', alpha=0.5)plt.title('"瘦肉型白条猪肉出厂价格指数"全国走势图',pad=20, fontsize=12, loc='left')plt.grid(True, linestyle='--', alpha=0.3)plt.xticks(rotation=45)plt.tight_layout()plt.savefig(os.path.join(self.output_dir, "猪肉价格走势图.png"),dpi=300,bbox_inches='tight')plt.close()self.logger.info("猪肉价格走势图已生成")except Exception as e:self.logger.error(f"生成猪肉价格走势图失败: {str(e)}")def plot_pig_price_region_trend(self, start_date, end_date):"""绘制猪肉分区域价格走势图"""try:data = self.fetch_pig_price_data(start_date, end_date)if not data:returnplt.figure(figsize=(12, 6), facecolor='#f8fcfa')ax = plt.gca()ax.set_facecolor('#f8fcfa')dates = [item["日期"] for item in data]regions = [("东北", "#40a9ff"),("华南", "#73d13d"),("华", "#ffa940"),("华中", "#ff4d4f"),("华东", "#9254de"),("西南", "#36cfc9")]for region, color in regions:values = [item[region] for item in data]plt.plot(dates, values, color=color, marker='o',markersize=4, linewidth=2, label=region)plt.title('"瘦肉型条猪肉出厂价格指数"区域走势图',pad=20, fontsize=12, loc='left')plt.grid(True, linestyle='--', alpha=0.3)plt.xticks(rotation=45)plt.legend(loc='upper right', frameon=False)plt.tight_layout()plt.savefig(os.path.join(self.output_dir, "猪肉价格区域走势图.png"),dpi=300,bbox_inches='tight')plt.close()self.logger.info("猪肉价格区域走势图已生成")except Exception as e:self.logger.error(f"生成猪肉价格区域走势图失败: {str(e)}")def plot_grain_price_trend(self, start_date, end_date):"""绘制粮油价格走势图"""try:data = self.fetch_grain_price_data(start_date, end_date)if not data:returnplt.figure(figsize=(12, 6), facecolor='#f8fcfa')ax = plt.gca()ax.set_facecolor('#f8fcfa')dates = [item["日期"] for item in data]indices = [("通义粮价指数", "#40a9ff"),("通义粮市指数", "#73d13d"),("通义粮市第1号", "#ffa940"),("通义粮天指数", "#ff4d4f"),("通义粮指", "#9254de"),("通义粮天指数(干粮)", "#36cfc9")]for name, color in indices:values = [item[name] for item in data]plt.plot(dates, values, color=color, marker='o',markersize=4, linewidth=2, label=name)plt.title('中国通义粮油发价格指数走势图',pad=20, fontsize=12, loc='left')plt.grid(True, linestyle='--', alpha=0.3)plt.xticks(rotation=45)plt.legend(loc='upper right', frameon=False)plt.tight_layout()plt.savefig(os.path.join(self.output_dir, "粮油价格指数走势图.png"),dpi=300,bbox_inches='tight')plt.close()self.logger.info("粮油价格指数走势图已生成")except Exception as e:self.logger.error(f"生成粮油价格指数走势失败: {str(e)}")def export_data(self, start_date, end_date, format='excel'):"""导出数据Args:start_date: 开始日期end_date: 结束日期format: 导出格式,支持 'excel'/'csv'/'json'"""try:# 获取数据data = {'index': self.fetch_index_data(start_date, end_date),'pig': self.fetch_pig_price_data(start_date, end_date),'grain': self.fetch_grain_price_data(start_date, end_date)}if not any(data.values()):return# 根据格式导出if format == 'excel':self._export_excel(data, start_date, end_date)elif format == 'csv':self._export_csv(data, start_date, end_date)elif format == 'json':self._export_json(data, start_date, end_date)else:self.logger.error(f"不支持的导出格式: {format}")except Exception as e:self.logger.error(f"导出数据失败: {str(e)}")def _clean_text(self, text):"""清理文本内容"""if not text:return ""# 去除多余空白字符text = ' '.join(text.split())# 修复可能的标点符号问题text = text.replace('。。', '。').replace(',。', '。').replace(';。', '。')# 修复中文编码text = text.encode('utf-8').decode('utf-8', 'ignore')return textdef _validate_report_data(self, report):"""验证报告数据完整性"""required_fields = ["indexConclusion","animalConclusion","aquaticConclusion","vegetablesConclusion","fruitsConclusion"]is_valid = Truefor field in required_fields:if not report.get(field):self.logger.warning(f"报告缺少 {field} 数据")is_valid = Falsereport[field] = "暂无数据"return is_validdef _export_excel(self, data, start_date, end_date):"""导出Excel数据"""try:filename = f"价格数据_{start_date.strftime('%Y%m%d')}_{end_date.strftime('%Y%m%d')}.xlsx"filepath = os.path.join(self.output_dir, filename)with pd.ExcelWriter(filepath) as writer:# 导出价格指数if data.get('index'):df_index = pd.DataFrame(data['index'])df_index.to_excel(writer, sheet_name='价格指数', index=False)# 导出猪肉价格if data.get('pig'):df_pig = pd.DataFrame(data['pig'])df_pig.to_excel(writer, sheet_name='猪肉价格', index=False)# 导出粮油价格if data.get('grain'):df_grain = pd.DataFrame(data['grain'])df_grain.to_excel(writer, sheet_name='粮油价格', index=False)self.logger.info(f"���据已导出至: {filepath}")return Trueexcept Exception as e:self.logger.error(f"导出Excel失败: {str(e)}")return Falsedef fetch_all_categories(self):"""获取所有品类数据"""categories = {'MH': '猪肉','SC': '蔬菜','SG': '水果','TL': '粮油','SC': '水产','DJ': '蛋鸡','NR': '牛肉','YR': '羊肉'}result = {}for code, name in categories.items():try:url = f"{API_BASE_URL}{API_ENDPOINTS['variety_list']}"params = {'pid': code}response = self._make_request(url, method='post', params=params)if response and response.json().get("code") == 200:result[name] = response.json().get("data", [])except Exception as e:self.logger.error(f"获取{name}品类数据失败: {str(e)}")return resultdef fetch_market_prices(self, market_id=None, variety_id=None, start_date=None, end_date=None):"""获取市场价格数据"""try:url = f"{API_BASE_URL}{API_ENDPOINTS['daily_price']}"params = {'marketId': market_id,'varietyId': variety_id,'startDate': start_date.strftime("%Y-%m-%d") if start_date else None,'endDate': end_date.strftime("%Y-%m-%d") if end_date else None}response = self._make_request(url, method='post', params=params)if response and response.json().get("code") == 200:return response.json().get("data", [])return Noneexcept Exception as e:self.logger.error(f"获取市场价格数据失败: {str(e)}")return Nonedef fetch_analysis_reports(self, page=1, page_size=10):"""获取分析报告列表"""try:url = f"{API_BASE_URL}{API_ENDPOINTS['analysis_report']}"params = {'pageNum': page,'pageSize': page_size}response = self._make_request(url, method='post', params=params)if response and response.json().get("code") == 200:return response.json().get("data", {}).get("list", [])return Noneexcept Exception as e:self.logger.error(f"获取分析报告失败: {str(e)}")return Nonedef crawl_all_data(self, start_date, end_date):"""爬取所有数据"""try:# 获取所有品类categories = self.fetch_all_categories()# 获取所有市场markets_response = self._make_request(f"{API_BASE_URL}{API_ENDPOINTS['market_list']}", method='post')markets = markets_response.json().get("data", []) if markets_response else []# 存储结果results = {'categories': categories,'markets': markets,'prices': {},'reports': []}# 获取每个品类的价格数据for category, varieties in categories.items():results['prices'][category] = {}for variety in varieties:variety_id = variety.get('id')if variety_id:prices = self.fetch_market_prices(variety_id=variety_id,start_date=start_date,end_date=end_date)results['prices'][category][variety.get('name')] = prices# 获取分析报告page = 1while True:reports = self.fetch_analysis_reports(page=page)if not reports:breakresults['reports'].extend(reports)page += 1return resultsexcept Exception as e:self.logger.error(f"爬取所有数据失败: {str(e)}")return Nonedef fetch_weekly_report_content(self, report_id=None):"""获取周度报告内容"""try:url = f"{API_BASE_URL}/product/analysis-report/getReportContent"params = {'id': report_id} if report_id else Noneresponse = self._make_request(url, method='post', params=params)if not response:return None# 解析HTML内容soup = BeautifulSoup(response.text, 'html.parser')# 提取报告基本信息title = soup.find('h1', class_='report-title').text.strip()date = soup.find('div', class_='report-date').text.strip()source = soup.find('div', class_='report-source').text.strip()# 提取报告主体内容content = soup.find('div', class_='report-content')# 提取表格数据tables = []for table in content.find_all('table'):df = pd.read_html(str(table))[0]tables.append(df.to_dict('records'))# 提取文本内容paragraphs = []for p in content.find_all('p'):text = p.text.strip()if text:paragraphs.append(text)return {'title': title,'date': date,'source': source,'content': {'text': paragraphs,'tables': tables}}except Exception as e:self.logger.error(f"获取报告内容失败: {str(e)}")return Nonedef crawl_all_reports(self, start_date=None, end_date=None):"""爬取所有报告"""try:reports = []page = 1while True:# 获取报告列表report_list = self.fetch_analysis_reports(page=page)if not report_list:break# 过滤日期范围if start_date or end_date:filtered_reports = []for report in report_list:report_date = datetime.strptime(report['publishDate'], '%Y-%m-%d')if start_date and report_date < start_date:continueif end_date and report_date > end_date:continuefiltered_reports.append(report)report_list = filtered_reports# 获取每个报告的详细内容for report in report_list:report_content = self.fetch_weekly_report_content(report['id'])if report_content:reports.append({'meta': report,'content': report_content})# 添加延时避免请求过快time.sleep(1)page += 1return reportsexcept Exception as e:self.logger.error(f"爬取报告失败: {str(e)}")return Nonedef save_reports(self, reports, output_dir='reports'):"""保存报告到文件"""try:if not os.path.exists(output_dir):os.makedirs(output_dir)for report in reports:# 生成文件名date = datetime.strptime(report['meta']['publishDate'], '%Y-%m-%d')filename = f"{date.strftime('%Y%m%d')}_{report['meta']['id']}.json"filepath = os.path.join(output_dir, filename)# 保存为JSON文件with open(filepath, 'w', encoding='utf-8') as f:json.dump(report, f, ensure_ascii=False, indent=2)return Trueexcept Exception as e:self.logger.error(f"保存报告失败: {str(e)}")return Falseif __name__ == "__main__":try:# 检查并安装依赖包check_and_install_packages()# 运行爬虫crawler = ReportCrawler()crawler.run()except KeyboardInterrupt:print("\n程序已被用中断")sys.exit(0)except Exception as e:print(f"\n程序运行出错: {str(e)}")sys.exit(1)
一、功能介绍
本程序用于爬取农业农村部发布的农产品价格监测报告,包括以下功能:
1. 日度报告
- 生成今日分析报告
- 生成指定日期报告
- 包含价格指数、分品类分析等
2. 周度报告
- 生成本周分析报告
- 生成指定周报告
- 汇总周内价格变化
3. 价格走势
- 农产品价格200指数走势
- 猪肉价格全国走势
- 猪肉价格区域走势
- 粮油价格指数走势
4. 数据导出
- 支持Excel格式导出
- 包含多个数据分类
- 支持时间范围选择
二、使用说明
1. 环境要求
- Python 3.7+
- 依赖包会自动安装:
- requests: HTTP请求
- pandas: 数据处理
- matplotlib: 绘图支持
- urllib3: HTTP客户端
- openpyxl: Excel支持
- colorama: 控制台颜色
2. 运行方法
python
直接运行程序
python report_crawler.py
3. 功能菜单
农产品价格报告爬虫系统
生成今日分析报告
生成本周分析报告
生成指定日期报告
生成指定周报告
生成价格指数走势图
生成猪肉价格走势图
生成区域价格走势图
生成粮油价格走势图
导出Excel数据
退出系统
4. 输出文件
- reports/daily: 日度分析报告
- reports/weekly: 周度分析报告
- reports: 价格走势图和Excel数据
三、数据来源
- 农业农村部市场信息中心
- 数据更新频率: 每日14:00
四、注意事项
- 首次运行会自动检查并安装依赖包
- 所有数据仅供学习交流使用
- 建议使用时设置合理的时间范围
- 如遇到问题可查看日志文件
五、更新记录
v1.0.0 (2024-12-05)
- 实现基础数据爬取功能
- 支持生成分析报告
- 支持绘制价格走势图
- 支持导出Excel数据
六、联系方式
作者: xiaohai
仅用于学习交流
相关文章:
农产品价格报告爬虫使用说明
农产品价格报告爬虫使用说明 # ************************************************************************** # * * # * 农产品价格报告爬虫 …...
Java 大视界 -- Java 大数据中的隐私增强技术全景解析(64)
💖亲爱的朋友们,热烈欢迎来到 青云交的博客!能与诸位在此相逢,我倍感荣幸。在这飞速更迭的时代,我们都渴望一方心灵净土,而 我的博客 正是这样温暖的所在。这里为你呈上趣味与实用兼具的知识,也…...
实战Linux Swap扩展分区
文章目录 定义命令格式案例注释 定义 Swap分区是Linux系统中的一种虚拟内存实现方式,它是磁盘上预留的专用区域。当系统的物理内存不足时,会将部分不活跃的数据从物理内存移动到Swap分区,从而释放更多可用内存空间。 命令格式 关闭Swap分区…...
doris:Parquet导入数据
本文介绍如何在 Doris 中导入 Parquet 格式的数据文件。 支持的导入方式 以下导入方式支持 Parquet 格式的数据导入: Stream LoadBroker LoadINSERT INTO FROM S3 TVFINSERT INTO FROM HDFS TVF 使用示例 本节展示了不同导入方式下的 Parquet 格式使用方法…...
Synology 群辉NAS安装(6)安装mssql
Synology 群辉NAS安装(6)安装mssql 写在前面mssql 2019:成功安装说明,这个最终成功了 mssql 2022没有成功1. pull image2.启动mssql docker container 远程连接 写在前面 mssq是一个重要节点。 这是因为我对mysql没有一丝好感。虽然接触了许…...
使用.NET 8构建高效的时间日期帮助类
使用.NET 8构建高效的时间日期帮助类 在现代Web应用程序中,处理日期和时间是一个常见的需求。无论是记录日志、生成报告还是进行数据分析,正确处理日期和时间对于确保数据的准确性和一致性至关重要。本文将详细介绍如何使用ASP.NET Core和C#构建一个高效…...
第26篇 基于ARM A9处理器用C语言实现中断<二>
Q:基于ARM A9处理器怎样编写C语言工程,使用按键中断将数字显示在七段数码管上呢? A:基本原理:主程序需要首先调用子程序set_A9_IRQ_stack()初始化IRQ模式的ARM A9堆栈指针;然后主程序调用子程序config_GIC…...
dm8在Linux环境安装精简步骤说明(2024年12月更新版dm8)
dm8在Linux环境安装详细步骤 - - 2025年1月之后dm8 环境介绍1 修改操作系统资源限制2 操作系统创建用户3 操作系统配置4 数据库安装5 初始化数据库6 实例参数优化7 登录数据库配置归档与备份8 配置审计9 创建用户10 屏蔽关键字与数据库兼容模式11 jdbc连接串配置12 更多达梦数据…...
Nginx部署的前端项目刷新404问题
1,查看问题 我部署的81端口是监听tlias项目的,我直接访问端口页面可以出现内容。 我在浏览器舒服端口之后回车,会重定向页面。但是我在重定向之后的页面刷新浏览器就会出现404的问题。 下面是刷新浏览器后的效果 2,在nginx.cnf …...
H2 Database安装部署
H2 Database H2 Database官网 H2 中文文档 安装部署H2 java版本要高于java 11 ## 下载java21 wget https://download.oracle.com/java/21/latest/jdk-21_linux-x64_bin.tar.gz[rootlocalhost ~]# tar xf jdk-21_linux-x64_bin.tar.gz -C /usr/local/ [rootlocalhost ~]# vi…...
Day40:列表的排序
在 Python 中,排序是处理列表数据时常用的一种操作。排序可以帮助我们按照一定的规则(如升序或降序)对列表中的元素进行排列。Python 提供了内置的排序方法 sort() 和 sorted() 来实现这一功能。 1. 使用 sort() 方法排序 1.1 sort() 方法简…...
TypeScript进阶(三):断言
文章目录 一、前言二、类型断言2.1 利用 as 实现断言2.2 利用 <> 实现断言2.3 应用示例2.3.1 如果 A,B 如果是类并且有继承关系2.3.2 如果 A,B 如果是类,但没有继承关系2.3.3 如果 A 是类,B 是接口,并且 A 类实现…...
塔罗牌(基础):大阿卡那牌
塔罗牌(基础) 大啊卡那牌魔术师女祭司皇后皇帝教皇恋人战车力量隐士命运之轮正义吊人死神节制恶魔高塔星星月亮太阳审判世界 大啊卡那牌 魔术师 作为一个起点,象征:意识行动和创造力。 一个【显化】的概念,即是想法变…...
微服务(一)
文章目录 项目地址一、微服务1.1 分析User的Domian Verb和Nouns 二、运行docker和k8s2.1 Docker1. 编写dockerfile2. 创建docker image3. 运行docker使用指定端口4. 查看当前运行的镜像5. 停止当前所有运行的docker6. 删除不用的docker images7. 将本地的image上传到hub里 2.2 …...
JAVA(SpringBoot)集成Kafka实现消息发送和接收。
SpringBoot集成Kafka实现消息发送和接收。 一、Kafka 简介二、Kafka 功能三、POM依赖四、配置文件五、生产者六、消费者 君子之学贵一,一则明,明则有功。 一、Kafka 简介 Kafka 是由 Apache 软件基金会开发的一个开源流处理平台,最初由 Link…...
Oracle之开窗函数使用
开窗函数是数据开发面试的必知必会,必须认真对待,上难度: 开窗函数语法格式如下,一共五部分: ①函数(a)over(②<partition by b> ③<order by c> ④<windowing_clause> ⑤开窗范围)1、函数…...
mysql_store_result的概念和使用案例
mysql_store_result() 是 MySQL C API 中的一个函数,用于检索一个完整的结果集到一个客户端。当执行一个查询(通常是 SELECT 查询)并希望处理所有返回的数据时,可以使用此函数。 概念 mysql_store_result() 函数的原型如下&…...
【大数据】数据治理浅析
在数字化时代,数据作为企业的核心资产,其管理和利用显得尤为关键。数据治理,作为数据管理的重要组成部分,旨在确保数据的准确性、一致性、安全性和可用性。本文将从数据治理的基本概念、应用场景、必要性、需求分析等方面出发&…...
第 25 场 蓝桥月赛
4.喜糖摆放【算法赛】 - 蓝桥云课 问题描述 在过年时,蓝桥村的孩子们充满活力,他们化身为捣蛋鬼,挨家挨户寻讨喜糖。他们一共收到了N颗糖,每颗糖的甜度各不相同,第i颗糖的甜度为Ai。 然而,如何分配这些喜…...
LigerUI在MVC模式下的响应原则
LigerUI是基于jQuery的UI框架,故他也是遵守jQuery的开发模式,但是也具有其特色的侦听函数,那么当LigerUI作为View层的时候,他所发送后端的必然是表单的数据,在此我们以俩个div为例: {Layout "~/View…...
Vue2下篇
插槽: 基本插槽: 普通插槽:父组件向子组件传递静态内容。基本插槽只能有一个slot标签,因为这个是默认的位置,所以只能有一个 <!-- ParentComponent.vue --> <template> <ChildComponent> <p>…...
python 变量范围的定义与用法
文章目录 1. 局部变量(Local Scope)示例: 2. 嵌套函数变量(Enclosing Scope)示例:说明: 3. 全局变量(Global Scope)示例:说明: 4. 内置变量&#…...
for...in 和 Object.keys().forEach的区别
for…in 和 Object.keys().forEach的区别 1、遍历范围: for…in 会遍历 自身及原型链上的可枚举属性,需用 hasOwnProperty 过滤。 Object.keys() 仅遍历 自身可枚举属性,更安全。 // 定义一个父对象,包含原型链上的属性 const…...
API接口设计模板
API 员工登录接口设计 基本信息 Path: /admin/staff/login **Method:**POST 接口描述: 请求参数 Query 参数名称是否必须示例备注username是admin用户名password是mima密码 返回数据 名称类型是否必须默认值备注其他信息codeinteger必须dat…...
新电脑安装系统找不到硬盘原因和解决方法来了
有不少网友反馈新电脑采用官方u盘方式装win10或win100出现找不到硬盘是怎么回事?后来研究半天发现是bios中开启了rst(vmd)模式。如果关闭rst模式肯定是可以安装的,但这会影响硬盘性能,有没有办法解决开启rst模式的情况安装win10或win11呢&…...
”彩色的验证码,使用pytesseract识别出来的验证码内容一直是空“的解决办法
问题:彩色的验证码,使用pytesseract识别出来的验证码内容一直是空字符串 原因:pytesseract只识别黑色部分的内容 解决办法:先把彩色图片精确转换成黑白图片。再将黑白图片进行反相,将验证码部分的内容变成黑色&#…...
网站上的图片无法使用右键“图片另存为”
某些网站想要下载图片,无法使用右键“图片另存为”,网站截获了鼠标右键的快捷键,没法弹出右键菜单。 可以打开“开发者工具”,使用“Elements”面板找到这个元素,在元素上右键,选择“Open in new tab” 结…...
Linux:生产者消费者模型
一、普通生产者消费者模型 1.1 什么是生产者消费者模型 现实生活中,我们也会有像生物世界的生产者和消费者的概念,但是我们的消费者在大多数情况下并不和生产者直接联系,就比如说食物,不能说我今天去找供货商要十个面包ÿ…...
网络安全 | F5-Attack Signatures详解
关注:CodingTechWork 关于攻击签名 攻击签名是用于识别 Web 应用程序及其组件上攻击或攻击类型的规则或模式。安全策略将攻击签名中的模式与请求和响应的内容进行比较,以查找潜在的攻击。有些签名旨在保护特定的操作系统、Web 服务器、数据库、框架或应…...
自然元素有哪些选择?
在设计浪漫风格的壁纸时,自然元素是营造温馨、梦幻氛围的关键。以下是一些常见的自然元素选择,以及它们在壁纸设计中建议: 一、花朵 玫瑰: 特点:玫瑰是浪漫的象征,尤其是红色和粉色玫瑰,能够传…...
基于微信阅读网站小程序的设计与实现(LW+源码+讲解)
专注于大学生项目实战开发,讲解,毕业答疑辅导,欢迎高校老师/同行前辈交流合作✌。 技术范围:SpringBoot、Vue、SSM、HLMT、小程序、Jsp、PHP、Nodejs、Python、爬虫、数据可视化、安卓app、大数据、物联网、机器学习等设计与开发。 主要内容:…...
linux挂载新硬盘,查看新硬盘,格式化分区,创建挂载点,挂载逻辑卷,整盘方式挂载,LVM方式挂载,查看linux 磁盘卷组的剩余空间,ext4与xfs区别
摘要 挂载新硬盘,本文作者整理了几乎所有相关的知识点 作者采用的是本文第二种挂载方式(LVM),只用了下面6条命令搞定 # 说明: # /dev/mapper/appvg-mylv1 逻辑卷完整名称 # # /dev/mapper目录是Linux系统中用…...
Web3.0时代的挑战与机遇:以开源2+1链动模式AI智能名片S2B2C商城小程序为例的深度探讨
摘要:Web3.0作为互联网的下一代形态,承载着去中心化、开放性和安全性的重要愿景。然而,其高门槛、用户体验差等问题阻碍了Web3.0的主流化进程。本文旨在深入探讨Web3.0面临的挑战,并提出利用开源21链动模式、AI智能名片及S2B2C商城…...
AIGC专栏18——EasyAnimateV5.1版本详解 应用Qwen2 VL作为文本编码器,支持轨迹控制与相机镜头控制
AIGC专栏18——EasyAnimateV5.1版本详解 应用Qwen2 VL作为文本编码器,支持轨迹控制与相机镜头控制 学习前言相关地址汇总源码下载地址HF测试链接MS测试链接 测试效果Image to VideoText to Video轨迹控制镜头控制 EasyAnimate详解技术储备Qwen2 VLStable Diffusion …...
测试的基本原则
1.SDLC 才是王道:软件开发生命周期(SDLC)对于软件开发而言,是如同基石般的关键流程,每一位开发人员都应该对其了如指掌。从最初的需求定义,到最终软件上线后的维护,SDLC 的各个阶段环…...
如何建设一个企业级的数据湖
建设一个企业级的数据湖是一项复杂且系统化的工程,需要从需求分析、技术选型、架构设计到实施运维等多个方面进行综合规划和实施。以下是基于我搜索到的资料,详细阐述如何建设企业级数据湖的步骤和关键要点: 一、需求分析与规划 明确业务需…...
Ubuntu介绍、与centos的区别、基于VMware安装Ubuntu Server 22.04、配置远程连接、安装jdk+Tomcat
目录 ?编辑 一、Ubuntu22.04介绍 二、Ubuntu与Centos的区别 三、基于VMware安装Ubuntu Server 22.04 下载 VMware安装 1.创建新的虚拟机 2.选择类型配置 3.虚拟机硬件兼容性 4.安装客户机操作系统 5.选择客户机操作系统 6.命名虚拟机 7.处理器配置 8.虚拟机内存…...
springfox-swagger-ui 3.0.0 配置
在3.0中,访问地址URL变了。 http://地址:端口/项目名/swagger-ui/ SpringBoot maven项目引入 <dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger2</artifactId><version>3.0.0</version> </…...
【PyTorch][chapter 29][李宏毅深度学习]Fine-tuning LLM
参考: https://www.youtube.com/watch?veC6Hd1hFvos 目录: 什么是 Fine-tune 为什么需要Fine-tuning 如何进行Fine-tune Fine-tuning- Supervised Fine-tuning 流程 Fine-tuning参数训练的常用方案 LORA 简介 示例代码 一 什么是 Fine-tune …...
Spring无法解决的循环依赖
在Spring框架中,循环依赖是指两个或多个Bean相互依赖,形成一个闭环。例如,Bean A依赖于Bean B,而Bean B又依赖于Bean A。虽然Spring通过三级缓存(一级缓存、二级缓存、三级缓存)机制解决了大多数情况下的循…...
C++的类Class
文章目录 一、C的struct和C的类的区别二、关于OOP三、举例:一个商品类CGoods四、构造函数和析构函数1、定义一个顺序栈2、用构造和析构代替s.init(5);和s.release();3、在不同内存区域构造对象4、深拷贝和浅拷贝5、构造函数和深拷贝的简单应用6、构造函数的初始化列…...
如何应对离别之:短暂离别
《若道离别》(一):如何应对离别之短暂离别 大多数人还是不能很全心愉快地面对离别,哪怕只是短暂,还是从有到无的失落感,有人一天就适应,有人需要很久 不求离别无动于衷,但求使用部分…...
Harmony Next 跨平台开发入门
ArkUI-X 官方介绍 官方文档:https://gitee.com/arkui-x/docs/tree/master/zh-cn ArkUI跨平台框架(ArkUI-X)进一步将ArkUI开发框架扩展到了多个OS平台:目前支持OpenHarmony、Android、 iOS,后续会逐步增加更多平台支持。开发者基于一套主代码…...
笔试-二维数组2
应用 现有M(1<M<10)个端口组,每个端口组是长度为N(1<N<100),元素均为整数。如果这些端口组间存在2个及以上的元素相同,则认为端口组可以关联合并;若可以关联合并,请用二位数组表示输出结果。其中…...
/opt安装软件,就可以使用man xx命令是为什么
引言 以neovim的安装过程为例 下载 curl -LO https://github.com/neovim/neovim/releases/latest/download/nvim-linux64.tar.gz sudo rm -rf /opt/nvim sudo tar -C /opt -xzf nvim-linux64.tar.gz添加环境变量前,是无法使用man nvim的 Then add this to your sh…...
vue3和vue2的区别有哪些差异点
Vue3 vs Vue2 主要差异对比指南 官网 1. 核心架构差异 1.1 响应式系统 Vue2:使用 Object.defineProperty 实现响应式 // Vue2 响应式实现 Object.defineProperty(obj, key, {get() {// 依赖收集return value},set(newValue) {// 触发更新value newValue} })Vue3…...
记录备战第十六届蓝桥杯的过程
1.学会了原来字符串也有比较方法,也就是字符串987 > 98 等等,可以解决拼最大数问题 题目链接:5.拼数 - 蓝桥云课 (lanqiao.cn) 2.今天又复习了一下bfs,感觉还是很不熟练,可能是那个过程我些许有点不熟悉ÿ…...
【PVE】Proxmox VE8.0+创建LXC容器安装docker
为了不影响PVE宿主机,通常使用套娃的形式安装Docker容器,再安装相关docker应用。首先在CT模板中创建 Linux 容器,推荐使用Debian。开启ssh登录,修改debian配置,安装docker 一、创建 LXC 容器 1、CT模板下载 点击“模…...
Semantic Kernel - Kernel理解
目录 一、关于Kernel 二、案例实战 三、运行截图 一、关于Kernel 微软的 Semantic Kernel 项目中,Semantic Kernel 是一个工具框架,旨在使得开发人员能够更容易地将大语言模型(如GPT)集成到不同的应用中。它通过提供一组接口、任务模板和集成模块,使开发者能够轻松地设计…...
【JavaWeb06】Tomcat基础入门:架构理解与基本配置指南
文章目录 🌍一. WEB 开发❄️1. 介绍 ❄️2. BS 与 CS 开发介绍 ❄️3. JavaWeb 服务软件 🌍二. Tomcat❄️1. Tomcat 下载和安装 ❄️2. Tomcat 启动 ❄️3. Tomcat 启动故障排除 ❄️4. Tomcat 服务中部署 WEB 应用 ❄️5. 浏览器访问 Web 服务过程详…...