[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"site-settings":3,"post-regex":48,"footer-friends":93,"related-regex":105,"nav-regex":282,"comments-19":294,"ad-article_top":298,"ad-article_bottom":300},{"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",{"id":49,"title":50,"slug":51,"summary":52,"content":53,"coverImage":54,"categoryId":55,"authorId":56,"viewCount":57,"likeCount":57,"commentCount":57,"isTop":57,"isRecommend":57,"status":56,"rejectReason":5,"publishedAt":58,"createdAt":59,"updatedAt":60,"category":61,"tags":65,"author":85,"wordCount":91,"readingTime":92},19,"Python 正则表达式速查手册：12 个最常用的模式","regex","正则表达式不用背，记住元字符逻辑 + 收藏这份速查表就够了。手机号、邮箱、URL 提取等 12 个实战模式全部经过验证可直接用。","## 正则的本质：用模式描述文本\n\n正则表达式（Regex）是一种**描述文本模式的语言**。与其说「找所有手机号」，不如用模式描述「1 开头，第二位是 3-9，后面再跟 9 个数字」——这就是正则干的事。\n\nPython 用内置的 `re` 模块，零安装：\n\n```python\nimport re\n\ntext = \"联系电话：13812345678，备用：19900001111\"\nphones = re.findall(r\"1[3-9]\\d{9}\", text)\nprint(phones)  # ['13812345678', '19900001111']\n```\n\n## 四个核心函数\n\n```python\nimport re\n\n# 1. findall：找所有匹配，返回列表\nre.findall(r\"\\d+\", \"我有3个苹果和25个橘子\")  # ['3', '25']\n\n# 2. search：找第一个匹配，返回 Match 对象（没有则返回 None）\nm = re.search(r\"\\d+\", \"我有3个苹果\")\nif m:\n    print(m.group())  # '3'\n\n# 3. sub：替换\nre.sub(r\"\\d+\", \"N\", \"我有3个苹果和25个橘子\")  # '我有N个苹果和N个橘子'\n\n# 4. split：按模式切分\nre.split(r\"[,，]\", \"苹果,香蕉，橘子\")  # ['苹果', '香蕉', '橘子']\n```\n\n## 元字符速查（正则的字母表）\n\n| 符号 | 含义 | 示例 |\n|------|------|------|\n| `.` | 任意字符（除换行） | `a.c` 匹配 abc、a1c |\n| `\\d` | 数字 | `\\d\\d` 匹配 42 |\n| `\\w` | 字母\u002F数字\u002F下划线 | `\\w+` 匹配 hello_1 |\n| `\\s` | 空白（空格\u002F制表\u002F换行） | |\n| `^` `$` | 行首 \u002F 行尾 | `^hello` 要求行首是 hello |\n| `*` | 前一个出现 0 次或多次 | `ab*` 匹配 a、ab、abbb |\n| `+` | 前一个出现 1 次或多次 | `ab+` 匹配 ab、abbb |\n| `?` | 前一个出现 0 次或 1 次 | `colou?r` 匹配 color 和 colour |\n| `{n}` `{n,}` `{n,m}` | 指定次数 | `\\d{4}` 恰好 4 位数字 |\n| `[...]` | 字符集合 | `[aeiou]` 元音之一 |\n| `[^...]` | 排除集合 | `[^0-9]` 非数字 |\n| `(...)` | 分组（可提取） | 见下文 |\n| `A\\|B` | 或 | `cat\\|dog` |\n\n大写是取反：`\\D` 非数字，`\\W` 非单词字符，`\\S` 非空白。\n\n**写 Python 正则一定用原始字符串** `r\"...\"`，否则 `\\d` 会被 Python 先转义一层。\n\n## 12 个实战模式（复制即用）\n\n```python\nimport re\n\n# 1. 手机号\nr\"1[3-9]\\d{9}\"\n\n# 2. 邮箱\nr\"[\\w.+-]+@[\\w-]+\\.[\\w.]+\"\n\n# 3. URL\nr\"https?:\u002F\u002F[\\w.-]+(?:\u002F[\\w.\u002F?=&%-]*)?\"\n\n# 4. 身份证号（18位，简单校验）\nr\"\\d{17}[\\dXx]\"\n\n# 5. 日期 2026-09-29 或 2026\u002F09\u002F29\nr\"\\d{4}[-\u002F]\\d{1,2}[-\u002F]\\d{1,2}\"\n\n# 6. 时间 14:30 或 14:30:05\nr\"\\d{1,2}:\\d{2}(?::\\d{2})?\"\n\n# 7. IP 地址（简单版，不校验 0-255 范围）\nr\"(?:\\d{1,3}\\.){3}\\d{1,3}\"\n\n# 8. 中文字符\nr\"[一-龥]+\"\n\n# 9. 纯数字（整数，可带负号）\nr\"-?\\d+\"\n\n# 10. 小数（可带负号）\nr\"-?\\d+\\.?\\d*\"\n\n# 11. HTML 标签\nr\"\u003C[^>]+>\"\n\n# 12. 匹配汉字开头的用户名（2-6位中文或数字字母）\nr\"^[一-龥][\\w一-龥]{1,5}$\"\n```\n\n## 分组提取：不只匹配，还要拿出来\n\n圆括号 `()` 分组后，可以分别提取每部分内容：\n\n```python\nimport re\n\nlog = \"2026-09-29 14:30:05 [ERROR] 数据库连接失败\"\n\n# 三个分组：日期、时间、日志内容\npattern = r\"(\\d{4}-\\d{2}-\\d{2}) (\\d{2}:\\d{2}:\\d{2}) \\[(\\w+)\\] (.+)\"\nm = re.search(pattern, log)\nif m:\n    date, time_, level, msg = m.groups()\n    print(date, time_, level, msg)  # 2026-09-29 14:30:05 ERROR 数据库连接失败\n```\n\n**命名分组**更可读：\n\n```python\npattern = r\"(?P\u003Cdate>\\d{4}-\\d{2}-\\d{2}) (?P\u003Ctime>\\d{2}:\\d{2}:\\d{2}) \\[(?P\u003Clevel>\\w+)\\] (?P\u003Cmsg>.+)\"\nm = re.search(pattern, log)\nprint(m.group(\"msg\"))  # 按名字取，不用数括号顺序\n```\n\n## 贪婪与非贪婪：最容易踩的坑\n\n默认是**贪婪匹配**——尽可能多地吃字符：\n\n```python\nimport re\n\ntext = \"\u003Cp>第一段\u003C\u002Fp>\u003Cp>第二段\u003C\u002Fp>\"\n\n# 贪婪：从第一个 \u003Cp> 一直吃到最后一个 \u003C\u002Fp>\nre.findall(r\"\u003Cp>.*\u003C\u002Fp>\", text)\n# ['\u003Cp>第一段\u003C\u002Fp>\u003Cp>第二段\u003C\u002Fp>']  ← 不是想要的！\n\n# 非贪婪（加 ?）：吃到最近的 \u003C\u002Fp> 就停\nre.findall(r\"\u003Cp>.*?\u003C\u002Fp>\", text)\n# ['\u003Cp>第一段\u003C\u002Fp>', '\u003Cp>第二段\u003C\u002Fp>']  ← 正确\n```\n\n记住：`*`、`+`、`{n,m}` 后面加 `?` 就变非贪婪。解析 HTML\u002F括号配对内容时，几乎总是用非贪婪。\n\n## 编译复用与标志位\n\n循环里反复用同一个正则，先编译一次提速：\n\n```python\nPHONE_RE = re.compile(r\"1[3-9]\\d{9}\")  # 编译一次\nfor line in lines:\n    m = PHONE_RE.search(line)           # 复用\n```\n\n常用标志位：\n\n```python\nre.findall(r\"hello\", text, re.IGNORECASE)  # 忽略大小写，HELLO 也能匹配\nre.findall(r\"^第.章\", text, re.MULTILINE)  # 多行模式：^ $ 匹配每行的首尾\nre.findall(r\"a.c\", text, re.DOTALL)        # 让 . 也能匹配换行符\n```\n\n## 什么时候不该用正则\n\n正则很强，但有边界：\n\n1. **别用正则解析 HTML**：HTML 不是规则语言，嵌套和异常写法会让正则崩溃。用 BeautifulSoup\u002Flxml\n2. **别用正则校验复杂规则**：密码强度「同时含大小写+数字+符号且不复读」，用代码逐项判断比一条超长正则清晰得多\n3. **简单字符串操作别上正则**：`s.startswith()`、`s.split()`、`in` 能解决的，用它们更快更易读\n\n## 练习与调试工具\n\n- **regex101.com**：在线调试神器，粘贴文本和正则，实时高亮匹配、逐字符解释正则含义\n- **pythex.org**：轻量替代\n- 写复杂正则时，先写 3-5 个「应该匹配」和「不应该匹配」的测试用例，边写边验证\n\n## 写在最后\n\n正则不用「学会」再开始用——它是个查表工具。收藏本文的 12 个模式，遇到新需求时在 regex101 上边试边改，三次之后你就会发现自己已经不怎么看表了。\n","\u002Fstatic\u002Fuploads\u002Fimages\u002F2026\u002F09\u002F29\u002F1790636138723006263.png",8,1,0,"2026-09-29T06:56:31.458490256+08:00","2026-09-29T06:55:38.723133538+08:00","2026-09-29T06:56:31.458543123+08:00",{"id":55,"name":62,"slug":63,"description":62,"icon":5,"sortOrder":57,"postCount":57,"parentId":57,"createdAt":64,"updatedAt":64},"Python专题","Python","2026-09-29T06:55:05.938571812+08:00",[66,71,76,81],{"id":67,"name":63,"slug":68,"color":69,"postCount":57,"createdAt":70},6,"python","#e2e8f0","2026-09-29T04:25:30.347030306+08:00",{"id":72,"name":73,"slug":74,"color":69,"postCount":57,"createdAt":75},147,"正则表达式","zheng-ze-biao-da-shi","2026-09-29T06:55:38.700014658+08:00",{"id":77,"name":78,"slug":79,"color":69,"postCount":57,"createdAt":80},148,"文本处理","wen-ben-chu-li","2026-09-29T06:55:38.708683837+08:00",{"id":82,"name":83,"slug":83,"color":69,"postCount":57,"createdAt":84},149,"re","2026-09-29T06:55:38.716286829+08:00",{"id":56,"username":86,"nickname":87,"avatarUrl":88,"bio":5,"rewardWechatQr":89,"rewardAlipayQr":90},"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",453,2,[94,100],{"id":92,"name":95,"url":96,"logo":97,"description":95,"sortOrder":57,"status":56,"createdAt":98,"updatedAt":99},"哔哩哔哩","https:\u002F\u002Fwww.bilibili.com\u002F","\\images\\social\\bilibili.png","0001-01-01T00:00:00Z","2026-09-29T04:44:41.867019475+08:00",{"id":56,"name":101,"url":102,"logo":103,"description":101,"sortOrder":57,"status":56,"createdAt":104,"updatedAt":104},"GitHub","https:\u002F\u002Fgithub.com","\\images\\social\\github.png","2026-09-29T04:32:39.65740234+08:00",[106,135,167,194,222,254],{"id":107,"title":108,"slug":109,"summary":110,"content":111,"coverImage":112,"categoryId":55,"authorId":56,"viewCount":56,"likeCount":57,"commentCount":57,"isTop":57,"isRecommend":57,"status":56,"rejectReason":5,"publishedAt":113,"createdAt":114,"updatedAt":115,"category":116,"tags":117},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":55,"name":62,"slug":63,"description":62,"icon":5,"sortOrder":57,"postCount":57,"parentId":57,"createdAt":64,"updatedAt":64},[118,119,124,127,131],{"id":67,"name":63,"slug":68,"color":69,"postCount":57,"createdAt":70},{"id":120,"name":121,"slug":122,"color":69,"postCount":57,"createdAt":123},118,"虚拟环境","xu-ni-huan-jing","2026-09-29T06:55:38.308255024+08:00",{"id":125,"name":109,"slug":109,"color":69,"postCount":57,"createdAt":126},119,"2026-09-29T06:55:38.316270691+08:00",{"id":128,"name":129,"slug":129,"color":69,"postCount":57,"createdAt":130},120,"conda","2026-09-29T06:55:38.323353011+08:00",{"id":132,"name":133,"slug":133,"color":69,"postCount":57,"createdAt":134},121,"uv","2026-09-29T06:55:38.330626142+08:00",{"id":136,"title":137,"slug":138,"summary":139,"content":140,"coverImage":141,"categoryId":55,"authorId":56,"viewCount":57,"likeCount":57,"commentCount":57,"isTop":57,"isRecommend":57,"status":56,"rejectReason":5,"publishedAt":142,"createdAt":143,"updatedAt":144,"category":145,"tags":146},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":55,"name":62,"slug":63,"description":62,"icon":5,"sortOrder":57,"postCount":57,"parentId":57,"createdAt":64,"updatedAt":64},[147,148,153,157,162],{"id":67,"name":63,"slug":68,"color":69,"postCount":57,"createdAt":70},{"id":149,"name":150,"slug":151,"color":69,"postCount":57,"createdAt":152},122,"爬虫","pa-chong","2026-09-29T06:55:38.359394779+08:00",{"id":154,"name":155,"slug":155,"color":69,"postCount":57,"createdAt":156},123,"requests","2026-09-29T06:55:38.366815934+08:00",{"id":158,"name":159,"slug":160,"color":69,"postCount":57,"createdAt":161},124,"BeautifulSoup","beautifulsoup","2026-09-29T06:55:38.372320794+08:00",{"id":163,"name":164,"slug":165,"color":69,"postCount":57,"createdAt":166},125,"数据采集","shu-ju-cai-ji","2026-09-29T06:55:38.379703082+08:00",{"id":168,"title":169,"slug":170,"summary":171,"content":172,"coverImage":173,"categoryId":55,"authorId":56,"viewCount":57,"likeCount":57,"commentCount":57,"isTop":57,"isRecommend":57,"status":56,"rejectReason":5,"publishedAt":174,"createdAt":175,"updatedAt":176,"category":177,"tags":178},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":55,"name":62,"slug":63,"description":62,"icon":5,"sortOrder":57,"postCount":57,"parentId":57,"createdAt":64,"updatedAt":64},[179,180,184,189],{"id":67,"name":63,"slug":68,"color":69,"postCount":57,"createdAt":70},{"id":181,"name":182,"slug":170,"color":69,"postCount":57,"createdAt":183},126,"Pandas","2026-09-29T06:55:38.411798198+08:00",{"id":185,"name":186,"slug":187,"color":69,"postCount":57,"createdAt":188},127,"数据分析","shu-ju-fen-xi","2026-09-29T06:55:38.419436825+08:00",{"id":190,"name":191,"slug":192,"color":69,"postCount":57,"createdAt":193},128,"数据处理","shu-ju-chu-li","2026-09-29T06:55:38.426699835+08:00",{"id":195,"title":196,"slug":197,"summary":198,"content":199,"coverImage":200,"categoryId":55,"authorId":56,"viewCount":57,"likeCount":57,"commentCount":57,"isTop":57,"isRecommend":57,"status":56,"rejectReason":5,"publishedAt":201,"createdAt":202,"updatedAt":203,"category":204,"tags":205},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","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":55,"name":62,"slug":63,"description":62,"icon":5,"sortOrder":57,"postCount":57,"parentId":57,"createdAt":64,"updatedAt":64},[206,207,212,217],{"id":67,"name":63,"slug":68,"color":69,"postCount":57,"createdAt":70},{"id":208,"name":209,"slug":210,"color":69,"postCount":57,"createdAt":211},129,"装饰器","zhuang-shi-qi","2026-09-29T06:55:38.452280181+08:00",{"id":213,"name":214,"slug":215,"color":69,"postCount":57,"createdAt":216},130,"闭包","bi-bao","2026-09-29T06:55:38.460791287+08:00",{"id":218,"name":219,"slug":220,"color":69,"postCount":57,"createdAt":221},131,"进阶","jin-jie","2026-09-29T06:55:38.468837931+08:00",{"id":223,"title":224,"slug":225,"summary":226,"content":227,"coverImage":228,"categoryId":55,"authorId":56,"viewCount":57,"likeCount":57,"commentCount":57,"isTop":57,"isRecommend":57,"status":56,"rejectReason":5,"publishedAt":229,"createdAt":230,"updatedAt":231,"category":232,"tags":233},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":55,"name":62,"slug":63,"description":62,"icon":5,"sortOrder":57,"postCount":57,"parentId":57,"createdAt":64,"updatedAt":64},[234,235,239,244,249],{"id":67,"name":63,"slug":68,"color":69,"postCount":57,"createdAt":70},{"id":236,"name":237,"slug":225,"color":69,"postCount":57,"createdAt":238},132,"FastAPI","2026-09-29T06:55:38.496902061+08:00",{"id":240,"name":241,"slug":242,"color":69,"postCount":57,"createdAt":243},133,"Web开发","web-kai-fa","2026-09-29T06:55:38.503802445+08:00",{"id":245,"name":246,"slug":247,"color":69,"postCount":57,"createdAt":248},134,"REST API","rest-api","2026-09-29T06:55:38.511505671+08:00",{"id":250,"name":251,"slug":252,"color":69,"postCount":57,"createdAt":253},135,"后端","hou-duan","2026-09-29T06:55:38.519097242+08:00",{"id":255,"title":256,"slug":257,"summary":258,"content":259,"coverImage":260,"categoryId":55,"authorId":56,"viewCount":57,"likeCount":57,"commentCount":57,"isTop":57,"isRecommend":57,"status":56,"rejectReason":5,"publishedAt":261,"createdAt":262,"updatedAt":263,"category":264,"tags":265},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":55,"name":62,"slug":63,"description":62,"icon":5,"sortOrder":57,"postCount":57,"parentId":57,"createdAt":64,"updatedAt":64},[266,267,272,277],{"id":67,"name":63,"slug":68,"color":69,"postCount":57,"createdAt":70},{"id":268,"name":269,"slug":270,"color":69,"postCount":57,"createdAt":271},136,"异常处理","yi-chang-chu-li","2026-09-29T06:55:38.549655932+08:00",{"id":273,"name":274,"slug":275,"color":69,"postCount":57,"createdAt":276},137,"日志","ri-zhi","2026-09-29T06:55:38.556461509+08:00",{"id":278,"name":279,"slug":280,"color":69,"postCount":57,"createdAt":281},138,"最佳实践","zui-jia-shi-jian","2026-09-29T06:55:38.563680858+08:00",{"newer":283,"older":293},{"id":284,"title":285,"slug":286,"summary":287,"content":288,"coverImage":289,"categoryId":55,"authorId":56,"viewCount":57,"likeCount":57,"commentCount":57,"isTop":57,"isRecommend":57,"status":56,"rejectReason":5,"publishedAt":290,"createdAt":291,"updatedAt":292},18,"Python 并发编程入门：多线程、多进程、异步怎么选","concurrency","GIL 到底是什么？为什么 Python 多线程跑不满 CPU？多线程、多进程、asyncio 三种并发方案的原理、适用场景和实战代码对比。","## 先搞懂 GIL：Python 并发的总开关\n\nGIL（全局解释器锁）是 CPython 的一个机制：**同一时刻，一个进程里只允许一个线程执行 Python 字节码**。\n\n带来的直接后果：\n\n- **CPU 密集型任务**（计算、图像处理）：多线程在 Python 里没用，甚至更慢（多了切换开销）\n- **IO 密集型任务**（网络请求、读写文件、数据库）：多线程有效！因为线程在等待 IO 时会释放 GIL，别的线程趁机干活\n\n判断你的任务类型，方案就选对了一半：\n\n| 任务类型 | 例子 | 正确方案 |\n|---------|------|---------|\n| IO 密集 | 爬虫、调 API、读写数据库 | 多线程 或 asyncio |\n| CPU 密集 | 视频转码、科学计算、数据加密 | 多进程 |\n\n## 方案一：多线程（IO 密集场景的简单解）\n\n**串行 vs 多线程**下载 10 个网页：\n\n```python\nimport time\nfrom concurrent.futures import ThreadPoolExecutor\n\nimport requests\n\nURLS = [\"https:\u002F\u002Fhttpbin.org\u002Fdelay\u002F1\"] * 10  # 每个请求固定延迟1秒\n\ndef fetch(url: str) -> int:\n    resp = requests.get(url, timeout=10)\n    return len(resp.content)\n\n# 串行：约 10 秒\nstart = time.time()\nfor u in URLS:\n    fetch(u)\nprint(f\"串行耗时：{time.time() - start:.1f}s\")\n\n# 10 线程：约 1 秒\nstart = time.time()\nwith ThreadPoolExecutor(max_workers=10) as pool:\n    list(pool.map(fetch, URLS))\nprint(f\"多线程耗时：{time.time() - start:.1f}s\")\n```\n\n10 倍的差距，代码只改了三行。`ThreadPoolExecutor.map` 是日常最够用的写法：**把任务列表扔进去，拿回结果列表**，线程池帮你搞定创建和回收。\n\n**线程安全提醒**：多个线程同时改同一个列表\u002F字典会出诡异 bug。要么用 `queue.Queue`（线程安全队列），要么给共享数据加锁：\n\n```python\nimport threading\n\nlock = threading.Lock()\ncounter = 0\n\ndef add():\n    global counter\n    for _ in range(100000):\n        with lock:          # 加锁，同一时刻只有一个线程能改\n            counter += 1\n```\n\n## 方案二：多进程（CPU 密集的唯一解）\n\n```python\nimport time\nfrom concurrent.futures import ProcessPoolExecutor\n\ndef heavy_compute(n: int) -> int:\n    \"\"\"模拟重计算：算第 n 个斐波那契数（故意用低效递归）\"\"\"\n    if n \u003C 2:\n        return n\n    return heavy_compute(n - 1) + heavy_compute(n - 2)\n\nTASKS = [32] * 8\n\n# 串行\nstart = time.time()\nfor t in TASKS:\n    heavy_compute(t)\nprint(f\"串行：{time.time() - start:.1f}s\")\n\n# 8 进程（吃满多核 CPU）\nstart = time.time()\nwith ProcessPoolExecutor(max_workers=8) as pool:\n    list(pool.map(heavy_compute, TASKS))\nprint(f\"多进程：{time.time() - start:.1f}s\")\n```\n\n接口和线程池几乎一样（`ProcessPoolExecutor`），但底层是开多个 Python 进程，每个进程有独立的 GIL，所以能真正吃满多核。\n\n**代价**：进程开销比线程大得多（内存、启动慢），进程间不能直接共享变量，要通信用 `multiprocessing.Queue` 或共享内存。进程数别乱开，一般等于 CPU 核数。\n\n## 方案三：asyncio（高并发 IO 的终极形态）\n\n多线程的问题是线程多了开销大、切换频繁。asyncio 用**单线程 + 协程**实现并发：一个线程里，哪个协程在等 IO 就切走，不切操作系统线程，切换成本几乎为零。\n\n单机轻松维持上万并发连接——这是线程池做不到的。\n\n```python\nimport asyncio\nimport time\n\nimport aiohttp\n\nURLS = [\"https:\u002F\u002Fhttpbin.org\u002Fdelay\u002F1\"] * 20\n\nasync def fetch(session: aiohttp.ClientSession, url: str) -> int:\n    async with session.get(url) as resp:\n        data = await resp.read()\n        return len(data)\n\nasync def main():\n    async with aiohttp.ClientSession() as session:\n        # 并发发起所有请求\n        tasks = [fetch(session, u) for u in URLS]\n        results = await asyncio.gather(*tasks)\n        print(f\"完成 {len(results)} 个请求\")\n\nstart = time.time()\nasyncio.run(main())   # 20 个请求约 1 秒完成，且只用了 1 个线程\nprint(f\"异步耗时：{time.time() - start:.1f}s\")\n```\n\n关键语法：\n\n- `async def` 定义协程函数\n- `await` 表示「这里要等 IO，先让别的协程跑」\n- `asyncio.gather` 并发执行一批协程\n- 网络库要用异步版：`aiohttp` 替代 `requests`\n\n**控制并发量**（别把目标服务器打爆）：\n\n```python\nsem = asyncio.Semaphore(5)  # 最多同时5个\n\nasync def fetch_limited(session, url):\n    async with sem:   # 拿到令牌才能执行\n        return await fetch(session, url)\n```\n\n## 三方案对比实测（20 个延迟 1 秒的请求）\n\n| 方案 | 耗时 | 线程\u002F进程数 | 适合场景 |\n|------|------|-----------|---------|\n| 串行 | 20s | 1 | 任务少且快 |\n| 多线程(10) | 2s | 10 | 几十个并发，代码简单优先 |\n| asyncio | 1s | 1（协程并发） | 成百上千并发，追求极致 |\n| 多进程(8) | ≈20s | 8 | 这是 IO 任务，多进程帮不上忙 |\n\n## 怎么选：决策流程\n\n```text\n任务主要是等待（网络\u002F磁盘\u002F数据库）？\n├── 是 → 并发量多大？\n│   ├── 几十上百 → ThreadPoolExecutor（简单）\n│   └── 成千上万 → asyncio + aiohttp（高性能）\n└── 否（CPU 计算为主）→ ProcessPoolExecutor\n    （或更优：用 numpy 这类底层 C 实现的库，天然绕开 GIL）\n```\n\n## 常见坑\n\n**1. 在协程里调了同步阻塞函数**\n\n```python\nasync def bad():\n    time.sleep(1)        # 错误！整个事件循环卡住1秒\n    await asyncio.sleep(1)  # 正确：让出控制权\n```\n\n同理：协程里用 `requests`（同步库）会卡死事件循环，必须用 `aiohttp`。\n\n**2. 共享数据没加保护**\n\nasyncio 单线程看似安全，但 `await` 点之间数据可能被其他协程改掉。跨 `await` 的复合操作仍需 `asyncio.Lock`。\n\n**3. 盲目开几百个线程**\n\n线程不是越多越好，IO 任务开到几十个收益就见顶，再多只剩切换开销和内存浪费。\n\n## 写在最后\n\n并发编程的复杂度不在 API 而在**思维方式**：从「一步一步来」切换到「同时推进很多事」。建议的入门路径：先把 `ThreadPoolExecutor.map` 用到熟（能解决 80% 的需求），需要高并发时再学 asyncio，CPU 瓶颈出现时自然懂得上多进程。\n","\u002Fstatic\u002Fuploads\u002Fimages\u002F2026\u002F09\u002F29\u002F1790636138676846283.png","2026-09-29T06:56:33.026099861+08:00","2026-09-29T06:55:38.676981638+08:00","2026-09-29T06:56:33.026156397+08:00",null,{"items":295,"pagination":296},[],{"page":56,"pageSize":297,"total":57,"totalPages":56},20,{"materials":299},[],{"materials":301},[]]