今日已结

This commit is contained in:
xuelijun
2026-01-21 16:28:53 +08:00
parent 7bb698c9c0
commit 6f1c990d51
7 changed files with 481 additions and 0 deletions

12
pom.xml
View File

@@ -78,6 +78,18 @@
<artifactId>tess4j</artifactId>
<version>5.8.0</version>
</dependency>-->
<!-- pom.xml -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- 使用Caffeine作为缓存实现 -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.1.8</version>
</dependency>
</dependencies>
<build>
<plugins>

View File

@@ -0,0 +1,42 @@
package com.tem.bocai.config;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import java.util.concurrent.TimeUnit;
@Configuration
@EnableCaching // 启用缓存支持
public class CacheConfig {
/**
* 主缓存管理器 - 用于token缓存19分钟过期
*/
@Bean
@Primary // 标记为主缓存管理器
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(Caffeine.newBuilder()
.expireAfterWrite(19, TimeUnit.MINUTES) // 19分钟过期
.maximumSize(100) // 最大缓存数量
.recordStats() // 记录统计信息
);
return cacheManager;
}
/**
* 备用缓存管理器 - 如果没有配置Caffeine使用ConcurrentMap
*/
@Bean
@ConditionalOnMissingBean(CacheManager.class)
public CacheManager fallbackCacheManager() {
return new org.springframework.cache.concurrent.ConcurrentMapCacheManager();
}
}

View File

@@ -24,6 +24,12 @@ public class LoginCrawler {
return ResponseEntity.ok(result);
}
@GetMapping("/ocr/completedToday")
public ResponseEntity<String> completedToday() throws IOException, TesseractException {
String result = loginService.completedToday();
return ResponseEntity.ok(result);
}
}

View File

@@ -7,4 +7,8 @@ public interface LoginService {
//获取token
String getToken(String username, String password, String loginUrl);
//获取token
String completedToday();
}

View File

@@ -1,6 +1,7 @@
package com.tem.bocai.service.impl;
import com.tem.bocai.service.LoginService;
import com.tem.bocai.util.CompletedTodayCrawler;
import com.tem.bocai.util.LotteryDataPipeline;
import com.tem.bocai.util.LotteryWebMagicCrawler;
import com.tem.bocai.util.TokenCacheService;
@@ -107,6 +108,25 @@ public class LoginServiceImpl implements LoginService {
return "";
}
@Override
public String completedToday() {
String token = tokenCacheService.getToken();
System.out.println("得到token = " + token);
if (token != null && !token.isEmpty()) {
// 2. 创建爬虫实例传入token
CompletedTodayCrawler crawler = new CompletedTodayCrawler(token);
// 4. 执行爬虫
String url = "https://4701268539-esh.qdk63ayw8g.com/member/bets?settled=true";
Spider.create(crawler)
.addUrl(url)
.thread(1)
.run();
}
return "";
}
/**
* 单次登录尝试
*/

View File

