跳到主要内容

油价调价提醒

收藏0

国内成品油调价日提醒:只在调价窗口日检查发改委调价通知,一发布就把本省新价格、涨跌幅和下一次调价窗口日推送到 QQ。

让 AI 安装

Minsaroun最新版本 0.1.0更新于 2026-10-30下载 0 次

oil_notify.py

8.1 KB
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# oil_notify.py —— 调价日油价提醒(QQ 推送 + 动态 cron 调度)
4# 只在调价窗口日由动态 cron 调度运行:窗口日当天检查油价调整通知是否已发布,
5# 已发布则推送 QQ(新价+涨幅+下次窗口日),并把 cron 重写到下一次窗口日。
6# 用法:oil_notify.py [--dry-run] (--dry-run:只打印不推送不改 cron)
7import json, os, re, subprocess, sys
8from datetime import date, datetime, timedelta
9import urllib.request
10
11WORKDIR = os.path.dirname(os.path.abspath(__file__))
12CONFIG_FILE = os.path.join(WORKDIR, 'oil_notify_config.json')
13
14
15def load_cfg():
16 try:
17 with open(CONFIG_FILE) as f:
18 return json.load(f)
19 except Exception:
20 return {}
21
22
23CFG = load_cfg()
24OWNER_QQ = os.environ.get('QQ_USER_ID') or str(CFG.get('qq_user_id') or '')
25ONE_BOT_URL = os.environ.get('ONE_BOT_URL') or CFG.get('one_bot_url') or 'http://127.0.0.1:3000/send_private_msg'
26ONE_BOT_TOKEN = os.environ.get('ONE_BOT_TOKEN') or CFG.get('one_bot_token') or ''
27HOME_URL = 'https://www.soyoujia.cn/'
28HUNAN_URL = 'https://www.soyoujia.cn/hunan'
29LOG = os.path.join(WORKDIR, 'oil_notify.log')
30CRON_MARKER = 'oil_notify' # crontab 里由本脚本管理的行
31CRON_BAK = os.path.join(WORKDIR, 'crontab.bak')
32UA = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'}
33
34DRY = '--dry-run' in sys.argv
35SIM_DATE = None
36if '--sim-date' in sys.argv:
37 i = sys.argv.index('--sim-date')
38 SIM_DATE = date.fromisoformat(sys.argv[i + 1])
39
40
41def log(msg):
42 line = f'{datetime.now().strftime("%Y-%m-%d %H:%M:%S")} {msg}'
43 print(line, flush=True)
44 if not DRY:
45 try:
46 with open(LOG, 'a') as f:
47 f.write(line + '\n')
48 except Exception:
49 pass
50
51
52def fetch(url):
53 req = urllib.request.Request(url, headers=UA)
54 with urllib.request.urlopen(req, timeout=30) as r:
55 return r.read().decode('utf-8', 'replace')
56
57
58def send_qq(text):
59 if DRY:
60 log(f'[dry-run] 将推送:\n{text}')
61 return 'dry-run'
62 body = json.dumps({'user_id': OWNER_QQ, 'message': text}).encode()
63 req = urllib.request.Request(ONE_BOT_URL, data=body,
64 headers={'Content-Type': 'application/json',
65 'Authorization': f'Bearer {ONE_BOT_TOKEN}'})
66 try:
67 with urllib.request.urlopen(req, timeout=15) as r:
68 return r.read().decode()
69 except Exception as e:
70 log(f'推送失败: {e}')
71 return ''
72
73
74def parse_next_effect(html):
75 """首页倒计时:下次调整生效日。返回 date 或 None。
76 '距下次调整还有 <b>14</b> 天(<span>11月13日</span> <span>周五</span>)'"""
77 m = re.search(r'距下次调整还有\s*<b>\d+</b>\s*天(<span>(\d+)月(\d+)日</span>', html)
78 if not m:
79 return None
80 month, day = int(m.group(1)), int(m.group(2))
81 y = date.today().year
82 d = date(y, month, day)
83 if d <= date.today():
84 d = date(y + 1, month, day) # 跨年
85 return d
86
87
88def parse_hunan_latest(html):
89 """湖南页近 3 个月调价表第一行(最新一次调价):(生效日期, [(油品名, 新价, 涨幅)])。
90 soyoujia 新版布局为 div(无 table):数据行 = w-[140px] 日期 div + 每油品 flex 列(价格 span + 涨跌幅 span)。"""
91 m = re.search(r'class="w-\[140px\] shrink-0[^"]*">\s*(20\d{2}-\d{2}-\d{2})\s*</div>', html)
92 if not m:
93 return None, []
94 adj_date = m.group(1)
95 seg = html[m.end():]
96 prices = re.findall(r'text-\[14px\] font-semibold text-ink font-display">([\d.]+)</span>', seg)
97 deltas = re.findall(r'text-\[12px\] font-bold font-body">\s*(?:<span[^>]*>)?([+-]?)<span>([\d.]+)</span>', seg)
98 names = ['89汽油', '92汽油', '95汽油', '98汽油', '0柴油']
99 out = []
100 for i, name in enumerate(names):
101 if i < len(prices):
102 delta = ''
103 if i < len(deltas):
104 delta = deltas[i][0] + deltas[i][1] # '+0.28' / '-0.30'
105 out.append((name, prices[i], delta))
106 return adj_date, out
107
108
109def set_cron(day, month, hour=17, minute=0):
110 """把 crontab 里的 oil_notify 行重写为指定日期 hour:00(动态调度)。
111 时间依据:发改委调价日 17:00 左右发布调价通知(要调价时;搁浅时 15:00 左右),
112 17:00 查询通知刚发布;万一页面还没更新有次日 08:00 补查兜底。
113 推送参数(QQ 号、OneBot 地址与 token)写在 oil_notify_config.json 里,cron 行只带路径。"""
114 line = (f'{minute} {hour} {day} {month} * cd {WORKDIR} && '
115 f'python3 oil_notify.py >> oil_notify.log 2>&1')
116 if DRY:
117 log(f'[dry-run] 将重写 cron 为: {line}')
118 return
119 try:
120 cur = subprocess.run(['crontab', '-l'], capture_output=True, text=True).stdout
121 keep = [l for l in cur.splitlines() if l.strip() and CRON_MARKER not in l]
122 with open(CRON_BAK, 'w') as f:
123 f.write(cur)
124 new = '\n'.join(keep + [line]) + '\n'
125 p = subprocess.run(['crontab', '-'], input=new, text=True, capture_output=True)
126 if p.returncode != 0:
127 log(f'crontab 重写失败: {p.stderr.strip()}')
128 else:
129 log(f'cron 已设为 {month}月{day}日 {hour}:{minute:02d}')
130 except Exception as e:
131 log(f'crontab 操作异常: {e}')
132
133
134def main():
135 today = SIM_DATE or date.today()
136 try:
137 home = fetch(HOME_URL)
138 next_effect = parse_next_effect(home) # 下次调整生效日(窗口日 24 时后)
139 except Exception as e:
140 log(f'首页抓取失败: {e}')
141 return
142 if next_effect is None:
143 log('解析不到下次调价日期,跳过(保留下次 cron 不再动)')
144 return
145 window = next_effect - timedelta(days=1) # 窗口日(发通知那天,24 时生效)
146 log(f'今天={today} 下次生效日={next_effect} 窗口日={window}')
147
148 try:
149 hunan_html = fetch(HUNAN_URL)
150 adj_date, items = parse_hunan_latest(hunan_html)
151 except Exception as e:
152 log(f'湖南页抓取失败: {e}')
153 adj_date, items = None, []
154
155 # 今天是否该检查:窗口日当天,或生效日补查
156 should_check = today in (window, next_effect)
157 if should_check:
158 notified = False
159 if adj_date:
160 try:
161 adj = date.fromisoformat(adj_date)
162 # 页面最新调价行日期在 今天±1 天内都算通知已出(窗口日晚可能已显示次日生效)
163 notified = abs((today - adj).days) <= 1
164 except ValueError:
165 notified = False
166 if notified and items:
167 lines = [f'📢 今日油价调整通知(湖南,{adj_date} 生效)']
168 for name, price, delta in items:
169 if price == '—' or not price:
170 continue
171 d = f'(较上次 {delta})' if delta else ''
172 lines.append(f'・{name} {price} 元/升 {d}')
173 lines.append(f'下次调价窗口:{window2_fmt(next_effect)},爱丽丝那天再来看。')
174 text = '\n'.join(lines)
175 log('调价通知已出,推送 QQ')
176 resp = send_qq(text)
177 log(f'推送响应: {resp}')
178 # 下个窗口日:粗算 生效日+13 天(约 10 个工作日),下次运行时会用首页倒计时重新校准
179 nxt_window = next_effect + timedelta(days=13)
180 set_cron(nxt_window.day, nxt_window.month)
181 else:
182 # 窗口日到了但页面还没更新(通知未出/滞后)→ 明天早上补查
183 retry = today + timedelta(days=1)
184 log(f'窗口日已到但通知还没出(最新调价行={adj_date}),明天 {retry} 08:00 补查')
185 set_cron(retry.day, retry.month, hour=8)
186 else:
187 # 非窗口日被误触发:把 cron 校准回正确窗口日,不推送
188 log('今天不是调价日,不推送;把 cron 校准到窗口日')
189 set_cron(window.day, window.month)
190
191
192def window2_fmt(effect):
193 w = effect - timedelta(days=1)
194 return f'{w.month}月{w.day}日24时'
195
196
197if __name__ == '__main__':
198 main()

文件内容是平台检查时读出的文本,只供查看。平台不会运行包里的任何脚本或程序。