{"id":520,"date":"2025-05-12T10:54:28","date_gmt":"2025-05-12T02:54:28","guid":{"rendered":"https:\/\/www.subkme.com\/?p=520"},"modified":"2025-05-12T10:57:10","modified_gmt":"2025-05-12T02:57:10","slug":"%e5%8f%8c%e8%bf%87%e5%8d%8a%e6%b4%bb%e5%8a%a8%e8%87%aa%e5%8a%a8%e7%bb%9f%e8%ae%a1%e6%8a%a5%e8%a1%a8%e4%bb%a3%e7%a0%81","status":"publish","type":"post","link":"https:\/\/subk.me\/?p=520","title":{"rendered":"\u53cc\u8fc7\u534a\u6d3b\u52a8\u81ea\u52a8\u7edf\u8ba1\u62a5\u8868\u4ee3\u7801"},"content":{"rendered":"<p>\u5904\u7406\u53cc\u8fc7\u534a\u901a\u62a5\u6570\u636e\uff0c\u81ea\u52a8\u6458\u5f55\u76f8\u5173\u6570\u636e\u548c\u901a\u62a5\u3002<\/p>\n<pre><code># Author: subk\n# Time: 2025\/5\/1 13:44\n# Desc: \u5904\u740625\u5e74\u96c6\u56e2\u5e02\u573a\u53cc\u8fc7\u534a\u901a\u62a5\u6570\u636e\n# Version: 1.1\n\nimport requests\nimport pandas as pd\nimport re\nimport smtplib\nimport schedule\nimport time\nfrom datetime import datetime, timedelta\nfrom bs4 import BeautifulSoup\nfrom email.mime.text import MIMEText\nfrom email.mime.multipart import MIMEMultipart\n\n# \u2500\u2500 \u7528\u6237\u914d\u7f6e \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nusername    = &quot;XXX.com&quot;\npassword    = &quot;3B3XXXXXXXXXXXX200&quot;\nsmtp_server = &quot;smtp.XX.com&quot;\nsmtp_port   = 465\nrecipients  = [\n    &quot;XX&quot;,\n    &quot;XX&quot;\n]\n\nREGIONS = [&quot;\u6d9f\u6c34&quot;,&quot;\u6e05\u6c5f\u6d66&quot;,&quot;\u6dee\u5b89\u533a&quot;,&quot;\u6dee\u9634\u533a&quot;,&quot;\u76f1\u7719&quot;,&quot;\u6dee\u5f00&quot;,&quot;\u91d1\u6e56&quot;,&quot;\u6d2a\u6cfd&quot;,&quot;\u6218\u5ba2&quot;]\nKEY_WEAK = [\n    &quot;\u6218\u5ba2\u5ba2\u6237\u51c0\u589e&quot;,&quot;\u653f\u4f01\u65b0\u5165\u7f51&quot;,&quot;\u653f\u4f01\u6709\u6548\u65b0\u589e&quot;,&quot;\u7535\u5b50\u5b66\u751f\u8bc1&quot;,&quot;\u4e91\u89c6\u8baf\u65b0\u589e\u7ec8\u7aef\u6570&quot;,\n    &quot;\u548c\u5bf9\u8bb2\u65b0\u589e\u7ec8\u7aef\u6570&quot;,&quot;\u5546\u5ba2\u5e02\u573a\u8ba1\u8d39\u5bbd\u5e26\u65b0\u589e&quot;,&quot;\u667a\u7b97\u8d44\u6e90\u9500\u552e&quot;,&quot;\u4e13\u7ebfFTTO\u65b0\u589e&quot;,\n    &quot;\u6218\u5ba2\u96c6\u56e2\u6e17\u900f&quot;,&quot;\u6cdb\u4f4f\u5bbf\u573a\u666f\u5bbd\u5e26\u65b0\u589e&quot;,&quot;\u6cdb\u4e91\u4e3b\u673a&quot;,&quot;\u4e0a\u7f51\u4e13\u7ebf\u65b0\u589e\u6761\u6570&quot;,\n    &quot;BC\u878d\u5408\u6210\u5458\u6570&quot;,&quot;\u6b20\u8d39\u56de\u6536&quot;\n]\n\n# \u2500\u2500 \u72b6\u6001\u63a7\u5236 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nlast_sent_date = None  # \u683c\u5f0f\uff1a&quot;YYYYMMDD&quot;\n\n# \u2500\u2500 \u901a\u7528\u51fd\u6570 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\ndef get_dates():\n    today = datetime.now().strftime(&quot;%Y\u5e74%m\u6708%d\u65e5&quot;)\n    yesterday = (datetime.now() - timedelta(days=1)).strftime(&quot;%Y%m%d&quot;)\n    return today, yesterday\n\ndef send_email(subject: str, html_body: str):\n    msg = MIMEMultipart()\n    msg[&quot;From&quot;]    = username\n    msg[&quot;To&quot;]      = &quot;, &quot;.join(recipients)\n    msg[&quot;Subject&quot;] = subject\n    msg.attach(MIMEText(html_body, &quot;html&quot;, &quot;utf-8&quot;))\n    with smtplib.SMTP_SSL(smtp_server, smtp_port) as s:\n        s.login(username, password)\n        s.sendmail(username, recipients, msg.as_string())\n\ndef fetch_web_content(url: str) -&gt; str:\n    r = requests.get(url, timeout=10)\n    r.raise_for_status()\n    return r.text\n\ndef highlight_keyword(content: str, keyword: str) -&gt; str:\n    return content.replace(keyword, f&quot;&lt;span style=&#039;background-color:#FFD700&#039;&gt;{keyword}&lt;\/span&gt;&quot;)\n\ndef print_styled_table(df: pd.DataFrame, title: str) -&gt; str:\n    last = df.index[-1]\n    styler = (\n        df.style\n          .set_table_attributes(&#039;style=&quot;margin-left:auto;margin-right:auto;border-collapse:collapse;&quot;&#039;)\n          .set_caption(f&quot;&lt;strong style=&#039;font-size:1.3em;color:#0000FF&#039;&gt;{title}&lt;\/strong&gt;&quot;)\n          .set_table_styles([\n              {&quot;selector&quot;:&quot;thead th&quot;,\n               &quot;props&quot;:[(&quot;background-color&quot;,&quot;#4390FF&quot;),(&quot;font-weight&quot;,&quot;bold&quot;),\n                        (&quot;font-size&quot;,&quot;14px&quot;),(&quot;border&quot;,&quot;1px solid #AAA&quot;),(&quot;padding&quot;,&quot;8px&quot;)]},\n              {&quot;selector&quot;:&quot;tbody th&quot;, &quot;props&quot;:[(&quot;border&quot;,&quot;1px solid #AAA&quot;),(&quot;padding&quot;,&quot;6px&quot;)]}\n          ])\n          .set_properties(**{&quot;border&quot;:&quot;1px solid #AAA&quot;,&quot;padding&quot;:&quot;6px&quot;,&quot;text-align&quot;:&quot;center&quot;})\n          .apply(lambda row: [&quot;background-color:#F2F3F4&quot; if row.name==last else &quot;&quot; for _ in row], axis=1)\n    )\n    return f&quot;&lt;div style=&#039;margin:30px 0;&#039;&gt;{styler.to_html()}&lt;\/div&gt;&quot;\n\n# \u2500\u2500 \u89e3\u6790\u51fd\u6570 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\ndef parse_overall_table(soup: BeautifulSoup) -&gt; pd.DataFrame:\n    tables = pd.read_html(str(soup), header=[0,1])\n    df = tables[0]\n    flat = [h0 + (h1 if isinstance(h1,str) else &#039;&#039;) for h0,h1 in df.columns]\n    df.columns = flat\n    df = df.rename(columns={df.columns[0]:&#039;county&#039;})\n    front_col = next((c for c in df.columns if c.startswith(&#039;\u603b\u4f53\u60c5\u51b5&#039;) and c.endswith(&#039;\u524d\u4e09\u9879\u76ee&#039;)), None)\n    back_col  = next((c for c in df.columns if c.startswith(&#039;\u603b\u4f53\u60c5\u51b5&#039;) and c.endswith(&#039;\u540e\u4e09\u9879\u76ee&#039;)), None)\n    if not front_col or not back_col:\n        raise ValueError(&quot;\u672a\u627e\u5230&ldquo;\u524d\u4e09\u9879\u76ee&rdquo;\u6216&ldquo;\u540e\u4e09\u9879\u76ee&rdquo;\u5217&quot;)\n    df = df[[&#039;county&#039;, front_col, back_col]].rename(columns={front_col:&#039;front&#039;, back_col:&#039;back&#039;})\n    df[&#039;front&#039;] = pd.to_numeric(df[&#039;front&#039;], errors=&#039;coerce&#039;)\n    df[&#039;back&#039;]  = pd.to_numeric(df[&#039;back&#039;],  errors=&#039;coerce&#039;)\n    return df\n\ndef extract_lianshui(df: pd.DataFrame) -&gt; dict:\n    sub = df[df[&#039;county&#039;]==&#039;\u6d9f\u6c34&#039;]\n    if sub.empty:\n        raise KeyError(&quot;\u6d9f\u6c34\u884c\u7f3a\u5931&quot;)\n    front, back = int(sub[&#039;front&#039;].values[0]), int(sub[&#039;back&#039;].values[0])\n    df_f = df.sort_values(&#039;front&#039;, ascending=False).reset_index(drop=True)\n    fr = int((df_f[&#039;county&#039;]==&#039;\u6d9f\u6c34&#039;).idxmax() + 1)\n    df_b = df.sort_values(&#039;back&#039;, ascending=False).reset_index(drop=True)\n    br = int((df_b[&#039;county&#039;]==&#039;\u6d9f\u6c34&#039;).idxmax() + 1)\n    return {&#039;front&#039;:front,&#039;back&#039;:back,&#039;front_rank&#039;:fr,&#039;back_rank&#039;:br}\n\ndef extract_deadline_text(soup: BeautifulSoup) -&gt; str:\n    for d in soup.find_all(&quot;div&quot;, class_=&quot;title_content&quot;):\n        t = d.get_text(strip=True)\n        if t.startswith(&quot;\u622a\u6b62&quot;):\n            return t\n    return &quot;&quot;\n\ndef extract_fall_behind_metrics(soup: BeautifulSoup) -&gt; dict:\n    weak = {r: [] for r in REGIONS}\n    for d in soup.find_all(&quot;div&quot;, class_=&quot;title_content&quot;):\n        txt = d.get_text(strip=True)\n        m = re.match(r&#039;^\\d+\u3001(.+?)\u53cc\u8fc7\u534a&#039;, txt)\n        if not m: continue\n        metric = m.group(1).strip()\n        for f in d.find_all(&quot;font&quot;, attrs={&quot;color&quot;:&quot;red&quot;}):\n            items = re.findall(r&#039;([\\u4e00-\\u9fa5]+)\\([^)]*\\)&#039;, f.get_text())\n            for region in items:\n                if region in weak:\n                    weak[region].append(metric)\n    return weak\n\ndef extract_top_metrics(soup: BeautifulSoup) -&gt; dict:\n    topm = {r: [] for r in REGIONS}\n    for d in soup.find_all(&quot;div&quot;, class_=&quot;title_content&quot;):\n        txt = d.get_text(strip=True)\n        m = re.match(r&#039;^\\d+\u3001(.+?)\u53cc\u8fc7\u534a&#039;, txt)\n        if not m: continue\n        metric = m.group(1).strip()\n        for f in d.find_all(&quot;font&quot;, attrs={&quot;color&quot;:&quot;blue&quot;}):\n            items = re.findall(r&#039;([\\u4e00-\\u9fa5]+)\\([^)]*\\)&#039;, f.get_text())\n            for region in items:\n                if region in topm:\n                    topm[region].append(metric)\n    return topm\n\n# \u2500\u2500 \u62a5\u8868\u751f\u6210 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\ndef generate_report():\n    today, yesterday = get_dates()\n    url = f&quot;http:\/\/10.XXXXXXXXXXXXXXXXXXXXX&amp;cfg_id=205&amp;day_time={yesterday}&quot;\n    html_raw = fetch_web_content(url)\n    soup = BeautifulSoup(html_raw, &quot;html.parser&quot;)\n    overall = parse_overall_table(soup)\n\n    if &#039;\u6d9f\u6c34&#039; not in overall[&#039;county&#039;].values:\n        raise KeyError(&quot;\u6d9f\u6c34\u884c\u4e0d\u5b58\u5728&quot;)\n\n    parts = []\n    parts.append(f&quot;&lt;p style=&#039;text-align:center;font-size:1.2em;color:#FF0000;&#039;&gt;{extract_deadline_text(soup)}&lt;\/p&gt;&quot;)\n    ls = extract_lianshui(overall)\n    summary = (f&quot;\u6d9f\u6c34\u524d\u4e09\u9879\u76ee\u6570{ls[&#039;front&#039;]}\uff0c\u6392\u540d{ls[&#039;front_rank&#039;]}\uff1b&quot;\n               f&quot;\u540e\u4e09\u9879\u76ee\u6570{ls[&#039;back&#039;]}\uff0c\u6392\u540d{ls[&#039;back_rank&#039;]}\u3002&quot;)\n    parts.append(f&quot;&lt;p style=&#039;text-align:center;font-size:1.2em;font-weight:bold;&#039;&gt;{summary}&lt;\/p&gt;&quot;)\n\n    weak = extract_fall_behind_metrics(soup)\n    maxlen = max(len(v) for v in weak.values())\n    df_weak = pd.DataFrame({r: weak[r]+[&#039;&#039;]*(maxlen-len(weak[r])) for r in REGIONS})\n    df_weak.loc[df_weak.shape[0]] = {r: len(weak[r]) for r in REGIONS}\n    df_weak.index = [f&quot;\u540e\u4e09\u9879\u76ee{i+1}&quot; for i in range(df_weak.shape[0]-1)] + [&quot;\u540e\u4e09\u9879\u76ee\u4e2a\u6570&quot;]\n    parts.append(print_styled_table(df_weak, &quot;\u540e\u4e09\u9879\u76ee\u5217\u8868&quot;))\n\n    topm = extract_top_metrics(soup)\n    maxlen2 = max(len(v) for v in topm.values())\n    df_top = pd.DataFrame({r: topm[r]+[&#039;&#039;]*(maxlen2-len(topm[r])) for r in REGIONS})\n    df_top.loc[df_top.shape[0]] = {r: len(topm[r]) for r in REGIONS}\n    df_top.index = [f&quot;\u524d\u4e09\u9879\u76ee{i+1}&quot; for i in range(df_top.shape[0]-1)] + [&quot;\u524d\u4e09\u9879\u76ee\u4e2a\u6570&quot;]\n    parts.append(print_styled_table(df_top, &quot;\u524d\u4e09\u9879\u76ee\u5217\u8868&quot;))\n\n    matched = {r: [m for m in weak[r] if m in KEY_WEAK] for r in REGIONS}\n    maxlen3 = max(len(v) for v in matched.values())\n    df_matched = pd.DataFrame({r: matched[r]+[&#039;&#039;]*(maxlen3-len(matched[r])) for r in REGIONS})\n    df_matched.loc[df_matched.shape[0]] = {r: len(matched[r]) for r in REGIONS}\n    df_matched.index = [f&quot;\u91cd\u70b9\u5f31\u9879{i+1}&quot; for i in range(df_matched.shape[0]-1)] + [&quot;\u91cd\u70b9\u5f31\u9879\u4e2a\u6570&quot;]\n    parts.append(print_styled_table(df_matched, &quot;\u5468\u8c03\u5ea6\u91cd\u70b9\u5f31\u9879\u5339\u914d\u60c5\u51b5&quot;))\n\n    highlighted = highlight_keyword(html_raw, &quot;\u6d9f\u6c34&quot;)\n    parts.append(&quot;\n&lt;hr&gt;&lt;div style=&#039;margin-top:20px;font-size:13px;&#039;&gt;&lt;strong&gt;\u539f\u59cb\u9875\u9762\u5185\u5bb9\uff08\u542b\u6d9f\u6c34\u9ad8\u4eae\uff09\uff1a&lt;\/strong&gt;&lt;br&gt;&quot;)\n    parts.append(f&quot;&lt;div style=&#039;border:1px solid #ccc; padding:10px;&#039;&gt;{highlighted}&lt;\/div&gt;&lt;\/div&gt;&quot;)\n\n    subject = f&quot;\u3010\u62a2\u5148\u7248\u3011\u653f\u4f01\\&quot;\u53cc\u8fc7\u534a\\&quot;\u901a\u62a5\uff1a{summary}&quot;\n    return subject, &quot;&quot;.join(parts)\n\n# \u2500\u2500 \u8c03\u5ea6\u903b\u8f91 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\ndef job_wrapper():\n    global last_sent_date\n    now = datetime.now()\n    today_str = now.strftime(&quot;%Y%m%d&quot;)\n\n    if now.hour &gt;= 15 and last_sent_date != today_str:\n        try:\n            subject, body = generate_report()\n            send_email(subject, body)\n            last_sent_date = today_str\n            print(f&quot;[{now}] \u62a5\u544a\u5df2\u53d1\u9001\uff0c\u5df2\u6807\u8bb0\u4eca\u65e5\u3002&quot;)\n        except Exception as e:\n            print(f&quot;[{now}] \u62a5\u544a\u751f\u6210\/\u53d1\u9001\u5931\u8d25\uff1a{e}&quot;)\n\ndef main():\n    global last_sent_date\n    now = datetime.now()\n    today_str = now.strftime(&quot;%Y%m%d&quot;)\n\n    try:\n        subject, body = generate_report()\n        send_email(subject, body)\n        if now.hour &gt;= 15:\n            last_sent_date = today_str\n            print(f&quot;[{now}] \u624b\u52a8\u6267\u884c\uff1a\u62a5\u544a\u5df2\u53d1\u9001\u5e76\u6807\u8bb0\u3002&quot;)\n        else:\n            print(f&quot;[{now}] \u624b\u52a8\u6267\u884c\uff1a\u62a5\u544a\u5df2\u53d1\u9001\uff08\u4e0d\u6807\u8bb0\u53d1\u9001\u72b6\u6001\uff09\u3002&quot;)\n    except Exception:\n        print(&quot;\u624b\u52a8\u6267\u884c\uff1a\u8bf7\u7a0d\u7b49\uff0c\u6570\u636e\u6682\u65f6\u6ca1\u51fa\u6765\u3002&quot;)\n\n    schedule.every(5).minutes.do(job_wrapper)\n\n    while True:\n        schedule.run_pending()\n        time.sleep(1)\n\nif __name__ == &#039;__main__&#039;:\n    main()\n<\/code><\/pre>","protected":false},"excerpt":{"rendered":"<p>\u5904\u7406\u53cc\u8fc7\u534a\u901a\u62a5\u6570\u636e\uff0c\u81ea\u52a8\u6458\u5f55\u76f8\u5173\u6570\u636e\u548c\u901a\u62a5\u3002 # Author: subk # Time: 2025\/5\/1  [&hellip;]<\/p>","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[5],"tags":[23],"class_list":["post-520","post","type-post","status-publish","format-standard","hentry","category-it","tag-python"],"_links":{"self":[{"href":"https:\/\/subk.me\/index.php?rest_route=\/wp\/v2\/posts\/520","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/subk.me\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/subk.me\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/subk.me\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/subk.me\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=520"}],"version-history":[{"count":3,"href":"https:\/\/subk.me\/index.php?rest_route=\/wp\/v2\/posts\/520\/revisions"}],"predecessor-version":[{"id":523,"href":"https:\/\/subk.me\/index.php?rest_route=\/wp\/v2\/posts\/520\/revisions\/523"}],"wp:attachment":[{"href":"https:\/\/subk.me\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=520"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/subk.me\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=520"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/subk.me\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=520"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}