@@ -0,0 +1,396 @@
package com.tem.bocai.util;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import us.codecraft.webmagic.Page;
import us.codecraft.webmagic.Site;
import us.codecraft.webmagic.Spider;
import us.codecraft.webmagic.processor.PageProcessor;
import us.codecraft.webmagic.selector.Html;
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class CompletedTodayCrawler implements PageProcessor {
private final String token;
private Site site;
private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public CompletedTodayCrawler(String token) {
this.token = token;
initSite();
}
private void initSite() {
site = Site.me()
.setRetryTimes(3)
.setSleepTime(2000) // 增加等待时间
.setTimeOut(15000) // 增加超时时间
.setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36")
.addHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8")
.addHeader("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8")
.addHeader("Accept-Encoding", "gzip, deflate, br")
.addHeader("Connection", "keep-alive")
.addHeader("Upgrade-Insecure-Requests", "1")
.addHeader("Sec-Fetch-Dest", "document")
.addHeader("Sec-Fetch-Mode", "navigate")
.addHeader("Sec-Fetch-Site", "same-origin")
.addHeader("Sec-Fetch-User", "?1");
// 设置cookie
if (token != null && !token.isEmpty()) {
site.addHeader("cookie", "token=" + token);
}
}
@Override
public void process(Page page) {
String url = page.getUrl().toString();
System.out.println("处理页面: " + url);
Html html = page.getHtml();
String content = html.toString();
// 打印一些基本信息
System.out.println("页面标题: " + html.xpath("//title/text()").get());
System.out.println("页面大小: " + content.length() + " 字符");
// 检查是否有"暂无数据"提示
if (content.contains("暂无数据")) {
System.out.println("警告: 页面显示'暂无数据'");
return;
}
// 解析注单数据
List<Map<String, Object>> betList = parseBetHtml(content);
if (betList.isEmpty()) {
System.out.println("未解析到注单数据");
// 尝试从其他可能的位置解析
extractDebugInfo(html);
} else {
System.out.println("解析到 " + betList.size() + " 条注单数据");
// 打印部分数据示例
printSampleData(betList);
}
// 保存原始HTML用于调试
saveHtmlForDebug(content, url);
}
/**
* 解析注单HTML数据
*/
private List<Map<String, Object>> parseBetHtml(String htmlContent) {
List<Map<String, Object>> betList = new ArrayList<>();
try {
Document doc = Jsoup.parse(htmlContent);
// 查找注单表格
Element table = doc.selectFirst("table.list");
if (table == null) {
System.out.println("未找到注单表格");
return betList;
}
// 查找表头
Elements headers = table.select("thead th");
List<String> headerList = new ArrayList<>();
for (Element header : headers) {
headerList.add(header.text().trim());
}
System.out.println("表头信息: " + headerList);
// 查找数据行(跳过表头)
Elements rows = table.select("tbody tr");
for (Element row : rows) {
// 跳过"暂无数据"的行
if (row.select("td.nodata").size() > 0) {
continue;
}
Map<String, Object> betData = new HashMap<>();
Elements cells = row.select("td");
// 按列解析数据
for (int i = 0; i < cells.size() && i < headerList.size(); i++) {
String header = headerList.get(i);
String value = cells.get(i).text().trim();
// 根据表头映射到对应的字段名
switch (header) {
case "注单号":
betData.put("bet_id", value);
break;
case "时间":
betData.put("time", value);
break;
case "类型":
betData.put("type", value);
break;
case "玩法":
betData.put("game_type", value);
break;
case "":
betData.put("plate", value);
break;
case "下注金额":
betData.put("bet_amount", parseAmount(value));
break;
case "退水(%)":
betData.put("rebate_rate", parseRate(value));
break;
case "结果":
betData.put("result", parseResult(value));
betData.put("result_amount", parseResultAmount(value));
break;
default:
betData.put(header, value);
}
}
// 添加额外信息
if (!betData.isEmpty()) {
betData.put("parse_time", dateFormat.format(new Date()));
betData.put("source", "completed_today");
// 提取期数信息(从玩法中提取)
extractPeriodInfo(betData);
betList.add(betData);
}
}
} catch (Exception e) {
System.err.println("解析HTML时出错: " + e.getMessage());
e.printStackTrace();
}
return betList;
}
/**
* 从玩法中提取期数信息
*/
private void extractPeriodInfo(Map<String, Object> betData) {
try {
Object gameTypeObj = betData.get("game_type");
if (gameTypeObj instanceof String) {
String gameType = (String) gameTypeObj;
// 尝试匹配期数模式,如"2024001", "001", "期号2024001"等
Pattern pattern = Pattern.compile("(\\d{7})|期[号码]?(\\d{3,7})|(\\d{3,4})期");
Matcher matcher = pattern.matcher(gameType);
if (matcher.find()) {
for (int i = 1; i <= matcher.groupCount(); i++) {
if (matcher.group(i) != null) {
betData.put("period", matcher.group(i));
break;
}
}
}
}
} catch (Exception e) {
// 忽略提取错误
}
}
/**
* 解析金额(去除货币符号)
*/
private Double parseAmount(String amountStr) {
try {
if (amountStr == null || amountStr.isEmpty()) {
return 0.0;
}
// 移除非数字字符(保留小数点和负号)
String cleaned = amountStr.replaceAll("[^\\d.-]", "");
return cleaned.isEmpty() ? 0.0 : Double.parseDouble(cleaned);
} catch (Exception e) {
return 0.0;
}
}
/**
* 解析退水率
*/
private Double parseRate(String rateStr) {
try {
if (rateStr == null || rateStr.isEmpty()) {
return 0.0;
}
// 移除百分号
String cleaned = rateStr.replace("%", "").trim();
return cleaned.isEmpty() ? 0.0 : Double.parseDouble(cleaned) / 100;
} catch (Exception e) {
return 0.0;
}
}
/**
* 解析结果状态
*/
private String parseResult(String resultStr) {
if (resultStr == null) {
return "未知";
}
if (resultStr.contains("") || resultStr.contains("+")) {
return "";
} else if (resultStr.contains("") || resultStr.contains("-")) {
return "";
} else if (resultStr.contains("") || resultStr.contains("0")) {
return "";
} else if (resultStr.contains("取消")) {
return "取消";
} else {
return "未知";
}
}
/**
* 解析结果金额
*/
private Double parseResultAmount(String resultStr) {
try {
// 提取数字部分(包含负号)
Pattern pattern = Pattern.compile("[-+]?\\d+\\.?\\d*");
Matcher matcher = pattern.matcher(resultStr);
if (matcher.find()) {
return Double.parseDouble(matcher.group());
}
return 0.0;
} catch (Exception e) {
return 0.0;
}
}
/**
* 转换数据结构以适应数据库
*/
private List<Map<String, Object>> convertForDatabase(List<Map<String, Object>> betList) {
List<Map<String, Object>> dbData = new ArrayList<>();
for (Map<String, Object> bet : betList) {
Map<String, Object> dbRecord = new HashMap<>();
dbRecord.put("id", bet.get("bet_id"));
dbRecord.put("bet_id", bet.get("bet_id"));
dbRecord.put("period", bet.get("period"));
dbRecord.put("bet_time", bet.get("time"));
dbRecord.put("game_type", bet.get("game_type"));
dbRecord.put("plate", bet.get("plate"));
dbRecord.put("bet_amount", bet.get("bet_amount"));
dbRecord.put("rebate_rate", bet.get("rebate_rate"));
dbRecord.put("result", bet.get("result"));
dbRecord.put("result_amount", bet.get("result_amount"));
dbRecord.put("parse_time", bet.get("parse_time"));
dbRecord.put("source", bet.get("source"));
dbData.add(dbRecord);
}
return dbData;
}
/**
* 保存HTML用于调试
*/
private void saveHtmlForDebug(String content, String url) {
try {
String safeUrl = url.replaceAll("[^a-zA-Z0-9]", "_");
String fileName = "debug_" + safeUrl + "_" + System.currentTimeMillis() + ".html";
String filePath = "output/debug/" + fileName;
File directory = new File("output/debug");
if (!directory.exists()) {
directory.mkdirs();
}
File outputFile = new File(filePath);
java.nio.file.Files.write(outputFile.toPath(), content.getBytes());
System.out.println("调试HTML已保存: " + outputFile.getAbsolutePath());
} catch (Exception e) {
System.err.println("保存调试HTML失败: " + e.getMessage());
}
}
/**
* 提取调试信息
*/
private void extractDebugInfo(Html html) {
System.out.println("\n=== 调试信息 ===");
// 检查所有表格
List<String> tables = html.xpath("//table/@class").all();
System.out.println("所有表格class: " + tables);
// 检查所有tr
int trCount = html.xpath("//tr").all().size();
System.out.println("TR数量: " + trCount);
// 检查所有td
int tdCount = html.xpath("//td").all().size();
System.out.println("TD数量: " + tdCount);
// 检查cookie相关元素
String cookieScript = html.xpath("//script[contains(text(), 'token')]/text()").get();
if (cookieScript != null && cookieScript.contains("token")) {
System.out.println("发现token相关脚本");
}
// 检查是否有JavaScript重定向
String redirectScript = html.xpath("//script[contains(text(), 'location.href') or contains(text(), 'window.location')]/text()").get();
if (redirectScript != null) {
System.out.println("发现重定向脚本: " + redirectScript.substring(0, Math.min(100, redirectScript.length())));
}
}
/**
* 打印示例数据
*/
private void printSampleData(List<Map<String, Object>> betList) {
System.out.println("\n=== 前3条数据示例 ===");
int count = Math.min(3, betList.size());
for (int i = 0; i < count; i++) {
Map<String, Object> bet = betList.get(i);
System.out.printf("注单%d: ID=%s, 时间=%s, 金额=%.2f, 结果=%s, 金额=%.2f%n",
i + 1,
bet.get("bet_id"),
bet.get("time"),
bet.get("bet_amount"),
bet.get("result"),
bet.get("result_amount"));
}
}
@Override
public Site getSite() {
return site;
}
public static void main(String[] args) {
String url = "https://4701268539-esh.qdk63ayw8g.com/member/bets?settled=true";
// 创建爬虫
Spider.create(new CompletedTodayCrawler(""))
.addUrl(url) // 添加起始URL
.thread(1) // 线程数
.run(); // 开始爬取
}
}

View File

@@ -23,6 +23,7 @@ import java.io.File;
import java.io.IOException;
import java.util.*;
//开奖的历史结果
public class LotteryWebMagicCrawler implements PageProcessor {
private final String token;