[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"site-settings":3,"footer-friends":48,"post-decorator":63,"related-decorator":106,"nav-decorator":285,"comments-14":288,"ad-article_top":292,"ad-article_bottom":294},{"alipayQr":4,"beianNumber":5,"commentsRequireApproval":6,"contactEmail":7,"copyrightNotice":5,"donationEnabled":6,"emailCodeExpireMinutes":8,"enableRegistration":6,"followBilibiliUrl":9,"followGithubUrl":10,"followQqGroup":11,"followQqQr":12,"followWechatQr":13,"footerHtml":5,"gaTrackingId":5,"heroEnabled":6,"heroSearchPlaceholder":5,"heroSubtitle":14,"heroTitle":15,"homeCategoryTabsCount":16,"homePageSize":17,"homeProjectsCount":18,"homeSpecialsCount":19,"homeToolsCount":18,"icpNumber":20,"inviteDefaultQuota":21,"inviteRequired":22,"logoUrl":23,"mailPasswordChangedBody":24,"mailPasswordChangedSubject":25,"mailRegisterCodeBody":26,"mailRegisterCodeSubject":27,"mailResetCodeBody":28,"mailResetCodeSubject":29,"mailWelcomeBody":30,"mailWelcomeSubject":31,"miniappAppid":32,"miniappEnabled":6,"profileAvatar":33,"profileBio":34,"profileName":35,"profileWechatQr":36,"promoteEnabled":6,"promoteNotice":5,"promoteWechat":5,"requireEmailVerify":6,"sectionArticleEnabled":6,"sectionArticleTitle":37,"sectionProjectsEnabled":6,"sectionProjectsTitle":38,"sectionSpecialEnabled":6,"sectionSpecialTitle":39,"sectionToolsEnabled":6,"sectionToolsTitle":40,"seoAuthor":41,"seoBaiduTongjiId":5,"seoBaiduVerify":5,"seoCanonicalEnabled":6,"seoDescription":42,"seoGoogleVerify":5,"seoKeywords":5,"seoOgImage":23,"seoRobots":5,"seoSitemapEnabled":6,"seoTitle":43,"seoTitleTemplate":44,"showHeroStats":6,"showSidebarDonations":6,"showSidebarFeedback":6,"showSidebarFollowUs":6,"showSidebarProfile":6,"showSidebarTagCloud":6,"siteDescription":42,"siteFavicon":45,"siteName":41,"siteUrl":46,"wechatQr":47},"\u002Fstatic\u002Fuploads\u002Fimages\u002F2026\u002F09\u002F29\u002F1790628031878385634.png","","true","enghin110@qq.com","10","https:\u002F\u002Fspace.bilibili.com\u002F470616503","https:\u002F\u002Fgithub.com\u002Fitwuge","606262780","\u002Fstatic\u002Fuploads\u002Fimages\u002F2026\u002F09\u002F29\u002F1790627647190853446.png","\u002Fstatic\u002Fuploads\u002Fimages\u002F2026\u002F09\u002F29\u002F1790627638856258082.jpg","精选 AI 教程、开源项目与实用工具，助你从入门到精通","探索人工智能的无限可能","6","7","4","3","赣ICP备2023014108号-2","5","false","\u002Fimages\u002Flogo\u002Flogo.png","\u003C!DOCTYPE html>\n\u003Chtml lang=\"zh-CN\">\n\u003Chead>\u003Cmeta charset=\"UTF-8\">\u003Cmeta name=\"viewport\" content=\"width=device-width,initial-scale=1.0\">\u003C\u002Fhead>\n\u003Cbody style=\"margin:0;padding:24px 0;background:#f1f5f9;font-family:'Microsoft YaHei','PingFang SC',Helvetica,Arial,sans-serif;\">\n  \u003Ctable role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"background:#f1f5f9;\">\n    \u003Ctr>\n      \u003Ctd align=\"center\" style=\"padding:0 16px;\">\n        \u003Ctable role=\"presentation\" width=\"600\" cellpadding=\"0\" cellspacing=\"0\" style=\"max-width:600px;width:100%;\">\n          \u003C!-- 品牌头部 -->\n          \u003Ctr>\n            \u003Ctd style=\"background:#3b82f6;border-radius:12px 12px 0 0;padding:20px 28px;\">\n              \u003Cspan style=\"font-size:18px;font-weight:bold;color:#ffffff;\">\u003Ca href=\"{site_url}\" style=\"color:#ffffff;text-decoration:none;\">{site_name}\u003C\u002Fa>\u003C\u002Fspan>\n              \u003Cspan style=\"float:right;font-size:12px;color:#dbeafe;line-height:26px;\">{site_description}\u003C\u002Fspan>\n            \u003C\u002Ftd>\n          \u003C\u002Ftr>\n          \u003C!-- 正文卡片 -->\n          \u003Ctr>\n            \u003Ctd style=\"background:#ffffff;padding:32px 28px;border-radius:0 0 12px 12px;box-shadow:0 2px 8px rgba(15,23,42,0.06);\">\n              \n  \u003Ch2 style=\"margin:0 0 16px;font-size:20px;color:#1e293b;\">🔒 账户密码修改成功\u003C\u002Fh2>\n  \u003Cp style=\"margin:0 0 16px;font-size:14px;line-height:1.8;color:#475569;\">\n    你好，\u003Cstrong style=\"color:#1e293b;\">{username}\u003C\u002Fstrong>，你的账户密码已于刚刚修改成功。\n  \u003C\u002Fp>\n  \u003Ctable role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"background:#f8fafc;border-radius:10px;margin:8px 0 4px;\">\n    \u003Ctr>\u003Ctd style=\"padding:14px 18px;font-size:13px;line-height:2;color:#475569;\">\n      账户用户名：\u003Cstrong style=\"color:#1e293b;\">{username}\u003C\u002Fstrong>\u003Cbr>\n      账户邮箱：\u003Cstrong style=\"color:#1e293b;\">{email}\u003C\u002Fstrong>\u003Cbr>\n      修改时间：\u003Cstrong style=\"color:#1e293b;\">{change_time}\u003C\u002Fstrong>\n    \u003C\u002Ftd>\u003C\u002Ftr>\n  \u003C\u002Ftable>\n  \u003Cp style=\"margin:16px 0 0;font-size:13px;line-height:1.8;color:#64748b;\">\n    如果这是你本人的操作，请忽略本邮件；\u003Cstrong style=\"color:#dc2626;\">如果不是你本人操作，说明账户密码可能已泄露\u003C\u002Fstrong>，\n    请立即使用「忘记密码」功能重置密码，并通过页脚联系邮箱告知站长。\n  \u003C\u002Fp>\n  \n  \u003Cp style=\"text-align:center;margin:28px 0 8px;\">\n    \u003Ca href=\"{site_url}\" style=\"display:inline-block;padding:12px 36px;background:#3b82f6;color:#ffffff;font-size:15px;font-weight:bold;text-decoration:none;border-radius:8px;\">进入{site_name}\u003C\u002Fa>\n  \u003C\u002Fp>\n            \u003C\u002Ftd>\n          \u003C\u002Ftr>\n          \u003C!-- 页脚 -->\n          \u003Ctr>\n            \u003Ctd align=\"center\" style=\"padding:20px 16px 0;\">\n              \n              \u003Cp style=\"margin:6px 0 0;font-size:12px;color:#94a3b8;\">联系邮箱：\u003Ca href=\"mailto:{contact_email}\" style=\"color:#64748b;text-decoration:none;\">{contact_email}\u003C\u002Fa>\u003C\u002Fp>\n              \u003Cp style=\"margin:6px 0 0;font-size:12px;color:#94a3b8;\">此邮件由系统自动发送，请勿直接回复 · © {year} {site_name}\u003C\u002Fp>\n            \u003C\u002Ftd>\n          \u003C\u002Ftr>\n        \u003C\u002Ftable>\n      \u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftable>\n\u003C\u002Fbody>\n\u003C\u002Fhtml>","【{site_name}】你的账户密码已修改","\u003C!DOCTYPE html>\n\u003Chtml lang=\"zh-CN\">\n\u003Chead>\u003Cmeta charset=\"UTF-8\">\u003Cmeta name=\"viewport\" content=\"width=device-width,initial-scale=1.0\">\u003C\u002Fhead>\n\u003Cbody style=\"margin:0;padding:24px 0;background:#f1f5f9;font-family:'Microsoft YaHei','PingFang SC',Helvetica,Arial,sans-serif;\">\n  \u003Ctable role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"background:#f1f5f9;\">\n    \u003Ctr>\n      \u003Ctd align=\"center\" style=\"padding:0 16px;\">\n        \u003Ctable role=\"presentation\" width=\"600\" cellpadding=\"0\" cellspacing=\"0\" style=\"max-width:600px;width:100%;\">\n          \u003C!-- 品牌头部 -->\n          \u003Ctr>\n            \u003Ctd style=\"background:#3b82f6;border-radius:12px 12px 0 0;padding:20px 28px;\">\n              \u003Cspan style=\"font-size:18px;font-weight:bold;color:#ffffff;\">\u003Ca href=\"{site_url}\" style=\"color:#ffffff;text-decoration:none;\">{site_name}\u003C\u002Fa>\u003C\u002Fspan>\n              \u003Cspan style=\"float:right;font-size:12px;color:#dbeafe;line-height:26px;\">{site_description}\u003C\u002Fspan>\n            \u003C\u002Ftd>\n          \u003C\u002Ftr>\n          \u003C!-- 正文卡片 -->\n          \u003Ctr>\n            \u003Ctd style=\"background:#ffffff;padding:32px 28px;border-radius:0 0 12px 12px;box-shadow:0 2px 8px rgba(15,23,42,0.06);\">\n              \n  \u003Ch2 style=\"margin:0 0 16px;font-size:20px;color:#1e293b;\">注册邮箱验证码\u003C\u002Fh2>\n  \u003Cp style=\"margin:0 0 16px;font-size:14px;line-height:1.8;color:#475569;\">欢迎注册 \u003Cstrong>{site_name}\u003C\u002Fstrong>！感谢你的加入，请使用下方验证码完成邮箱验证：\u003C\u002Fp>\n  \u003Ctable role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin:8px 0 4px;\">\n    \u003Ctr>\n      \u003Ctd align=\"center\" style=\"background:#f8fafc;border:1px dashed #cbd5e1;border-radius:10px;padding:22px 0;\">\n        \u003Cspan style=\"font-size:32px;font-weight:bold;letter-spacing:10px;color:#3b82f6;font-family:Consolas,Menlo,monospace;\">{code}\u003C\u002Fspan>\n      \u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftable>\n  \u003Cp style=\"margin:12px 0 0;font-size:13px;color:#64748b;\">\n    验证码 \u003Cstrong style=\"color:#dc2626;\">{expire} 分钟\u003C\u002Fstrong> 内有效，为保障账户安全，请勿向他人泄露。\n  \u003C\u002Fp>\n  \u003Chr style=\"border:none;border-top:1px solid #e2e8f0;margin:24px 0;\">\n  \n  \u003Cp style=\"margin:0 0 12px;font-size:14px;line-height:1.8;color:#475569;\">\n    这里是 \u003Cstrong style=\"color:#1e293b;\">{site_name}\u003C\u002Fstrong>，{site_description}\n  \u003C\u002Fp>\n  \u003Ctable role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin:8px 0 4px;\">\n    \u003Ctr>\n      \u003Ctd width=\"33%\" valign=\"top\" style=\"padding:6px;\">\n        \u003Cdiv style=\"background:#eff6ff;border-radius:8px;padding:14px 10px;text-align:center;\">\n          \u003Cdiv style=\"font-size:15px;\">📚\u003C\u002Fdiv>\n          \u003Cdiv style=\"font-size:13px;font-weight:bold;color:#1e40af;margin-top:4px;\">精选教程\u003C\u002Fdiv>\n          \u003Cdiv style=\"font-size:12px;color:#64748b;margin-top:2px;\">从入门到实战\u003C\u002Fdiv>\n        \u003C\u002Fdiv>\n      \u003C\u002Ftd>\n      \u003Ctd width=\"33%\" valign=\"top\" style=\"padding:6px;\">\n        \u003Cdiv style=\"background:#ecfdf5;border-radius:8px;padding:14px 10px;text-align:center;\">\n          \u003Cdiv style=\"font-size:15px;\">🚀\u003C\u002Fdiv>\n          \u003Cdiv style=\"font-size:13px;font-weight:bold;color:#065f46;margin-top:4px;\">开源项目\u003C\u002Fdiv>\n          \u003Cdiv style=\"font-size:12px;color:#64748b;margin-top:2px;\">精选可部署方案\u003C\u002Fdiv>\n        \u003C\u002Fdiv>\n      \u003C\u002Ftd>\n      \u003Ctd width=\"34%\" valign=\"top\" style=\"padding:6px;\">\n        \u003Cdiv style=\"background:#fff7ed;border-radius:8px;padding:14px 10px;text-align:center;\">\n          \u003Cdiv style=\"font-size:15px;\">🧰\u003C\u002Fdiv>\n          \u003Cdiv style=\"font-size:13px;font-weight:bold;color:#9a3412;margin-top:4px;\">在线工具\u003C\u002Fdiv>\n          \u003Cdiv style=\"font-size:12px;color:#64748b;margin-top:2px;\">即开即用提效\u003C\u002Fdiv>\n        \u003C\u002Fdiv>\n      \u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftable>\n  \u003Cp style=\"margin:20px 0 0;font-size:12px;line-height:1.8;color:#94a3b8;\">\n    如果这不是你本人的操作，请忽略本邮件，你的账户安全不会受到影响。\n  \u003C\u002Fp>\n            \u003C\u002Ftd>\n          \u003C\u002Ftr>\n          \u003C!-- 页脚 -->\n          \u003Ctr>\n            \u003Ctd align=\"center\" style=\"padding:20px 16px 0;\">\n              \n              \u003Cp style=\"margin:6px 0 0;font-size:12px;color:#94a3b8;\">联系邮箱：\u003Ca href=\"mailto:{contact_email}\" style=\"color:#64748b;text-decoration:none;\">{contact_email}\u003C\u002Fa>\u003C\u002Fp>\n              \u003Cp style=\"margin:6px 0 0;font-size:12px;color:#94a3b8;\">此邮件由系统自动发送，请勿直接回复 · © {year} {site_name}\u003C\u002Fp>\n            \u003C\u002Ftd>\n          \u003C\u002Ftr>\n        \u003C\u002Ftable>\n      \u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftable>\n\u003C\u002Fbody>\n\u003C\u002Fhtml>","【{site_name}】注册验证码","\u003C!DOCTYPE html>\n\u003Chtml lang=\"zh-CN\">\n\u003Chead>\u003Cmeta charset=\"UTF-8\">\u003Cmeta name=\"viewport\" content=\"width=device-width,initial-scale=1.0\">\u003C\u002Fhead>\n\u003Cbody style=\"margin:0;padding:24px 0;background:#f1f5f9;font-family:'Microsoft YaHei','PingFang SC',Helvetica,Arial,sans-serif;\">\n  \u003Ctable role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"background:#f1f5f9;\">\n    \u003Ctr>\n      \u003Ctd align=\"center\" style=\"padding:0 16px;\">\n        \u003Ctable role=\"presentation\" width=\"600\" cellpadding=\"0\" cellspacing=\"0\" style=\"max-width:600px;width:100%;\">\n          \u003C!-- 品牌头部 -->\n          \u003Ctr>\n            \u003Ctd style=\"background:#3b82f6;border-radius:12px 12px 0 0;padding:20px 28px;\">\n              \u003Cspan style=\"font-size:18px;font-weight:bold;color:#ffffff;\">\u003Ca href=\"{site_url}\" style=\"color:#ffffff;text-decoration:none;\">{site_name}\u003C\u002Fa>\u003C\u002Fspan>\n              \u003Cspan style=\"float:right;font-size:12px;color:#dbeafe;line-height:26px;\">{site_description}\u003C\u002Fspan>\n            \u003C\u002Ftd>\n          \u003C\u002Ftr>\n          \u003C!-- 正文卡片 -->\n          \u003Ctr>\n            \u003Ctd style=\"background:#ffffff;padding:32px 28px;border-radius:0 0 12px 12px;box-shadow:0 2px 8px rgba(15,23,42,0.06);\">\n              \n  \u003Ch2 style=\"margin:0 0 16px;font-size:20px;color:#1e293b;\">找回密码验证码\u003C\u002Fh2>\n  \u003Cp style=\"margin:0 0 16px;font-size:14px;line-height:1.8;color:#475569;\">你正在找回账户密码，请使用下方验证码完成重置操作：\u003C\u002Fp>\n  \u003Ctable role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin:8px 0 4px;\">\n    \u003Ctr>\n      \u003Ctd align=\"center\" style=\"background:#f8fafc;border:1px dashed #cbd5e1;border-radius:10px;padding:22px 0;\">\n        \u003Cspan style=\"font-size:32px;font-weight:bold;letter-spacing:10px;color:#3b82f6;font-family:Consolas,Menlo,monospace;\">{code}\u003C\u002Fspan>\n      \u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftable>\n  \u003Cp style=\"margin:12px 0 0;font-size:13px;color:#64748b;\">\n    验证码 \u003Cstrong style=\"color:#dc2626;\">{expire} 分钟\u003C\u002Fstrong> 内有效，为保障账户安全，请勿向他人泄露。\n  \u003C\u002Fp>\n  \u003Chr style=\"border:none;border-top:1px solid #e2e8f0;margin:24px 0;\">\n  \n  \u003Cp style=\"margin:0 0 12px;font-size:14px;line-height:1.8;color:#475569;\">\n    这里是 \u003Cstrong style=\"color:#1e293b;\">{site_name}\u003C\u002Fstrong>，{site_description}\n  \u003C\u002Fp>\n  \u003Ctable role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin:8px 0 4px;\">\n    \u003Ctr>\n      \u003Ctd width=\"33%\" valign=\"top\" style=\"padding:6px;\">\n        \u003Cdiv style=\"background:#eff6ff;border-radius:8px;padding:14px 10px;text-align:center;\">\n          \u003Cdiv style=\"font-size:15px;\">📚\u003C\u002Fdiv>\n          \u003Cdiv style=\"font-size:13px;font-weight:bold;color:#1e40af;margin-top:4px;\">精选教程\u003C\u002Fdiv>\n          \u003Cdiv style=\"font-size:12px;color:#64748b;margin-top:2px;\">从入门到实战\u003C\u002Fdiv>\n        \u003C\u002Fdiv>\n      \u003C\u002Ftd>\n      \u003Ctd width=\"33%\" valign=\"top\" style=\"padding:6px;\">\n        \u003Cdiv style=\"background:#ecfdf5;border-radius:8px;padding:14px 10px;text-align:center;\">\n          \u003Cdiv style=\"font-size:15px;\">🚀\u003C\u002Fdiv>\n          \u003Cdiv style=\"font-size:13px;font-weight:bold;color:#065f46;margin-top:4px;\">开源项目\u003C\u002Fdiv>\n          \u003Cdiv style=\"font-size:12px;color:#64748b;margin-top:2px;\">精选可部署方案\u003C\u002Fdiv>\n        \u003C\u002Fdiv>\n      \u003C\u002Ftd>\n      \u003Ctd width=\"34%\" valign=\"top\" style=\"padding:6px;\">\n        \u003Cdiv style=\"background:#fff7ed;border-radius:8px;padding:14px 10px;text-align:center;\">\n          \u003Cdiv style=\"font-size:15px;\">🧰\u003C\u002Fdiv>\n          \u003Cdiv style=\"font-size:13px;font-weight:bold;color:#9a3412;margin-top:4px;\">在线工具\u003C\u002Fdiv>\n          \u003Cdiv style=\"font-size:12px;color:#64748b;margin-top:2px;\">即开即用提效\u003C\u002Fdiv>\n        \u003C\u002Fdiv>\n      \u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftable>\n  \u003Cp style=\"margin:20px 0 0;font-size:12px;line-height:1.8;color:#94a3b8;\">\n    如果这不是你本人的操作，请忽略本邮件，你的账户安全不会受到影响。\n  \u003C\u002Fp>\n            \u003C\u002Ftd>\n          \u003C\u002Ftr>\n          \u003C!-- 页脚 -->\n          \u003Ctr>\n            \u003Ctd align=\"center\" style=\"padding:20px 16px 0;\">\n              \n              \u003Cp style=\"margin:6px 0 0;font-size:12px;color:#94a3b8;\">联系邮箱：\u003Ca href=\"mailto:{contact_email}\" style=\"color:#64748b;text-decoration:none;\">{contact_email}\u003C\u002Fa>\u003C\u002Fp>\n              \u003Cp style=\"margin:6px 0 0;font-size:12px;color:#94a3b8;\">此邮件由系统自动发送，请勿直接回复 · © {year} {site_name}\u003C\u002Fp>\n            \u003C\u002Ftd>\n          \u003C\u002Ftr>\n        \u003C\u002Ftable>\n      \u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftable>\n\u003C\u002Fbody>\n\u003C\u002Fhtml>","【{site_name}】找回密码验证码","\u003C!DOCTYPE html>\n\u003Chtml lang=\"zh-CN\">\n\u003Chead>\u003Cmeta charset=\"UTF-8\">\u003Cmeta name=\"viewport\" content=\"width=device-width,initial-scale=1.0\">\u003C\u002Fhead>\n\u003Cbody style=\"margin:0;padding:24px 0;background:#f1f5f9;font-family:'Microsoft YaHei','PingFang SC',Helvetica,Arial,sans-serif;\">\n  \u003Ctable role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"background:#f1f5f9;\">\n    \u003Ctr>\n      \u003Ctd align=\"center\" style=\"padding:0 16px;\">\n        \u003Ctable role=\"presentation\" width=\"600\" cellpadding=\"0\" cellspacing=\"0\" style=\"max-width:600px;width:100%;\">\n          \u003C!-- 品牌头部 -->\n          \u003Ctr>\n            \u003Ctd style=\"background:#3b82f6;border-radius:12px 12px 0 0;padding:20px 28px;\">\n              \u003Cspan style=\"font-size:18px;font-weight:bold;color:#ffffff;\">\u003Ca href=\"{site_url}\" style=\"color:#ffffff;text-decoration:none;\">{site_name}\u003C\u002Fa>\u003C\u002Fspan>\n              \u003Cspan style=\"float:right;font-size:12px;color:#dbeafe;line-height:26px;\">{site_description}\u003C\u002Fspan>\n            \u003C\u002Ftd>\n          \u003C\u002Ftr>\n          \u003C!-- 正文卡片 -->\n          \u003Ctr>\n            \u003Ctd style=\"background:#ffffff;padding:32px 28px;border-radius:0 0 12px 12px;box-shadow:0 2px 8px rgba(15,23,42,0.06);\">\n              \n  \u003Ch2 style=\"margin:0 0 4px;font-size:20px;color:#1e293b;\">🎉 欢迎加入 {site_name}！\u003Cspan style=\"display:inline-block;margin-left:8px;padding:2px 8px;background:#dcfce7;color:#15803d;font-size:12px;border-radius:10px;vertical-align:middle;\">邮箱已验证\u003C\u002Fspan>\u003C\u002Fh2>\n  \u003Cp style=\"margin:0 0 16px;font-size:14px;line-height:1.8;color:#475569;\">\n    你好，\u003Cstrong style=\"color:#1e293b;\">{username}\u003C\u002Fstrong>，你的账户已注册成功。\n  \u003C\u002Fp>\n  \u003Ctable role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"background:#f8fafc;border-radius:10px;margin:8px 0 4px;\">\n    \u003Ctr>\u003Ctd style=\"padding:14px 18px;font-size:13px;line-height:2;color:#475569;\">\n      账户用户名：\u003Cstrong style=\"color:#1e293b;\">{username}\u003C\u002Fstrong>\u003Cbr>\n      注册邮箱：\u003Cstrong style=\"color:#1e293b;\">{email}\u003C\u002Fstrong>\u003Cbr>\n      注册奖励：\u003Cstrong style=\"color:#dc2626;\">+{register_points} 积分\u003C\u002Fstrong>已发放\n    \u003C\u002Ftd>\u003C\u002Ftr>\n  \u003C\u002Ftable>\n  \u003Chr style=\"border:none;border-top:1px solid #e2e8f0;margin:24px 0;\">\n  \n  \u003Cp style=\"margin:0 0 12px;font-size:14px;line-height:1.8;color:#475569;\">\n    这里是 \u003Cstrong style=\"color:#1e293b;\">{site_name}\u003C\u002Fstrong>，{site_description}\n  \u003C\u002Fp>\n  \u003Ctable role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin:8px 0 4px;\">\n    \u003Ctr>\n      \u003Ctd width=\"33%\" valign=\"top\" style=\"padding:6px;\">\n        \u003Cdiv style=\"background:#eff6ff;border-radius:8px;padding:14px 10px;text-align:center;\">\n          \u003Cdiv style=\"font-size:15px;\">📚\u003C\u002Fdiv>\n          \u003Cdiv style=\"font-size:13px;font-weight:bold;color:#1e40af;margin-top:4px;\">精选教程\u003C\u002Fdiv>\n          \u003Cdiv style=\"font-size:12px;color:#64748b;margin-top:2px;\">从入门到实战\u003C\u002Fdiv>\n        \u003C\u002Fdiv>\n      \u003C\u002Ftd>\n      \u003Ctd width=\"33%\" valign=\"top\" style=\"padding:6px;\">\n        \u003Cdiv style=\"background:#ecfdf5;border-radius:8px;padding:14px 10px;text-align:center;\">\n          \u003Cdiv style=\"font-size:15px;\">🚀\u003C\u002Fdiv>\n          \u003Cdiv style=\"font-size:13px;font-weight:bold;color:#065f46;margin-top:4px;\">开源项目\u003C\u002Fdiv>\n          \u003Cdiv style=\"font-size:12px;color:#64748b;margin-top:2px;\">精选可部署方案\u003C\u002Fdiv>\n        \u003C\u002Fdiv>\n      \u003C\u002Ftd>\n      \u003Ctd width=\"34%\" valign=\"top\" style=\"padding:6px;\">\n        \u003Cdiv style=\"background:#fff7ed;border-radius:8px;padding:14px 10px;text-align:center;\">\n          \u003Cdiv style=\"font-size:15px;\">🧰\u003C\u002Fdiv>\n          \u003Cdiv style=\"font-size:13px;font-weight:bold;color:#9a3412;margin-top:4px;\">在线工具\u003C\u002Fdiv>\n          \u003Cdiv style=\"font-size:12px;color:#64748b;margin-top:2px;\">即开即用提效\u003C\u002Fdiv>\n        \u003C\u002Fdiv>\n      \u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftable>\n  \u003Cp style=\"margin:16px 0 0;font-size:14px;line-height:1.8;color:#475569;\">登录后你可以：\u003C\u002Fp>\n  \u003Cul style=\"margin:8px 0 0;padding-left:22px;font-size:14px;line-height:2;color:#475569;\">\n    \u003Cli>浏览、收藏优质内容与实用工具\u003C\u002Fli>\n    \u003Cli>投稿教程与项目，审核通过后获得积分奖励\u003C\u002Fli>\n    \u003Cli>参与评论、打分，与其他用户交流\u003C\u002Fli>\n    \u003Cli>通过签到、投稿、互动持续累积积分与等级\u003C\u002Fli>\n  \u003C\u002Ful>\n  \n  \u003Cp style=\"text-align:center;margin:28px 0 8px;\">\n    \u003Ca href=\"{site_url}\" style=\"display:inline-block;padding:12px 36px;background:#3b82f6;color:#ffffff;font-size:15px;font-weight:bold;text-decoration:none;border-radius:8px;\">进入{site_name}\u003C\u002Fa>\n  \u003C\u002Fp>\n            \u003C\u002Ftd>\n          \u003C\u002Ftr>\n          \u003C!-- 页脚 -->\n          \u003Ctr>\n            \u003Ctd align=\"center\" style=\"padding:20px 16px 0;\">\n              \n              \u003Cp style=\"margin:6px 0 0;font-size:12px;color:#94a3b8;\">联系邮箱：\u003Ca href=\"mailto:{contact_email}\" style=\"color:#64748b;text-decoration:none;\">{contact_email}\u003C\u002Fa>\u003C\u002Fp>\n              \u003Cp style=\"margin:6px 0 0;font-size:12px;color:#94a3b8;\">此邮件由系统自动发送，请勿直接回复 · © {year} {site_name}\u003C\u002Fp>\n            \u003C\u002Ftd>\n          \u003C\u002Ftr>\n        \u003C\u002Ftable>\n      \u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftable>\n\u003C\u002Fbody>\n\u003C\u002Fhtml>","【{site_name}】欢迎加入{site_name}，账户注册成功","7111479@qq.com","\u002Fstatic\u002Fuploads\u002Fimages\u002F2026\u002F09\u002F29\u002F1790627623420492239.jpg","全栈开发者，专注 AI 应用与效率工具","武哥","\u002Fstatic\u002Fuploads\u002Fimages\u002F2026\u002F09\u002F29\u002F1790629453503799753.png","热门爆款","精选项目","精选专题","精选工具","AI导航工具站","精选优质 AI 教程、AI 工具与开源项目，助力开发者快速上手人工智能","AI导航工具站 - AI 教程 · AI 项目 · AI 工具","%s - AI导航工具站","\u002Fimages\u002Flogo\u002Ffavicon.ico","https:\u002F\u002Fwuge.fun","\u002Fstatic\u002Fuploads\u002Fimages\u002F2026\u002F09\u002F29\u002F1790628026196296604.png",[49,58],{"id":50,"name":51,"url":52,"logo":53,"description":51,"sortOrder":54,"status":55,"createdAt":56,"updatedAt":57},2,"哔哩哔哩","https:\u002F\u002Fwww.bilibili.com\u002F","\\images\\social\\bilibili.png",0,1,"0001-01-01T00:00:00Z","2026-09-29T04:44:41.867019475+08:00",{"id":55,"name":59,"url":60,"logo":61,"description":59,"sortOrder":54,"status":55,"createdAt":62,"updatedAt":62},"GitHub","https:\u002F\u002Fgithub.com","\\images\\social\\github.png","2026-09-29T04:32:39.65740234+08:00",{"id":64,"title":65,"slug":66,"summary":67,"content":68,"coverImage":69,"categoryId":70,"authorId":55,"viewCount":54,"likeCount":54,"commentCount":54,"isTop":54,"isRecommend":54,"status":55,"rejectReason":5,"publishedAt":71,"createdAt":72,"updatedAt":73,"category":74,"tags":78,"author":99,"wordCount":105,"readingTime":50},14,"Python 装饰器详解：从原理到实战的 5 个经典用法","decorator","装饰器是 Python 最优雅也最劝退的语法。从闭包原理讲起，用计时、缓存、权限校验等 5 个真实场景把装饰器彻底讲透。","## 装饰器解决什么问题\n\n先看一个真实痛点。项目里有一堆函数，现在要给**每一个**都加上「记录执行时间」的功能：\n\n```python\ndef get_user_list():\n    # ... 业务逻辑\n    pass\n\ndef create_order():\n    # ... 业务逻辑\n    pass\n```\n\n笨办法：每个函数内部都加两行计时代码。20 个函数改 20 处，以后要加「记录日志」再改 20 处——烦且容易漏。\n\n装饰器的思路：**写一个「包装机」，把原函数包一层，在不改原函数代码的前提下增加新功能**。\n\n```python\n@timer   # 这一行就是装饰器，自动给函数加上计时功能\ndef get_user_list():\n    pass\n```\n\n## 原理：装饰器就是「函数套函数」\n\n理解装饰器只需要知道两件事：\n\n1. **函数可以当参数传递**，也可以当返回值返回\n2. **闭包**：内部函数可以记住外部函数的变量\n\n最简装饰器长这样：\n\n```python\nimport functools\nimport time\n\ndef timer(func):\n    \"\"\"装饰器：统计函数执行时间\"\"\"\n    @functools.wraps(func)  # 保留原函数的名字和文档，好习惯\n    def wrapper(*args, **kwargs):\n        start = time.time()\n        result = func(*args, **kwargs)   # 执行原函数\n        print(f\"{func.__name__} 耗时 {time.time() - start:.3f} 秒\")\n        return result\n    return wrapper\n\n@timer\ndef slow_function():\n    time.sleep(1)\n\nslow_function()  # 输出：slow_function 耗时 1.002 秒\n```\n\n`@timer` 这行代码等价于 `slow_function = timer(slow_function)`——把原函数传进 timer，用返回的 wrapper 替换掉原来的名字。之后每次调用 `slow_function()`，实际执行的是包装后的 wrapper。\n\n## 实战 1：日志记录\n\n```python\nimport functools\nimport logging\n\nlogging.basicConfig(level=logging.INFO, format=\"%(asctime)s %(message)s\")\n\ndef log_calls(func):\n    \"\"\"记录函数调用的参数和返回值\"\"\"\n    @functools.wraps(func)\n    def wrapper(*args, **kwargs):\n        logging.info(f\"调用 {func.__name__}，args={args}, kwargs={kwargs}\")\n        result = func(*args, **kwargs)\n        logging.info(f\"{func.__name__} 返回 {result}\")\n        return result\n    return wrapper\n\n@log_calls\ndef add(a, b):\n    return a + b\n\nadd(3, 5)\n# 输出两行日志：调用 add，args=(3, 5), kwargs={} \u002F add 返回 8\n```\n\n## 实战 2：缓存（记忆化）\n\n递归算斐波那契数列，不加缓存时 fib(35) 要算好几秒，因为重复计算了大量子问题。加个缓存装饰器：\n\n```python\nimport functools\n\ndef cache(func):\n    \"\"\"把算过的结果存起来，相同参数直接返回\"\"\"\n    saved = {}\n    @functools.wraps(func)\n    def wrapper(*args):\n        if args not in saved:\n            saved[args] = func(*args)\n        return saved[args]\n    return wrapper\n\n@cache\ndef fib(n):\n    if n \u003C 2:\n        return n\n    return fib(n - 1) + fib(n - 2)\n\nprint(fib(100))  # 秒出结果\n```\n\n**其实不用自己写**：标准库 `functools.lru_cache` 就是现成的缓存装饰器，还带容量淘汰策略：\n\n```python\nfrom functools import lru_cache\n\n@lru_cache(maxsize=128)\ndef fib(n):\n    if n \u003C 2:\n        return n\n    return fib(n - 1) + fib(n - 2)\n```\n\n## 实战 3：权限校验（Web 开发天天用）\n\n```python\nimport functools\n\n# 模拟当前登录用户\ncurrent_user = {\"name\": \"张三\", \"role\": \"admin\"}\n\ndef require_role(role):\n    \"\"\"带参数的装饰器：要求特定角色才能访问\"\"\"\n    def decorator(func):\n        @functools.wraps(func)\n        def wrapper(*args, **kwargs):\n            if current_user.get(\"role\") != role:\n                raise PermissionError(f\"需要 {role} 权限\")\n            return func(*args, **kwargs)\n        return wrapper\n    return decorator\n\n@require_role(\"admin\")\ndef delete_all_data():\n    print(\"数据已删除\")\n\ndelete_all_data()  # 正常执行\n# 把 current_user[\"role\"] 改成 \"guest\" 再调用 → PermissionError: 需要 admin 权限\n```\n\n注意这里变成了**三层函数嵌套**：`require_role(\"admin\")` 先拿到参数返回装饰器，装饰器再接收函数。规律：**装饰器带参数，就多套一层**。\n\nFlask 里的 `@app.route(\"\u002Fapi\")`、登录校验 `@login_required`，全都是这个模式。\n\n## 实战 4：重试机制\n\n调用不稳定的网络接口时特别好用：\n\n```python\nimport functools\nimport time\n\ndef retry(times=3, delay=1):\n    \"\"\"失败自动重试\"\"\"\n    def decorator(func):\n        @functools.wraps(func)\n        def wrapper(*args, **kwargs):\n            for attempt in range(1, times + 1):\n                try:\n                    return func(*args, **kwargs)\n                except Exception as e:\n                    print(f\"第 {attempt} 次失败：{e}\")\n                    if attempt == times:\n                        raise\n                    time.sleep(delay)\n        return wrapper\n    return decorator\n\n@retry(times=3, delay=2)\ndef call_unstable_api():\n    import random\n    if random.random() \u003C 0.7:   # 70% 概率失败，模拟不稳定接口\n        raise ConnectionError(\"网络超时\")\n    return \"成功\"\n```\n\n## 实战 5：函数注册表（进阶但极其有用）\n\n插件系统、命令分发器的标准写法：\n\n```python\nCOMMANDS = {}  # 命令注册表\n\ndef register(name):\n    \"\"\"把函数注册到命令表\"\"\"\n    def decorator(func):\n        COMMANDS[name] = func\n        return func   # 不改函数行为，只登记\n    return decorator\n\n@register(\"hello\")\ndef cmd_hello():\n    print(\"你好！\")\n\n@register(\"bye\")\ndef cmd_bye():\n    print(\"再见！\")\n\n# 根据用户输入动态调用\nuser_input = \"hello\"\nCOMMANDS[user_input]()   # 输出：你好！\n```\n\n新增命令只需要加新函数并装饰，调用处的分发逻辑一行不用改——这就是「开放封闭原则」。\n\n## 避坑指南\n\n**1. 别忘了 @functools.wraps**\n\n不加的话，被装饰函数的 `__name__`、`__doc__` 都会变成 wrapper 的，调试和文档生成时会错乱。\n\n**2. 装饰顺序很重要**\n\n```python\n@timer\n@log_calls\ndef f(): ...\n# 等价于 f = timer(log_calls(f))\n# 执行顺序：先 timer 的包装，进去后先记日志再执行，出来后计时\n```\n\n**3. 装饰器在导入时就执行**\n\n`@register` 这类装饰器在**模块导入时**就跑完了，不是调用时才跑。利用这个特性做自动注册，也要注意别在装饰器里写耗时操作拖慢启动。\n\n## 写在最后\n\n装饰器的学习路径：先会用 `@lru_cache`、`@app.route` 这些现成的 → 再读懂别人的装饰器 → 最后才会写自己的。别反过来。先把本文的 5 个例子各敲一遍，用到的场景出现时，你自然会长出「这里该用装饰器」的直觉。\n","\u002Fstatic\u002Fuploads\u002Fimages\u002F2026\u002F09\u002F29\u002F1790636138476604049.png",8,"2026-09-29T06:56:39.89932248+08:00","2026-09-29T06:55:38.476769783+08:00","2026-09-29T06:56:39.899403489+08:00",{"id":70,"name":75,"slug":76,"description":75,"icon":5,"sortOrder":54,"postCount":54,"parentId":54,"createdAt":77,"updatedAt":77},"Python专题","Python","2026-09-29T06:55:05.938571812+08:00",[79,84,89,94],{"id":80,"name":76,"slug":81,"color":82,"postCount":54,"createdAt":83},6,"python","#e2e8f0","2026-09-29T04:25:30.347030306+08:00",{"id":85,"name":86,"slug":87,"color":82,"postCount":54,"createdAt":88},129,"装饰器","zhuang-shi-qi","2026-09-29T06:55:38.452280181+08:00",{"id":90,"name":91,"slug":92,"color":82,"postCount":54,"createdAt":93},130,"闭包","bi-bao","2026-09-29T06:55:38.460791287+08:00",{"id":95,"name":96,"slug":97,"color":82,"postCount":54,"createdAt":98},131,"进阶","jin-jie","2026-09-29T06:55:38.468837931+08:00",{"id":55,"username":100,"nickname":101,"avatarUrl":102,"bio":5,"rewardWechatQr":103,"rewardAlipayQr":104},"wuge","站长","\u002Fstatic\u002Fuploads\u002Favatars\u002F2026\u002F09\u002F29\u002F1790630804049538805.jpg","\u002Fstatic\u002Fuploads\u002Fimages\u002F2026\u002F09\u002F29\u002F1790630754672518549.png","\u002Fstatic\u002Fuploads\u002Fimages\u002F2026\u002F09\u002F29\u002F1790630762294788382.png",437,[107,136,168,195,227,255],{"id":108,"title":109,"slug":110,"summary":111,"content":112,"coverImage":113,"categoryId":70,"authorId":55,"viewCount":55,"likeCount":54,"commentCount":54,"isTop":54,"isRecommend":54,"status":55,"rejectReason":5,"publishedAt":114,"createdAt":115,"updatedAt":116,"category":117,"tags":118},11,"Python 虚拟环境完全指南：venv、conda、uv 怎么选","venv","为什么每个 Python 项目都需要虚拟环境？venv、conda、uv 三大方案深度对比，附常见坑排查清单，看完彻底告别依赖地狱。","## 为什么需要虚拟环境\n\n假设你同时在开发两个项目：\n\n- 项目 A 是去年写的，依赖 `Django 3.2`\n- 项目 B 是新项目，要用 `Django 5.0`\n\nPython 默认把第三方包装在**同一个全局目录**里。装了 Django 5.0，项目 A 直接跑不起来；退回 3.2，项目 B 又报错。这就是「依赖地狱」。\n\n虚拟环境的解决思路很直接：**给每个项目造一个独立的 Python 运行空间**，各装各的包，互不干扰。\n\n```text\n电脑\n├── 全局 Python 3.12\n├── 项目A\u002Fvenv\u002F   ← Django 3.2\n├── 项目B\u002Fvenv\u002F   ← Django 5.0\n└── 项目C\u002Fvenv\u002F   ← 一堆 AI 库\n```\n\n## 方案一：venv（官方内置，默认首选）\n\nPython 3.3+ 自带，零安装，适合 90% 的纯 Python 项目。\n\n**创建与激活**：\n\n```bash\n# 在项目目录下创建虚拟环境（目录名约定俗成叫 .venv 或 venv）\npython -m venv .venv\n\n# Windows 激活\n.venv\\Scripts\\activate\n\n# macOS\u002FLinux 激活\nsource .venv\u002Fbin\u002Factivate\n```\n\n激活成功的标志：命令行提示符前面出现 `(.venv)`。\n\n**日常使用**：\n\n```bash\n# 装包（装在虚拟环境里，不污染全局）\npip install requests pandas\n\n# 导出依赖清单（项目交接、部署时用）\npip freeze > requirements.txt\n\n# 别人拿到项目后还原环境\npip install -r requirements.txt\n\n# 退出虚拟环境\ndeactivate\n```\n\n**优点**：官方标准、零依赖、任何 Python 环境都有。\n\n**缺点**：只能管理 Python 包，管不了 CUDA、MySQL 客户端这类系统级依赖；不能切换 Python 版本（venv 创建时用什么版本就是什么版本）。\n\n## 方案二：conda（数据科学标配）\n\nAnaconda\u002FMiniconda 提供的环境管理器，**不仅能管 Python 包，还能管 Python 解释器本身和非 Python 依赖**。\n\n**常用命令**：\n\n```bash\n# 创建环境，同时指定 Python 版本\nconda create -n myproject python=3.11\n\n# 激活 \u002F 退出\nconda activate myproject\nconda deactivate\n\n# 装包\nconda install numpy pandas\n# conda 仓库没有的包，可以混用 pip\npip install some-package\n\n# 导出与还原\nconda env export > environment.yml\nconda env create -f environment.yml\n\n# 查看所有环境 \u002F 删除环境\nconda env list\nconda remove -n myproject --all\n```\n\n**建议装 Miniconda 而不是 Anaconda**：Anaconda 预装了几百个包，体积 3G+；Miniconda 只有核心，按需安装。\n\n**什么时候必须选 conda**：需要特定版本的 CUDA\u002FcuDNN（深度学习训练）、需要 GDAL 这类难装的地理信息库、需要在同一台机器维护多个 Python 版本。\n\n## 方案三：uv（2024 年后的新王）\n\nAstral 公司（ruff 的作者）用 Rust 写的包管理器，**速度比 pip 快 10-100 倍**，正在迅速成为新标准。\n\n```bash\n# 安装 uv（Windows PowerShell）\npowershell -c \"irm https:\u002F\u002Fastral.sh\u002Fuv\u002Finstall.ps1 | iex\"\n\n# 创建项目（自动生成 .venv、pyproject.toml）\nuv init myproject\ncd myproject\n\n# 装包（秒装，第一次用会惊到）\nuv add requests pandas\n\n# 运行脚本（自动用项目环境，不用手动激活）\nuv run main.py\n\n# 锁定依赖（生成 uv.lock，保证全团队环境一致）\nuv lock\n```\n\nuv 最大的改进是**把 venv、pip、pyproject.toml、锁定文件整合成一套连贯的工作流**，以前要四五个工具配合的事，现在一个命令搞定。\n\n## 怎么选：一张决策表\n\n| 你的情况 | 推荐 |\n|---------|------|\n| 纯 Python 项目，图省心 | venv |\n| 深度学习\u002F数据科学，要 CUDA | conda |\n| 新项目，追求速度和现代工作流 | uv |\n| 团队协作、要锁定依赖版本 | uv |\n| 老旧项目维护 | 跟着项目原来的方案走 |\n\n**新手建议**：从 venv 学起理解原理，新项目直接上 uv，碰到 CUDA 相关的再上 conda。三者概念相通，切换成本很低。\n\n## 常见坑排查清单\n\n**1. 装了包却提示 ModuleNotFoundError**\n\n90% 的原因是：包装在了 A 环境，程序却在 B 环境运行。排查：\n\n```bash\n# 看当前 python 在哪\npython -c \"import sys; print(sys.executable)\"\n\n# 看包装在了哪\npip show 包名 | findstr Location\n```\n\n两个路径对不上就是环境问题。\n\n**2. Windows 激活脚本报错「禁止运行脚本」**\n\nPowerShell 执行策略限制，管理员身份运行一次：\n\n```powershell\nSet-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser\n```\n\n**3. 虚拟环境目录要不要提交 Git**\n\n**不要**。`.venv\u002F` 动辄几百 MB，应该加进 `.gitignore`，用 `requirements.txt` 或 `uv.lock` 传递依赖信息。\n\n**4. IDE 里代码飘红但命令行能跑**\n\nIDE 用的解释器和命令行不是同一个。VS Code 按 `Ctrl+Shift+P` → `Python: Select Interpreter`，选项目 `.venv` 里的那个。\n\n**5. requirements.txt 写成一团乱**\n\n手动维护比 `pip freeze` 更好——freeze 会把所有间接依赖都写进去。推荐只写直接依赖：\n\n```text\nrequests>=2.31\npandas>=2.0\n```\n\n## 写在最后\n\n虚拟环境是 Python 工程化的第一课。养成习惯：**每个新项目第一件事就是建虚拟环境**。项目越多的老司机越清楚——今天你省下的 30 秒，就是明天某次神秘报错时要还的债。\n","\u002Fstatic\u002Fuploads\u002Fimages\u002F2026\u002F09\u002F29\u002F1790636138336381447.png","2026-09-29T06:56:46.91774372+08:00","2026-09-29T06:55:38.336536895+08:00","2026-09-29T06:56:46.917805846+08:00",{"id":70,"name":75,"slug":76,"description":75,"icon":5,"sortOrder":54,"postCount":54,"parentId":54,"createdAt":77,"updatedAt":77},[119,120,125,128,132],{"id":80,"name":76,"slug":81,"color":82,"postCount":54,"createdAt":83},{"id":121,"name":122,"slug":123,"color":82,"postCount":54,"createdAt":124},118,"虚拟环境","xu-ni-huan-jing","2026-09-29T06:55:38.308255024+08:00",{"id":126,"name":110,"slug":110,"color":82,"postCount":54,"createdAt":127},119,"2026-09-29T06:55:38.316270691+08:00",{"id":129,"name":130,"slug":130,"color":82,"postCount":54,"createdAt":131},120,"conda","2026-09-29T06:55:38.323353011+08:00",{"id":133,"name":134,"slug":134,"color":82,"postCount":54,"createdAt":135},121,"uv","2026-09-29T06:55:38.330626142+08:00",{"id":137,"title":138,"slug":139,"summary":140,"content":141,"coverImage":142,"categoryId":70,"authorId":55,"viewCount":54,"likeCount":54,"commentCount":54,"isTop":54,"isRecommend":54,"status":55,"rejectReason":5,"publishedAt":143,"createdAt":144,"updatedAt":145,"category":146,"tags":147},12,"Python requests 爬虫入门：从发送请求到解析 HTML","requests-spider","Python 爬虫第一课：用 requests + BeautifulSoup 抓取网页数据。从 GET\u002FPOST 请求、请求头伪装到数据提取，附完整实战案例和反爬应对策略。","## 先立规矩：合法合规爬取\n\n写代码之前必须说清楚：\n\n- 只爬**公开数据**，不碰需要登录才能看的内容\n- 遵守网站的 `robots.txt` 协议（在域名后加 `\u002Frobots.txt` 可查看）\n- 控制频率，别把人家服务器压垮（就是讲礼貌）\n- 数据仅供个人学习，不用于商业用途，不爬个人隐私信息\n\n技术无罪，用法有责。下面的练习我们以公开的静态页面为例。\n\n## 爬虫的本质：三步走\n\n```text\n1. 发送请求（requests）→ 服务器返回 HTML 文本\n2. 解析内容（BeautifulSoup）→ 从 HTML 里提取要的数据\n3. 保存数据（csv\u002Fjson\u002F数据库）→ 落盘\n```\n\n安装依赖：\n\n```bash\npip install requests beautifulsoup4 lxml\n```\n\n## 第一步：requests 发送请求\n\n**最基本的 GET 请求**：\n\n```python\nimport requests\n\nresp = requests.get(\"https:\u002F\u002Fexample.com\")\nprint(resp.status_code)  # 200 表示成功\nprint(resp.text[:200])   # 网页 HTML 前 200 字符\n```\n\n**带参数的 GET**：\n\n```python\n# 等价于访问 https:\u002F\u002Fexample.com\u002Fsearch?q=python&page=2\nparams = {\"q\": \"python\", \"page\": 2}\nresp = requests.get(\"https:\u002F\u002Fexample.com\u002Fsearch\", params=params)\nprint(resp.url)  # 自动拼好的完整 URL\n```\n\n**POST 请求**（模拟表单提交）：\n\n```python\ndata = {\"username\": \"test\", \"password\": \"123456\"}\nresp = requests.post(\"https:\u002F\u002Fexample.com\u002Flogin\", data=data)\n```\n\n**响应对象常用属性**：\n\n```python\nresp.status_code   # 状态码：200 成功，404 不存在，403 被拒绝\nresp.text          # 文本内容（自动按推测编码解码）\nresp.content       # 二进制内容（下载图片\u002F文件用这个）\nresp.json()        # 接口返回 JSON 时直接转成字典\nresp.encoding      # 乱码时手动指定：resp.encoding = \"utf-8\"\n```\n\n## 第二步：请求头伪装（过不了反爬就看这里）\n\n很多网站会检查请求头，发现是程序访问就拒绝。加上请求头伪装成浏览器：\n\n```python\nheaders = {\n    \"User-Agent\": \"Mozilla\u002F5.0 (Windows NT 10.0; Win64; x64) \"\n                  \"AppleWebKit\u002F537.36 (KHTML, like Gecko) \"\n                  \"Chrome\u002F120.0.0.0 Safari\u002F537.36\",\n    \"Referer\": \"https:\u002F\u002Fexample.com\u002F\",  # 有些站检查来源页\n}\nresp = requests.get(url, headers=headers, timeout=10)\n```\n\n`User-Agent` 可以直接从浏览器复制：F12 打开开发者工具 → Network → 随便点一个请求 → Headers → Request Headers。\n\n**超时设置是必须的**：不加 `timeout`，目标服务器不响应时程序会永远卡住。\n\n## 第三步：BeautifulSoup 解析 HTML\n\n拿到 HTML 后，用 CSS 选择器提取数据：\n\n```python\nfrom bs4 import BeautifulSoup\n\nhtml = \"\"\"\n\u003Chtml>\u003Cbody>\n  \u003Cdiv class=\"article\">\n    \u003Ch2>标题一\u003C\u002Fh2>\n    \u003Cp class=\"intro\">简介一\u003C\u002Fp>\n  \u003C\u002Fdiv>\n  \u003Cdiv class=\"article\">\n    \u003Ch2>标题二\u003C\u002Fh2>\n    \u003Cp class=\"intro\">简介二\u003C\u002Fp>\n  \u003C\u002Fdiv>\n\u003C\u002Fbody>\u003C\u002Fhtml>\n\"\"\"\n\nsoup = BeautifulSoup(html, \"lxml\")\n\n# 最常用的 4 个方法\nsoup.select(\"h2\")            # CSS 选择器，返回所有匹配的列表\nsoup.select_one(\"h2\")        # 只返回第一个\nsoup.find(\"div\", class_=\"article\")   # 按标签+属性查找\nsoup.find_all(\"h2\")          # 返回所有匹配\n\n# 提取内容\nfor article in soup.select(\"div.article\"):\n    title = article.select_one(\"h2\").text.strip()\n    intro = article.select_one(\"p.intro\").text.strip()\n    print(title, \"|\", intro)\n```\n\n**提取属性**（如链接地址、图片地址）：\n\n```python\nlink = soup.select_one(\"a\")\nprint(link[\"href\"])     # 取 href 属性\nprint(link.get_text())  # 取链接文字\n```\n\n## 完整实战：抓取本站文章列表\n\n下面是一个可以直接改造的实战模板：\n\n```python\nimport csv\nimport time\n\nimport requests\nfrom bs4 import BeautifulSoup\n\nHEADERS = {\n    \"User-Agent\": \"Mozilla\u002F5.0 (Windows NT 10.0; Win64; x64) \"\n                  \"AppleWebKit\u002F537.36 Chrome\u002F120.0.0.0 Safari\u002F537.36\"\n}\n\ndef fetch_page(url: str) -> str:\n    \"\"\"下载页面，带重试\"\"\"\n    for attempt in range(3):\n        try:\n            resp = requests.get(url, headers=HEADERS, timeout=10)\n            resp.raise_for_status()  # 4xx\u002F5xx 直接抛异常\n            resp.encoding = \"utf-8\"\n            return resp.text\n        except requests.RequestException as e:\n            print(f\"第 {attempt + 1} 次请求失败: {e}\")\n            time.sleep(2)\n    return \"\"\n\ndef parse_page(html: str) -> list[dict]:\n    \"\"\"解析页面，提取文章列表（选择器按目标网站实际结构调整）\"\"\"\n    soup = BeautifulSoup(html, \"lxml\")\n    articles = []\n    for item in soup.select(\"div.post-item\"):\n        title_tag = item.select_one(\"h2 a\")\n        if not title_tag:\n            continue\n        articles.append({\n            \"title\": title_tag.get_text(strip=True),\n            \"url\": title_tag.get(\"href\", \"\"),\n            \"date\": item.select_one(\".date\").get_text(strip=True)\n                    if item.select_one(\".date\") else \"\",\n        })\n    return articles\n\ndef save_csv(data: list[dict], filename: str = \"articles.csv\"):\n    \"\"\"保存到 CSV，Excel 可直接打开\"\"\"\n    with open(filename, \"w\", newline=\"\", encoding=\"utf-8-sig\") as f:\n        writer = csv.DictWriter(f, fieldnames=[\"title\", \"url\", \"date\"])\n        writer.writeheader()\n        writer.writerows(data)\n    print(f\"已保存 {len(data)} 条到 {filename}\")\n\nif __name__ == \"__main__\":\n    all_data = []\n    for page in range(1, 4):  # 抓前 3 页\n        url = f\"https:\u002F\u002Fexample.com\u002Fposts?page={page}\"\n        html = fetch_page(url)\n        if html:\n            all_data.extend(parse_page(html))\n        time.sleep(1.5)  # 礼貌间隔，别给人家服务器压力\n    save_csv(all_data)\n```\n\n## 动态渲染页面怎么办\n\n上面的方法只适合**服务端渲染**的页面（HTML 里直接有数据）。如果数据是 JS 动态加载的（`resp.text` 里找不到要的内容），两条路：\n\n**路线一：找接口（优先）**\n\nF12 → Network → Fetch\u002FXHR，刷新页面，找返回 JSON 的接口。很多网站前端就是调这些接口拿数据的，直接请求接口比解析 HTML 省事十倍：\n\n```python\nresp = requests.get(\"https:\u002F\u002Fexample.com\u002Fapi\u002Fposts?page=1\", headers=HEADERS)\ndata = resp.json()  # 直接拿到结构化数据，连解析都不用\n```\n\n**路线二：上浏览器自动化**\n\n接口藏得太深时，用 Playwright 驱动真实浏览器：\n\n```bash\npip install playwright && playwright install chromium\n```\n\n```python\nfrom playwright.sync_api import sync_playwright\n\nwith sync_playwright() as p:\n    browser = p.chromium.launch(headless=True)\n    page = browser.new_page()\n    page.goto(\"https:\u002F\u002Fexample.com\")\n    page.wait_for_selector(\"div.post-item\")  # 等内容渲染出来\n    html = page.content()\n    browser.close()\n# 之后照常 BeautifulSoup 解析\n```\n\n## 常见反爬与应对\n\n| 反爬手段 | 表现 | 应对 |\n|---------|------|------|\n| UA 检测 | 403 拒绝 | 加真实 User-Agent |\n| 频率限制 | 429 或封 IP | time.sleep 降速、换 IP |\n| 登录墙 | 跳转登录页 | requests.Session() 保持 Cookie |\n| 验证码 | 弹验证 | 降低频率避开触发点，不建议硬刚 |\n| 动态渲染 | HTML 里没数据 | 找接口或用 Playwright |\n\n## 写在最后\n\n爬虫 80% 的工作量是「分析目标网站」而不是写代码：数据在 HTML 里还是接口里？分页参数长什么样？有没有反爬？先花 20 分钟用 F12 把网站看透，代码往往 20 行就够了。\n","\u002Fstatic\u002Fuploads\u002Fimages\u002F2026\u002F09\u002F29\u002F1790636138387839504.png","2026-09-29T06:56:45.03460923+08:00","2026-09-29T06:55:38.387959519+08:00","2026-09-29T06:56:45.034666561+08:00",{"id":70,"name":75,"slug":76,"description":75,"icon":5,"sortOrder":54,"postCount":54,"parentId":54,"createdAt":77,"updatedAt":77},[148,149,154,158,163],{"id":80,"name":76,"slug":81,"color":82,"postCount":54,"createdAt":83},{"id":150,"name":151,"slug":152,"color":82,"postCount":54,"createdAt":153},122,"爬虫","pa-chong","2026-09-29T06:55:38.359394779+08:00",{"id":155,"name":156,"slug":156,"color":82,"postCount":54,"createdAt":157},123,"requests","2026-09-29T06:55:38.366815934+08:00",{"id":159,"name":160,"slug":161,"color":82,"postCount":54,"createdAt":162},124,"BeautifulSoup","beautifulsoup","2026-09-29T06:55:38.372320794+08:00",{"id":164,"name":165,"slug":166,"color":82,"postCount":54,"createdAt":167},125,"数据采集","shu-ju-cai-ji","2026-09-29T06:55:38.379703082+08:00",{"id":169,"title":170,"slug":171,"summary":172,"content":173,"coverImage":174,"categoryId":70,"authorId":55,"viewCount":54,"likeCount":54,"commentCount":54,"isTop":54,"isRecommend":54,"status":55,"rejectReason":5,"publishedAt":175,"createdAt":176,"updatedAt":177,"category":178,"tags":179},13,"Pandas 数据处理 10 个高频操作，一篇就够","pandas","日常数据分析 80% 的工作只用这 10 个操作：读写文件、筛选、分组聚合、缺失值处理、表合并。每个操作附可直接套用的代码模板。","## 为什么是 Pandas\n\n处理表格数据，Excel 能做的事 Pandas 都能做，而且：\n\n- **快**：几十万行数据秒级处理，Excel 早就卡死\n- **可复现**：代码写一次，下个月的新数据重新跑一遍就行\n- **可串联**：读数据 → 清洗 → 分析 → 出图一条龙\n\n```bash\npip install pandas openpyxl  # openpyxl 用于读写 Excel\n```\n\n## 准备演示数据\n\n后面的例子都用这份「销售数据」：\n\n```python\nimport pandas as pd\n\ndf = pd.DataFrame({\n    \"姓名\": [\"张三\", \"李四\", \"王五\", \"张三\", \"李四\", \"赵六\"],\n    \"部门\": [\"华东\", \"华北\", \"华东\", \"华东\", \"华北\", \"华南\"],\n    \"销售额\": [12000, 8500, 15000, 13500, None, 9000],\n    \"入职日期\": [\"2023-01-15\", \"2023-03-20\", \"2022-11-05\",\n                 \"2023-01-15\", \"2023-03-20\", \"2024-02-01\"],\n})\n```\n\n## 操作 1：读写 CSV \u002F Excel\n\n```python\n# 读取\ndf = pd.read_csv(\"sales.csv\")                      # CSV\ndf = pd.read_excel(\"sales.xlsx\", sheet_name=\"1月\")  # Excel 指定工作表\n\n# 中文乱码？指定编码\ndf = pd.read_csv(\"sales.csv\", encoding=\"gbk\")\n\n# 保存（index=False 去掉行号，utf-8-sig 保证 Excel 打开不乱码）\ndf.to_csv(\"output.csv\", index=False, encoding=\"utf-8-sig\")\ndf.to_excel(\"output.xlsx\", index=False)\n```\n\n## 操作 2：快速查看数据\n\n拿到任何数据，先做这四个动作：\n\n```python\ndf.head()        # 前 5 行长什么样\ndf.info()        # 每列的类型、有没有缺失\ndf.describe()    # 数值列的统计：均值\u002F最值\u002F分位数\ndf.shape         # 几行几列\n```\n\n## 操作 3：选列与筛选行\n\n```python\n# 选列\ndf[\"销售额\"]                    # 单列（Series）\ndf[[\"姓名\", \"销售额\"]]          # 多列（DataFrame）\n\n# 条件筛选（最常用的操作，没有之一）\ndf[df[\"销售额\"] > 10000]\ndf[df[\"部门\"] == \"华东\"]\ndf[(df[\"销售额\"] > 10000) & (df[\"部门\"] == \"华东\")]  # 多条件用 & |，必须加括号\n\n# 模糊匹配\ndf[df[\"姓名\"].str.contains(\"张\")]\n\n# 在列表内匹配\ndf[df[\"部门\"].isin([\"华东\", \"华南\"])]\n```\n\n## 操作 4：排序与去重\n\n```python\n# 排序\ndf.sort_values(\"销售额\", ascending=False)           # 按销售额降序\ndf.sort_values([\"部门\", \"销售额\"], ascending=[True, False])  # 先部门升序再销售额降序\n\n# 去重\ndf.drop_duplicates()                      # 整行重复才删\ndf.drop_duplicates(subset=[\"姓名\"])       # 同一人只留一条\ndf.drop_duplicates(subset=[\"姓名\"], keep=\"last\")  # 留最后一次出现的\n```\n\n## 操作 5：缺失值处理\n\n```python\n# 查看缺失情况\ndf.isna().sum()      # 每列有几个缺失\n\n# 处理：三选一\ndf.dropna()                              # 1. 删掉含缺失的行\ndf[\"销售额\"].fillna(0)                   # 2. 填固定值\ndf[\"销售额\"].fillna(df[\"销售额\"].mean()) # 3. 填均值（数值列常用）\n```\n\n## 操作 6：分组聚合（groupby，核心中的核心）\n\n「按部门统计销售额」这类需求的标准答案：\n\n```python\n# 按部门分组，算销售额总和\ndf.groupby(\"部门\")[\"销售额\"].sum()\n\n# 一次算多个统计量\ndf.groupby(\"部门\")[\"销售额\"].agg([\"sum\", \"mean\", \"count\"])\n\n# 多列分组 + 不同列不同聚合\ndf.groupby(\"部门\").agg(\n    总销售额=(\"销售额\", \"sum\"),\n    人均销售额=(\"销售额\", \"mean\"),\n    人数=(\"姓名\", \"count\"),\n).reset_index()  # 把分组键变回普通列，方便后续处理\n```\n\n## 操作 7：新增计算列\n\n```python\n# 直接运算\ndf[\"提成\"] = df[\"销售额\"] * 0.05\n\n# 条件赋值：销售额过万的标记为「高」\nimport numpy as np\ndf[\"等级\"] = np.where(df[\"销售额\"] > 10000, \"高\", \"普通\")\n\n# 对文本列做处理\ndf[\"姓名长度\"] = df[\"姓名\"].str.len()\n```\n\n## 操作 8：表合并（merge 与 concat）\n\n```python\n# concat：上下堆叠（合并多个月份的同结构表格）\ndf_all = pd.concat([df_1月, df_2月, df_3月], ignore_index=True)\n\n# merge：左右拼接（类似 SQL 的 JOIN）\ndf_员工 = pd.DataFrame({\"姓名\": [\"张三\", \"李四\"], \"职级\": [\"P5\", \"P6\"]})\ndf.merge(df_员工, on=\"姓名\", how=\"left\")  # 左连接：保留左表全部行\n```\n\n`how` 的四种取值：`left`（保留左表）、`right`、`inner`（交集）、`outer`（并集）。日常 90% 用 `left`。\n\n## 操作 9：日期处理\n\n```python\n# 字符串转日期\ndf[\"入职日期\"] = pd.to_datetime(df[\"入职日期\"])\n\n# 提取年\u002F月\u002F星期几\ndf[\"入职年\"] = df[\"入职日期\"].dt.year\ndf[\"入职月\"] = df[\"入职日期\"].dt.month\n\n# 按月统计\ndf.groupby(df[\"入职日期\"].dt.to_period(\"M\"))[\"销售额\"].sum()\n\n# 日期差（算工龄）\ndf[\"工龄天数\"] = (pd.Timestamp.now() - df[\"入职日期\"]).dt.days\n```\n\n## 操作 10：透视表（pivot_table）\n\nExcel 透视表的代码版，多维交叉统计一把梭：\n\n```python\n# 行=部门，列=等级，值=销售额总和\ndf.pivot_table(\n    index=\"部门\",\n    columns=\"等级\",\n    values=\"销售额\",\n    aggfunc=\"sum\",\n    fill_value=0,      # 空位填 0\n    margins=True,      # 加合计行列\n)\n```\n\n## 避坑指南\n\n**1. SettingWithCopyWarning 警告**\n\n```python\n# 错误：先筛选再赋值，改的可能不是原表\ndf[df[\"部门\"] == \"华东\"][\"销售额\"] = 0\n\n# 正确：用 loc 一步定位\ndf.loc[df[\"部门\"] == \"华东\", \"销售额\"] = 0\n```\n\n**2. 链式赋值结果没生效**\n\nPandas 很多操作**返回新对象而不改原表**。要么接返回值：\n\n```python\ndf = df.dropna()  # 接返回值\n# 或者\ndf.dropna(inplace=True)  # 用 inplace 参数\n```\n\n**3. 数字被读成字符串**\n\nCSV 里带「¥」「,」的数字会被当成文本，先清洗再计算：\n\n```python\ndf[\"销售额\"] = df[\"销售额\"].str.replace(\"¥\", \"\").str.replace(\",\", \"\").astype(float)\n```\n\n## 写在最后\n\nPandas 的学习曲线不在语法而在**思维转换**：别写 for 循环逐行处理，学会用「向量化」的批量操作。遇到需求先问自己——这能不能用筛选、分组、合并这三板斧解决？90% 的情况答案是能。\n","\u002Fstatic\u002Fuploads\u002Fimages\u002F2026\u002F09\u002F29\u002F1790636138431846763.png","2026-09-29T06:56:42.1304477+08:00","2026-09-29T06:55:38.43196821+08:00","2026-09-29T06:56:42.13052425+08:00",{"id":70,"name":75,"slug":76,"description":75,"icon":5,"sortOrder":54,"postCount":54,"parentId":54,"createdAt":77,"updatedAt":77},[180,181,185,190],{"id":80,"name":76,"slug":81,"color":82,"postCount":54,"createdAt":83},{"id":182,"name":183,"slug":171,"color":82,"postCount":54,"createdAt":184},126,"Pandas","2026-09-29T06:55:38.411798198+08:00",{"id":186,"name":187,"slug":188,"color":82,"postCount":54,"createdAt":189},127,"数据分析","shu-ju-fen-xi","2026-09-29T06:55:38.419436825+08:00",{"id":191,"name":192,"slug":193,"color":82,"postCount":54,"createdAt":194},128,"数据处理","shu-ju-chu-li","2026-09-29T06:55:38.426699835+08:00",{"id":196,"title":197,"slug":198,"summary":199,"content":200,"coverImage":201,"categoryId":70,"authorId":55,"viewCount":54,"likeCount":54,"commentCount":54,"isTop":54,"isRecommend":54,"status":55,"rejectReason":5,"publishedAt":202,"createdAt":203,"updatedAt":204,"category":205,"tags":206},15,"FastAPI 快速上手：30 分钟写一个可部署的 REST API","fastapi","Python 写接口最快的框架。从第一个路由到参数校验、自动文档、数据库连接，再到生产部署，一篇讲完整条链路。","## 为什么选 FastAPI\n\nPython Web 框架里，Flask 简单但太原始，Django 功能全但太重。FastAPI 是新一代的选择：\n\n- **快**：性能接近 Node.js 和 Go（基于 Starlette + Pydantic）\n- **自动文档**：写完代码自动生成交互式 API 文档，前端看了直呼内行\n- **类型安全**：用 Python 类型注解做参数校验，类型错了直接返回 422\n- **异步原生**：天生支持 async\u002Fawait，IO 密集场景性能碾压\n\n```bash\npip install fastapi uvicorn\n```\n\n## 第一个接口：5 行代码\n\n```python\n# main.py\nfrom fastapi import FastAPI\n\napp = FastAPI(title=\"我的第一个API\")\n\n@app.get(\"\u002F\")\ndef hello():\n    return {\"message\": \"Hello FastAPI\"}\n\n@app.get(\"\u002Fitems\u002F{item_id}\")\ndef get_item(item_id: int):   # 声明 int 类型，自动转换和校验\n    return {\"item_id\": item_id, \"name\": f\"商品{item_id}\"}\n```\n\n启动：\n\n```bash\nuvicorn main:app --reload --port 8000\n```\n\n- `--reload`：改代码自动重启，开发必备\n- `main:app`：指 main.py 文件里的 app 对象\n\n现在访问：\n\n- `http:\u002F\u002F127.0.0.1:8000\u002Fitems\u002F42` → 返回 JSON\n- `http:\u002F\u002F127.0.0.1:8000\u002Fitems\u002Fabc` → 自动返回 422 错误（因为 abc 不是 int）\n- `http:\u002F\u002F127.0.0.1:8000\u002Fdocs` → **自动生成的 Swagger 文档页面**，可以直接在页面上测试接口\n\n## 参数校验：Pydantic 的威力\n\nPOST 请求的数据校验，用 Pydantic 模型声明：\n\n```python\nfrom fastapi import FastAPI\nfrom pydantic import BaseModel, Field\n\napp = FastAPI()\n\nclass ItemCreate(BaseModel):\n    name: str = Field(min_length=1, max_length=50, description=\"商品名\")\n    price: float = Field(gt=0, description=\"价格必须大于0\")\n    stock: int = Field(default=0, ge=0)\n    tags: list[str] = []\n\n@app.post(\"\u002Fitems\")\ndef create_item(item: ItemCreate):\n    return {\"message\": \"创建成功\", \"data\": item}\n```\n\n发请求时：\n\n- 字段缺失\u002F类型错误\u002F不满足约束 → 自动返回详细的 422 错误信息\n- 一切正常 → `item` 是一个类型安全的对象，IDE 有完整提示\n\n**再也不用手写 `if not name: return error` 这类校验代码**。\n\n## 查询参数与路径参数\n\n```python\nfrom typing import Optional\n\n@app.get(\"\u002Fitems\")\ndef list_items(\n    keyword: Optional[str] = None,   # 可选查询参数 ?keyword=xxx\n    page: int = 1,                    # 带默认值\n    size: int = Field(default=20, le=100),  # 限制最大100\n):\n    return {\"keyword\": keyword, \"page\": page, \"size\": size}\n```\n\n## 一个完整的小项目：待办事项 API\n\n把前面的知识串起来，写一个带增删改查的完整 API：\n\n```python\n# todo.py\nfrom fastapi import FastAPI, HTTPException\nfrom pydantic import BaseModel, Field\n\napp = FastAPI(title=\"待办事项API\")\n\n# 用列表模拟数据库（下节换成真实数据库）\ntodos = []\nnext_id = 1\n\nclass TodoCreate(BaseModel):\n    title: str = Field(min_length=1, max_length=100)\n    done: bool = False\n\nclass Todo(TodoCreate):\n    id: int\n\n@app.get(\"\u002Ftodos\", response_model=list[Todo])\ndef list_todos():\n    \"\"\"获取所有待办\"\"\"\n    return todos\n\n@app.post(\"\u002Ftodos\", response_model=Todo, status_code=201)\ndef create_todo(item: TodoCreate):\n    \"\"\"创建待办\"\"\"\n    global next_id\n    todo = Todo(id=next_id, **item.model_dump())\n    next_id += 1\n    todos.append(todo)\n    return todo\n\n@app.get(\"\u002Ftodos\u002F{todo_id}\", response_model=Todo)\ndef get_todo(todo_id: int):\n    \"\"\"获取单个待办\"\"\"\n    for t in todos:\n        if t.id == todo_id:\n            return t\n    raise HTTPException(status_code=404, detail=\"待办不存在\")\n\n@app.put(\"\u002Ftodos\u002F{todo_id}\", response_model=Todo)\ndef update_todo(todo_id: int, item: TodoCreate):\n    \"\"\"更新待办\"\"\"\n    for i, t in enumerate(todos):\n        if t.id == todo_id:\n            todos[i] = Todo(id=todo_id, **item.model_dump())\n            return todos[i]\n    raise HTTPException(status_code=404, detail=\"待办不存在\")\n\n@app.delete(\"\u002Ftodos\u002F{todo_id}\", status_code=204)\ndef delete_todo(todo_id: int):\n    \"\"\"删除待办\"\"\"\n    for i, t in enumerate(todos):\n        if t.id == todo_id:\n            todos.pop(i)\n            return\n    raise HTTPException(status_code=404, detail=\"待办不存在\")\n```\n\n`response_model` 的好处：自动过滤返回值（比如不暴露内部字段）、生成准确的文档、做响应校验。\n\n## 连接真实数据库（以 SQLite + SQLAlchemy 为例）\n\n```bash\npip install sqlalchemy\n```\n\n```python\n# database.py\nfrom sqlalchemy import Column, Integer, String, Boolean, create_engine\nfrom sqlalchemy.orm import declarative_base, sessionmaker\n\nengine = create_engine(\"sqlite:\u002F\u002F\u002F.\u002Ftodos.db\", connect_args={\"check_same_thread\": False})\nSessionLocal = sessionmaker(bind=engine)\nBase = declarative_base()\n\nclass TodoModel(Base):\n    __tablename__ = \"todos\"\n    id = Column(Integer, primary_key=True, index=True)\n    title = Column(String(100), nullable=False)\n    done = Column(Boolean, default=False)\n\nBase.metadata.create_all(engine)  # 自动建表\n```\n\n在 FastAPI 中用依赖注入管理数据库会话：\n\n```python\nfrom fastapi import Depends\nfrom sqlalchemy.orm import Session\n\ndef get_db():\n    \"\"\"每个请求一个会话，结束自动关闭\"\"\"\n    db = SessionLocal()\n    try:\n        yield db\n    finally:\n        db.close()\n\n@app.get(\"\u002Ftodos\")\ndef list_todos(db: Session = Depends(get_db)):\n    return db.query(TodoModel).all()\n\n@app.post(\"\u002Ftodos\", status_code=201)\ndef create_todo(item: TodoCreate, db: Session = Depends(get_db)):\n    todo = TodoModel(**item.model_dump())\n    db.add(todo)\n    db.commit()\n    db.refresh(todo)\n    return todo\n```\n\n## 生产部署\n\n开发用 `--reload`，生产环境这样跑：\n\n```bash\n# 4 个 worker 进程，绑定所有网卡\nuvicorn main:app --host 0.0.0.0 --port 8000 --workers 4\n```\n\n更稳的方案是 `gunicorn` 管理 uvicorn worker（Linux）：\n\n```bash\npip install gunicorn\ngunicorn main:app -w 4 -k uvicorn.workers.UvicornWorker -b 0.0.0.0:8000\n```\n\n前面再挂 Nginx 反向代理，搞定域名和 HTTPS，就是标准的生产架构。\n\n## 进阶方向\n\n到这里你已经能写可用的 API 了。下一步按需学习：\n\n- **用户认证**：`fastapi-users` 或手写 JWT（`python-jose`）\n- **跨域 CORS**：前端联调必配 `CORSMiddleware`\n- **后台任务**：`BackgroundTasks` 处理发邮件这类慢操作\n- **限流**：`slowapi` 防止接口被刷\n- **测试**：`fastapi.testclient` 基于 httpx，写接口测试很顺手\n\n## 写在最后\n\nFastAPI 的设计哲学是「别让我重复写样板代码」。类型注解即校验、代码即文档——把这两个特性用足，你的接口开发效率会提升一个量级。建议把待办事项的例子亲手跑通，再把它改造成一个你自己真正需要的小工具。\n","\u002Fstatic\u002Fuploads\u002Fimages\u002F2026\u002F09\u002F29\u002F1790636138525796879.png","2026-09-29T06:56:37.777573439+08:00","2026-09-29T06:55:38.52593956+08:00","2026-09-29T06:56:37.777643867+08:00",{"id":70,"name":75,"slug":76,"description":75,"icon":5,"sortOrder":54,"postCount":54,"parentId":54,"createdAt":77,"updatedAt":77},[207,208,212,217,222],{"id":80,"name":76,"slug":81,"color":82,"postCount":54,"createdAt":83},{"id":209,"name":210,"slug":198,"color":82,"postCount":54,"createdAt":211},132,"FastAPI","2026-09-29T06:55:38.496902061+08:00",{"id":213,"name":214,"slug":215,"color":82,"postCount":54,"createdAt":216},133,"Web开发","web-kai-fa","2026-09-29T06:55:38.503802445+08:00",{"id":218,"name":219,"slug":220,"color":82,"postCount":54,"createdAt":221},134,"REST API","rest-api","2026-09-29T06:55:38.511505671+08:00",{"id":223,"name":224,"slug":225,"color":82,"postCount":54,"createdAt":226},135,"后端","hou-duan","2026-09-29T06:55:38.519097242+08:00",{"id":228,"title":229,"slug":230,"summary":231,"content":232,"coverImage":233,"categoryId":70,"authorId":55,"viewCount":54,"likeCount":54,"commentCount":54,"isTop":54,"isRecommend":54,"status":55,"rejectReason":5,"publishedAt":234,"createdAt":235,"updatedAt":236,"category":237,"tags":238},16,"Python 异常处理最佳实践：别再只会 try-except 了","exception","except Exception 一把梭是最常见的错误姿势。本文讲清异常体系、精准捕获、自定义异常、日志记录和重试机制，让你的程序崩溃得体面。","## 为什么「裸 except」是灾难\n\n先看新手最常写的代码：\n\n```python\ntry:\n    data = json.loads(resp.text)\n    save_to_db(data)\nexcept Exception:\n    pass  # 出错了？吞掉，假装没事\n```\n\n这段代码的问题是：**任何错误都被静默吞掉**——JSON 解析失败、数据库连接断开、磁盘写满、甚至代码里的拼写错误，全都悄无声息。出问题时你没有任何线索，只能对着「数据怎么没存进去」发呆两小时。\n\n异常处理的核心原则只有一条：\n\n> **只捕获你能处理的异常，并且处理时必须留下痕迹。**\n\n## Python 异常体系速览\n\n异常是有继承层次的，常用的几个：\n\n```text\nBaseException\n├── KeyboardInterrupt      # Ctrl+C\n├── SystemExit             # sys.exit()\n└── Exception              # 日常说的「异常」都继承它\n    ├── ValueError         # 值不对：int(\"abc\")\n    ├── TypeError          # 类型不对：\"1\" + 1\n    ├── KeyError           # 字典键不存在\n    ├── IndexError         # 列表下标越界\n    ├── FileNotFoundError  # 文件不存在\n    ├── ConnectionError    # 网络连接失败\n    └── TimeoutError       # 超时\n```\n\n了解层次的意义在于：**捕获父类会连带捕获所有子类**。`except OSError` 会同时接住 `FileNotFoundError`、`PermissionError` 等一堆子类。\n\n## 正确姿势一：精准捕获\n\n```python\ntry:\n    with open(\"config.json\", encoding=\"utf-8\") as f:\n        config = json.load(f)\n    port = int(config[\"port\"])\nexcept FileNotFoundError:\n    print(\"配置文件不存在，使用默认配置\")\n    config = {\"port\": 8000}\n    port = 8000\nexcept json.JSONDecodeError as e:\n    print(f\"配置文件格式错误：{e}\")\n    raise  # 处理不了就继续抛，别硬撑\nexcept KeyError:\n    print(\"配置文件缺少 port 字段\")\n    raise\n```\n\n每个 `except` 只处理自己能搞定的场景，搞不定的用 `raise` 原样抛出，让上层或顶层去处理。\n\n## 正确姿势二：try 块越小越好\n\n```python\n# 差：整个流程都包进去，出错时根本不知道是哪一步\ntry:\n    data = download()\n    cleaned = clean(data)\n    save(cleaned)\n    notify()\nexcept Exception as e:\n    print(e)\n\n# 好：每个步骤单独处理，错误定位精确到环节\ndata = download()  # 下载失败应该直接崩溃报错，不用包\ntry:\n    cleaned = clean(data)\nexcept ValueError as e:\n    print(f\"数据清洗失败：{e}\")\n    return\nsave(cleaned)\n```\n\n`try` 块里只放**真正可能抛出你所捕获异常的那几行**。\n\n## 正确姿势三：善用 else 和 finally\n\n完整的异常处理有四个块：\n\n```python\ntry:\n    f = open(\"data.txt\", encoding=\"utf-8\")\nexcept FileNotFoundError:\n    print(\"文件不存在\")\nelse:\n    # 没有异常时执行——把成功后的逻辑放这里，职责更清晰\n    content = f.read()\n    f.close()\nfinally:\n    # 无论是否异常都执行——释放资源的专用位置\n    print(\"清理工作\")\n```\n\n`finally` 的典型用途：关文件、关数据库连接、释放锁。（不过现代 Python 更推荐 `with` 上下文管理器，它内部就是用 finally 实现的。）\n\n## 正确姿势四：自定义异常\n\n业务逻辑的错误，不要只会 `raise Exception(\"xxx\")`，定义自己的异常类型：\n\n```python\nclass BusinessError(Exception):\n    \"\"\"业务异常基类\"\"\"\n    pass\n\nclass InsufficientBalanceError(BusinessError):\n    \"\"\"余额不足\"\"\"\n    def __init__(self, balance: float, amount: float):\n        super().__init__(f\"余额不足：当前 {balance} 元，需支付 {amount} 元\")\n        self.balance = balance\n        self.amount = amount\n\ndef withdraw(balance: float, amount: float) -> float:\n    if amount > balance:\n        raise InsufficientBalanceError(balance, amount)\n    return balance - amount\n\n# 调用方可以精准处理\ntry:\n    withdraw(100, 200)\nexcept InsufficientBalanceError as e:\n    print(f\"支付失败：{e}\")\n    # e.balance 和 e.amount 还能拿出来做后续处理\n```\n\n好处：调用方能区分「业务错误」（余额不足，提示用户）和「系统错误」（数据库挂了，报警给运维）。\n\n## 正确姿势五：记录日志而不是 print\n\n程序上服务器后，`print` 基本等于没写。标准做法：\n\n```python\nimport logging\n\nlogging.basicConfig(\n    level=logging.INFO,\n    format=\"%(asctime)s [%(levelname)s] %(name)s: %(message)s\",\n    handlers=[\n        logging.FileHandler(\"app.log\", encoding=\"utf-8\"),  # 写文件\n        logging.StreamHandler(),                            # 同时输出到控制台\n    ],\n)\nlogger = logging.getLogger(__name__)\n\ntry:\n    result = process(data)\nexcept ValueError as e:\n    logger.warning(f\"数据格式问题：{e}\")\nexcept Exception:\n    # 意料外的异常，用 exception 方法记录完整堆栈\n    logger.exception(\"处理数据时发生未知错误\")\n    raise\n```\n\n**`logger.exception()` 是关键**——它会把完整的错误堆栈（哪一行、调用链）都记进日志，比 `logger.error(str(e))` 信息量多十倍。\n\n## 正确姿势六：失败重试\n\n网络请求、数据库连接这类「可能暂时性失败」的操作，加重试：\n\n```python\nimport time\nimport requests\n\ndef fetch_with_retry(url: str, times: int = 3, delay: float = 1.0):\n    \"\"\"带重试的请求：指数退避\"\"\"\n    for attempt in range(1, times + 1):\n        try:\n            resp = requests.get(url, timeout=10)\n            resp.raise_for_status()\n            return resp\n        except requests.RequestException as e:\n            logger.warning(f\"第 {attempt}\u002F{times} 次请求失败：{e}\")\n            if attempt == times:\n                raise  # 重试耗尽，抛出最后一次异常\n            time.sleep(delay * attempt)  # 每次等更久：1s、2s、3s\n```\n\n**哪些错误不该重试**：参数错误（400）、权限不足（403）、资源不存在（404）——这些是确定性错误，重试一万次也没用，直接抛。\n\n## 一张决策表\n\n遇到异常时问自己三个问题：\n\n| 问题 | 答案 | 做法 |\n|------|------|------|\n| 这个错误我能处理吗？ | 能（如文件不存在→用默认值） | except 住，处理，继续 |\n| | 不能 | 不捕获或 raise 抛给上层 |\n| 是暂时性故障吗？ | 是（网络抖动） | 重试 + 间隔 |\n| | 否（参数错误） | 立即失败 |\n| 用户需要知道吗？ | 需要 | 转成友好的中文提示 |\n| | 不需要 | 记日志即可 |\n\n## 写在最后\n\n异常处理的本质不是「防止程序崩溃」，而是**让程序崩溃时留下足够的信息，并且在能恢复的地方优雅恢复**。记住三条铁律：精准捕获、绝不静默、日志留痕。做到这三点，半夜被报警叫醒的次数会少很多。\n","\u002Fstatic\u002Fuploads\u002Fimages\u002F2026\u002F09\u002F29\u002F1790636138571123376.png","2026-09-29T06:56:36.117024762+08:00","2026-09-29T06:55:38.571251765+08:00","2026-09-29T06:56:36.117090124+08:00",{"id":70,"name":75,"slug":76,"description":75,"icon":5,"sortOrder":54,"postCount":54,"parentId":54,"createdAt":77,"updatedAt":77},[239,240,245,250],{"id":80,"name":76,"slug":81,"color":82,"postCount":54,"createdAt":83},{"id":241,"name":242,"slug":243,"color":82,"postCount":54,"createdAt":244},136,"异常处理","yi-chang-chu-li","2026-09-29T06:55:38.549655932+08:00",{"id":246,"name":247,"slug":248,"color":82,"postCount":54,"createdAt":249},137,"日志","ri-zhi","2026-09-29T06:55:38.556461509+08:00",{"id":251,"name":252,"slug":253,"color":82,"postCount":54,"createdAt":254},138,"最佳实践","zui-jia-shi-jian","2026-09-29T06:55:38.563680858+08:00",{"id":256,"title":257,"slug":258,"summary":259,"content":260,"coverImage":261,"categoryId":70,"authorId":55,"viewCount":54,"likeCount":54,"commentCount":54,"isTop":54,"isRecommend":54,"status":55,"rejectReason":5,"publishedAt":262,"createdAt":263,"updatedAt":264,"category":265,"tags":266},17,"用 Python 操作 Excel：openpyxl 与 pandas 实战对比","excel","还在手动复制粘贴处理 Excel？用 Python 三分钟搞定。openpyxl 精细控制 vs pandas 批量分析，附批量合并 100 个文件的实战案例。","## 两个库怎么选\n\nPython 操作 Excel 主要用这两个库，分工不同：\n\n| | pandas | openpyxl |\n|---|--------|----------|\n| 定位 | 数据分析 | Excel 文件读写控制 |\n| 擅长 | 批量数据处理、统计、筛选 | 单元格样式、合并、公式、图表 |\n| 操作粒度 | 整表\u002F整行\u002F整列 | 精确到每个单元格 |\n| 学习成本 | 低 | 中 |\n\n**经验法则**：\n\n- 「处理数据」用 pandas（读进来 → 算 → 写出去）\n- 「操作格式」用 openpyxl（改样式、加批注、做模板报表）\n- 两者经常配合使用：pandas 算数据，openpyxl 美化输出\n\n```bash\npip install pandas openpyxl\n```\n\n## pandas 读写 Excel\n\n**读取**：\n\n```python\nimport pandas as pd\n\n# 读单个工作表\ndf = pd.read_excel(\"销售数据.xlsx\", sheet_name=\"1月\")\n\n# 读所有工作表，返回 {表名: DataFrame} 字典\nall_sheets = pd.read_excel(\"销售数据.xlsx\", sheet_name=None)\n\n# 实用参数\npd.read_excel(\"data.xlsx\", skiprows=2)       # 跳过前2行标题说明\npd.read_excel(\"data.xlsx\", usecols=\"A:C\")    # 只读A到C列\npd.read_excel(\"data.xlsx\", dtype={\"工号\": str})  # 工号按字符串读，防止前导零丢失\n```\n\n**写入**：\n\n```python\n# 单表写入\ndf.to_excel(\"结果.xlsx\", index=False)\n\n# 多表写入一个文件\nwith pd.ExcelWriter(\"汇总.xlsx\", engine=\"openpyxl\") as writer:\n    df1.to_excel(writer, sheet_name=\"汇总\", index=False)\n    df2.to_excel(writer, sheet_name=\"明细\", index=False)\n```\n\n## openpyxl 精细控制\n\n**基本读写**：\n\n```python\nfrom openpyxl import Workbook, load_workbook\n\n# 读已有文件\nwb = load_workbook(\"模板.xlsx\")\nws = wb[\"Sheet1\"]           # 按名取工作表\nws = wb.active              # 或取活动表\n\nprint(ws[\"A1\"].value)       # 读单元格\nws[\"B2\"] = \"hello\"          # 写单元格\nws.cell(row=3, column=2, value=100)  # 按行列写\n\n# 遍历数据区\nfor row in ws.iter_rows(min_row=2, max_row=100, values_only=True):\n    print(row)  # 每行是一个元组\n\nwb.save(\"输出.xlsx\")\n```\n\n**设置样式**（做正式报表必备）：\n\n```python\nfrom openpyxl.styles import Alignment, Border, Font, PatternFill, Side\n\nws = wb.active\n\n# 表头样式：加粗白字深蓝底，居中\nheader_font = Font(bold=True, color=\"FFFFFF\", size=12)\nheader_fill = PatternFill(\"solid\", start_color=\"2F5597\")\nfor cell in ws[1]:\n    cell.font = header_font\n    cell.fill = header_fill\n    cell.alignment = Alignment(horizontal=\"center\", vertical=\"center\")\n\n# 边框\nthin = Side(style=\"thin\", color=\"999999\")\nborder = Border(left=thin, right=thin, top=thin, bottom=thin)\nfor row in ws.iter_rows(min_row=1, max_row=ws.max_row, max_col=5):\n    for cell in row:\n        cell.border = border\n\n# 列宽行高\nws.column_dimensions[\"A\"].width = 20\nws.row_dimensions[1].height = 28\n\n# 冻结首行（滚动时表头不动）\nws.freeze_panes = \"A2\"\n```\n\n**公式与合并**：\n\n```python\nws[\"E10\"] = \"=SUM(E2:E9)\"          # 写公式，Excel 打开时自动计算\nws.merge_cells(\"A1:E1\")            # 合并单元格做标题\n```\n\n## 实战：批量合并 100 个 Excel 文件\n\n真实场景：每月底收到 30 个分公司发来的同格式报表，要合并成一个总表。\n\n```python\nfrom pathlib import Path\n\nimport pandas as pd\n\ndef merge_excels(folder: str, output: str) -> None:\n    \"\"\"合并目录下所有 xlsx 的第一个工作表\"\"\"\n    files = list(Path(folder).glob(\"*.xlsx\"))\n    if not files:\n        print(\"目录下没有 xlsx 文件\")\n        return\n\n    frames = []\n    for f in files:\n        try:\n            df = pd.read_excel(f)\n            df[\"来源文件\"] = f.stem  # 加一列标记数据来源，方便追溯\n            frames.append(df)\n            print(f\"已读取：{f.name}（{len(df)} 行）\")\n        except Exception as e:\n            print(f\"读取失败 {f.name}：{e}\")  # 某个文件坏了不影响整体\n\n    result = pd.concat(frames, ignore_index=True)\n    result.to_excel(output, index=False)\n    print(f\"\\n合并完成：{len(result)} 行 → {output}\")\n\nmerge_excels(\".\u002F分公司报表\", \".\u002F合并结果.xlsx\")\n```\n\n## 实战：生成带样式的月度报表\n\n数据分析完，输出一份「拿得出手」的报表：\n\n```python\nimport pandas as pd\nfrom openpyxl import load_workbook\nfrom openpyxl.styles import Alignment, Font, PatternFill\n\n# 第一步：pandas 算数据\ndf = pd.read_excel(\"原始数据.xlsx\")\nsummary = df.groupby(\"部门\").agg(\n    总销售额=(\"销售额\", \"sum\"),\n    订单数=(\"订单号\", \"count\"),\n).reset_index()\n\n# 第二步：写入文件\nsummary.to_excel(\"月度报表.xlsx\", index=False, sheet_name=\"汇总\")\n\n# 第三步：openpyxl 美化\nwb = load_workbook(\"月度报表.xlsx\")\nws = wb.active\n\n# 标题行样式\nfor cell in ws[1]:\n    cell.font = Font(bold=True, color=\"FFFFFF\")\n    cell.fill = PatternFill(\"solid\", start_color=\"305496\")\n    cell.alignment = Alignment(horizontal=\"center\")\n\n# 数字格式：千分位 + 保留两位小数\nfor row in ws.iter_rows(min_row=2, min_col=2, max_col=2):\n    for cell in row:\n        cell.number_format = \"#,##0.00\"\n\n# 自适应列宽（按内容长度估算）\nfor col in ws.columns:\n    max_len = max(len(str(c.value or \"\")) for c in col)\n    ws.column_dimensions[col[0].column_letter].width = max_len * 2 + 4\n\nwb.save(\"月度报表.xlsx\")\n```\n\n## 常见坑\n\n**1. xls 与 xlsx 要分清**\n\n- `.xlsx`（2007 后的格式）→ openpyxl\n- `.xls`（老格式）→ 需要 `xlrd` 库，且 xlrd 2.0+ 只支持 xls\n- 简单办法：先用 Excel 把 xls 另存为 xlsx\n\n**2. 大文件内存爆炸**\n\n十万行以上的文件，openpyxl 普通模式会很吃内存，用只读模式：\n\n```python\nwb = load_workbook(\"大文件.xlsx\", read_only=True)\n```\n\npandas 则分块读（仅限 CSV；Excel 大文件建议先转 CSV）。\n\n**3. 数字变科学计数法 \u002F 丢失前导零**\n\n身份证号、手机号、工号这些「数字字符串」，读的时候指定 `dtype=str`，写的时候设置单元格为文本格式：\n\n```python\ncell.number_format = \"@\"  # 文本格式，18位身份证不会被转成科学计数\n```\n\n**4. 公式不计算**\n\nopenpyxl 写的公式，只有用 Excel\u002FWPS 打开时才会计算。如果程序里要读公式结果，先用 Excel 打开保存一次，或读取时用 `data_only=True`（前提是文件被 Excel 保存过）。\n\n## 写在最后\n\n办公自动化的回报是立竿见影的：昨天还花一下午的合并报表，今天一个脚本 30 秒跑完。从你身边最烦的那个重复性 Excel 工作开始——那就是你的第一个自动化脚本最好的题材。\n","\u002Fstatic\u002Fuploads\u002Fimages\u002F2026\u002F09\u002F29\u002F1790636138623334148.png","2026-09-29T06:56:34.579061313+08:00","2026-09-29T06:55:38.623451323+08:00","2026-09-29T06:56:34.579126509+08:00",{"id":70,"name":75,"slug":76,"description":75,"icon":5,"sortOrder":54,"postCount":54,"parentId":54,"createdAt":77,"updatedAt":77},[267,268,272,276,280],{"id":80,"name":76,"slug":81,"color":82,"postCount":54,"createdAt":83},{"id":269,"name":270,"slug":258,"color":82,"postCount":54,"createdAt":271},139,"Excel","2026-09-29T06:55:38.596869557+08:00",{"id":273,"name":274,"slug":274,"color":82,"postCount":54,"createdAt":275},140,"openpyxl","2026-09-29T06:55:38.603778674+08:00",{"id":277,"name":171,"slug":278,"color":82,"postCount":54,"createdAt":279},141,"pandas-2","2026-09-29T06:55:38.611186815+08:00",{"id":281,"name":282,"slug":283,"color":82,"postCount":54,"createdAt":284},142,"办公自动化","ban-gong-zi-dong-hua","2026-09-29T06:55:38.617834232+08:00",{"newer":286,"older":287},{"id":169,"title":170,"slug":171,"summary":172,"content":173,"coverImage":174,"categoryId":70,"authorId":55,"viewCount":54,"likeCount":54,"commentCount":54,"isTop":54,"isRecommend":54,"status":55,"rejectReason":5,"publishedAt":175,"createdAt":176,"updatedAt":177},null,{"items":289,"pagination":290},[],{"page":55,"pageSize":291,"total":54,"totalPages":55},20,{"materials":293},[],{"materials":295},[]]