Files
lottery/server/services/ocr.js
T
DeveloperandClaude Opus 4.6 7b1073e23b Initial commit: lottery project structure
Add core project files including:
- Client and server directories
- Backup script
- Package configuration
- Git ignore rules

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 11:26:49 +08:00

208 lines
6.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const Tesseract = require('tesseract.js');
const path = require('path');
// 彩票类型识别规则
const LOTTERY_PATTERNS = {
// 福利彩票
ssq: { name: '双色球', type: 'welfare', pattern: /双\s*色\s*球|SSQ/i, price: 2 },
fc3d: { name: '福彩3D', type: 'welfare', pattern: /福彩\s*3D|3D/i, price: 2 },
qlc: { name: '七乐彩', type: 'welfare', pattern: /七\s*乐\s*彩/i, price: 2 },
// 体育彩票
dlt: { name: '大乐透', type: 'sports', pattern: /大\s*乐\s*透|超级\s*大\s*乐\s*透/i, price: 2 },
pl3: { name: '排列3', type: 'sports', pattern: /排列\s*3|排列\s*三/i, price: 2 },
pl5: { name: '排列5', type: 'sports', pattern: /排列\s*5|排列\s*五/i, price: 2 },
qxc: { name: '七星彩', type: 'sports', pattern: /七\s*星\s*彩/i, price: 2 },
};
// 使用本地tesseract.js-core进行OCR识别
async function parseWithLocalTesseract(imagePath) {
try {
// 使用本地core文件
const corePath = path.join(__dirname, '../node_modules/tesseract.js-core');
const { data } = await Tesseract.recognize(imagePath, 'chi_sim+eng', {
corePath: corePath,
logger: m => console.log('OCR:', m.status, m.progress)
});
return data.text;
} catch (err) {
console.error('tesseract.js识别失败:', err.message);
throw err;
}
}
// 解析彩票图片
async function parseTicketImage(imagePath) {
let text = '';
try {
text = await parseWithLocalTesseract(imagePath);
console.log('OCR识别完成, 文本长度:', text.length);
} catch (err) {
console.error('OCR识别失败:', err);
throw new Error('图片识别失败');
}
// 调试:打印识别的文本
console.log('识别的文本:', text.substring(0, 500));
return parseTicketText(text);
}
// 解析彩票文本
function parseTicketText(text) {
const result = {
type: null,
game: null,
issue: null,
numbers: [],
bet_count: 1,
multiple: 1,
is_additional: false,
cost: 0,
raw_text: text
};
// 识别彩票类型
for (const [key, lottery] of Object.entries(LOTTERY_PATTERNS)) {
if (lottery.pattern.test(text)) {
result.type = lottery.type;
result.game = lottery.name;
break;
}
}
// 提取期号 - 优先匹配"期号:xxx"格式
const issueMatch = text.match(/期\s*号\s*[:]\s*(\d{5,})/);
if (issueMatch) {
result.issue = issueMatch[1];
} else {
const issueMatch2 = text.match(/第?\s*(\d{5,})\s*期/);
if (issueMatch2) {
result.issue = issueMatch2[1];
}
}
// 提取注数
const betMatch = text.match(/(\d+)\s*注/);
if (betMatch) {
result.bet_count = parseInt(betMatch[1]);
}
// 提取倍数
const multipleMatch = text.match(/(\d+)\s*倍/);
if (multipleMatch) {
result.multiple = parseInt(multipleMatch[1]);
}
// 检测是否追加
result.is_additional = /追加|追加投注/.test(text);
// 提取金额 - 支持多种格式
const costMatch = text.match(/金额\s*[:]\s*(\d+(?:[.,]\d+)?)\s*元|合计[:]\s*(\d+(?:\.\d+)?)\s*元|(\d+(?:\.\d+)?)\s*元/);
if (costMatch) {
const costStr = (costMatch[1] || costMatch[2] || costMatch[3]).replace(',', '.');
result.cost = parseFloat(costStr);
}
// 提取号码 - 根据不同彩票类型
result.numbers = extractNumbers(text, result.game);
// 根据识别到的号码数量更新注数
if (result.numbers.length > 0) {
result.bet_count = result.numbers.length;
}
// 如果没有识别到金额,根据注数和倍数计算
if (!result.cost && result.game && result.numbers.length > 0) {
const lottery = Object.values(LOTTERY_PATTERNS).find(l => l.name === result.game);
if (lottery) {
let basePrice = lottery.price * result.bet_count * result.multiple;
if (result.is_additional) basePrice += result.bet_count * result.multiple;
result.cost = basePrice;
}
}
return result;
}
// 提取号码
function extractNumbers(text, game) {
const numbers = [];
// 预处理文本:修复常见OCR错误
let cleanText = text
.replace(/[oO]/g, '0') // o和O经常被误识别为0
.replace(/[D]/g, '0') // D经常被误识别
.replace(/[lI|]/g, '1') // l和I和|经常被误识别为1
.replace(/[S\$§]/g, '5') // S和$和§经常被误识别为5
.replace(/[@®»«]/g, '') // 去掉这些特殊字符
.replace(/[:]/g, ' ') // 冒号替换为空格
.replace(/[g]/g, '9'); // g经常被误识别为9
// 处理连在一起的数字:将6位以上连续数字按每2位分割
cleanText = cleanText.replace(/\b(\d{6,})\b/g, (match) => {
return match.match(/.{1,2}/g).join(' ');
});
// 处理4位连续数字
cleanText = cleanText.replace(/\b(\d{4})\b/g, (match) => {
return match.slice(0, 2) + ' ' + match.slice(2);
});
// 移除开奖号码行(包含"开奖号码"的行)
cleanText = cleanText.split('\n')
.filter(line => !line.includes('开奖') && !line.includes('号码'))
.join('\n');
if (game === '双色球') {
// 双色球: 6红+1蓝,支持多种格式
const patterns = [
/(\d{2})\s+(\d{2})\s+(\d{2})\s+(\d{2})\s+(\d{2})\s+(\d{2})[-+](\d{2})/g,
/(\d{2})\s+(\d{2})\s+(\d{2})\s+(\d{2})\s+(\d{2})\s+(\d{2})\s*[+|]\s*(\d{2})/g,
];
for (const pattern of patterns) {
const matches = cleanText.matchAll(pattern);
for (const match of matches) {
numbers.push({
red: [match[1], match[2], match[3], match[4], match[5], match[6]],
blue: match[7]
});
}
if (numbers.length > 0) break;
}
} else if (game === '大乐透') {
// 大乐透: 5前区+2后区
const patterns = [
// 带空格的标准格式
/(\d{2})\s+(\d{2})\s+(\d{2})\s+(\d{2})\s+(\d{2})[-+](\d{2})\s+(\d{2})/g,
/(\d{2})\s+(\d{2})\s+(\d{2})\s+(\d{2})\s+(\d{2})\s*[+|]\s*(\d{2})\s+(\d{2})/g,
// 连在一起无空格的格式(第一注可能这样)
/(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})[-+](\d{2})(\d{2})/g,
/(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})[+|](\d{2})(\d{2})/g,
];
for (const pattern of patterns) {
const matches = cleanText.matchAll(pattern);
for (const match of matches) {
numbers.push({
front: [match[1], match[2], match[3], match[4], match[5]],
back: [match[6], match[7]]
});
}
if (numbers.length > 0) break;
}
} else if (game === '福彩3D' || game === '排列3') {
// 3位数
const matches = cleanText.matchAll(/[^\d](\d)\s*(\d)\s*(\d)[^\d]/g);
for (const match of matches) {
numbers.push([match[1], match[2], match[3]]);
}
}
return numbers;
}
module.exports = { parseTicketImage, parseTicketText };