


[{"content":"后端工程师 / AI 应用从业者，专注于高性能高可用后端架构与 RAG、Agent 智能应用 更多 → 3 篇文章 · 1.1w 字 ","date":"2026-04-04","externalUrl":null,"permalink":"/zh-cn/","section":"Jim Deng 的博客","summary":"后端工程师 / AI 应用从业者，专注于高性能高可用后端架构与 RAG、Agent 智能应用 更多 → 3 篇文章 · 1.1w 字 ","title":"Jim Deng 的博客","type":"page"},{"content":" 👋 嗨，我是 Jim Deng # 后端工程师 | AI 应用从业者\n我是一名专注于后端技术和 AI 应用开发的工程师，热衷于构建高性能、高可用的系统架构，同时积极探索人工智能在实际场景中的落地应用。\n🛠️ 技术栈 # 核心语言 # Go · Python · C++ · Java 技术框架 # Docker · Kubernetes · CI/CD · n8n · Workflow · Agent · RAG 数据库 # MySQL · Redis · MongoDB · Elasticsearch 💡 专长领域 # 高性能后端服务 — 构建高性能、高可用、高扩展的后端系统 RAG 与 Agent 系统 — 设计和开发基于 RAG 和 Agent 的智能应用 Python 自动化方案 — 使用 Python 打造高效的自动化解决方案 🚀 正在做 # 正从事 AI 应用开发，并在博客中分享该领域的最新技术和见解 持续学习和实践 大模型应用、智能体开发 等前沿方向 📬 联系方式 # 欢迎联系或合作！\n博客：jimdeng.com 邮箱：jimdengdev@qq.com GitHub：jimdengdev ","date":"2026-04-04","externalUrl":null,"permalink":"/zh-cn/about/","section":"Jim Deng 的博客","summary":"👋 嗨，我是 Jim Deng # 后端工程师 | AI 应用从业者\n我是一名专注于后端技术和 AI 应用开发的工程师，热衷于构建高性能、高可用的系统架构，同时积极探索人工智能在实际场景中的落地应用。\n","title":"关于我","type":"page"},{"content":"","date":"2025-11-09","externalUrl":null,"permalink":"/zh-cn/categories/agent/","section":"Categories","summary":"","title":"Agent","type":"categories"},{"content":"","date":"2025-11-09","externalUrl":null,"permalink":"/zh-cn/tags/ai-%E8%B5%84%E8%AE%AF%E7%AE%80%E6%8A%A5/","section":"标签","summary":"","title":"AI 资讯简报","type":"tags"},{"content":"","date":"2025-11-09","externalUrl":null,"permalink":"/zh-cn/tags/ai%E5%B7%A5%E4%BD%9C%E6%B5%81/","section":"标签","summary":"","title":"AI工作流","type":"tags"},{"content":"","date":"2025-11-09","externalUrl":null,"permalink":"/zh-cn/categories/","section":"Categories","summary":"","title":"Categories","type":"categories"},{"content":"","date":"2025-11-09","externalUrl":null,"permalink":"/zh-cn/tags/llm/","section":"标签","summary":"","title":"LLM","type":"tags"},{"content":"","date":"2025-11-09","externalUrl":null,"permalink":"/zh-cn/tags/n8n/","section":"标签","summary":"","title":"N8n","type":"tags"},{"content":"","date":"2025-11-09","externalUrl":null,"permalink":"/zh-cn/tags/workflow/","section":"标签","summary":"","title":"Workflow","type":"tags"},{"content":"Blowfish 支持基于 Hugo 的所有分类方法。同时，当前的标签预览页也支持展示自定义内容。\n在这里可以为每个分类添加额外的描述信息。查看下面的高级标签页面，了解更多。\n","date":"2025-11-09","externalUrl":null,"permalink":"/zh-cn/tags/","section":"标签","summary":"Blowfish 支持基于 Hugo 的所有分类方法。同时，当前的标签预览页也支持展示自定义内容。\n在这里可以为每个分类添加额外的描述信息。查看下面的高级标签页面，了解更多。\n","title":"标签","type":"tags"},{"content":"","date":"2025-11-09","externalUrl":null,"permalink":"/zh-cn/posts/","section":"博客","summary":"","title":"博客","type":"posts"},{"content":"","date":"2025-11-09","externalUrl":null,"permalink":"/zh-cn/tags/%E5%A4%A7%E6%A8%A1%E5%9E%8B/","section":"标签","summary":"","title":"大模型","type":"tags"},{"content":" 一键生成高质量 AI 简报：n8n 工作流最佳实践 # 一、简介 # n8n 是一款开源的低代码工作流自动化工具，专注于将各种应用和服务连接起来，形成自动化的业务流程。它提供了超过400个预置集成，覆盖各类SaaS服务和数据库。既可以通过简单的拖拽操作构建工作流，也可以通过js或Python代码进行更复杂的定制。 支持Docker私有化部署，完全不吃配置，1核1G的服务器应该都能跑。 适合人群： 需要高度定制自动化流程的团队、开发者、以及追求效率最大化的中小企业。 可以个人或者企业内部但是不能外部商用，商用推荐 dfly 官网：https://n8n.io/ 二、部署 n8n # 准备一台已安装 docker 的 1h1g 以上的服务器、nas 或者本地电脑 https://github.com/n8n-io/n8n # 创建数据存储卷 docker volume create n8n_data # 后台运行n8n服务，--rm 如果有改容器就会删除 docker run -it --rm --name n8n -p 5678:5678 -v n8n_data:/home/node/.n8n docker.n8n.io/n8nio/n8n docker启动后本地电脑使用http://localhost:5678， 服务器用 http://ip:5678 三、汉化 # 汉化包：https://github.com/other-blowsnow/n8n-i18n-chinese/releases 如果有汉化需求可以参考该github项目 readme 操作 四、jina-reader 部署 # 本文采用 jina reader 来爬取网页文本。Jina Reader 是基于网页抓取、内容清洗、自然语言处理等技术，确保提取内容的准确性和结构化，能够将网页内容转换为适合 LLM 处理的纯文本格式，支持多种输出格式。 网址：https://jina.ai/ 计费：免费用户每个账号可以获取 10,000,000 个 token，当然也可以自己部署。 准备 1h1g 服务器即可运行，当然配置越高支持的并发越高。 采用 docker 部署，执行下面指令 部署完成后，可以用下面指令试下效果 curl -H \u0026#34;X-Respond-With: markdown\u0026#34; \u0026#39;http://localhost:3000/https://www.baidu.com\u0026#39; 五、AI 简报工作流实战 # 5.1 提前准备 # 目标：定时从掘金人工智能热榜取榜单，并用 jina reader 抓取正文，然后用大模型总结成新闻简报，最后推送到邮箱\n数据源：https://juejin.cn/hot/articles/6809637773935378440 打开调试台获取榜单api\nhttps://api.juejin.cn/content_api/v1/content/article_rank?category_id=6809637773935378440\u0026amp;type=hot\u0026amp;aid=2608\u0026amp;uuid=6963437645070976542\u0026amp;spider=1 大模型：使用硅基流动 api，新用户有一定的额度，在下面 API 密钥复制密钥，后续会用。\njina reader： 如上图所示\nqq 邮箱授权码：qq 邮箱网页版→设置→ 账号与安全→安全设置→生成授权码→短信验证，获取授权码\n5.2 流程实现 # 打开n8n，新建一个 workflow，点右上角+新增节点\n触发方式：n8n 支持非常多的工作流触发方式，先选第一个手动触发，方便调试，工作流建好后换成定时，比如每天 8:00 am\n抓取 AI 热榜：选择 http request 拉取热榜数据，如下两张图，URL 填：https://api.juejin.cn/content_api/v1/content/article_rank?category_id=6809637773935378440\u0026amp;type=hot\u0026amp;aid=2608\u0026amp;uuid=6963437645070976542\u0026amp;spider=1\nSplit Out 节点：把 data 数据拆成 20 个 items\nLimit 节点：这里填 5 条，可以根据个人偏好选择条数\n爬取正文：添加 http 节点，URL填：\nhttp://localhost:3000/https://juejin.cn/post/{{ $json.content.content_id }} http://localhost:3000 换成jina reader 链路 {{ $json.content.content_id }} 文章 id 大模型总结：1）AI→AI Agent 点开AI Agent，在Source for Prompt (User Message) 选择 Define below；在Prompt (User Message) 拖左边 input 的 data 拉过来；在Options 加一个System Message，见 6.1。不用管循环，n8n 会自动处理循环。\n2）AI Agent 中加 Chat Model：可以用最下面的openai chat model。api key和 base-url 使用硅基流动账号。\n格式处理：第 6 步要求大模型输出 json，但是很多时候并不能完全按照要求输出，可以对输出格式化处理。选择 code 节点，Language选择 JavaScript，脚本如下：\n// 提取并解析每个 output 中的 JSON const result = $input.all().map(item =\u0026gt; { // 移除可能存在的 markdown 代码块标记 let jsonStr = item.json.output.replace(/```json\\n?/g, \u0026#39;\u0026#39;).replace(/```\\n?/g, \u0026#39;\u0026#39;).trim(); // 解析 JSON 字符串 return JSON.parse(jsonStr); }); return result json to HTML：选择 code 节点，Language选择 JavaScript，脚本如 6.2\n发送到qq 邮箱：添加 Send email 节点，凭证如下第二张图，Host使用 smtp 地址 smtp.qq.com，Port：465，Password 用之前申请的qq 邮箱授权码\n5.3 成果展现 # 按照上面操作完成，点下面的执行工作流程，看执行效果\n六、附件 # 6.1 LLM总结 system prompt # 你是一个擅长总结长文本的助手，能够总结用户给出的文章，请根据下方的文章内容做三件事： 1. **文章摘要** 清晰、简洁地总结内容，在3-4句话内吸引读者。 2. **文章评分（1-100）-重要** 严格评估文章内容，不考虑类别，但根据以下标准： - **重要性和影响（最高30分）** 对公众有影响或具有长期政策影响的文章 - **新内容和信息（最高25分）** 提供新信息、新观点或深入背景的文章 - **报道质量（最高25分）** 写作清晰、有引用、有结构，不仅仅是报道文章 - **报道的吸引力（最高20分）** 生动有趣，有细节或与当前事件相关 **立即扣分** 如果发现以下情况： - 广告/宣传文章（-20至-30分） - 内容陈旧、重复的新闻文章（-10至-20分） - 缺乏来源或使用模糊语言的文章（-10分） 3. **文章标签** 阅读文章内容后给文章打上标签，标签通常是领域、学科或专有名词，要求 2-5 个 --- 文章内容： {{ $json.data }} --- 输出格式： 请严格以JSON格式回答，需包含以下key： { \u0026#34;title\u0026#34;: \u0026#34;xxxx\u0026#34; \u0026#34;abstract\u0026#34;: \u0026#34;xxx\u0026#34; \u0026#34;source\u0026#34;: \u0026#34;https://juejin.cn/post/7560167720013692974\u0026#34; \u0026#34;score\u0026#34;: \u0026#34;75\u0026#34; \u0026#34;tags\u0026#34;: \u0026#34;AIGC,Python\u0026#34; } 6.2 json转html # function convertToEmailHTML(articles) { // 处理 tags，统一转换为字符串 const formatTags = (tags) =\u0026gt; { if (Array.isArray(tags)) { return tags.join(\u0026#39;, \u0026#39;); } return tags; }; // 根据分数返回不同颜色 const getScoreColor = (score) =\u0026gt; { if (score \u0026gt;= 90) return \u0026#39;#10b981\u0026#39;; // 绿色 if (score \u0026gt;= 80) return \u0026#39;#3b82f6\u0026#39;; // 蓝色 if (score \u0026gt;= 70) return \u0026#39;#f59e0b\u0026#39;; // 橙色 return \u0026#39;#ef4444\u0026#39;; // 红色 }; const htmlContent = ` \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;zh-CN\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;UTF-8\u0026#34;\u0026gt; \u0026lt;meta name=\u0026#34;viewport\u0026#34; content=\u0026#34;width=device-width, initial-scale=1.0\u0026#34;\u0026gt; \u0026lt;title\u0026gt;精选文章推荐\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body style=\u0026#34;margin: 0; padding: 0; background-color: #f3f4f6; font-family: -apple-system, BlinkMacSystemFont, \u0026#39;Segoe UI\u0026#39;, Roboto, \u0026#39;Helvetica Neue\u0026#39;, Arial, sans-serif;\u0026#34;\u0026gt; \u0026lt;table width=\u0026#34;100%\u0026#34; cellpadding=\u0026#34;0\u0026#34; cellspacing=\u0026#34;0\u0026#34; style=\u0026#34;background-color: #f3f4f6; padding: 20px 0;\u0026#34;\u0026gt; \u0026lt;tr\u0026gt; \u0026lt;td align=\u0026#34;center\u0026#34;\u0026gt; \u0026lt;table width=\u0026#34;600\u0026#34; cellpadding=\u0026#34;0\u0026#34; cellspacing=\u0026#34;0\u0026#34; style=\u0026#34;background-color: #ffffff; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);\u0026#34;\u0026gt; \u0026lt;!-- Header --\u0026gt; \u0026lt;tr\u0026gt; \u0026lt;td style=\u0026#34;background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 30px; text-align: center; border-radius: 8px 8px 0 0;\u0026#34;\u0026gt; \u0026lt;h1 style=\u0026#34;margin: 0; color: #ffffff; font-size: 28px; font-weight: 600;\u0026#34;\u0026gt;📚 精选文章推荐\u0026lt;/h1\u0026gt; \u0026lt;p style=\u0026#34;margin: 10px 0 0 0; color: #e0e7ff; font-size: 14px;\u0026#34;\u0026gt;为您精心挑选的优质内容\u0026lt;/p\u0026gt; \u0026lt;/td\u0026gt; \u0026lt;/tr\u0026gt; \u0026lt;!-- Content --\u0026gt; \u0026lt;tr\u0026gt; \u0026lt;td style=\u0026#34;padding: 30px;\u0026#34;\u0026gt; ${articles.map((article, index) =\u0026gt; ` \u0026lt;table width=\u0026#34;100%\u0026#34; cellpadding=\u0026#34;0\u0026#34; cellspacing=\u0026#34;0\u0026#34; style=\u0026#34;margin-bottom: ${index \u0026lt; articles.length - 1 ? \u0026#39;25px\u0026#39; : \u0026#39;0\u0026#39;}; border: 1px solid #e5e7eb; border-radius: 6px; overflow: hidden;\u0026#34;\u0026gt; \u0026lt;tr\u0026gt; \u0026lt;td style=\u0026#34;padding: 20px; background-color: #fafafa;\u0026#34;\u0026gt; \u0026lt;!-- Title --\u0026gt; \u0026lt;h2 style=\u0026#34;margin: 0 0 12px 0; font-size: 18px; font-weight: 600; color: #1f2937;\u0026#34;\u0026gt; \u0026lt;a href=\u0026#34;${article.json.source}\u0026#34; style=\u0026#34;color: #1f2937; text-decoration: none;\u0026#34; target=\u0026#34;_blank\u0026#34;\u0026gt; ${article.json.title} \u0026lt;/a\u0026gt; \u0026lt;/h2\u0026gt; \u0026lt;!-- Summary --\u0026gt; \u0026lt;p style=\u0026#34;margin: 0 0 15px 0; color: #4b5563; font-size: 14px; line-height: 1.5;\u0026#34;\u0026gt; ${article.json.abstract || \u0026#39;暂无摘要\u0026#39;} \u0026lt;/p\u0026gt; \u0026lt;!-- Score and Tags in one line --\u0026gt; \u0026lt;div style=\u0026#34;margin-bottom: 0; display: flex; align-items: center; flex-wrap: wrap; gap: 8px;\u0026#34;\u0026gt; \u0026lt;span style=\u0026#34;display: inline-block; background-color: ${getScoreColor(article.json.score)}; color: #ffffff; padding: 4px 12px; border-radius: 12px; font-size: 12px; font-weight: 600;\u0026#34;\u0026gt; ⭐ 评分: ${article.json.score} \u0026lt;/span\u0026gt; ${formatTags(article.json.tags).split(\u0026#39;,\u0026#39;).map(tag =\u0026gt; ` \u0026lt;span style=\u0026#34;display: inline-block; background-color: #ede9fe; color: #7c3aed; padding: 3px 10px; border-radius: 4px; font-size: 12px;\u0026#34;\u0026gt; ${tag.trim()} \u0026lt;/span\u0026gt; `).join(\u0026#39;\u0026#39;)} \u0026lt;/div\u0026gt; \u0026lt;/td\u0026gt; \u0026lt;/tr\u0026gt; \u0026lt;/table\u0026gt; `).join(\u0026#39;\u0026#39;)} \u0026lt;/td\u0026gt; \u0026lt;/tr\u0026gt; \u0026lt;!-- Footer --\u0026gt; \u0026lt;tr\u0026gt; \u0026lt;td style=\u0026#34;background-color: #f9fafb; padding: 20px; text-align: center; border-radius: 0 0 8px 8px; border-top: 1px solid #e5e7eb;\u0026#34;\u0026gt; \u0026lt;p style=\u0026#34;margin: 0; color: #6b7280; font-size: 12px;\u0026#34;\u0026gt; 本邮件由系统自动发送，请勿直接回复 \u0026lt;/p\u0026gt; \u0026lt;p style=\u0026#34;margin: 10px 0 0 0; color: #9ca3af; font-size: 11px;\u0026#34;\u0026gt; © ${new Date().getFullYear()} All rights reserved \u0026lt;/p\u0026gt; \u0026lt;/td\u0026gt; \u0026lt;/tr\u0026gt; \u0026lt;/table\u0026gt; \u0026lt;/td\u0026gt; \u0026lt;/tr\u0026gt; \u0026lt;/table\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; `.trim(); return htmlContent; } // 生成 HTML const emailHTML = convertToEmailHTML($input.all()); return { \u0026#34;output\u0026#34;: emailHTML } 6.3 n8n 工作流 # https://github.com/jimdengdev/n8n-workflow/blob/main/AINews.json ","date":"2025-11-09","externalUrl":null,"permalink":"/zh-cn/posts/n8n-workflow-lecture/","section":"博客","summary":"一键生成高质量 AI 简报：n8n 工作流最佳实践","title":"一键生成高质量 AI 简报：n8n 工作流最佳实践","type":"posts"},{"content":"","date":"2024-09-07","externalUrl":null,"permalink":"/zh-cn/tags/go%E8%AF%AD%E8%A8%80/","section":"标签","summary":"","title":"Go语言","type":"tags"},{"content":"","date":"2024-09-07","externalUrl":null,"permalink":"/zh-cn/authors/jim-deng/","section":"作者列表示例","summary":"","title":"Jim Deng","type":"authors"},{"content":"","date":"2024-09-07","externalUrl":null,"permalink":"/zh-cn/series/","section":"Series","summary":"","title":"Series","type":"series"},{"content":"","date":"2024-09-07","externalUrl":null,"permalink":"/zh-cn/series/%E5%89%91%E6%8C%87offer/","section":"Series","summary":"","title":"剑指offer","type":"series"},{"content":" 剑指offer(Go版本)-数组 # 1.和为S的两个数字 # 输入一个递增排序的数组和一个数字S，在数组中查找两个数，使得他们的和正好是S，如果有多对数字的和等于S，输出两个数的乘积最小的。\n对应每个测试案例，输出两个数，小的先输出。\n思路：双指针，i := 0 j := length - 1 func findNumbersWithSum(a []int, sum int) []int { result := []int{} length := len(a) if length == 0 { return result } i := 0 j := length - 1 for i \u0026lt; j { if a[i]+a[j] == sum { result = append(result, i, j) break } if a[i]+a[j] \u0026lt; sum { i++ } if a[i]+a[j] \u0026gt; sum { j-- } } return result } 2.和为S的连续正数序列 # 小明很喜欢数学,有一天他在做数学作业时,要求计算出9~16的和,他马上就写出了正确答案是100。但是他并不满足于此,他在想究竟有多少种连续的正数序列的和为100(至少包括两个数)。没多久,他就得到另一组连续正数和为100的序列:18,19,20,21,22。\n现在把问题交给你,你能不能也很快的找出所有和为S的连续正数序列?\n输出描述:\n输出所有和为S的连续正数序列。序列内按照从小至大的顺序，序列间按照开始数字从小到大的顺序。\n思路：双指针：cur := (low + high) * (high - low + 1) / 2 ,cur关于high的正相关，关于low的负相关。 func findContinuousSequence(sum int) [][]int { result := [][]int{} low := 1 high := 2 for low \u0026lt; high { cur := (low + high) * (high - low + 1) / 2 if cur == sum { temp := []int{} for i := low; i \u0026lt;= high; i++ { temp = append(temp, i) } result = append(result, temp) low++ } if cur \u0026lt; sum { high++ } if cur \u0026gt; sum { low++ } } return result } 3.连续子数组的最大和 # HZ偶尔会拿些专业问题来忽悠那些非计算机专业的同学。今天测试组开完会后,他又发话了:在古老的一维模式识别中,常常需要计算连续子向量的最大和,当向量全为正数的时候,问题很好解决。但是,如果向量中包含负数,是否应该包含某个负数,并期望旁边的正数会弥补它呢？例如:{6,-3,-2,7,-15,1,2,2},连续子向量的最大和为8(从第0个开始,到第3个为止)。给一个数组，返回它的最大连续子序列的和，你会不会被他忽悠住？(子向量的长度至少是1)\n思路：dp/优化后的dp // 使用dp table func maxSubArray(nums []int) int { length := len(nums) dp := make([]int, length) dp[0] = nums[0] ans := dp[0] for i := 1; i \u0026lt; length; i++ { if dp[i-1] \u0026gt; 0 { dp[i] = dp[i-1] + nums[i] } else { dp[i] = nums[i] } if ans \u0026lt; dp[i] { ans = dp[i] } } return ans } //dp优化，节省空间复杂度 func maxSubArray(nums []int) int { length := len(nums) sum = nums[0] ans := dp[0] for i := 1; i \u0026lt; length; i++ { if sum \u0026gt; 0 { // 此时sum对应dp[i-1] sum += nums[i] // 更新后sum表示dp[i] } else { sum = nums[i] // 更新后sum表示dp[i] } if ans \u0026lt; sum { ans = sum } } return ans } 4.数字在排序数组中出现的次数 # 统计一个数字在排序数组中出现的次数。\n看到排序数组，要想到用二分查找。\n先找到最前面的数字k，再找到最后面的数字k，通过下标求出次数。\n思路：单增数组二分查找 func getNumberOfK(num []int, k int) int { length := len(num) firstK := getFirstK(num, k, 0, length-1) lastK := getLastK(num, k, 0, length-1) if firstK != -1 \u0026amp;\u0026amp; lastK != -1 { return lastK - firstK + 1 } return 0 } func getFirstK(num []int, k int, start int, end int) int { if start \u0026gt; end { return -1 } mid := (start + end) / 2 if num[mid] \u0026gt; k { return getFirstK(num, k, start, mid-1) } else if num[mid] \u0026lt; k { return getFirstK(num, k, mid+1, end) } else if mid-1 \u0026gt;= 0 \u0026amp;\u0026amp; num[mid-1] == k { return getFirstK(num, k, start, mid-1) } else { return mid } } func getLastK(num []int, k int, start int, end int) int { length := len(num) mid := (start + end) / 2 for start \u0026lt;= end { if num[mid] \u0026gt; k { end = mid - 1 } else if num[mid] \u0026lt; k { start = mid + 1 } else if mid+1 \u0026lt;= length-1 \u0026amp;\u0026amp; num[mid+1] == k { start = mid + 1 } else { return mid } mid = (start + end) / 2 } return -1 } 5.数组中只出现一次的数字 # 一个整型数组里除了两个数字之外，其他的数字都出现了两次。请写程序找出这两个只出现一次的数字。\n正常能想到哈希表来处理，但此题考查的是异或的知识，不同则为1，相同则为0，可以发现，0^任何数就等于数本身。\n简单来说从0开始时，异或一个数相当于加上这个数，再异或这个数时，相当于减掉这个数，最后剩下的就是唯一存在的数了。\n思路：位运算，相同两个数异或为0，0与任何数异或为本身 func singleNumber(nums []int) int { result := 0 for _, x := range nums { result ^= x } return result } 6.旋转数组的最小数字! # 把一个数组最开始的若干个元素搬到数组的末尾，我们称之为数组的旋转。\n输入一个非递减排序的数组的一个旋转，输出旋转数组的最小元素。\n例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转，该数组的最小值为1。\nNOTE：给出的所有元素都大于0，若数组大小为0，请返回0。\n思路：二分查找 func minNumberInRotateArray(rotate []int) int { length := len(rotate) if length == 0 { return 0 } if length == 1 { return rotate[0] } for i := 0; i \u0026lt; length-1; i++ { if rotate[i] \u0026gt; rotate[i+1] { return rotate[i+1] } else { if i == length-2 { return rotate[0] } } } return 0 } func minNumberInRotateArray(rotate []int) int { length := len(rotate) if length == 0 { return 0 } if length == 1 { return rotate[0] } low := 0 high := length - 1 for low \u0026lt; high { mid := (low + high) / 2 if rotate[mid] \u0026gt; rotate[high] { low = mid + 1 } else if rotate[mid] \u0026lt; rotate[high] { high = mid } else if rotate[mid] == rotate[high] { high-- } } return rotate[low] } 7.数组中的逆序对 # 在数组中的两个数字，如果前面一个数字大于后面的数字，则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数P。并将P对1000000007取模的结果输出。即输出P%1000000007。\n输入描述:\n题目保证输入的数组中没有的相同的数字。\n数据范围：\n对于%50的数据,size\u0026lt;=10^4\n对于%75的数据,size\u0026lt;=10^5\n对于%100的数据,size\u0026lt;=2*10^5\n示例： 输入\n1,2,3,4,5,6,7,0\n输出\n7\n思路：归并排序。 mergSort 归并排序，两个作用，一是将nums的[l,r]元素进行排序，二是计算在归并排序过程中的逆序对。 归并排序中如何计算逆序对？归并排序过程：想要让整个数组有序，先让左半部分和右半部分数组有序，然后将这两个有序数组排序。而左半部分和右半部分分别看成一个数组递归的进行上面操作，直到数组只有一个元素。关键部分就是将两个有序数组排序。lptr，rPtr是有序数组排序过程中的待排序的两个指针。当前 lPtr 指向的数字比 rPtr 小，但是比 RR 中 [0 \u0026hellip; rPtr - 1] 的其他数字大，[0 \u0026hellip; rPtr - 1] 的其他数字本应当排在 lPtr 对应数字的左边，但是它排在了右边，所以这里就贡献了 rPtr 个逆序对。 func reversePairs(nums []int) int { return mergeSort(nums, 0, len(nums)-1) } func mergeSort(nums []int, l, r int) int { if l \u0026gt;= r { return 0 } mid := l + (r-l)/2 cnt := mergeSort(nums, l, mid) + mergeSort(nums, mid+1, r) tmp := []int{} l1, r1 := l, mid+1 for l1 \u0026lt;= mid \u0026amp;\u0026amp; r1 \u0026lt;= r { if nums[l1] \u0026lt;= nums[r1] { cnt += r1 - (mid+1) tmp = append(tmp, nums[l1]) l1++ } else { tmp = append(tmp, nums[r1]) r1++ } } for ; l1 \u0026lt;= mid; l1++ { cnt += r+1 - (mid+1) tmp = append(tmp, nums[l1]) } for ; r1 \u0026lt;= r; r1++ { tmp = append(tmp, nums[r1]) } for i := l; i \u0026lt;= r; i++ { nums[i] = tmp[i-l] } return cnt } 8.最小的K个数 # 输入n个整数，找出其中最小的K个数。例如输入4,5,1,6,2,7,3,8这8个数字，则最小的4个数字是1,2,3,4,。\n思路：方法一：排序，然后取前k个数，O(nlogn)，O(1)。方法二：堆排序，O(nlogk),O(k)。方法三：快排分治思想，O(n),O(1) func getLeastNumbers(input []int, k int) []int { if len(input) == 0 || k \u0026lt;= 0 { return nil } if k \u0026gt;= len(input) { return input } sort.Ints(input) return input[0 : k-1] } // getLeastNumbers 用heap实现大根堆，然后用大根堆实现最小的k个数 func getLeastNumbers(arr []int, k int) []int { if k == 0 || len(arr) == 0 { return []int{} } d := \u0026amp;IntHeap{} heap.Init(d) for _, v := range arr { if d.Len() \u0026lt; k { heap.Push(d, v) } else { if (*d)[0] \u0026gt; v { heap.Pop(d) heap.Push(d, v) } } } return *d } // IntHeap 堆demo,利用heap实现大、小根堆，需要实现5个方法，Len,Less,Swap, Push,Pop。前三个是排序接口，后面两个是heap接口补充的 type IntHeap []int func (h *IntHeap) Len() int { return len(*h) } // Less 定义比较规则。大根堆，Less在大于时返回小于 func (h *IntHeap) Less(i, j int) bool { return (*h)[i] \u0026gt; (*h)[j] } func (h *IntHeap) Swap(i, j int) { (*h)[i], (*h)[j] = (*h)[j], (*h)[i] } func (h *IntHeap) Push(x interface{}) { *h = append(*h, x.(int)) } func (h *IntHeap) Pop() interface{} { old := *h n := len(old) x := old[n-1] *h = old[:n-1] return x } func getLeastNumbers(arr []int, k int) []int { if len(arr) == 0 || k \u0026lt;= 0 { return []int{} } return quickSearch(arr, 0, len(arr)-1, k) } // quickSearch 对arr中[i,j]元素进行pivot=arr[i]的划分函数处理，并将函数返回下标t与k-1比较，如果相等即返回arr[:k]，若下标小于k-1，则对[t+1,j] quickSearch递归处理，若下标大于k-1，则对[i,t-1]quickSearch递归处理。 func quickSearch(arr []int, i, j, k int) []int { t := partition(arr, i, j) if t == k-1 { return arr[:k] } if t \u0026lt; k-1 { return quickSearch(arr, t+1, j, k) } return quickSearch(arr, i, t-1, k) } // partition 划分函数，将nums的[i,j]位置元素进行划分，pivot为第一个元素nums[i]，结果是对nums原地修改，大于pivot的元素都在pivot右边，比pivot小的元素都在pivot左边。并返回pivot下标。 func partition(nums []int,i,j int) int { l,m,r:=i,i,j for l\u0026lt;r { for l\u0026lt;r \u0026amp;\u0026amp; nums[r]\u0026gt;=nums[m] { r-- } for l\u0026lt;r \u0026amp;\u0026amp; nums[l]\u0026lt;=nums[m] { l++ } if l\u0026lt;r { nums[l],nums[r]=nums[r],nums[l] } } nums[m],nums[l]=nums[l],nums[m] return l } 9.数组中出现次数超过一半的数字 # 数组中有一个数字出现的次数超过数组长度的一半，请找出这个数字。\n你可以假设数组是非空的，并且给定的数组总是存在多数元素。\n示例 1:\n输入: [1, 2, 3, 2, 2, 2, 5, 4, 2] 输出: 2\n限制：\n1 \u0026lt;= 数组长度 \u0026lt;= 50000\n思路：方法一：哈希 O(n), O(n)；方法二：Boyer-Moore投票算法，\n投票算法：维护一个候选众数 candidate 和它出现的次数 count。初始时 candidate 可以为任意值，count 为 0；遍历数组 nums 中的所有元素，对于每个元素 x，在判断 x 之前，如果 count=0，先将 x 的值赋予candidate，随后我们判断 x：\n如果 x 与 candidate 相等，那么计数器 count 的值增加 1；\n如果 x 与 candidate 不等，那么计数器 count 的值减少 1。\n在遍历完成后，candidate 即为整个数组的众数。\nO(n), O(1)\nfunc majorityElement(nums []int) int { mp := make(map[int]int) for _, item := range nums { mp[item]++ if mp[item] \u0026gt; len(nums)/2 { return item } } return 0 } func majorityElement(nums []int) int { candidate, count := 0, 0 for _, num := range nums { if count == 0 { candidate = num count++ break } if cadidate == num { count++ } else { count-- } } return cadidate } 10.把数组排成最小的数 # 输入一个正整数数组，把数组里所有数字拼接起来排成一个数，打印能拼接出的所有数字中最小的一个。例如输入数组{3，32，321}，则打印出这三个数字能排成的最小数字为321323。\n思路：排序算法，修改排序规则,O(nlogn)，O(n) 设数组 numsnums 中任意两数字的字符串为 xx 和 yy ，则规定 排序判断规则 为： 若拼接字符串 x + y \u0026gt; y + xx+y\u0026gt;y+x ，则 xx “大于” yy ； 反之，若 x + y \u0026lt; y + xx+y\u0026lt;y+x ，则 xx “小于” yy ； xx “小于” yy 代表：排序完成后，数组中 xx 应在 yy 左边；“大于” 则反之。 func minNumber(nums []int) string { sort.Slice(nums, func(i, j int) bool { m := strconv.Itoa(nums[i]) + strconv.Itoa(nums[j]) n := strconv.Itoa(nums[j]) + strconv.Itoa(nums[i]) return m \u0026lt; n }) ans := \u0026#34;\u0026#34; for _, item := range nums { ans += strconv.Itoa(item) } return ans } 11.数组中重复的数字 # 在一个长度为n的数组里的所有数字都在0到n-1的范围内。数组中某些数字是重复的，但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任意一个重复的数字。例如，如果输入长度为7的数组{2,3,1,0,2,5,3}，那么对应的输出是第一个重复的数字2。\n思路：可以用hash来实现，遍历的时候，元素作为key存入map，当出现重复元素时，判断key已存在，输出元素即可。O(n)，O(n)\n数组中数字范围在0 ~ n-1 之间，可用现有数组设置标志，当一个数字被访问过后，设置对应位上的数 +n，之后再遇到相同的数时，会发现对应位上的数已经大于等于n了，直接返回这个数即可，这样不需要额外的数组或者map来处理，但是需要修改原数组。O(n)，O(1)\n// 哈希表 func findRepeatNumber(nums []int) int { mp := make(map[int]int) for _, item := range nums { mp[item]++ if mp[item] \u0026gt; 1 { return item } } return -1 } // 原地修改数组 func findRepeatNumber(nums []int) int { length := len(nums) for _, x := range nums { if x \u0026gt;= length { x -= length } if nums[x] \u0026gt;= length { return x } nums[x] += length } return -1 } 12.滑动窗口的最大值 # 给定一个数组和滑动窗口的大小，找出所有滑动窗口里数值的最大值。\n给定一个数组 nums 和滑动窗口的大小 k，请找出所有滑动窗口里的最大值。\n示例:\n输入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3 输出: [3,3,5,5,6,7] 解释:\n滑动窗口的位置 最大值\n[1 3 -1] -3 5 3 6 7 3 1 [3 -1 -3] 5 3 6 7 3 1 3 [-1 -3 5] 3 6 7 5 1 3 -1 [-3 5 3] 6 7 5 1 3 -1 -3 [5 3 6] 7 6 1 3 -1 -3 5 [3 6 7] 7\n提示：\n你可以假设 k 总是有效的，在输入数组不为空的情况下，1 ≤ k ≤ 输入数组的大小。\n思路： 方法一：暴力求解，共有n-k+1个框，每个框求最大值, 时间复杂度O((n-k+1)k)=O(nk)。 方法二： func maxInWindows(nums []int, k int) []int { length := len(nums) if length == 0 || k \u0026lt;= 0 || length \u0026lt; k { return nil } var result []int for i := 0; i \u0026lt;= length-k; i++ { if k == 1 { return nums } temp := nums[i] for j := i+1; j \u0026lt; i+k; j++ { if nums[j] \u0026gt; temp { temp = nums[j] } } result = append(result, temp) } return result } 13.构建乘积数组 # 构建乘积数组\n给定一个数组 A[0,1,…,n-1]，请构建一个数组 B[0,1,…,n-1]，其中 B[i] 的值是数组 A 中除了下标 i 以外的元素的积, 即 B[i]=A[0]×A[1]×…×A[i-1]×A[i+1]×…×A[n-1]。不能使用除法。\n示例:\n输入: [1,2,3,4,5] 输出: [120,60,40,30,24]\n提示：\n所有元素乘积之和不会溢出 32 位整数 a.length \u0026lt;= 100000\n思路：方法一：先将数组中所有元素相乘，然后遍历到那个元素直接除以该元素即可，但是如果数组中有0则失效。 方法二：构建左右乘积列表，遍历两次数组，得到左右两个乘积列表L[i],R[i]。其中L[i] = L[i-1]*a[i]。i在[1,n-1],R[i]=R[i+1]*a[i] i在[n-2,0],从后往前。O(n),O(n) 方法三：在方法二的基础上，把结果数组和L[i]共有，并且没有R[i]，R在动态创建。具体就是先初试话res[i]为L[i]，然后R = R * a[i], res[i] = res[i]*R, R更新res[i]也在更新。 O(n)，O(1) func constructArr(a []int) []int { if len(a) == 0 { return []int{} } aLen := len(a) L, R, res := make([]int, aLen), make([]int, aLen), make([]int, aLen) L[0], R[aLen-1] = 1, 1 for i := 1; i \u0026lt; aLen; i++{ L[i] = L[i-1] * a[i-1] R[aLen-1-i] = R[aLen-i] * a[aLen-i] } for i := 0; i \u0026lt; aLen; i++ { res[i] = L[i] * R[i] } return res } func constructArr(a []int) []int { if len(a) == 0 { return []int{} } aLen := len(a) res := make([]int, aLen) res[0] = 1 for i := 1; i \u0026lt; aLen; i++{ res[i] = res[i-1] * a[i-1] } R := 1 for i := aLen-2; i \u0026gt;= 0; i-- { R = R * a[i+1] res[i] = res[i] * R } return res } 14.二维数组中的查找 # 在一个二维数组中（每个一维数组的长度相同），每一行都按照从左到右递增的顺序排序，每一列都按照从上到下递增的顺序排序。请完成一个函数，输入这样的一个二维数组和一个整数，判断数组中是否含有该整数。\n思路：数组遍历，从右上角元素开始，大于目标元素则左移，小于目标元素则下移，直到找到或便利一遍为止.O(n)，O(1) func searchMatrix(matrix [][]int, target int) bool { if matrix == nil || len(matrix[0]) \u0026lt; 1 { return false } row := 0 col := len(matrix[0]) - 1 //从右上角元素开始，大于目标元素则左移，小于目标元素则下移，直到找到或便利一遍为止 for row \u0026lt;= len(matrix) - 1 \u0026amp;\u0026amp; col \u0026gt;= 0 { if matrix[row][col] \u0026gt; target { col -- } else if matrix[row][col] \u0026lt; target { row ++ } else { return true } } return false } 15.顺时针打印矩阵 # 输入一个矩阵，按照从外向里以顺时针的顺序依次打印出每一个数字。\n示例 1：\n输入：matrix = [[1,2,3],[4,5,6],[7,8,9]] 输出：[1,2,3,6,9,8,7,4,5] 示例 2：\n输入：matrix = [[1,2,3,4],[5,6,7,8],[9,10,11,12]] 输出：[1,2,3,4,8,12,11,10,9,5,6,7]\n思路：从左到右，从上到下，从右到左，从下到上依次遍历数组，用l,r,t,b四个指针用于限定遍历区间，遍历过程是循环的，每循环依次更新一次四个指针。O(mn)，O(1) func spiralOrder(matrix [][]int) []int { if len(matrix) == 0 { return []int{} } m, n := len(matrix), len(matrix[0]) ans := make([]int, 0) l, r, t, b := 0, n-1, 0, m-1 for l \u0026lt;= r \u0026amp;\u0026amp; t \u0026lt;= b { for i := l; i \u0026lt;= r; i++ { ans = append(ans, matrix[t][i]) } for i := t+1; i \u0026lt;= b; i++ { ans = append(ans, matrix[i][r]) } if b \u0026gt; t { // 注意 从右到左遍历需要b\u0026gt;t，否则可能与从左到右遍历的同一行 for i := r-1; i \u0026gt;= l; i-- { ans = append(ans, matrix[b][i]) } } if l \u0026lt; r { // 注意 从下到上遍历需要l\u0026lt;r，否则可能与从上到下遍历的同一列 for i := b-1; i \u0026gt;= t+1; i-- { ans = append(ans, matrix[i][l]) } } l++ r-- t++ b-- } return ans } 16.扑克牌中的顺子 # 从若干副扑克牌中随机抽 5 张牌，判断是不是一个顺子，即这5张牌是不是连续的。2～10为数字本身，A为1，J为11，Q为12，K为13，而大、小王为 0 ，可以看成任意数字。A 不能视为 14。\n示例 1:\n输入: [1,2,3,4,5] 输出: True\n示例 2:\n输入: [0,0,1,2,5] 输出: True\n限制：\n数组长度为 5\n数组的数取值为 [0, 13] .\n思路：55 张牌是顺子的 充分条件 如下： 除大小王外，所有牌 无重复 ； 设此 55 张牌中最大的牌为 maxmax ，最小的牌为 minmin （大小王除外），则需满足：max - min \u0026lt; 5 判断重复问题用map，最大值和最小值在遍历时用ma，mi标记。 O(1)，O(1), mp和nums只有5个数，所以O(5)=O(1) func isStraight(nums []int) bool { mp := make(map[int]int) mi,ma := 20, -1 for _, item := range nums { if item == 0 { continue } mp[item]++ if mp[item] \u0026gt; 1 { return false } if item \u0026gt; ma { ma = item } if item \u0026lt; mi { mi = item } } return ma - mi \u0026lt; 5 } 17.调整数组顺序使奇数位于偶数前面 # 输入一个整数数组，实现一个函数来调整该数组中数字的顺序，使得所有奇数在数组的前半部分，所有偶数在数组的后半部分。\n示例：\n输入：nums = [1,2,3,4] 输出：[1,3,2,4] 注：[3,1,2,4] 也是正确的答案之一。\n提示：\n0 \u0026lt;= nums.length \u0026lt;= 50000 0 \u0026lt;= nums[i] \u0026lt;= 10000\n思路： 方法一：两个数组，一个保留奇数，一个保留偶数。O(n)，O(n). 方法二：不用偶数切片，只需要先计算好奇数个数count，然后偶数直接放在ans切片从count位置开始的后面即可。O(n)，O(1) 方法三：前两种方法都保留了之前数字的相对位置，如果不用保留相对位置并且可以修改原数组的话，可以用双指针从两端遍历，左指针找偶数，右指针找奇数，然后互换就行了。O(n)，O(1) func exchange(nums []int) []int { odd, even := make([]int, 0), make([]int, 0) for _, item := range nums { if item \u0026amp; 1 == 1 { // 奇数和1与运算为1，偶数与1与运算为0 odd = append(odd, item) } else { even = append(even, item) } } odd = append(odd, even...) return odd } func exchange(nums []int) []int { ans := make([]int, len(nums)) count := 0 for _, item := range nums { if item \u0026amp; 1 == 1 { // 奇数和1与运算为1，偶数与1与运算为0 ans[count] = item count++ } } // 这里oddCount其实是下标 for _, item := range nums { if item \u0026amp; 1 != 1 { ans[count] = item count++ } } return ans } func exchange(nums []int) []int { l, r := 0, len(nums)-1 for l \u0026lt; r { for l \u0026lt; r \u0026amp;\u0026amp; nums[l] \u0026amp; 1 == 1 { l++ } for l \u0026lt; r \u0026amp;\u0026amp; nums[r] \u0026amp; 1 == 0 { r-- } if l \u0026lt; r { // 这里也需要判断l\u0026lt;r，因为前面两个for循环可能从l\u0026lt;r中跳出来即l=r，此时不需要交换。 nums[l], nums[r] = nums[r], nums[l] } l++ r-- } return nums } 18.0～n-1中缺失的数字 # 一个长度为n-1的递增排序数组中的所有数字都是唯一的，并且每个数字都在范围0～n-1之内。在范围0～n-1内的n个数字中有且只有一个数字不在该数组中，请找出这个数字。\n示例 1:\n输入: [0,1,3] 输出: 2 示例 2:\n输入: [0,1,2,3,4,5,6,7,9] 输出: 8\n限制：\n1 \u0026lt;= 数组长度 \u0026lt;= 10000\n思路：缺失有三种情况： 1)在0~n-1中间缺少，比如0,1,3。 2)缺少最右边元素，比如0,1 3)缺少最左边元素，比如1,2 方法一：对于第一种和第三种，遍历一遍，对每个元素与下标对比，如果不相等就输出item-1。第二种，会通过前面的判断，此时缺少的就是最右边元素，只需输出len(nums)。O(n),O(1) 方法二：二分查找，判断nums[mid] 与 mid是否相等，若相等则左边[0,mid]是从0开始连续的，mid右移，否则不连续左移。当l与r相等时还要判断一次，比如到了[7,9]时，mid=7 == nums[mid] = 7，表示[0,7]都是有序的，mid右移,此时l = r = 8,还需要循环一次，mid=8 != nums[mid]=9 此时r = mid-1, l \u0026gt; r退出循环，l所指位置即为所缺数字8。如果缺最右边数字，比如0,1，l会一直在第一个判断中循环，最后l=len(nums) \u0026gt; r退出循环体。O(logn),O(1) func missingNumber(nums []int) int { for i, item := range nums { if item != i { return item-1 } } return len(nums) } func missingNumber(nums []int) int { l, r := 0, len(nums)-1 for l \u0026lt;= r { mid := l + (r - l) \u0026gt;\u0026gt; 1 if nums[mid] == mid { l = mid + 1 } else { r = mid - 1 } } return l } 19.在排序数组中查找数字！ # 统计一个数字在排序数组中出现的次数\n示例 1:\n输入: nums = [5,7,7,8,8,10], target = 8 输出: 2 示例 2:\n输入: nums = [5,7,7,8,8,10], target = 6 输出: 0\n提示：\n0 \u0026lt;= nums.length \u0026lt;= 105 -109 \u0026lt;= nums[i] \u0026lt;= 109 nums 是一个非递减数组 -109 \u0026lt;= target \u0026lt;= 109\n思路：二分查找，找左右边界 func search(nums []int, target int) int { if len(nums) == 0 { return 0 } lm, rm := getLeftMin(nums, target), getRightMax(nums, target) if rm - lm \u0026lt; 0 { return 0 } return rm -lm + 1 } func getLeftMin(nums []int, target int) int { left, right := 0, len(nums)-1 for left \u0026lt; right { mid := left + (right-left)/2 if nums[mid] \u0026gt;= target { right = mid } else if nums[mid] \u0026lt; target { left = mid+1 } } if nums[left] == target { return left } return len(nums) } func getRightMax(nums []int, target int) int { left, right := 0, len(nums)-1 for left \u0026lt; right { mid := left + (right-left+1) /2 if nums[mid] \u0026gt; target { right = mid-1 } else if nums[mid] \u0026lt;= target { left = mid } } if nums[left] == target { return left } return -1 } ","date":"2024-09-07","externalUrl":null,"permalink":"/zh-cn/posts/%E5%89%91%E6%8C%87offer/","section":"博客","summary":"剑指offer(Go版本)-数组 # 1.和为S的两个数字 # 输入一个递增排序的数组和一个数字S，在数组中查找两个数，使得他们的和正好是S，如果有多对数字的和等于S，输出两个数的乘积最小的。\n","title":"剑指offer(Go版本)-数组","type":"posts"},{"content":"","date":"2024-09-07","externalUrl":null,"permalink":"/zh-cn/tags/%E6%95%B0%E7%BB%84/","section":"标签","summary":"","title":"数组","type":"tags"},{"content":"","date":"2024-09-07","externalUrl":null,"permalink":"/zh-cn/categories/%E7%AE%97%E6%B3%95/","section":"Categories","summary":"","title":"算法","type":"categories"},{"content":"在你的文章中添加不同作者的简单示例。\n","date":"2024-09-07","externalUrl":null,"permalink":"/zh-cn/authors/","section":"作者列表示例","summary":"在你的文章中添加不同作者的简单示例。\n","title":"作者列表示例","type":"authors"},{"content":"","date":"2024-09-07","externalUrl":null,"permalink":"/zh-cn/tags/%E7%88%AC%E8%99%AB/","section":"标签","summary":"","title":"爬虫","type":"tags"},{"content":"爬取猫眼TOP100榜 # 1. 爬取流程 # 主要有以下四步：\n爬取单页内容：利利⽤用requests请求⽬目标站点，得 到单个⽹网⻚页HTML代码，返回结果。 正则表达式分析：根据HTML代码分析得到电影的 名称、主演、上映时间、评分、 图⽚片链接等信息。 保存至文件：通过⽂文件的形式将结果保存，每 一部电影一个结果一行Json字符串，图片保存成jpg格式。 开启循环及多线程：对多⻚页内容遍历，开启多线程提 ⾼高抓取速度。 2. 具体分析 # 1. 爬取单页内容 # 观察到每页的页数有offset 这个变量来控制，100条数据，10页，每页10条，所以offset从0到9。以offset作为变量，先爬取单页，然后循环爬取所有页。\nbase_url = \u0026#39;https://maoyan.com\u0026#39; url = \u0026#39;https://maoyan.com/board/4?offset=\u0026#39; + str(offset) html = get_one_page(url) def get_one_page(url): try: response = requests.get(url) if response.status_code == 200: return response.text else: return None except RequestException: return None 爬取部分通过requests中的get方法来实现，这是基本套路。正常情况下返回200状态码，非正常情况下返回None\n2. 正则表达式分析 # def parse_one_page(base_url, html): pattern = re.compile(\u0026#39;\u0026lt;dd\u0026gt;.*?\u0026lt;i.*?board-index.*?\u0026#34;\u0026gt;(\\d+)\u0026lt;/i\u0026gt;.*?href=\u0026#34;(.*?)\u0026#34;.*?data-src=\u0026#34;(.*?)\u0026#34;.*?data-val=.*?\u0026gt;(.*?)\u0026lt;/a\u0026gt;.*?star\u0026#34;\u0026gt;(.*?)\u0026lt;/p\u0026gt;\u0026#39; \u0026#39;.*?releasetime\u0026#34;\u0026gt;(.*?)\u0026lt;/p\u0026gt;.*?score.*?integer\u0026#34;\u0026gt;(.*?)\u0026lt;/i\u0026gt;.*?fraction\u0026#34;\u0026gt;(.*?)\u0026lt;/i\u0026gt;.*?\u0026lt;/dd\u0026gt;\u0026#39;, re.S) items = re.findall(pattern, html) for item in items: yield { \u0026#39;index\u0026#39;: item[0], \u0026#39;url\u0026#39;: base_url + item[1], \u0026#39;image\u0026#39;: item[2], \u0026#39;title\u0026#39;: item[3], \u0026#39;actor\u0026#39;: item[4].strip()[3:], \u0026#39;time\u0026#39;: item[5].strip()[5:], \u0026#39;score\u0026#39;: item[6] + item[7] } 这部分主要是正则表达式的书写，对于学爬虫的童鞋，正则是基本功，既基础又重要，不会的自行谷歌。虽然对于网页的解析有很多中方法，比如BeautifulSoup，PyQuery等，会一些网页前端的知识就能掌握，但是别人问你学了爬虫会正则吗，你好意思说不会吗？模式串写好了，然后通过findall方法爬取所有符合模式串的字符串，并返回给items，这是一个列表。最后通过yield生成器生成一个字典返回\n3. 保存文件 # def write_to_file(content): with open(\u0026#39;result.txt\u0026#39;,\u0026#39;a\u0026#39;, encoding=\u0026#39;utf-8\u0026#39;) as f: # 当在下面打印出来是中文汉字，而写成的文件出现编码格式则在json转换时出问题了 f.write(json.dumps(content, ensure_ascii=False) + \u0026#39;\\n\u0026#39;) # 每行字典转换成json，再加上\u0026#39;\\n\u0026#39;,最后写入到文件 def download_image(url): print(\u0026#39;Downloading\u0026#39;, url) try: response = requests.get(url) if response.status_code == 200: save_image(response.content) return None except ConnectionError: return None def save_image(content): file_path = \u0026#39;{0}/{1}.{2}\u0026#39;.format(os.getcwd() + r\u0026#39;/images/\u0026#39;, md5(content).hexdigest(), \u0026#39;jpg\u0026#39;) print(file_path) if not os.path.exists(file_path): with open(file_path, \u0026#39;wb\u0026#39;) as f: f.write(content) f.close() 这部分其实是很简单，非常套路，不过要注意的是文件的编码问题，中文编码要用\u0026rsquo;utf-8\u0026rsquo;，当让还有其他编码，源码中可以找到。\n4. 开启循环及多线程 # pool = Pool() pool.map(main, [i*10 for i in range(10)]) 就是开一进程池，然后用调用map方法，写上循环次数搞定。\nimport requests from requests.exceptions import RequestException import re import json from multiprocessing import Pool # 通过进程池实现多进程抓取 import os from hashlib import md5 def get_one_page(url): try: response = requests.get(url) if response.status_code == 200: return response.text else: return None except RequestException: return None def parse_one_page(base_url, html): pattern = re.compile(\u0026#39;\u0026lt;dd\u0026gt;.*?\u0026lt;i.*?board-index.*?\u0026#34;\u0026gt;(\\d+)\u0026lt;/i\u0026gt;.*?href=\u0026#34;(.*?)\u0026#34;.*?data-src=\u0026#34;(.*?)\u0026#34;.*?data-val=.*?\u0026gt;(.*?)\u0026lt;/a\u0026gt;.*?star\u0026#34;\u0026gt;(.*?)\u0026lt;/p\u0026gt;\u0026#39; \u0026#39;.*?releasetime\u0026#34;\u0026gt;(.*?)\u0026lt;/p\u0026gt;.*?score.*?integer\u0026#34;\u0026gt;(.*?)\u0026lt;/i\u0026gt;.*?fraction\u0026#34;\u0026gt;(.*?)\u0026lt;/i\u0026gt;.*?\u0026lt;/dd\u0026gt;\u0026#39;, re.S) items = re.findall(pattern, html) for item in items: yield { \u0026#39;index\u0026#39;: item[0], \u0026#39;url\u0026#39;: base_url + item[1], \u0026#39;image\u0026#39;: item[2], \u0026#39;title\u0026#39;: item[3], \u0026#39;actor\u0026#39;: item[4].strip()[3:], \u0026#39;time\u0026#39;: item[5].strip()[5:], \u0026#39;score\u0026#39;: item[6] + item[7] } def write_to_file(content): with open(\u0026#39;result.txt\u0026#39;,\u0026#39;a\u0026#39;, encoding=\u0026#39;utf-8\u0026#39;) as f: # 当在下面打印出来是中文汉字，而写成的文件出现编码格式则在json转换时出问题了 f.write(json.dumps(content, ensure_ascii=False) + \u0026#39;\\n\u0026#39;) # 每行字典转换成json，再加上\u0026#39;\\n\u0026#39;,最后写入到文件 def download_image(url): print(\u0026#39;Downloading\u0026#39;, url) try: response = requests.get(url) if response.status_code == 200: save_image(response.content) return None except ConnectionError: return None def save_image(content): file_path = \u0026#39;{0}/{1}.{2}\u0026#39;.format(os.getcwd() + r\u0026#39;/images/\u0026#39;, md5(content).hexdigest(), \u0026#39;jpg\u0026#39;) print(file_path) if not os.path.exists(file_path): with open(file_path, \u0026#39;wb\u0026#39;) as f: f.write(content) f.close() def main(offset): base_url = \u0026#39;https://maoyan.com\u0026#39; url = \u0026#39;https://maoyan.com/board/4?offset=\u0026#39; + str(offset) html = get_one_page(url) # print(html) for item in parse_one_page(base_url, html): print(item) write_to_file(item) download_image(item[\u0026#39;image\u0026#39;]) if __name__ == \u0026#39;__main__\u0026#39;: # for i in range(10): # main(i*10) # 先构造一个进程池，然后使用map方法，让进程进入进程池 pool = Pool() pool.map(main, [i*10 for i in range(10)])","date":"2024-09-07","externalUrl":null,"permalink":"/zh-cn/posts/crawl-maoyan/","section":"博客","summary":"爬取猫眼TOP100榜 # 1. 爬取流程 # 主要有以下四步：\n爬取单页内容：利利⽤用requests请求⽬目标站点，得 到单个⽹网⻚页HTML代码，返回结果。 正则表达式分析：根据HTML代码分析得到电影的 名称、主演、上映时间、评分、 图⽚片链接等信息。 保存至文件：通过⽂文件的形式将结果保存，每 一部电影一个结果一行Json字符串，图片保存成jpg格式。 开启循环及多线程：对多⻚页内容遍历，开启多线程提 ⾼高抓取速度。 ","title":"爬虫项目-猫眼TOP100爬取","type":"posts"},{"content":"假装这里有一份 Nuno 的简介。\n","externalUrl":null,"permalink":"/zh-cn/authors/jimdeng/","section":"作者列表示例","summary":"假装这里有一份 Nuno 的简介。\n","title":"Jim Deng","type":"authors"},{"content":"这是高级标记。类似其他 Blowfish 中的其他列表页面，你可以在分类列表页添加自定义内容，这部分内容会显示在顶部。\u0026#x1f680;\n你也可以用这些内容来定义 Hugo 的元数据，比如标题和描述。这些内容可以被用来增强 SEO 或其他目的。\n","externalUrl":null,"permalink":"/zh-cn/tags/advanced/","section":"标签","summary":"这是高级标记。类似其他 Blowfish 中的其他列表页面，你可以在分类列表页添加自定义内容，这部分内容会显示在顶部。🚀\n你也可以用这些内容来定义 Hugo 的元数据，比如标题和描述。这些内容可以被用来增强 SEO 或其他目的。\n","title":"高级","type":"tags"},{"content":" 🛠️ 开发工具 # 精选实用的开发工具推荐，持续更新中\u0026hellip;\n🤖 AI 工具 # 精选好用的 AI 工具推荐，持续更新中\u0026hellip;\n","externalUrl":null,"permalink":"/zh-cn/treasure/","section":"精选工具","summary":"🛠️ 开发工具 # 精选实用的开发工具推荐，持续更新中…\n🤖 AI 工具 # 精选好用的 AI 工具推荐，持续更新中…\n","title":"精选工具","type":"treasure"},{"content":"这里将展示我的摄影作品与生活记录，敬请期待 📸\n","externalUrl":null,"permalink":"/zh-cn/gallery/","section":"图集","summary":"这里将展示我的摄影作品与生活记录，敬请期待 📸\n","title":"图集","type":"gallery"},{"content":"这里将展示我的开源项目和个人作品，敬请期待 🚀\n","externalUrl":null,"permalink":"/zh-cn/projects/","section":"项目","summary":"这里将展示我的开源项目和个人作品，敬请期待 🚀\n","title":"项目","type":"projects"}]