简介:面向 Java 开发者的新闻类爬虫入门资源,适合需要按关键词批量抓取百度新闻、今日头条并入库的初学者。内置 16 个 Java 源文件、1 个 Maven 配置文件和 1 个属性配置文件,构成完整的 HTTP 请求、页面解析、数据存储流程;XML 与 properties 分别用于项目依赖与运行参数配置,整体 19KB,共 20 个文件,结构紧凑便于直接阅读。资源附 README 说明与 .gitignore 等辅助文件,可帮助理解工程组织方式。已有 447 人浏览学习。下载后可根据关键词运行抓取任务,观察 URL 队列、响应解析与数据库写入的完整链路;对爬虫协议、User-Agent 设置及简单反爬应对也有直观体现。适合作为课程设计参考或毕业设计起步模板,也可用于快速验证新闻聚合类数据采集思路。
1. 先把“关键字新闻爬虫”说清楚:这份Java资源能做什么
如果你干过一段时间数据采集,一定经历过这种场景:老板扔过来一个需求,说“把百度新闻和今日头条上所有带某某关键字的新闻给我抓下来存库里”。听起来简单,真做起来,搜索页的HTML结构三天两头变,今日头条还得滚动加载,百度新闻现在点了链接直接跳转到原网站,存进库里的往往是一堆残缺数据。这个资源解的就是这个具体问题。它是一个Java工程,用Selenium驱动真实浏览器去抓百度新闻和今日头条,按你配置的关键字循环搜索,把标题、链接、发布时间、正文摘要和来源站点解析出来,去重后批量写入Oracle或MySQL数据库。它不是给只想跑一遍看热闹的人准备的,是给需要稳定落库、还要能自己调参修bug的从业者用的。
2. 从Java工程到可跑通的爬虫:模块拆解与抓取逻辑
2.1 工程结构先摸清:别一上来就改代码
下载下来解压后,第一眼看到的是一堆目录。别急着双击运行,先花两分钟把结构摸清楚。这个资源是Maven工程,标准布局:根目录有pom.xml、README.md,源码都在src/main下面。pom.xml里管理着所有依赖,重点看三个:selenium-java版本、数据库驱动(Oracle和MySQL各有一个)、以及数据库连接池的版本。这几个版本决定了你的运行环境要求。
我一般拿到手第一步是打开pom.xml,把Maven仓库地址和依赖版本扫一遍,确认JDK版本匹配。常见情况是资源作者用的是JDK 8,而机器上装的是JDK 11甚至17,直接跑会报ClassNotFoundException之类的问题,不是代码错了,是编译环境和运行环境不一致。
2.2 抓取核心逻辑:为什么用Selenium而不是HttpClient
看代码之前先弄明白一个关键选型问题:为什么用Selenium,而不是用HttpClient直接发请求?百度新闻的搜索结果页和今日头条的信息流页面,内容都是动态渲染出来的,直接用HTTP请求拿到的HTML里只有一堆JavaScript脚本,真正的新闻数据是浏览器执行脚本后才出现在DOM里的。当然可以用Requests或者Jsoup配接口逆向去拿数据,但接口签名经常变,今天能用明天就报错。Selenium是直接驱动真实浏览器,页面怎么渲染它就怎么抓,稳定性高很多,代价是慢、耗资源。
代码主流程是这样的:先读取配置文件拿到关键字列表,然后为每个关键字构造搜索URL,启动浏览器驱动访问页面,等待页面加载完成,用XPath或CSS选择器定位新闻列表的DOM节点,逐条提取标题、链接、时间、摘要。提取完一批后,在内存里做一个URL去重——这里用的是HashSet,把已经处理过的链接存起来,避免同一个新闻重复入库。所有字段封装成一个NewsItem对象,最后交给DAO层批量写入数据库。
// 以今日头条搜索为例,演示Selenium的核心抓取流程 WebDriver driver = new ChromeDriver(options); try { driver.get("https://www.toutiao.com/search/?keyword=" + URLEncoder.encode(keyword, "UTF-8")); // 今日头条是无限滚动加载,需要循环滚动页面触发新内容出现 for (int i = 0; i < 8; i++) { ((JavascriptExecutor) driver).executeScript("window.scrollTo(0, document.body.scrollHeight);"); Thread.sleep(1500); // 等新内容渲染完成,时间太短会漏数据 } List<WebElement> items = driver.findElements(By.cssSelector("div[class*='articleCard']")); for (WebElement item : items) { String title = item.findElement(By.cssSelector("a[class*='title']")).getText(); String url = item.findElement(By.cssSelector("a[class*='title']")).getAttribute("href"); String source = item.findElement(By.cssSelector("div[class*='source']")).getText(); if (deduplicatedSet.add(url)) { // 内存去重,防止同一链接重复入库 newsList.add(new NewsItem(keyword, title, url, source)); } } } finally { driver.quit(); // 每个关键字抓完就关闭浏览器,释放内存 }这里的核心参数有三个。第一个是window.scrollTo(0, document.body.scrollHeight),作用是把滚动条拉到页面底部,触发今日头条的懒加载机制,新内容才会出现在DOM里。第二个是Thread.sleep(1500),等渲染的时间,太短会漏掉还没加载出来的条目,太长浪费时间,1.5秒是个折中值,网络差可以调到3秒。第三个是deduplicatedSet,这个去重集合必须放在外层循环里定义,如果在每次循环内部新建,那同一个关键字下的重复新闻就没法过滤了。
2.3 数据落库部分:批量提交别一条条插
看完抓取逻辑,再看存库部分。资源里封装的DAO层写得比较规整,核心思路是批量插入。批量插入和单条插入的差别非常大:一次性提交100条数据只用一次网络往返,逐条提交就要100次,在数据量大时性能差距是数量级的。JDBC的批量提交要用addBatch和executeBatch配合,同时关闭自动提交,最后手动commit。
public void batchInsert(List<NewsItem> newsList) throws SQLException { String sql = "INSERT INTO news_data(keyword, title, url, publish_time, source, create_time) VALUES(?,?,?,?,?,?)"; Connection conn = dataSource.getConnection(); PreparedStatement ps = conn.prepareStatement(sql); conn.setAutoCommit(false); // 关闭自动提交,批量执行完再统一提交 for (NewsItem item : newsList) { ps.setString(1, item.getKeyword()); ps.setString(2, item.getTitle()); ps.setString(3, item.getUrl()); ps.setString(4, item.getPublishTime()); ps.setString(5, item.getSource()); ps.setTimestamp(6, new Timestamp(System.currentTimeMillis())); ps.addBatch(); if (newsList.size() % 500 == 0) { // 每500条刷一次,防止内存积压过多 ps.executeBatch(); } } ps.executeBatch(); // 提交剩余未刷的数据 conn.commit(); }这里有个细节很多人没注意:批量插入不是一次全塞进去就好。数据库驱动和数据库服务器对单次批量执行的SQL条数有限制,一次塞几万条可能内存溢出。所以每500条执行一次executeBatch,这是常规做法。另外conn.setAutoCommit(false)必须写在批量操作之前,如果漏掉,每条插入都会隐式提交,批量就没意义了。参数keyword字段一定要存,因为一个库里会跑多个关键字,后续按关键字筛选数据全靠它。
3. 部署与参数调整:跑通本地再到服务器
3.1 你以为的“直接运行”和实际的运行差距
很多人下载这个资源后第一反应是双击运行,或者直接java -jar。这个工程不是那种打包好的一体化程序,它依赖外部浏览器和数据库,部署步骤必须先搞对。完整步骤如下:
第一步,装Chrome浏览器,版本无所谓,但要和chromedriver匹配。第二步,下载对应版本的chromedriver,放到指定路径。第三步,配置数据库,执行工程里的建表SQL。第四步,修改配置文件的数据库连接信息。第五步,用Maven打包或直接在IDE里运行主类。
这里最容易翻车的是chromedriver版本问题。Chrome是自动更新的,今天用的99版本,明天自动升级到100,chromedriver还是99的,Selenium启动浏览器时直接报SessionNotCreatedException。代码里通常会写死driver路径,你需要把这个路径改成自己机器上的实际位置。
# 启动主程序(假设已经打成jar包) java -jar news-crawler.jar --spring.profiles.active=prod这个工程的启动入口一般是带main方法的类,里面会依次做四件事:加载配置文件、初始化数据源、启动抓取循环、结果落库。启动成功后日志会打印当前操作的关键字和抓取条数。如果启动后没有日志输出,大概率是配置文件没加载到,检查config.properties是不是在classpath下。
3.2 数据库连接参数:Oracle和MySQL的切换逻辑
这个资源同时支持Oracle和MySQL,切换方式很直接:改配置文件里的驱动类名、连接URL、方言。工程里的DAO层封装了两种方言的适配,比如分页语法、自增主键的获取方式,Oracle用序列,MySQL用自增。切换数据库时不用改代码,只改这里的配置。
| 配置项 | MySQL写法 | Oracle写法 | 修改位置 |
|---|---|---|---|
| driverClassName | com.mysql.cj.jdbc.Driver | oracle.jdbc.OracleDriver | jdbc.properties |
| jdbcUrl | jdbc:mysql://localhost:3306/news_db?useUnicode=true&characterEncoding=utf8 | jdbc:oracle:thin:@localhost:1521:orcl | jdbc.properties |
| 建表语句 | id INT AUTO_INCREMENT PRIMARY KEY | id NUMBER PRIMARY KEY+ 序列 | schema.sql |
| 批量提交方言 | rewriteBatchedStatements=true | 默认支持 | jdbc.properties |
MySQL连接URL里面有一句rewriteBatchedStatements=true,这个参数容易被忽略,但很关键。不加这个参数,MySQL驱动会一条一条地执行批量SQL,性能提升非常有限;加上之后驱动才会真正重写SQL为多值INSERT,批量速度能提升十几倍。Oracle驱动默认就支持真正的批量,不需要额外配置。
建表语句在工程里已经给了SQL文件,MySQL和Oracle各一份。注意时间字段的类型,MySQL用datetime,Oracle用TIMESTAMP,这两个在Java代码里都映射到Timestamp类型,不需要额外处理。唯一要小心的是Oracle的保留字问题,表名别用COMMENT、ORDER这种Oracle的保留字,否则建表就报ORA-00903。资源的表名是news_data,不在保留字列表里,可以直接用。
3.3 配置文件里的抓取参数:频率、超时、重试
配置文件是整个资源的调度中心。核心参数有这么几个:crawl.interval是每一轮抓取之间休眠的毫秒数,默认是60000,也就是一分钟;crawl.timeout是页面加载超时时间,默认30秒;crawl.retry是失败重试次数,默认3次;crawl.keywordsFile是关键字配置文件的路径。这些参数可以直接改,不用动代码。
关键字配置文件是纯文本格式,每行一个关键字。抓取时程序按顺序逐个处理,每个关键字抓完一轮后休眠crawl.interval毫秒再抓下一个。如果关键字太多,一轮下来可能要跑很久,建议把crawl.interval调小,或者把不常用的关键字注释掉。头条的搜索结果是无限滚动的,抓取深度是硬编码在代码里的8次滚动,如果你觉得抓得不够深,把循环上限调大即可,但注意单次任务跑太久容易被站点限制访问。
提示:
crawl.timeout设置太短会导致页面还没加载完就报超时异常,太长会让整体效率变低。建议从默认的30秒开始,如果经常出现超时,先排查网络代理问题,再考虑上调超时时间。
4. 避坑手册:翻车最多的六个场景与排查方法
4.1 现象一:Chrome启动报错“unknown error: cannot find Chrome binary”
抓取程序突然中断,日志里出现WebDriverException: unknown error: cannot find Chrome binary。原因很直接:程序里设置System.setProperty("webdriver.chrome.driver", ...)只指定了driver路径,但Selenium还需要找到浏览器本身的安装路径。Linux服务器上默认Chrome装的位置在/opt/google/chrome/chrome,但某些精简安装或自定义安装路径下,Selenium找不到。
解决方法是显式设置Chrome二进制路径。在代码或启动参数里加上options.setBinary("/usr/bin/google-chrome-stable"),具体路径以你机器实测为准。另外,服务器上别装Snap版Chrome,Snap版本的路径映射特殊,Selenium经常识别不到。
4.2 现象二:今日头条能打开但抓不到任何内容
页面能打开,滚动条也滚动了,但findElements拿到的是空列表。先在浏览器里按F12看下articleCard这个class是否还存在。头条改版比较勤,CSS类名经常变。资源交付时的选择器是当时版本的,可能已经失效。解决思路是重新抓取页面看当前DOM里新闻条目的真实选择器,修改代码里的By.cssSelector参数。
这个坑是爬虫维护中最频繁的,没有一劳永逸的解法。我一般会给抓取逻辑加上日志输出,每次启动先打印页面上所有符合条件的节点数量,看到数字为0就知道选择器失效了,不用等运行到落库阶段才发现问题。
4.3 现象二:百度新闻抓下来的链接是跳转链接
百度新闻的搜索结果显示的是百度自己的跳转链接,格式类似https://www.baidu.com/link?url=xxx&wd=xxx,直接存库没意义,因为要做二次跳转才能到真实新闻页面。这个现象在资源作者写代码时可能还不存在,或作者没处理。现在一定要在落库前对URL做二次解析:用Selenium访问这个跳转链接,等待页面跳转完成,读取driver.getCurrentUrl()拿到最终地址,再替换掉原链接。
private String resolveRedirect(String redirectUrl, WebDriver driver) { try { driver.get(redirectUrl); Thread.sleep(5000); // 等待跳转完成,时间不够会拿到中间页的URL return driver.getCurrentUrl(); } catch (Exception e) { return redirectUrl; // 解析失败就保留原链接,不阻塞主流程 } }延迟5秒是实测出来的,百度跳转页面有时候会先加载一个过渡页再跳走,时间太短拿到的还是中间地址。如果你发现存储的URL依然带baidu.com/link前缀,就是延迟太短了。这个步骤会明显拖慢抓取效率,因为是串行跳转,一个关键字几百条新闻就要跑很久,可以考虑后用多线程并行解析提升速度。
4.4 现象三:数据库插入报“ORA-00001: unique constraint violated”
跑了几轮之后突然报唯一约束冲突。原因是资源里表设计时给url字段建了唯一索引,第一轮抓取的数据还在库里,第二轮抓到了相同新闻,虽然代码里有HashSet去重,但那是内存级别的,程序重启后HashSet清空,无法和数据库历史比对。解决这道问题的核心就是要有一个持久化的去重方案,资源代码里可能没有完整实现。常见做法是:插入前先按url查一遍数据库,存在就跳过。
-- 用一条SQL判断链接是否已存在,避免程序查询数据库再判断(少了网络往返) INSERT INTO news_data(keyword, title, url, publish_time, source, create_time) SELECT ? , ? , ? , ? , ? , SYSTIMESTAMP FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM news_data WHERE url = ?);这个写法在Oracle下可用,MySQL下把FROM DUAL去掉,SYSTIMESTAMP换成NOW()。利用主键或唯一约束做冲突规避,比先查询再插入的线程安全程度更高,并发插入时不会被两条相同记录击穿。这样处理之后,即使HashSet失效,数据库层面也能挡住重复数据。
4.5 现象四:批量插入大批数据后内存溢出
抓取顺利,运行半小时后OOM内存溢出。Selenium本身是重资源组件,一个浏览器实例大概占200到300MB内存,工程默认是串行一个浏览器,不存在多浏览器失控的可能。那问题大概率出在newsList上:主循环把所有抓到的数据都存在ArrayList里,攒够一批才批量提交,如果提交间隔太长,List无限膨胀。解决思路是控制单轮抓取的上限,或者每个关键字抓完后立即落库,不要等整轮结束再一次性存。
资源代码里的批量提交逻辑如果是在所有关键字循环结束后才执行一次,那内存占用会随关键字数量线性增长。我一般会调整成“每处理完一个页面就提交一次”,这样List的最大长度可控,内存也就稳住了。
4.6 现象五:定时任务用while(true)硬顶还是直接退出了
有人把它接到定时任务里,发现程序跑到一半就退出,没有异常日志。排查点一般在未捕获的运行时异常上。爬虫代码里网络异常、解析异常是多发的,如果主循环没有统一处理异常,碰到一次NoSuchElementException整个线程就挂了。解决思路是给主循环加最外层兜底,打印堆栈后continue。
while (running) { try { crawlAllKeywords(); } catch (Exception ex) { ex.printStackTrace(); // 打点观察是否持续抛错 } Thread.sleep(interval); }这个兜底很关键,能保证某个关键字失败时,程序继续跑下一个。但要注意,永远不能让异常被吞掉后还无限重试,时间久了会把站点打到封IP。建议加上失败次数的统计,比如某个关键字连续失败5次就跳过它,标记为待人工排查。
5. 进阶技巧:关键字设置、去重优化与合规边界
5.1 关键字文件的设计:多关键字策略决定抓取质量
关键词不是越多越好。在某些搜索场景里,过泛的关键字会匹配出大量无关内容,数据库里塞满噪音。我一般把关键字拆成三组:核心词、修饰词、排除词。核心词是业务必抓的词;修饰词用来做“核心词+场景词”的组合,比如“新能源汽车”+“补贴”;排除词则靠代码里对标题做过滤,比如标题或正文含“辟谣”“招聘”“广告”的条目直接丢弃。资源的抓取循环天然支持多关键字顺序执行,你只需要在关键字配置里把组合词每行写一个,例如新能源汽车 补贴这种带空格的写法,程序搜索时会按空格分词或原样搜索,不同站点行为不同。
头条的搜索是支持引号精确搜索的,关键字带双引号可能拿到更精准的结果。百度新闻则是对空格分词做OR匹配。如果你想做精确短语匹配,可以试试给关键字加双引号,但要注意,个别站点对引号的处理方案不同,加了之后可能反而什么都搜不到,需要实际测试。
5.2 去重逻辑再加强:应对标题相同但链接不同的场景
URL去重解决不了“同一篇新闻被多个站点转载”的问题。比如某公司发布一条公告,新浪发一遍、网易发一遍、腾讯发一遍,URL完全不同,但标题内容几乎一样。这种场景下,需要加一个标题相似度去重:把标题里的标点、空格全部去掉后做归一化,对归一化后的标题做MD5,存一个hash字段。入库前对比hash,如果同一批数据里出现相同的hash,就保留来源优先级高的那一条。
private String normalizeTitle(String title) { // 去掉常见标点和空格,让同一标题的不同变体归一化到同一个key return title.replaceAll("[\\s·,,。!!??、\\\\[\\]]", "").toLowerCase(Locale.ROOT); } private String getTitleHash(String normalizedTitle) { return DigestUtils.md5Hex(normalizedTitle); // 用MD5做定长hash,比对更快 }之所以用MD5而不是原文比对,是因为字段索引做等值匹配更高效。数据库加一列title_hash,和url联合建唯一索引,插入时按hash判断。你会发现,加了标题hash去重后,库里的数据量会大幅下降,但数据质量明显提升——因为一稿多发的重复新闻被过滤掉了。资源原始代码不带这个逻辑,但你可以自己在DAO层加,字段预留了扩展空间。
5.3 频率控制与合规边界:爬虫不是越快越好
抓取频率控制的逻辑有一段延时参数,默认60秒一轮。很多人觉得太慢,想调到10秒甚至5秒。这个调整要谨慎。对新闻网站搜索接口短时间内高频访问,轻则触发访问拦截验证码,重则IP被封。头条对异常请求的封禁力度比较大,封了之后别的业务也会受影响。我一般保持在30到60秒范围内,宁可慢一点,也不冒被封的风险。
合规方面多说一句。国内新闻网站的robots.txt对爬虫的约束不同,部分站点明确禁止爬虫抓取内容。使用爬虫技术前应当核实目标站点的政策,且抓取后的数据不能用于商业转售或公开传播。新闻标题和摘要的合理使用边界比较窄,个人学习、内部研究使用问题不大,但建库外发就有风险了。这个资源的技术栈是完整的,怎么用、用到什么程度,是需要自己把握的部分。
5.4 高阶玩法:把抓取成果对接到数据分析链路
数据落库只是第一步,真正有价值的是后续分析。资源抓下来的表结构有keyword、title、url、publish_time、source五个核心字段,这几列已经足够支撑基础的舆情分析。常见做法是按关键字分组统计新闻数量趋势,看热点变化;或者按source字段聚合,看哪些媒体对某个话题的报道最活跃。头条和百度新闻的时间字段格式不统一,写SQL做趋势分析时,建议先把publish_time截成日期格式,然后按日期分组,这样能输出每日新闻量折线图。
如果想做更深的内容分析,比如情感分类、主题聚类,把title和抓取到的摘要字段导出来,用Python调大模型接口做批次标注。这个工作适合离线做,不要在抓取进程里跑,太重了会影响采集稳定性。
我从拿到这份资源到完全跑通,中间大概折腾了两天。最初也是直接运行翻车,后来把chromedriver版本对齐、加了跳转解析、补了标题去重,才真正稳定下来。从那以后我每次部署爬虫,都强制走一遍“配置检查-驱动版本确认-单关键字试跑-全量抓取”的流程,省掉了无数半夜被报警吵醒的麻烦。希望这份工程的每个坑和补丁,都能帮你省下这两天的时间,把精力花在分析和业务上,而不是和选择器搏斗。
本文还有配套的精品资源,点击获取