ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

公众号文章批量下载与本地博客搭建:脚本、Excel、PDF全攻略

公众号文章批量下载与本地博客搭建:脚本、Excel、PDF全攻略 1. 从“翻历史文章”这件小事说起不知道你有没有过这种体验关注了一个特别对胃口的公众号作者写得又勤日更甚至一天好几篇。某天突然想起他半年前写过一篇讲某个具体问题的文章想翻出来再看看结果打开手机手指在屏幕上划拉了十几分钟从最新一路往下翻翻到手指发酸还是没找到。公众号自带的搜索功能吧关键词匹配得又不够精准搜出来的东西乱七八糟有时候明明记得标题里有某个词就是搜不出来。我关注的一个技术号就是这样作者从2019年就开始写到现在积累了上千篇原创。里面有不少讲具体工具配置、踩坑记录的文章我经常需要回头去查。每次翻历史记录都像大海捞针体验极差。后来我就琢磨能不能把这些文章都抓下来自己搭个博客做成一个可以按日期、按关键词快速检索的本地文章库说干就干前后折腾了大概一个周末现在这套东西已经稳定跑了小半年想看哪天的文章输入日期或者关键词一秒就能定位到。这篇文章就把我整个搭建过程拆开来讲清楚。从最开始的思路设计到脚本怎么写、数据怎么存、博客怎么搭再到中间踩过的坑和最后的优化技巧全部毫无保留地分享出来。不管你是刚接触脚本的新手还是已经有一定开发经验的老手都能从里面找到可以直接抄作业的东西。核心关键词就几个博客、脚本、Excel、PDF、批量下载整篇文章围绕这几个点展开但不会只停留在概念层面每一步都有具体的操作和参数说明。2. 整体方案设计与技术选型思路2.1 为什么选择“本地博客脚本抓取”这条路一开始我考虑过几种方案。第一种是用现成的笔记软件比如把文章复制粘贴到某个知识管理工具里。试了两天就放弃了手动复制效率太低而且格式全乱图片还得单独存根本不可持续。第二种是用浏览器插件做书签管理但插件只能存链接文章内容本身不在本地哪天原文被删了或者公众号迁移了链接就失效了。第三种就是我现在用的方案写脚本把文章内容抓下来存成结构化数据再用一个轻量级博客框架渲染出来本地跑一个服务浏览器打开就能看。这个方案的核心优势有三个。第一是数据完全本地化文章正文、图片、发布时间全部存在自己硬盘上不依赖任何第三方服务不怕原文消失。第二是检索速度极快因为数据就在本地不管是按日期筛还是按关键词搜都是毫秒级响应比在手机上一页页翻快太多了。第三是可扩展性强后面想加全文搜索、导出PDF、生成Excel目录都是在现有数据基础上加几行代码的事非常灵活。技术选型上抓取脚本我用的是Python主要因为Python在处理HTTP请求、解析HTML、操作Excel和PDF这些方面生态最成熟库多且稳定。博客框架选了一个基于Markdown的静态站点生成器轻量、启动快、配置简单不需要数据库所有文章以Markdown文件形式存储天然适合做版本管理和备份。数据存储方面原始数据用JSON保存方便脚本读写同时生成一份Excel索引表方便人工浏览和筛选需要离线保存的文章再单独导出PDF。2.2 数据流是怎么跑通的整个系统的数据流其实不复杂我画个简单的文字流程你就明白了。脚本启动后首先读取配置文件里面包含目标公众号的标识、抓取的时间范围、请求间隔等参数。然后脚本按照时间倒序逐页请求文章列表接口拿到每篇文章的标题、链接、发布时间、摘要等元信息。接着对每一篇文章再单独请求详情页解析出正文HTML、图片链接、作者等完整内容。正文HTML经过清洗和格式转换后存成Markdown文件图片下载到本地目录并把Markdown里的图片链接替换成本地路径。所有文章的元信息汇总到一个JSON文件里同时生成一份Excel表格作为索引。最后静态博客生成器读取这些Markdown文件和配置渲染成网页本地启动一个HTTP服务浏览器访问就能看到完整的文章库。这里面有几个关键设计决策值得展开说。第一为什么先抓列表再抓详情而不是直接遍历因为列表接口返回的数据量小、速度快可以快速拿到所有文章的元信息建立一个完整的索引。有了索引之后再根据索引去抓详情这样即使中途中断也能知道哪些抓了、哪些没抓方便断点续传。第二为什么用JSON而不是数据库因为这个项目的规模不大几千篇文章的元信息用JSON存完全够用而且JSON文件可以直接用文本编辑器打开查看调试方便不需要额外安装数据库服务。第三为什么同时生成Excel因为JSON是给程序读的Excel是给人看的。有时候我只想快速浏览一下某个月发了哪些文章打开Excel表格比打开博客网页更直接而且Excel支持筛选、排序、搜索做数据整理非常方便。2.3 需要提前准备的环境和工具在开始动手之前你需要准备以下环境。首先是Python 3.8或以上版本这是脚本运行的基础。然后需要安装几个关键的Python库requests用于发送HTTP请求beautifulsoup4和lxml用于解析HTMLopenpyxl用于生成Excel文件markdownify用于把HTML转换成MarkdownPillow用于处理图片格式转换。这些库都可以通过pip一键安装命令是pip install requests beautifulsoup4 lxml openpyxl markdownify Pillow。博客框架方面我选的是一个叫MkDocs的工具配合Material主题界面干净、搜索功能内置、配置简单。安装命令是pip install mkdocs mkdocs-material。如果你更喜欢Hugo或者Hexo也完全可以核心思路是一样的只是配置方式不同。另外建议安装一个Git用来对Markdown文件做版本管理万一改错了还能回滚。注意所有工具都建议在虚拟环境中安装避免污染系统Python环境。创建虚拟环境的命令是python -m venv venv激活后Windows下是venv\Scripts\activateMac和Linux下是source venv/bin/activate再安装依赖。3. 核心脚本的编写与关键细节3.1 抓取列表如何高效拿到所有文章的元信息抓取列表是整个流程的第一步目标是把目标公众号所有历史文章的标题、链接、发布时间、摘要等元信息拿到手。这里的关键在于找到正确的请求接口和参数。通常公众号的文章列表是通过一个分页接口返回的每页返回一定数量的文章通过调整页码参数可以逐页获取。我在脚本里定义了一个fetch_article_list函数接收公众号标识和起始页码作为参数。函数内部构造请求URL带上必要的请求头主要是User-Agent和Referer然后发送GET请求。返回的JSON数据里包含一个文章数组每篇文章有title、link、publish_time、digest等字段。脚本把这些字段提取出来追加到一个全局列表里然后判断是否还有下一页如果有就递归调用自己直到所有页面抓完。这里有几个细节需要注意。第一请求间隔一定要设置。我一开始图快连续快速请求结果触发了频率限制后面所有请求都返回错误。后来改成每次请求间隔1.5到2秒就再也没出过问题。第二请求头要模拟真实浏览器。特别是User-Agent和Referer缺了这两个很容易被识别为异常请求。第三异常处理要到位。网络请求可能因为各种原因失败比如超时、连接重置、返回非200状态码等。我在每个请求外面包了try-except失败后等待几秒重试重试三次还失败就记录到日志里跳过继续不阻塞整体流程。import requests import time import json def fetch_article_list(account_id, page1, max_retries3): url fhttps://example.com/api/articles?account{account_id}page{page} headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Referer: https://example.com/ } for attempt in range(max_retries): try: resp requests.get(url, headersheaders, timeout10) if resp.status_code 200: data resp.json() articles data.get(articles, []) if not articles: return [] result [] for item in articles: result.append({ title: item[title], link: item[link], publish_time: item[publish_time], digest: item.get(digest, ) }) time.sleep(1.5) next_page fetch_article_list(account_id, page 1) return result next_page else: time.sleep(3) except Exception as e: print(f请求失败第{attempt1}次重试{e}) time.sleep(3) return []上面这段代码就是列表抓取的核心逻辑。你可以看到我用了递归的方式来处理分页每次请求成功后等待1.5秒再请求下一页。如果某页请求失败会重试最多三次每次间隔3秒。如果三次都失败就返回空列表不再继续往下抓避免死循环。3.2 抓取详情正文解析与图片本地化拿到文章列表之后下一步就是逐篇抓取详情。这一步的目标是把每篇文章的正文内容、图片、作者信息完整地保存下来。我定义了一个fetch_article_detail函数接收文章链接作为参数发送请求拿到HTML页面然后用BeautifulSoup解析。正文的提取是这一步的核心难点。公众号文章的HTML结构通常比较规整正文一般在一个特定的div容器里id或class有固定特征。我通过观察页面源码找到了正文容器的选择器然后用soup.select_one定位到它。拿到正文HTML后用markdownify库把它转换成Markdown格式。转换过程中要注意几个问题第一图片链接要替换。原始HTML里的图片链接是远程地址我需要把它们下载到本地然后把Markdown里的链接改成本地相对路径。第二代码块要保留格式。公众号文章里的代码块有时候是用特殊标签包裹的转换时要确保缩进和换行不丢失。第三多余的空格和换行要清理。转换后的Markdown经常有多余的空行需要写个简单的清理函数处理一下。图片下载我单独写了一个函数download_image接收图片URL和保存路径发送请求拿到二进制数据写入本地文件。这里要注意图片格式的兼容性有些图片是webp格式有些是jpeg保存的时候要根据Content-Type或者URL后缀来确定文件扩展名。另外图片文件名我用的是URL的MD5哈希值这样可以避免文件名冲突也方便去重。import os import hashlib from bs4 import BeautifulSoup from markdownify import markdownify as md def download_image(img_url, save_dir): try: resp requests.get(img_url, timeout15) if resp.status_code 200: ext img_url.split(.)[-1].split(?)[0] if ext not in [jpg, jpeg, png, gif, webp]: ext jpg filename hashlib.md5(img_url.encode()).hexdigest() . ext filepath os.path.join(save_dir, filename) with open(filepath, wb) as f: f.write(resp.content) return filename except Exception as e: print(f图片下载失败{img_url}错误{e}) return None def fetch_article_detail(article, save_dir): resp requests.get(article[link], timeout15) soup BeautifulSoup(resp.text, lxml) content_div soup.select_one(#js_content) or soup.select_one(.rich_media_content) if not content_div: return None for img in content_div.find_all(img): img_url img.get(data-src) or img.get(src) if img_url: local_name download_image(img_url, save_dir) if local_name: img[src] fimages/{local_name} markdown_content md(str(content_div), heading_styleATX) return markdown_content这段代码展示了详情抓取和图片本地化的核心逻辑。注意img标签的>from openpyxl import Workbook from openpyxl.styles import Font, Alignment def export_to_excel(articles, output_path): wb Workbook() ws wb.active ws.title 文章索引 headers [序号, 标题, 发布时间, 摘要, 本地文件, 原始链接] ws.append(headers) for cell in ws[1]: cell.font Font(boldTrue) cell.alignment Alignment(horizontalcenter) for idx, article in enumerate(articles, 1): ws.append([ idx, article[title], article[publish_time], article[digest], article.get(local_path, ), article[link] ]) ws.column_dimensions[A].width 6 ws.column_dimensions[B].width 40 ws.column_dimensions[C].width 14 ws.column_dimensions[D].width 50 ws.column_dimensions[E].width 30 ws.column_dimensions[F].width 40 wb.save(output_path)这段代码生成的就是一份可以直接用的Excel索引表。你可以看到我设置了列宽让表格看起来更整齐。标题列和摘要列宽一些因为内容比较长序号列窄一些因为只是数字。这些细节虽然小但用起来体验会好很多。3.4 博客搭建让文章库变成一个可浏览的网站数据都准备好之后最后一步就是搭博客。我选的是MkDocs因为它配置简单而且内置了搜索功能正好满足我“一秒找到文章”的需求。MkDocs的核心是一个mkdocs.yml配置文件里面定义站点名称、主题、导航结构等。文章就是普通的Markdown文件放在docs目录下MkDocs会自动把它们渲染成网页。配置文件的写法很简单我贴一个精简版给你看。site_name是博客标题theme指定用Material主题nav定义导航栏plugins里开启搜索插件。nav部分我按年份和月份做了分组这样浏览起来更清晰。比如2024年1月的文章放在一个分组里2024年2月的放在另一个分组里依次类推。这个分组结构可以用脚本自动生成不需要手动维护。site_name: 我的公众号文章库 theme: name: material language: zh features: - search.suggest - search.highlight - navigation.tabs - navigation.top plugins: - search: lang: zh nav: - 首页: index.md - 2024年: - 1月: - 文章标题1: posts/2024-01-01-article1.md - 文章标题2: posts/2024-01-02-article2.md - 2月: - 文章标题3: posts/2024-02-01-article3.md配置好之后在终端里运行mkdocs serve本地就会启动一个HTTP服务默认地址是http://127.0.0.1:8000。浏览器打开这个地址就能看到完整的文章库了。搜索框在页面右上角输入关键词所有匹配的文章会实时显示出来点击就能跳转。实测下来搜索响应时间在50毫秒以内非常流畅。提示MkDocs的搜索功能是基于浏览器端的不需要服务端支持所以即使你把生成的静态文件部署到任何静态托管服务上搜索功能依然可用。不过考虑到文章内容可能涉及个人隐私我建议只在本地跑不要公开部署。4. 实操过程中踩过的坑和解决方案4.1 请求频率限制与反爬策略应对这个坑我在前面提过但值得单独拿出来详细说。刚开始写脚本的时候我图省事没有加请求间隔结果连续请求了大概二三十次之后接口开始返回错误码后面所有请求都失败了。当时我以为是代码写错了排查了半天才发现是频率限制。后来我加了1.5秒的间隔问题就解决了。但事情没那么简单过了一段时间我发现即使加了间隔偶尔还是会触发限制。经过观察我发现限制策略不是固定的有时候严格有时候宽松。为了保险起见我把间隔调到了2秒并且在每次请求失败后等待更长时间再重试。除了频率限制还有一个问题是请求头。有些接口会检查Referer和User-Agent如果缺失或者不对直接返回403。我的做法是完整模拟浏览器的请求头包括Accept、Accept-Language、Connection等字段。这些字段虽然看起来不起眼但缺了任何一个都可能被识别为异常请求。另外我还遇到过返回内容被压缩的情况。有些接口返回的是gzip压缩的数据如果请求头里没有正确声明Accept-Encoding拿到的是乱码。解决办法是在请求头里加上Accept-Encoding: gzip, deflate然后让requests库自动解压。这个坑比较隐蔽因为requests默认会处理gzip但如果服务端返回的是其他压缩格式就需要手动处理了。4.2 图片下载失败与格式兼容问题图片下载这块我踩的坑也不少。最常见的问题是图片链接失效或者返回403。公众号的图片通常有防盗链机制直接请求图片URL可能会被拒绝。解决办法是在请求图片时带上Referer头指向文章页面。另外有些图片是webp格式虽然现代浏览器都支持但如果你后续要把文章导出成PDFwebp格式可能不被某些PDF生成工具支持。我的做法是在下载时统一转换成jpeg格式用Pillow库处理一下。还有一个问题是图片文件名冲突。一开始我用URL的最后一段作为文件名结果发现很多图片的URL最后一段是一样的导致后面的图片覆盖了前面的。后来改成用URL的MD5哈希值作为文件名彻底解决了冲突问题。MD5哈希值虽然长但胜在唯一而且计算速度快对性能几乎没有影响。from PIL import Image import io def convert_to_jpeg(image_data): img Image.open(io.BytesIO(image_data)) if img.mode in (RGBA, P): img img.convert(RGB) output io.BytesIO() img.save(output, formatJPEG, quality85) return output.getvalue()这段代码展示了如何把任意格式的图片转换成jpeg。注意RGBA和P模式需要先转换成RGB否则保存jpeg时会报错。这个细节在处理png透明背景图片时特别重要。4.3 Markdown转换后的格式错乱修复HTML转Markdown听起来简单但实际做起来问题很多。最常见的问题是多余的空行和缩进错乱。公众号文章的HTML里经常有大量的br标签和空段落转换成Markdown后就是一堆空行看起来很不整洁。我的做法是写一个清理函数用正则表达式把连续三个以上的换行替换成两个把行尾的空格去掉把代码块外的多余缩进也清理掉。另一个问题是表格转换。公众号文章里的表格有时候是用table标签写的markdownify转换后可能格式不对齐。我的做法是检测到表格后手动解析tr和td重新生成Markdown表格。虽然麻烦一点但转换后的表格在博客里显示效果更好。还有一个坑是代码块的语言标识丢失。公众号文章里的代码块通常没有指定语言转换后就是普通的缩进代码块没有语法高亮。我的做法是在转换时检测代码内容根据关键词猜测语言然后手动加上语言标识。比如看到import和def就猜是Python看到function和const就猜是JavaScript。这个猜测不一定百分百准确但大部分情况下够用了。4.4 常见问题速查表问题现象可能原因解决方案请求返回403请求头缺失或不对补全User-Agent、Referer等字段请求返回429频率超限增加请求间隔失败后延长等待时间图片显示为占位图只读了src没读data-src优先读取data-src属性图片下载失败防盗链请求图片时带上Referer头Markdown空行过多HTML里有大量br标签用正则清理连续空行表格格式错乱markdownify转换不完善手动解析table标签重新生成代码块无高亮缺少语言标识根据代码内容猜测语言并添加Excel打开乱码编码问题确保写入时用UTF-8编码博客搜索不到中文搜索插件语言配置不对在mkdocs.yml里设置lang: zh脚本中途崩溃未捕获异常加try-except记录日志支持断点续传这张表是我在实际操作中总结出来的基本上涵盖了大部分常见问题。你可以把它打印出来贴在显示器旁边遇到问题先查表能省不少排查时间。5. 进阶优化与扩展玩法5.1 增量抓取只抓新文章不重复劳动第一次全量抓取之后后面只需要抓新增的文章就行了。我的做法是在JSON索引文件里记录每篇文章的发布时间每次脚本启动时先读取索引文件拿到最新一篇文章的发布时间然后只抓取这个时间之后发布的文章。这样每次运行只需要几秒钟非常高效。实现增量抓取的关键是时间比较。公众号返回的发布时间通常是Unix时间戳或者格式化字符串需要统一转换成可比较的格式。我一般转换成datetime对象然后直接比较大小。注意时区问题如果服务端返回的是UTC时间要转换成北京时间再比较否则可能漏抓或者重复抓。from datetime import datetime def load_latest_time(index_file): if not os.path.exists(index_file): return datetime(1970, 1, 1) with open(index_file, r, encodingutf-8) as f: articles json.load(f) if not articles: return datetime(1970, 1, 1) latest max(articles, keylambda x: x[publish_time]) return datetime.strptime(latest[publish_time], %Y-%m-%d %H:%M:%S)这段代码展示了如何从索引文件里读取最新文章的发布时间。有了这个时间抓取列表时就可以跳过所有早于这个时间的文章只处理新增的。5.2 全文搜索让找文章变成一种享受MkDocs自带的搜索是基于标题和正文的已经够用了。但如果你想要更强大的搜索功能比如支持模糊匹配、拼音搜索、搜索结果高亮等可以考虑集成一个本地的全文搜索引擎。我试过用whoosh库配合jieba分词效果不错但配置稍微复杂一点。如果你不想折腾MkDocs自带的搜索其实已经能满足90%的需求了。另外一个小技巧是在每篇文章的Markdown文件头部加上YAML front matter里面写上标题、日期、标签等元信息。这样MkDocs在渲染时会自动把这些信息显示在文章顶部搜索时也能匹配到标签。标签的写法很简单就是在文件开头加几行--- title: 文章标题 date: 2024-01-01 tags: - Python - 脚本 - 博客 ---加上标签之后你可以在博客里按标签浏览文章比如点击“Python”标签就能看到所有跟Python相关的文章。这个功能对于技术博客来说特别实用。5.3 批量导出PDF离线保存的终极方案有时候我需要把某几篇文章导出成PDF方便离线阅读或者打印。我的做法是写一个脚本读取指定的Markdown文件用pdfkit或者weasyprint转换成PDF。pdfkit依赖wkhtmltopdf这个外部工具安装稍微麻烦一点但转换效果最好。weasyprint是纯Python的安装简单但对复杂CSS的支持不如pdfkit。我最终选了pdfkit因为公众号文章的排版比较复杂weasyprint有时候会渲染错乱。安装wkhtmltopdf之后转换命令很简单import pdfkit def markdown_to_pdf(md_file, pdf_file): with open(md_file, r, encodingutf-8) as f: content f.read() html markdown.markdown(content, extensions[tables, fenced_code]) pdfkit.from_string(html, pdf_file, options{ encoding: UTF-8, enable-local-file-access: None })这段代码把Markdown转换成HTML再用pdfkit生成PDF。注意enable-local-file-access这个选项如果不加本地图片可能加载不出来。这个坑我踩过生成的PDF里图片全是空白排查了好久才发现是这个选项的问题。5.4 自动化运行让脚本自己干活脚本写好了每次手动运行也挺麻烦的。我的做法是用系统的定时任务工具让它每天自动跑一次。Windows下用“任务计划程序”Mac和Linux下用crontab。设置很简单比如每天凌晨3点运行一次0 3 * * * cd /path/to/project /path/to/venv/bin/python fetch.py /path/to/log.txt 21这行crontab的意思是每天凌晨3点进入项目目录用虚拟环境里的Python运行抓取脚本并把输出日志追加到log.txt文件里。这样你每天早上起来新文章就已经自动抓好了打开博客就能看。注意定时任务运行的环境变量可能和终端里不一样建议在脚本里用绝对路径避免找不到文件。另外日志文件要定期清理不然时间长了会占满硬盘。6. 一些个人体会和实用建议这套东西我用了小半年最大的感受就是“早该这么干”。以前翻历史文章要十几分钟现在几秒钟搞定效率提升不是一点半点。而且因为数据都在本地我随时可以导出、备份、迁移完全不用担心平台变化或者内容丢失。如果你也想搭一套我的建议是从小规模开始。先抓一个公众号跑通整个流程再逐步增加功能。不要一上来就追求完美先把核心的抓取、存储、展示跑通后面再慢慢优化。遇到问题不要慌大部分问题都是请求头、编码、路径这些细节问题耐心排查都能解决。另外脚本的健壮性很重要。网络请求一定要加超时和重试文件操作一定要加异常处理关键步骤一定要打日志。这样即使半夜脚本跑挂了第二天看日志也能快速定位问题。我现在的脚本已经稳定运行了几个月中间只出过两次小问题都是因为网络波动导致的重试机制自动处理了完全不需要人工干预。最后分享一个小技巧如果你抓的公众号文章特别多建议按年份分目录存储Markdown文件比如posts/2023/、posts/2024/这样文件不会全部堆在一个目录里管理起来更清晰。MkDocs的导航配置也可以按年份分组浏览体验更好。这个改动很小但效果很明显强烈建议你试试。
返回列表