Java爬虫实战:招聘网站数据采集与反爬应对
1. 为什么我选择用Java啃下招聘网站这块硬骨头做数据分析和求职辅助工具的人绕不开的一个需求就是批量获取招聘岗位信息。我最早用Python的requests加BeautifulSoup写过一版跑得挺顺但后来项目要集成到一个已有的Java后台管理系统里团队技术栈清一色Java再维护两套语言成本太高索性把采集模块整个用Java重写了一遍。这篇文章就是那次重写过程中沉淀下来的完整思路和踩坑记录。招聘类网站的数据价值不用多说——岗位名称、薪资范围、学历要求、经验门槛、技能标签、公司规模、融资阶段这些字段组合起来能做很多事分析某个技术方向的市场需求走势、对比不同城市的薪资差异、给求职者做岗位匹配推荐、甚至帮培训机构判断该开什么课。但这类网站的数据不是白给的它们有反爬机制、有登录墙、有动态渲染直接硬来基本是碰一鼻子灰。Java做网络爬虫的优势在于生态成熟、并发能力强、和业务系统集成方便。HttpClient负责发请求Jsoup负责解析HTMLJackson或Fastjson负责处理JSON再加上线程池做并发控制一套组合拳下来能覆盖绝大多数采集场景。这篇文章适合有Java基础、想动手做数据采集的开发者也适合已经写过简单爬虫但遇到反爬就卡住的人。我会从目标分析、请求构造、页面解析、反爬应对、数据存储这几个维度把整个流程拆开讲透代码可以直接参考复现。需要提前说明的是本文讨论的技术方案仅用于学习和技术研究目的实际使用时请遵守目标网站的服务条款和相关法律法规控制采集频率不要对目标服务器造成压力。技术本身是中性的怎么用取决于使用的人。2. 动手之前先把目标网站的请求链路摸清楚2.1 从浏览器开发者工具里挖出真正的数据接口很多人写爬虫的第一步是直接拿URL去请求HTML然后发现返回的内容里根本没有岗位数据。这是因为现代招聘网站基本都是前后端分离架构页面骨架是静态的岗位列表是JavaScript异步加载的。你右键查看网页源代码看到的可能只是一堆div容器真正的数据藏在XHR请求的响应里。正确的做法是打开浏览器的开发者工具切到Network面板筛选XHR或Fetch请求然后刷新页面或滚动加载更多岗位。你会看到一系列接口请求响应格式通常是JSON。找到那个返回岗位列表的接口把它的请求URL、请求方法、请求头、请求参数全部记录下来。这一步是整个爬虫的地基地基没打对后面全白费。以我实际分析的某招聘平台为例它的岗位搜索接口大致是这样的结构一个GET请求URL里带了query参数搜索关键词、city参数城市编码、page参数页码请求头里必须带User-Agent、Referer、Cookie等字段。响应是一个JSON对象里面有个data字段data里又有jobList数组每个元素包含jobName、salaryDesc、jobLabels、skills、experienceName等字段。这些字段名就是后面解析时的key。注意不同招聘平台的接口结构差异很大有的用POST有的参数做了加密签名有的需要登录态。不要照搬某个平台的方案一定要自己动手分析目标平台的实际请求。2.2 请求头里哪些字段是必须带的分析接口的时候重点看请求头。以下几个字段通常缺一不可User-Agent标识客户端类型不带的话服务器可能直接返回403。建议用真实浏览器的UA字符串不要用Java默认的。Referer告诉服务器你是从哪个页面发起的请求很多网站会校验这个字段。Cookie维持会话状态有些接口需要登录后才能访问Cookie里带着身份凭证。Accept声明期望的响应格式一般设为application/json或text/html。X-Requested-With部分网站用它判断是不是Ajax请求设为XMLHttpRequest。我习惯在代码里把这些请求头集中管理用一个Map或者配置类存起来方便统一修改。下面是一个典型的请求头配置示例MapString, String headers new HashMap(); headers.put(User-Agent, Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36); headers.put(Referer, https://example.com/job/search); headers.put(Accept, application/json, text/plain, */*); headers.put(Accept-Language, zh-CN,zh;q0.9); headers.put(Cookie, 你的Cookie字符串);2.3 分页参数与城市编码的对应关系招聘网站的搜索接口通常支持分页page参数从1开始递增。但要注意有些平台对翻页深度有限制比如只允许查看前30页或前100条记录。超过限制后要么返回空数据要么要求登录。这个限制在做数据量评估时必须提前确认否则你写了个循环跑了几百页结果后面全是重复数据或空响应。城市编码是另一个容易踩坑的地方。招聘网站一般用内部编码表示城市比如北京可能是101010100上海是101020100这些编码不会直接显示在页面上需要从接口请求参数里抓取或者从网站的城市列表接口获取。我的做法是先手动搜索一次目标城市在Network面板里找到实际发出的请求把city参数的值记下来硬编码到一个枚举或配置文件中。public enum CityEnum { BEIJING(101010100, 北京), SHANGHAI(101020100, 上海), GUANGZHOU(101280100, 广州), SHENZHEN(101280600, 深圳); private final String code; private final String name; CityEnum(String code, String name) { this.code code; this.name name; } public String getCode() { return code; } public String getName() { return name; } }3. 用HttpClient构建一个能扛住压力的请求层3.1 连接池配置与超时设置的门道Java原生的HttpURLConnection用起来太原始Apache HttpClient是更成熟的选择。但很多人直接new一个CloseableHttpClient就开跑没配连接池没设超时跑几十个请求还行上千个请求就开始报连接超时或资源耗尽。正确的做法是用PoolingHttpClientConnectionManager管理连接池设置最大连接数和每个路由的最大连接数。超时方面要分三层设置connectTimeout建立TCP连接的超时、socketTimeout等待数据的超时、connectionRequestTimeout从连接池获取连接的超时。这三个超时各有用途缺一不可。PoolingHttpClientConnectionManager connManager new PoolingHttpClientConnectionManager(); connManager.setMaxTotal(100); connManager.setDefaultMaxPerRoute(20); RequestConfig requestConfig RequestConfig.custom() .setConnectTimeout(5000) .setSocketTimeout(10000) .setConnectionRequestTimeout(3000) .build(); CloseableHttpClient httpClient HttpClients.custom() .setConnectionManager(connManager) .setDefaultRequestConfig(requestConfig) .build();实测下来connectTimeout设5秒、socketTimeout设10秒是比较稳妥的区间。设太短容易误杀正常请求设太长遇到死连接会拖慢整体进度。连接池的maxTotal根据你的并发线程数来定一般设成线程数的2到3倍就够了。3.2 重试机制不是简单加个循环网络请求失败是常态尤其是采集大量数据的时候。但重试不是无脑for循环重试三次就完事要区分哪些异常值得重试哪些重试也没用。值得重试的情况连接超时、读取超时、服务器返回502/503/504。不值得重试的情况403禁止访问、404不存在、401未授权。前者是临时性故障重试可能成功后者是权限或资源问题重试一百次结果一样。HttpClient自带了一个HttpRequestRetryHandler可以自定义重试逻辑HttpRequestRetryHandler retryHandler (exception, executionCount, context) - { if (executionCount 3) { return false; } if (exception instanceof ConnectTimeoutException) { return true; } if (exception instanceof SocketTimeoutException) { return true; } if (exception instanceof NoHttpResponseException) { return true; } return false; };把retryHandler设置到HttpClientBuilder上就能自动处理重试。但要注意重试之间最好加个短暂的等待避免瞬间密集重试触发风控。3.3 代理IP的合理使用方式采集量大了之后单IP请求频率过高会被限制。这时候需要引入代理IP池。代理的来源可以是商业代理服务也可以自建。使用方式是在请求配置里设置代理HttpHost proxy new HttpHost(代理IP, 端口); RequestConfig config RequestConfig.custom() .setProxy(proxy) .build();代理池的管理是个独立话题核心思路是维护一个可用代理列表每次请求随机选一个请求失败则标记该代理不可用并剔除定期补充新代理。我一般会写一个ProxyPool类内部用一个阻塞队列存代理后台线程定期检测代理可用性。提示代理IP的质量参差不齐免费代理基本不能用商业代理也要先小批量测试。建议在正式采集前用一批代理跑一轮测试请求统计成功率和响应时间筛选出可用的再投入生产。4. 解析JSON响应与字段提取的实战细节4.1 用Jackson做JSON反序列化的正确姿势拿到接口返回的JSON字符串后下一步是解析出需要的字段。Jackson是Java生态里最常用的JSON库性能好、功能全。基本用法是构建ObjectMapper然后readTree成JsonNode再按路径逐层取值。ObjectMapper mapper new ObjectMapper(); JsonNode rootNode mapper.readTree(responseBody); JsonNode jobList rootNode.path(data).path(jobList); for (JsonNode job : jobList) { String jobName job.path(jobName).asText(); String salary job.path(salaryDesc).asText(); String experience job.path(experienceName).asText(); String degree job.path(degreeName).asText(); // 技能标签是一个数组 ListString skills new ArrayList(); JsonNode skillsNode job.path(skills); if (skillsNode.isArray()) { for (JsonNode skill : skillsNode) { skills.add(skill.asText()); } } }用path()而不是get()的好处是当字段不存在时path()返回MissingNodeasText()返回空字符串不会抛NullPointerException。这在字段结构不稳定的接口里特别重要。4.2 薪资字段的清洗与结构化招聘网站返回的薪资通常是一个字符串比如15-25K·13薪、20-40K、8-12K·14薪这种格式。直接存字符串没法做数值分析需要拆解成最低薪资、最高薪资、薪资月数三个字段。public class SalaryParser { public static SalaryInfo parse(String salaryStr) { SalaryInfo info new SalaryInfo(); // 处理·13薪部分 int months 12; if (salaryStr.contains(·)) { String[] parts salaryStr.split(·); salaryStr parts[0]; String monthStr parts[1].replace(薪, ); months Integer.parseInt(monthStr); } // 处理15-25K部分 salaryStr salaryStr.replace(K, ).replace(k, ); if (salaryStr.contains(-)) { String[] range salaryStr.split(-); info.setMinSalary(Double.parseDouble(range[0])); info.setMaxSalary(Double.parseDouble(range[1])); } else { double single Double.parseDouble(salaryStr); info.setMinSalary(single); info.setMaxSalary(single); } info.setMonths(months); return info; } }实际数据里还会遇到面议、薪资面议这种非数值的情况解析时要单独处理标记为未知或跳过。另外有些岗位标注的是日薪或时薪单位不是K这些都要在解析逻辑里做兼容。4.3 技能标签的归一化处理岗位的技能标签是最有价值的数据之一但不同公司写的标签五花八门。同样是Java开发有的写Java有的写JAVA有的写java开发有的写Java工程师。如果不做归一化后续统计出来的结果会很乱。我的做法是维护一个映射表把常见变体映射到标准名称MapString, String skillNormalizeMap new HashMap(); skillNormalizeMap.put(java, Java); skillNormalizeMap.put(JAVA, Java); skillNormalizeMap.put(spring, Spring); skillNormalizeMap.put(springboot, Spring Boot); skillNormalizeMap.put(spring boot, Spring Boot); skillNormalizeMap.put(mysql, MySQL); skillNormalizeMap.put(redis, Redis);归一化之后再做词频统计结果就干净多了。这个映射表需要根据实际采集到的数据不断补充完善一开始不用追求大而全先覆盖高频技能即可。5. 反爬机制识别与应对策略的完整排查链路5.1 从403到验证码一次完整的踩坑记录我第一次跑批量采集的时候前50个请求一切正常第51个开始全部返回403。当时第一反应是IP被封了换了代理继续跑结果新IP跑了几十个请求又被封。这说明不是IP的问题是请求特征被识别了。排查过程是这样的先用单个请求手动测试确认请求头和参数没问题能正常返回数据。然后逐步增加请求频率发现当QPS超过某个阈值时开始出现403。接着检查响应内容发现403页面里有一段JavaScript代码是网站的反爬脚本在检测浏览器环境。再进一步分析发现网站还校验了Cookie中的某些字段这些字段是页面加载时由JavaScript生成的。这个排查链路告诉我们反爬不是单一维度的而是多维度组合的。频率限制、请求头校验、Cookie校验、JavaScript挑战可能同时存在。应对策略也要分层频率控制解决QPS问题请求头伪装解决特征识别问题Cookie维护解决会话校验问题。5.2 请求频率控制的工程化实现控制频率最粗暴的方式是Thread.sleep()但这样会阻塞线程浪费资源。更好的方式是用Guava的RateLimiter做令牌桶限流RateLimiter rateLimiter RateLimiter.create(2.0); // 每秒2个请求 public String fetchWithRateLimit(String url) { rateLimiter.acquire(); // 获取令牌不够就等待 return doFetch(url); }RateLimiter.create(2.0)表示每秒生成2个令牌acquire()会阻塞直到拿到令牌。这样既能控制速率又不会像sleep那样浪费固定时间。实际采集时建议把QPS控制在1到3之间具体看目标网站的容忍度。我一般会先小批量测试从QPS1开始逐步提高观察什么时候开始出现异常响应然后把生产环境的QPS设成临界值的一半。5.3 Cookie的获取与维护策略需要登录态的接口Cookie是绕不过去的。获取Cookie有两种方式一是手动登录后从浏览器复制二是用程序模拟登录流程自动获取。手动方式简单但Cookie会过期适合短期采集自动方式复杂但可持续适合长期运行。手动获取的Cookie一般有效期几小时到几天过期后需要重新获取。我的做法是把Cookie存在配置文件或数据库里程序启动时加载运行中检测到401或403且确认是Cookie过期导致的就发通知提醒更新。自动登录涉及模拟表单提交、处理验证码等环节复杂度较高。如果目标网站有验证码还需要接入打码服务或做OCR识别。这部分展开讲篇幅太大这里只提一个思路用Selenium或Playwright驱动真实浏览器完成登录然后从浏览器上下文里提取Cookie再交给HttpClient使用。这样既绕过了JavaScript加密的登录参数又能拿到有效的会话凭证。5.4 遇到IP封禁时的应急处理即使做了频率控制长时间采集还是可能触发IP封禁。封禁的表现通常是所有请求返回403或者返回一个特殊的验证页面。这时候需要切换到备用IP。代理池的切换逻辑要做得平滑主IP被封后自动从池中取下一个可用IP继续未完成的任务。同时记录被封的IP和封禁时间过一段时间后再检测是否解封。如果所有IP都被封了程序应该暂停并告警而不是继续无效请求。public class ProxyManager { private BlockingQueueProxy availableProxies; private SetProxy bannedProxies; public Proxy getProxy() { Proxy proxy availableProxies.poll(); if (proxy null) { throw new RuntimeException(代理池已耗尽); } return proxy; } public void reportBanned(Proxy proxy) { bannedProxies.add(proxy); // 延迟后重新加入可用池 scheduledExecutor.schedule(() - { bannedProxies.remove(proxy); availableProxies.offer(proxy); }, 30, TimeUnit.MINUTES); } }6. 数据存储方案选型与批量写入优化6.1 存文件还是存数据库根据数据量做决定采集到的数据存哪里取决于数据量和使用场景。如果只是几千条数据做一次性分析存CSV或JSON文件就够了简单直接。如果要持续采集、数据量到几十万上百万条或者需要频繁查询和关联分析那就得上数据库。我的建议是采集阶段先落地到文件JSON Lines格式每行一个JSON对象这样即使程序崩溃也不会丢数据而且方便排查问题。采集完成后再批量导入数据库做后续处理。JSON Lines的好处是追加写入方便不需要读取整个文件再修改。public void saveToFile(JobInfo job, String filePath) { try (BufferedWriter writer new BufferedWriter( new FileWriter(filePath, true))) { ObjectMapper mapper new ObjectMapper(); writer.write(mapper.writeValueAsString(job)); writer.newLine(); } catch (IOException e) { log.error(写入文件失败, e); } }6.2 MySQL表结构设计与索引优化如果选择MySQL存储表结构设计要考虑查询场景。岗位信息表的核心字段包括岗位ID唯一标识、岗位名称、公司名称、薪资范围、城市、经验要求、学历要求、技能标签、采集时间。CREATE TABLE job_info ( id BIGINT AUTO_INCREMENT PRIMARY KEY, job_id VARCHAR(64) NOT NULL COMMENT 岗位唯一标识, job_name VARCHAR(128) NOT NULL COMMENT 岗位名称, company_name VARCHAR(128) COMMENT 公司名称, salary_min DECIMAL(10,2) COMMENT 最低薪资(K), salary_max DECIMAL(10,2) COMMENT 最高薪资(K), salary_months INT DEFAULT 12 COMMENT 薪资月数, city VARCHAR(32) COMMENT 城市, experience VARCHAR(32) COMMENT 经验要求, degree VARCHAR(32) COMMENT 学历要求, skills VARCHAR(512) COMMENT 技能标签逗号分隔, crawl_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 采集时间, UNIQUE KEY uk_job_id (job_id), INDEX idx_city (city), INDEX idx_job_name (job_name), INDEX idx_crawl_time (crawl_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;job_id上建唯一索引是为了去重重复采集时用INSERT IGNORE或ON DUPLICATE KEY UPDATE避免重复插入。city和job_name上建普通索引是为了加速按城市和岗位名称的查询。crawl_time索引用于按时间范围筛选。6.3 批量插入的性能对比实测逐条INSERT在数据量大时性能很差每条都要走一次网络往返和事务提交。批量插入能显著提升吞吐量。我实测过三种方式的性能差异插入方式1万条耗时说明逐条INSERT约45秒每条一个事务批量INSERT500条一批约3秒需设置rewriteBatchedStatementstrueLOAD DATA INFILE约1秒需要文件权限适合超大批量批量插入的关键是在JDBC URL里加上rewriteBatchedStatementstrue否则MySQL驱动不会真正合并SQL语句。代码层面用addBatch()和executeBatch()String sql INSERT INTO job_info (job_id, job_name, company_name, salary_min, salary_max, city, experience, degree, skills) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?); try (PreparedStatement pstmt conn.prepareStatement(sql)) { conn.setAutoCommit(false); int count 0; for (JobInfo job : jobList) { pstmt.setString(1, job.getJobId()); pstmt.setString(2, job.getJobName()); // ... 设置其他参数 pstmt.addBatch(); if (count % 500 0) { pstmt.executeBatch(); conn.commit(); } } pstmt.executeBatch(); conn.commit(); }注意批量插入时如果某条数据违反唯一约束整个批次可能失败。用INSERT IGNORE可以跳过重复数据但会忽略其他错误。更稳妥的做法是先用SELECT查出已存在的job_id过滤后再批量插入。7. 并发采集的线程模型与任务调度设计7.1 用线程池管理采集任务单线程采集太慢假设每个请求耗时500毫秒采集1万条数据需要5000秒将近一个半小时。用10个线程并发理论上能压缩到9分钟左右。但线程不是越多越好太多线程会导致请求频率过高触发风控也会增加上下文切换开销。我的经验是线程数控制在5到10之间配合RateLimiter做全局限流。线程池用ThreadPoolExecutor手动创建不要用Executors.newFixedThreadPool()因为后者用的是无界队列任务积压时可能OOM。ThreadPoolExecutor executor new ThreadPoolExecutor( 5, // 核心线程数 10, // 最大线程数 60L, TimeUnit.SECONDS, // 空闲线程存活时间 new LinkedBlockingQueue(1000), // 有界队列 new ThreadFactoryBuilder().setNameFormat(crawler-%d).build(), new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略 );CallerRunsPolicy表示队列满了之后由提交任务的线程自己执行这样能起到背压作用防止任务无限堆积。7.2 分页任务的拆分与结果聚合采集任务可以按页码拆分每个线程负责若干页。比如总共100页10个线程每个线程负责10页。但要注意页码的分配要考虑请求间隔不能让所有线程同时请求相邻页那样请求特征太明显。更好的方式是用一个共享的AtomicInteger作为页码计数器每个线程从计数器里取下一页的页码取完就加一。这样线程之间自然错开不会重复采集同一页。AtomicInteger pageCounter new AtomicInteger(1); int totalPages 100; Runnable task () - { while (true) { int page pageCounter.getAndIncrement(); if (page totalPages) { break; } try { ListJobInfo jobs fetchPage(page); saveJobs(jobs); } catch (Exception e) { log.error(采集第{}页失败, page, e); } } };结果聚合方面每个线程采集到的数据先存到线程本地的List里最后统一汇总。或者直接用线程安全的ConcurrentLinkedQueue收集避免锁竞争。7.3 采集进度的监控与断点续传长时间运行的采集任务需要进度监控否则你不知道跑到哪了、还剩多少、有没有卡住。简单的做法是每完成一页就打印日志记录当前页码和已采集数量。更完善的做法是用AtomicLong统计成功和失败的数量定期输出统计信息。断点续传是另一个实用功能。程序意外中断后重新启动时能从上次中断的页码继续而不是从头再来。实现方式是把已完成的页码记录到文件或Redis里启动时读取并跳过这些页码。public class ProgressTracker { private SetInteger completedPages new HashSet(); private String progressFile progress.txt; public void load() { // 从文件加载已完成的页码 try (BufferedReader reader new BufferedReader(new FileReader(progressFile))) { String line; while ((line reader.readLine()) ! null) { completedPages.add(Integer.parseInt(line.trim())); } } catch (IOException e) { // 文件不存在则忽略 } } public void markCompleted(int page) { completedPages.add(page); // 追加写入文件 try (BufferedWriter writer new BufferedWriter(new FileWriter(progressFile, true))) { writer.write(String.valueOf(page)); writer.newLine(); } catch (IOException e) { log.error(记录进度失败, e); } } public boolean isCompleted(int page) { return completedPages.contains(page); } }8. 采集过程中的几个实用技巧与避坑经验8.1 请求间隔的随机化处理固定间隔的请求模式很容易被识别。比如你每隔1秒发一个请求连续发100个这个模式太规律了风控系统一眼就能看出来。解决办法是在基础间隔上叠加随机抖动public void randomDelay() { // 基础间隔1000ms随机浮动500ms long delay 1000 (long)(Math.random() * 500); try { Thread.sleep(delay); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }这样请求间隔在1000到1500毫秒之间随机分布看起来更像人工操作。如果配合RateLimiter使用可以把RateLimiter的速率设得稍高一些然后用随机延迟做二次控制。8.2 响应内容的编码问题中文网站经常遇到编码问题返回的JSON里中文变成乱码。原因是服务器返回的Content-Type里声明的编码和实际编码不一致或者HttpClient没有正确识别。解决办法是手动指定编码String responseBody EntityUtils.toString(entity, StandardCharsets.UTF_8);如果UTF-8还是乱码试试GBK。有些老网站用的是GB2312或GBK编码。可以在响应头里看Content-Type字段的charset值按那个来设置。8.3 日志记录的分级与脱敏采集程序的日志要分级记录INFO级别记录每页的采集结果WARN级别记录重试和异常ERROR级别记录失败和需要人工介入的问题。日志里不要打印完整的Cookie和请求头这些包含敏感信息泄露出去有安全风险。log.info(第{}页采集完成获取{}条数据, page, jobs.size()); log.warn(第{}页请求失败准备第{}次重试, page, retryCount); log.error(第{}页采集失败已重试{}次, page, maxRetry);8.4 数据去重的时机与策略去重可以在三个环节做采集时去重、入库时去重、分析时去重。采集时去重最省资源但需要维护一个已采集ID的集合内存占用随数据量增长。入库时去重用数据库唯一索引简单可靠但会增加数据库压力。分析时去重最灵活但每次分析都要处理重复数据。我的建议是入库时去重为主采集时去重为辅。采集时用一个布隆过滤器或Redis Set记录已采集的job_id快速判断是否重复入库时用唯一索引兜底确保最终数据不重复。// 用Redis Set做采集时去重 public boolean isDuplicate(String jobId) { Boolean added redisTemplate.opsForSet().add(crawled_job_ids, jobId); return added null || !added; }8.5 异常情况的兜底处理采集过程中会遇到各种异常网络超时、JSON解析失败、字段缺失、数据库连接断开。每个环节都要有兜底处理不能让一个异常导致整个程序崩溃。网络异常用重试机制处理重试多次仍失败则记录到失败队列后续单独处理。JSON解析失败通常是接口结构变了这时候要记录原始响应内容方便排查。字段缺失用默认值填充不要抛异常。数据库连接断开用连接池自动重连或者捕获异常后重新获取连接。try { ListJobInfo jobs parseResponse(responseBody); saveToDatabase(jobs); } catch (JsonParseException e) { log.error(JSON解析失败原始响应{}, responseBody, e); saveFailedRecord(responseBody); } catch (SQLException e) { log.error(数据库写入失败, e); saveToFile(jobs); // 降级到文件存储 }这套采集方案我在几个内部项目里跑过单机每天稳定采集几万条数据没有问题。核心不在于代码写得多复杂而在于对目标网站的理解够不够深、对异常情况的考虑够不够全。先把请求链路摸清楚再把反爬策略研究透最后把工程细节做扎实剩下的就是耐心跑数据了。

相关新闻

开源CLI代码审查工具:基于Git Diff与本地LLM的自动化Code Review方案

开源CLI代码审查工具:基于Git Diff与本地LLM的自动化Code Review方案

1. 项目概述:一个真正能落地的开源代码审查 CLI 工具“open-code-review”这个名字乍一听像某个 GitHub 上刚起步的玩具项目,但如果你最近在团队里被 PR 堆得喘不过气、被“已阅”式评论糊弄过、或者自己写完代码不敢合入怕出问题——那它就不是名字&…

2026/10/19 4:26:44 阅读更多 →
开源代码审查工作流:CLI+Git+LLM 安全协同实践

开源代码审查工作流:CLI+Git+LLM 安全协同实践

1. 项目概述:这不是一个“工具”,而是一套可落地的开源代码审查工作流“open-code-review”这四个字乍看像某个 GitHub 仓库名,或是某款新出的 CLI 工具代号,但真正把它拆开来看——open(开放)、code&#…

2026/10/19 4:26:44 阅读更多 →
API 风险监测能力排行:毫秒定胜负,金融场景高可用是安全底线

API 风险监测能力排行:毫秒定胜负,金融场景高可用是安全底线

一、市场背景[提示句]:金融交易链路对时延极度敏感,API 安全组件的故障会直接传导至核心交易业务。证券、银行的交易业务高度依托 API 接口完成数据交互,一次超过 500ms 的 API 响应延迟,就有可能触发交易系统雪崩,造成…

2026/10/19 4:25:43 阅读更多 →

最新新闻

Diagram as Code:用可执行图表构建系统逻辑骨架

Diagram as Code:用可执行图表构建系统逻辑骨架

1. 项目概述:这不是画图,是构建可执行的系统逻辑骨架“diagram-design”这个标题乍看像一个设计类工具操作,但在我过去十年带过的几十个跨领域项目里,它从来不是美术课——而是工程师、产品负责人、运维人员和业务方坐在一起&…

2026/10/19 5:53:33 阅读更多 →
diagram-design:用结构化图形重建信息表达逻辑

diagram-design:用结构化图形重建信息表达逻辑

1. 项目概述:这不是画图,是用结构化思维重建信息表达的底层逻辑“diagram-design”这个词组乍看像一个工具名或功能按钮,但在我带过的十几个跨领域协作项目里,它从来不是指“用Visio拖几个矩形框”,而是指一套完整的信…

2026/10/19 5:53:33 阅读更多 →
从布尔到时间盲注:SQL注入无回显场景的完整实战指南

从布尔到时间盲注:SQL注入无回显场景的完整实战指南

如果你在靶场里手动测过SQL注入,一定会经历一个非常分裂的阶段:刚开始觉得注入特别简单,参数后面丢一个单引号,看报错、看回显,再union select一撞,数据就哗啦啦全出来了,像开卷考试。但很快你就…

2026/10/19 5:53:33 阅读更多 →
diagram-design:从草图到可维护系统的图表设计实战指南

diagram-design:从草图到可维护系统的图表设计实战指南

1. 从一张草图到一套系统:diagram-design 到底在解决什么问题第一次看到 diagram-design 这个词,很多人会以为它只是一个画图工具的别名,或者某个设计模板库的代号。但真正在工程团队里待过几年的人会明白,它指向的是一套远比“画…

2026/10/19 5:53:33 阅读更多 →
DCMTK 3.6.5 Windows预编译包:开箱即用的DICOM工程解决方案

DCMTK 3.6.5 Windows预编译包:开箱即用的DICOM工程解决方案

简介:本资源是面向医学影像开发人员、生物医学工程研究人员及DICOM系统集成工程师的DCMTK 3.6.5 Windows 64位预编译工具包,专为免编译快速上手DICOM数据处理而优化,解决Windows环境下源码构建复杂、依赖配置繁琐等实际痛点。压缩包共238个文…

2026/10/19 5:53:33 阅读更多 →
基于最佳簇半径的无线传感器网络分簇路由算法详解

基于最佳簇半径的无线传感器网络分簇路由算法详解

做无线传感器网络这块也有几年了,前前后后调过不少路由协议,从最开始的洪泛、LEACH,到后来各种改进的PEGASIS、TEEN、EEUC都摸过一遍。说实话,分簇路由这个方向看着简单,就是把节点分成一组一组的,选个簇头…

2026/10/19 5:52:33 阅读更多 →

日新闻

频率跟踪型PWM电源设计:臭氧发生器谐振点漂移与软开关实现

频率跟踪型PWM电源设计:臭氧发生器谐振点漂移与软开关实现

简介:本资源为电源技术方向的学术研究文档,聚焦介质阻挡放电型臭氧发生器电源的频率跟踪型PWM控制策略,面向电力电子、高压电源及臭氧应用领域的科研人员与工程技术人员。针对负载呈容性、等效电容随外加电压升高而增大、谐振频率随之漂移的难…

2026/10/19 0:00:47 阅读更多 →
Vibe Coding 遇上 TaoToken:把开发者思维圣殿的灵感流接入统一 Key 通道

Vibe Coding 遇上 TaoToken:把开发者思维圣殿的灵感流接入统一 Key 通道

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/19 0:00:47 阅读更多 →
HundredCV-Chat话题建模实战教程:利用4,871个内置话题标签构建主题分析系统

HundredCV-Chat话题建模实战教程:利用4,871个内置话题标签构建主题分析系统

【免费下载链接】HundredCV-Chat 项目地址: https://ai.gitcode.com/hf_mirrors/Jax-dan/HundredCV-Chat 点击查看 免费下载 HundredCV-Chat 是一个由 100 位青年简历数据开发而来的中文多轮对话数据集,共包含 24,750 组日常闲聊对话和 4,871 个话题标签…

2026/10/19 0:00:47 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/18 0:00:55 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/18 0:00:55 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/19 1:30:52 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/19 2:54:50 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/17 17:22:31 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 12:16:16 阅读更多 →