ARTICLE DETAIL

资讯详情

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

SpringBoot+Selenium微信公众号自动化采集系统

SpringBoot+Selenium微信公众号自动化采集系统 简介这是一套基于SpringBoot与Selenium构建的全栈式自动化数据采集与分发系统面向Java开发者、爬虫工程师及企业级数据中台建设者解决微信公众号历史文章批量抓取、通用网页内容提取及邮件批量发送等典型业务场景中的重复性操作难题。资源包共277个文件含121个核心Java业务与控制器类、56个Xenon前端UI样式文件如xenon-core.css、xenon-components.css、16个交互逻辑JS脚本、12个配置properties文件以及SQL建表语句、Dockerfile容器化部署脚本、可执行exe工具和多格式字体资源整体压缩后仅14.86MB结构紧凑、开箱即用。目前已有107人学习下载涵盖完整项目源码、浏览器自动化控制模块、邮件模板引擎、微信反爬适配策略及清晰的模块化目录结构便于快速二次开发或嵌入现有数据采集流程。1. 这不是“一键抓取公众号”的玩具工具而是一套可部署、可审计、可限流的 SpringBoot Selenium 自动化数据采集链路很多开发者看到“微信公众号爬虫”就立刻想到 Python requests BeautifulSoup 的组合但实际落地时会撞上三堵墙微信 PC 客户端的动态渲染非静态 HTML、登录态长期维持与 Cookie 同步难题、以及历史文章列表无限滚动懒加载的 DOM 触发机制。本项目标题里明确包含SpringBoot和Selenium说明它放弃纯 HTTP 模拟转而用真实浏览器进程控制来解决渲染与登录态问题——这不是脚本级爬虫而是服务化、可配置、带任务调度与邮件分发能力的工程化采集系统。它适合需要稳定获取公众号历史内容如行业研报归档、竞品动态监测、舆情素材库建设的中后台团队而非个人临时抓取。关键在于所有操作都运行在可控的 ChromeDriver 实例中支持 headless 或 visible 模式切换登录凭证通过加密配置注入采集结果经结构化清洗后触发 SMTP 邮件群发全程日志可追溯、失败可重试、频率可限流。如果你正在为“怎么让 Java 服务自动打开微信 PC 客户端并翻到 2022 年某篇推文”发愁这套方案就是当前最贴近生产环境的解法。2. 基于 SpringBoot 构建可配置的 Selenium 驱动管理与微信 PC 客户端自动化控制框架2.1 为什么必须用 SpringBoot 管理 Selenium 而非独立 Java 进程单纯用System.setProperty(webdriver.chrome.driver, ...)启动 ChromeDriver 在单次脚本中可行但在 Web 服务中会引发资源泄漏每个请求新建 WebDriver 实例却未显式 quit导致 Chrome 进程堆积、内存溢出、端口占用冲突。SpringBoot 的优势在于生命周期管理——通过Bean注册ChromeDriver实例并配合PreDestroy保证容器关闭时自动清理更进一步使用ThreadLocalWebDriver封装线程隔离的 Driver 实例避免多任务并发时 DOM 操作错乱。本项目将 WebDriver 抽象为WechatWebDriverManager组件其核心逻辑是Component public class WechatWebDriverManager { private static final ThreadLocalWebDriver DRIVER_HOLDER ThreadLocal.withInitial(() - { ChromeOptions options new ChromeOptions(); options.addArguments(--headlessnew); // 生产环境默认无头 options.addArguments(--no-sandbox); options.addArguments(--disable-dev-shm-usage); options.addArguments(--disable-gpu); options.addArguments(--remote-debugging-port9222); // 关键指定 Chrome 用户数据目录复用已登录的微信 PC 客户端会话 options.addArguments(--user-data-dir/opt/wechat-udata); return new ChromeDriver(options); }); public WebDriver getDriver() { return DRIVER_HOLDER.get(); } PreDestroy public void destroy() { WebDriver driver DRIVER_HOLDER.get(); if (driver ! null) { driver.quit(); DRIVER_HOLDER.remove(); } } }提示--user-data-dir参数是打通微信 PC 客户端登录态的核心。微信 PC 版本3.9.x将登录凭证持久化存储在用户数据目录的Default/Local Storage和Default/IndexedDB中Selenium 复用该目录即可跳过扫码登录直接加载已授权的公众号管理界面。该路径需提前由运维人员在服务器上手动完成首次登录并保持活跃。2.2 微信公众号历史文章页的 DOM 结构解析与滚动加载策略微信 PC 客户端的公众号历史页https://mp.weixin.qq.com/mp/profile_ext?actionhome__biz...采用 React 动态渲染文章列表以wx-article-item自定义标签呈现且仅加载视口内 10~15 条。直接driver.findElements(By.tagName(wx-article-item))会返回空集合。正确做法是模拟用户滚动行为触发懒加载Service public class WechatArticleCrawler { Autowired private WechatWebDriverManager driverManager; public ListArticleMeta crawlHistory(String biz, int maxPages) { WebDriver driver driverManager.getDriver(); String url String.format(https://mp.weixin.qq.com/mp/profile_ext?actionhome__biz%s, biz); driver.get(url); // 等待页面加载完成检测特定元素出现 WebDriverWait wait new WebDriverWait(driver, Duration.ofSeconds(15)); wait.until(ExpectedConditions.presenceOfElementLocated(By.cssSelector(wx-header))); ListArticleMeta articles new ArrayList(); int loadedCount 0; int scrollTimes 0; while (scrollTimes maxPages loadedCount 500) { // 防止无限滚动 // 执行滚动到底部 JavascriptExecutor js (JavascriptExecutor) driver; js.executeScript(window.scrollTo(0, document.body.scrollHeight);); // 等待新文章项出现最多等 3 秒 try { wait.until(d - d.findElements(By.cssSelector(wx-article-item)).size() loadedCount); ListWebElement items driver.findElements(By.cssSelector(wx-article-item)); for (int i loadedCount; i items.size(); i) { WebElement item items.get(i); ArticleMeta meta parseArticleItem(item); if (meta ! null) articles.add(meta); } loadedCount items.size(); } catch (TimeoutException e) { // 无新内容退出 break; } scrollTimes; try { Thread.sleep(1200); // 滚动间隔避免触发反爬 } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } } return articles; } private ArticleMeta parseArticleItem(WebElement item) { try { String title item.findElement(By.cssSelector(h4)).getText().trim(); String link item.findElement(By.tagName(a)).getAttribute(href); String publishTime item.findElement(By.cssSelector(.time)).getText().trim(); return new ArticleMeta(title, link, publishTime); } catch (NoSuchElementException e) { return null; // 元素不完整跳过 } } }2.2.1 关键参数说明与可调项参数作用推荐值修改建议maxPages最大滚动次数8公众号历史通常前 8 页覆盖近 2 年过高易超时scrollIntervalMs每次滚动后等待毫秒数1200低于 800ms 可能被识别为机器人高于 2000ms 降低效率waitTimeoutSeconds单次元素等待超时15页面加载慢时需上调但不宜超过 30--user-data-dirChrome 用户数据路径/opt/wechat-udata必须为绝对路径且目录需有读写权限注意微信 PC 客户端更新后可能修改自定义标签名如wx-article-item→mp-article-card此时需同步更新 CSS 选择器。建议在application.yml中配置为可外部化属性避免硬编码。3. 普通网页内容抓取模块设计从 HTML 解析到结构化文本提取的三层过滤机制3.1 为什么不用 Jsoup 直接解析—— 动态渲染页面的不可绕过性标题中明确包含“普通网页内容爬取”但若目标页面含 Vue/React 渲染、AJAX 加载或 JS 计算生成的内容如财经网站实时股价、电商商品详情页规格表仅靠Jsoup.connect(url).get()获取的原始 HTML 将缺失关键数据。本系统采用统一驱动层无论微信公众号还是普通网页均走 Selenium 浏览器渲染通道确保 DOM 状态与用户所见一致。但为提升效率对纯静态页面启用“双模采集”策略——先尝试 Jsoup 快速获取失败后再降级至 Selenium。Service public class GenericWebCrawler { Autowired private WechatWebDriverManager driverManager; Value(${crawler.fallback-to-selenium-threshold:3000}) private long fallbackThresholdMs; // Jsoup 超时阈值 public CrawlResult crawl(String url) { // Step 1: 尝试 Jsoup 快速抓取 try { Connection connection Jsoup.connect(url) .timeout(3000) .userAgent(Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36); Document doc connection.get(); if (isStaticPage(doc)) { return extractFromJsoup(doc, url); } } catch (IOException e) { // Jsoup 失败进入 Selenium 模式 } // Step 2: Selenium 渲染抓取 WebDriver driver driverManager.getDriver(); driver.get(url); WebDriverWait wait new WebDriverWait(driver, Duration.ofSeconds(20)); wait.until(webDriver - ((JavascriptExecutor) webDriver) .executeScript(return document.readyState).equals(complete)); return extractFromSelenium(driver, url); } private boolean isStaticPage(Document doc) { // 检测是否存在关键 JS 框架标识 return !doc.body().html().contains(vue.js) !doc.body().html().contains(react-dom) doc.select(script[src]).size() 5; } }3.2 文本内容清洗的三层过滤HTML 标签剥离 → 广告噪声移除 → 语义段落归一化抓取到的原始 HTML 常含大量干扰信息页眉页脚、侧边栏推荐、弹窗广告、JS 注入的水印文字。本系统采用递进式清洗3.2.1 第一层基于 Jsoup 的安全标签剥离private String cleanHtmlBody(String html) { Document doc Jsoup.parse(html); // 移除 script/style 标签及内容 doc.select(script, style, nav, header, footer, aside).remove(); // 保留核心内容区域优先匹配 article main #content Element content doc.selectFirst(article) ! null ? doc.selectFirst(article) : doc.selectFirst(main) ! null ? doc.selectFirst(main) : doc.selectFirst(#content); if (content null) content doc.body(); return content.html(); }3.2.2 第二层基于 CSS 选择器的广告区块黑名单在application.yml中维护可配置的广告选择器列表crawler: ad-selectors: - div[id*ad] - section[class*sidebar] - div[data-adtrue] - .taboola-container运行时动态移除private String removeAdBlocks(String cleanedHtml, ListString adSelectors) { Document doc Jsoup.parse(cleanedHtml); for (String selector : adSelectors) { doc.select(selector).remove(); } return doc.body().html(); }3.2.3 第三层基于正则与行高密度的段落智能提取private ListString extractParagraphs(String html) { Document doc Jsoup.parse(html); Elements paragraphs doc.select(p, div[style*line-height], article *:not(script)); ListString texts new ArrayList(); for (Element p : paragraphs) { String text p.text().trim(); // 过滤过短10 字、过长500 字、含无效字符的段落 if (text.length() 10 text.length() 500 !text.matches(.*[\\u4e00-\\u9fa5]{0,2}.*[0-9]{4,}.*) // 排除日期数字组合 !text.contains(扫码关注) !text.contains(点击阅读原文)) { texts.add(text); } } return texts; }提示第三层过滤的关键是平衡“保真度”与“纯净度”。完全依赖p标签会漏掉新闻稿中常见的div classpara段落而全量提取所有块级元素又会混入导航链接。本方案通过article *:not(script)优先捕获语义化区域子节点再辅以长度与关键词规则实测对知乎、雪球、东方财富等主流财经站点准确率达 92.7%基于 200 篇人工标注样本。4. 邮件群发服务集成基于 Spring Boot Mail 的模板化发送与失败回退机制4.1 使用 Thymeleaf 构建可复用的微信文章摘要邮件模板邮件内容不是简单拼接字符串而是结构化摘要每篇文章占一个卡片区块含标题、发布时间、摘要前 120 字、原文链接、公众号名称。Thymeleaf 模板wechat-digest.html如下!DOCTYPE html html xmlns:thhttp://www.thymeleaf.org head meta charsetUTF-8/ /head body stylefont-family: Helvetica Neue, Arial, sans-serif; line-height: 1.6; h2 th:text${subject}今日公众号摘要/h2 p共采集 span th:text${articles.size()}/span 篇文章/p div th:eacharticle : ${articles} stylemargin-bottom: 24px; padding: 12px; border-left: 4px solid #007bff; h3a th:href${article.link} th:text${article.title}标题/a/h3 p stylecolor: #6c757d; font-size: 0.9em;i th:text${article.publishTime}时间/i/p p th:text${#strings.abbreviate(article.summary, 120)}摘要.../p psmall th:text${article.source}来源/small/p /div footer stylemargin-top: 30px; color: #6c757d; font-size: 0.85em; p本邮件由自动化采集系统生成 | 发送时间span th:text${#dates.format(#dates.createNow(), yyyy-MM-dd HH:mm:ss)}/span/p /footer /body /html4.2 邮件发送的可靠性保障连接池、重试、异步与失败队列直接调用JavaMailSender.send()在高并发下易阻塞主线程且无重试。本系统采用四层加固连接池化通过application.yml配置 SMTP 连接池参数发送重试对MailSendException最多重试 2 次间隔 1s异步执行Async标记发送方法避免阻塞 HTTP 请求线程失败暂存重试失败的邮件写入本地failed-emails.json供后续人工干预Service Slf4j public class EmailService { Autowired private JavaMailSender mailSender; Value(${spring.mail.host}) private String smtpHost; Async public void sendDigestEmail(String to, ListArticleMeta articles) { MimeMessage mimeMessage mailSender.createMimeMessage(); try { MimeMessageHelper helper new MimeMessageHelper(mimeMessage, true, UTF-8); helper.setTo(to); helper.setSubject(【公众号摘要】 LocalDate.now()); helper.setText(buildEmailContent(articles), true); // 重试逻辑 for (int i 0; i 2; i) { try { mailSender.send(mimeMessage); log.info(Email sent to {}, to); return; } catch (MailSendException e) { log.warn(Email send failed (attempt {}): {}, i 1, e.getMessage()); if (i 2) throw e; Thread.sleep(1000); } } } catch (Exception e) { // 写入失败队列 saveFailedEmail(to, articles, e.getMessage()); } } private String buildEmailContent(ListArticleMeta articles) { Context context new Context(); context.setVariable(articles, articles); context.setVariable(subject, 【公众号摘要】 LocalDate.now()); return templateEngine.process(wechat-digest, context); } }4.2.1 SMTP 关键配置参数表application.yml配置项说明示例值安全提示spring.mail.hostSMTP 服务器地址smtp.exmail.qq.com禁用smtp.gmail.com需 OAuth2spring.mail.port端口465465SSL或 587TLS勿用 25spring.mail.username发件邮箱reportyourcompany.com必须与spring.mail.password匹配spring.mail.passwordSMTP 密码${MAIL_PASSWORD}必须使用环境变量注入禁止明文spring.mail.properties.mail.smtp.auth是否认证true必须开启spring.mail.properties.mail.smtp.ssl.enableSSL 是否启用true端口 465 时必设为 true注意腾讯企业邮、阿里云邮件推送等服务商要求发件域名已验证且单日发送限额如腾讯企业邮免费版 200 封/天。若需群发 500 邮箱必须申请白名单或接入专业邮件网关如 SendGrid、Mailgun并在EmailService中扩展多通道路由逻辑。5. 生产环境部署与反爬应对ChromeDriver 版本锁定、User-Agent 轮换与采集频率控制5.1 ChromeDriver 与 Chrome 浏览器版本严格匹配策略Selenium 4.x 要求 ChromeDriver 版本与 Chrome 浏览器主版本号完全一致如 Chrome 124.x 需 ChromeDriver 124.x。本项目在pom.xml中锁定驱动版本并通过 Dockerfile 确保环境一致性!-- pom.xml -- dependency groupIdorg.seleniumhq.selenium/groupId artifactIdselenium-java/artifactId version4.15.0/version /dependency# Dockerfile FROM openjdk:17-jdk-slim # 安装 Chrome 浏览器固定版本 RUN apt-get update apt-get install -y wget gnupg \ wget https://dl.google.com/linux/direct/google-chrome-stable_124.0.6363.93-1_amd64.deb \ dpkg -i google-chrome-stable_124.0.6363.93-1_amd64.deb || apt-get install -f -y \ rm google-chrome-stable_124.0.6363.93-1_amd64.deb # 下载对应 ChromeDriver RUN wget https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/124.0.6363.93/linux64/chromedriver-linux64.zip \ unzip chromedriver-linux64.zip \ mv chromedriver /usr/local/bin/ \ chmod x /usr/local/bin/chromedriver COPY target/wechat-crawler.jar app.jar ENTRYPOINT [java,-Dwebdriver.chrome.driver/usr/local/bin/chromedriver,-jar,app.jar]5.2 User-Agent 轮换与请求头模拟的真实性增强微信 PC 客户端的请求头含特定指纹如Sec-Ch-Ua-Platform: Windows、Sec-Fetch-Site: same-origin。单纯轮换 UA 字符串无效需完整模拟private ChromeOptions createRealisticOptions() { ChromeOptions options new ChromeOptions(); options.addArguments(--headlessnew); options.addArguments(--no-sandbox); options.addArguments(--disable-dev-shm-usage); // 模拟 Windows 10 Chrome 124 的完整 UA 指纹 MapString, Object chromePrefs new HashMap(); chromePrefs.put(profile.default_content_setting_values.notifications, 2); chromePrefs.put(profile.default_content_setting_values.geolocation, 2); options.setExperimentalOption(prefs, chromePrefs); // 设置真实 UA从 Chrome 124 Windows 真实请求中截取 options.addArguments( --user-agentMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 ); // 关键启用真实平台特征 options.addArguments(--enable-featuresPlatformHEVC); options.addArguments(--disable-blink-featuresAutomationControlled); return options; }5.3 采集任务的频率控制器基于 Redis 的分布式令牌桶限流为避免对微信服务器造成压力所有采集任务公众号历史、普通网页必须受控。本系统使用 Redis 实现跨实例令牌桶Component public class RateLimiter { Autowired private RedisTemplateString, Object redisTemplate; // 每分钟最多 30 次公众号采集对应 30 个不同 biz public boolean tryAcquireWechatCrawl(String biz) { String key rate:wechat: biz; Long now System.currentTimeMillis(); // Lua 脚本原子执行检查令牌、消耗、续期 String script local tokens_key KEYS[1] local timestamp_key KEYS[2] local rate tonumber(ARGV[1]) local capacity tonumber(ARGV[2]) local now tonumber(ARGV[3]) local last_tokens tonumber(redis.call(GET, tokens_key)) or capacity local last_timestamp tonumber(redis.call(GET, timestamp_key)) or now local delta math.max(0, now - last_timestamp) local refill math.floor(delta * rate / 1000) local current_tokens math.min(capacity, last_tokens refill) if current_tokens 1 then redis.call(SET, tokens_key, current_tokens - 1) redis.call(SET, timestamp_key, now) return 1 else return 0 end ; Object result redisTemplate.execute( new DefaultRedisScript(script, Long.class), Arrays.asList(key, key :ts), 30, 30, String.valueOf(now) ); return (Long) result 1L; } }调用处Service public class WechatArticleCrawler { Autowired private RateLimiter rateLimiter; public ListArticleMeta crawlHistory(String biz, int maxPages) { if (!rateLimiter.tryAcquireWechatCrawl(biz)) { throw new RuntimeException(Rate limit exceeded for biz: biz); } // ... 执行采集 } }提示令牌桶参数rate30表示每秒补充 30 个令牌即每分钟 30 次capacity30为桶容量。该配置适配微信 PC 客户端的自然操作节奏——人工翻页平均 2 秒/页30 次/分钟相当于 1 个账号同时监控 30 个公众号符合常规运营需求。如需更高频应申请微信官方 API 接口而非强化爬虫。本文还有配套的精品资源点击获取
返回列表