Codetest

PHP Detected Guest 1 Views Size: 30.10 KB Posted on: Jul 13, 26 @ 2:54 AM
  1. 1scraper.php:
  2. 2
  3. 3<?php
  4. 4require_once 'utils.php';
  5. 5require_once __DIR__ . '/app/db.php';
  6. 6
  7. 7error_reporting(E_ALL);
  8. 8ini_set('display_errors', '1');
  9. 9
  10. 10function e($v){
  11. 11 return htmlspecialchars((string)$v, ENT_QUOTES, 'UTF-8');
  12. 12}
  13. 13
  14. 14/* ✅ LOG */
  15. 15function logAction($accion,$detalle){
  16. 16 try{
  17. 17 gm_db()->prepare("INSERT INTO scraping_logs (accion,detalle) VALUES(?,?)")
  18. 18 ->execute([$accion,$detalle]);
  19. 19 }catch(Exception $e){}
  20. 20}
  21. 21
  22. 22/* ✅ PARSER */
  23. 23function parseHistorico($html){
  24. 24
  25. 25 libxml_use_internal_errors(true);
  26. 26
  27. 27 $data = [
  28. 28 'evento'=>'',
  29. 29 'pista'=>'',
  30. 30 'distancia'=>'',
  31. 31 'hora'=>'',
  32. 32 'carrera'=>'',
  33. 33 'carrera_numero'=> null, // ✅ AÑADE ESTO
  34. 34 'estado'=>'Finalizada',
  35. 35 'runners'=>[]
  36. 36 ];
  37. 37
  38. 38 // ✅ PISTA
  39. 39 if(preg_match('/\b(Manawatu|Doncaster|Hove|Sunderland|Yarmouth)\b/i',$html,$m)){
  40. 40 $data['pista'] = $m[1];
  41. 41 }
  42. 42
  43. 43 // ✅ EVENTO
  44. 44 if(preg_match('/\n([A-Za-z\s]+Heat)/',$html,$m)){
  45. 45 $data['evento'] = trim($m[1]);
  46. 46 }
  47. 47
  48. 48 // ✅ DISTANCIA
  49. 49 if(preg_match('/(\d{2,4})m/',$html,$m)){
  50. 50 $data['distancia'] = $m[1];
  51. 51 }
  52. 52
  53. 53 // ✅ HORA
  54. 54 if(preg_match('/Empezada\s*(\d{2}:\d{2}:\d{2})/',$html,$m)){
  55. 55 $data['hora'] = $m[1];
  56. 56 }
  57. 57
  58. 58 // ✅ ESTADO
  59. 59 if(stripos($html,'Finalizada') !== false){
  60. 60 $data['estado'] = 'Finalizada';
  61. 61 }
  62. 62
  63. 63 // ✅ CARRERA
  64. 64 if(preg_match('/Carrera\s*(\d+)/',$html,$m)){
  65. 65 $data['carrera_numero'] = (int)$m[1];
  66. 66 }
  67. 67
  68. 68 $data['runners'] = [];
  69. 69
  70. 70 if(preg_match_all('/(\d+)º\s+(\d+)\s+([A-Za-z\s]+)/',$html,$matches,PREG_SET_ORDER)){
  71. 71
  72. 72 foreach($matches as $m){
  73. 73
  74. 74 $posicion = (int)$m[1];
  75. 75 $trap = (int)$m[2];
  76. 76 $nombre = trim($m[3]);
  77. 77
  78. 78 if($posicion < 1 || $posicion > 8) continue;
  79. 79
  80. 80 $data['runners'][] = [
  81. 81 'nombre' => $nombre,
  82. 82 'posicion' => $posicion,
  83. 83 'trap' => $trap
  84. 84 ];
  85. 85 }
  86. 86 }
  87. 87
  88. 88 // ✅ PARSER GLOBAL (robusto para Bet365 moderno)
  89. 89
  90. 90 // 🏟 pista (ej: Doncaster)
  91. 91 if(empty($data['pista']) && preg_match('/\b(Doncaster|Manukau|Hove|Sunderland|Yarmouth)\b/i',$html,$m)){
  92. 92 $data['pista'] = $m[1];
  93. 93 }
  94. 94
  95. 95 // 📏 distancia (ej: 275m)
  96. 96 if(empty($data['distancia']) && preg_match('/(\d{2,4})\s*m/i',$html,$m)){
  97. 97 $data['distancia'] = $m[1];
  98. 98 }
  99. 99
  100. 100 // 🕒 hora (ej: 08:26:23)
  101. 101 if(empty($data['hora']) && preg_match('/(\d{2}:\d{2}:\d{2})/',$html,$m)){
  102. 102 $data['hora'] = $m[1];
  103. 103 }
  104. 104
  105. 105 // 📊 estado
  106. 106 if(stripos($html,'finalizada') !== false){
  107. 107 $data['estado'] = 'Finalizada';
  108. 108 }elseif(stripos($html,'empezada') !== false){
  109. 109 $data['estado'] = 'En curso';
  110. 110 }
  111. 111
  112. 112 // 🏁 número de carrera (DE LAS BURBUJAS)
  113. 113 if(empty($data['carrera_numero'])){
  114. 114
  115. 115 if(preg_match('/>\s*(\d{1,2})\s*</',$html,$m)){
  116. 116 $data['carrera_numero'] = (int)$m[1];
  117. 117 }
  118. 118 }
  119. 119
  120. 120 if(empty($data['pista'])){
  121. 121 if(preg_match('/\b([A-Z][a-z]+)\b\s*</',$html,$m)){
  122. 122 $data['pista'] = $m[1];
  123. 123 }
  124. 124 }
  125. 125
  126. 126
  127. 127 // ✅ detectar número desde burbujas
  128. 128 if(empty($data['carrera_numero']) && !empty($html)){
  129. 129 if(preg_match('/>\s*(\d{1,2})\s*</',$html,$m)){
  130. 130 $data['carrera_numero'] = (int)$m[1];
  131. 131 }
  132. 132 }
  133. 133
  134. 134
  135. 135 // ✅ FALLBACK GLOBAL (muy importante)
  136. 136 if(empty($data['carrera_numero'])){
  137. 137
  138. 138 if(preg_match('/\b(\d{1,2})\b/', $html, $m)){
  139. 139 $data['carrera_numero'] = (int)$m[1];
  140. 140 }
  141. 141 }
  142. 142
  143. 143 return $data;
  144. 144}
  145. 145
  146. 146/* ✅ GUARDADO */
  147. 147function saveRaceDB($data){
  148. 148
  149. 149 $pdo = gm_db();
  150. 150 $pdo->beginTransaction();
  151. 151
  152. 152 try{
  153. 153
  154. 154 if(empty($data['runners'])){
  155. 155 throw new Exception("No hay runners");
  156. 156 }
  157. 157
  158. 158 $hash = md5(uniqid().serialize($data));
  159. 159
  160. 160 $stmt = $pdo->prepare("
  161. 161 INSERT INTO carreras
  162. 162 (pista, distancia, evento, hash, tipo, fuente, estado, fecha_carrera, pais, carrera_numero, codigo_unico)
  163. 163 VALUES (?, ?, ?, ?, 'Greyhound','html_manual','finalizada', ?, ?, ?, ?)
  164. 164 ");
  165. 165
  166. 166
  167. 167 // ✅ mapa de pistas
  168. 168 $TRACK_MAP = [
  169. 169 'Manukau' => ['pais'=>'Nueva Zelanda'],
  170. 170 'Healesville' => ['pais'=>'Australia'],
  171. 171 'Capalaba' => ['pais'=>'Australia'],
  172. 172 'Mount Gambier' => ['pais'=>'Australia'],
  173. 173 'Sale' => ['pais'=>'Australia'],
  174. 174 'Rockhampton' => ['pais'=>'Australia'],
  175. 175 'Richmond' => ['pais'=>'Australia'],
  176. 176 'Nowra' => ['pais'=>'Australia'],
  177. 177 'Q2 Parklands' => ['pais'=>'Australia'],
  178. 178 'Gawler' => ['pais'=>'Australia'],
  179. 179 'Temora' => ['pais'=>'Australia'],
  180. 180 'Darwin' => ['pais'=>'Australia'],
  181. 181 'Valley' => ['pais'=>'UK'],
  182. 182 'Doncaster' => ['pais'=>'UK'],
  183. 183 'Yarmouth' => ['pais'=>'UK'],
  184. 184 'Sunderland' => ['pais'=>'UK'],
  185. 185 'Hove' => ['pais'=>'UK'],
  186. 186 ];
  187. 187
  188. 188 // ✅ obtener país
  189. 189 $trackInfo = $TRACK_MAP[$data['pista']] ?? ['pais'=>'Desconocido'];
  190. 190
  191. 191 // ✅ generar código único
  192. 192 $codigo = generarCodigoCarrera([
  193. 193 'pais' => $trackInfo['pais'],
  194. 194 'pista' => $data['pista'],
  195. 195 'carrera_numero' => $data['carrera_numero'] ?? 0,
  196. 196 'evento' => $data['evento'],
  197. 197 'fecha_carrera' => $data['hora']
  198. 198 ? date('Y-m-d').' '.$data['hora']
  199. 199 : date('Y-m-d') // ✅ fallback seguro
  200. 200 ]);
  201. 201
  202. 202 // ✅ asegurar unicidad del código
  203. 203 $stmtCheck = $pdo->prepare("
  204. 204 SELECT COUNT(*) FROM carreras WHERE codigo_unico = ?
  205. 205 ");
  206. 206 $stmtCheck->execute([$codigo]);
  207. 207
  208. 208 if($stmtCheck->fetchColumn() > 0){
  209. 209
  210. 210 // 🔥 generar sufijo incremental limpio
  211. 211 $suffix = 1;
  212. 212
  213. 213 do {
  214. 214 $nuevoCodigo = $codigo . str_pad($suffix, 2, '0', STR_PAD_LEFT);
  215. 215
  216. 216 $stmtCheck = $pdo->prepare("
  217. 217 SELECT COUNT(*) FROM carreras WHERE codigo_unico = ?
  218. 218 ");
  219. 219 $stmtCheck->execute([$nuevoCodigo]);
  220. 220
  221. 221 $existe = $stmtCheck->fetchColumn();
  222. 222 $suffix++;
  223. 223
  224. 224 } while($existe > 0);
  225. 225
  226. 226 $codigo = $nuevoCodigo;
  227. 227 }
  228. 228
  229. 229 // ✅ generar fecha carrera
  230. 230 $fechaCarrera = $data['hora']
  231. 231 ? date('Y-m-d').' '.$data['hora']
  232. 232 : date('Y-m-d H:i:s');
  233. 233
  234. 234 // ✅ ejecutar
  235. 235 $stmt->execute([
  236. 236 $data['pista'], // 1
  237. 237 $data['distancia'], // 2
  238. 238 $data['evento'], // 3
  239. 239 $hash, // 4
  240. 240 $fechaCarrera, // 5
  241. 241 $trackInfo['pais'], // 6
  242. 242 $data['carrera_numero'] ?? null, // 7
  243. 243 $codigo // ✅ NUEVO
  244. 244 ]);
  245. 245
  246. 246
  247. 247 $carreraId = $pdo->lastInsertId();
  248. 248
  249. 249 foreach($data['runners'] as $r){
  250. 250
  251. 251 $nombre = trim($r['nombre']);
  252. 252
  253. 253 $stmt=$pdo->prepare("SELECT id_galgo FROM galgos WHERE nombre=?");
  254. 254 $stmt->execute([$nombre]);
  255. 255 $gid=$stmt->fetchColumn();
  256. 256
  257. 257 if(!$gid){
  258. 258
  259. 259 $slug = strtolower(trim(preg_replace('/[^A-Za-z0-9]+/', '-', $nombre)));
  260. 260 $slug = trim($slug, '-');
  261. 261
  262. 262 $stmt=$pdo->prepare("SELECT COUNT(*) FROM galgos WHERE slug=?");
  263. 263 $stmt->execute([$slug]);
  264. 264
  265. 265 if($stmt->fetchColumn()>0){
  266. 266 $slug .= "-".uniqid();
  267. 267 }
  268. 268
  269. 269 $stmt=$pdo->prepare("INSERT INTO galgos (nombre,slug) VALUES (?,?)");
  270. 270 $stmt->execute([$nombre,$slug]);
  271. 271
  272. 272 $gid=$pdo->lastInsertId();
  273. 273 }
  274. 274
  275. 275 $stmt=$pdo->prepare("
  276. 276 INSERT INTO participaciones (carrera_id,galgo_id,posicion,trap)
  277. 277 VALUES (?,?,?,?)
  278. 278 ");
  279. 279
  280. 280 $stmt->execute([
  281. 281 $carreraId,
  282. 282 $gid,
  283. 283 $r['posicion'],
  284. 284 $r['trap']
  285. 285 ]);
  286. 286 }
  287. 287
  288. 288 $pdo->commit();
  289. 289
  290. 290 return "✅ Guardado correctamente";
  291. 291
  292. 292 }catch(Exception $e){
  293. 293
  294. 294 $pdo->rollBack();
  295. 295
  296. 296 echo "<pre style='color:red'>";
  297. 297 echo "ERROR:\n".$e->getMessage();
  298. 298 echo "</pre>";
  299. 299 die();
  300. 300 }
  301. 301}
  302. 302
  303. 303/* ✅ CONTROL */
  304. 304$data=null;
  305. 305$msg=null;
  306. 306$html=$_POST['html'] ?? '';
  307. 307
  308. 308if($_SERVER['REQUEST_METHOD']=='POST'){
  309. 309
  310. 310 if($_POST['action']=='preview'){
  311. 311 $data=parseHistorico($html);
  312. 312 }
  313. 313
  314. 314 if($_POST['action']=='save'){
  315. 315 $payload=json_decode(base64_decode($_POST['payload']),true);
  316. 316 $msg=saveRaceDB($payload);
  317. 317 $data=$payload;
  318. 318 }
  319. 319
  320. 320}
  321. 321
  322. 322$logs = gm_db()->query("SELECT * FROM scraping_logs ORDER BY id DESC LIMIT 10")->fetchAll();
  323. 323?>
  324. 324
  325. 325<!doctype html>
  326. 326<html>
  327. 327<head>
  328. 328<meta charset="utf-8">
  329. 329<title>GalgoMaster PRO</title>
  330. 330
  331. 331<style>
  332. 332body{
  333. 333 background:linear-gradient(135deg,#020617,#0f172a);
  334. 334 color:#e2e8f0;
  335. 335 font-family:system-ui;
  336. 336}
  337. 337
  338. 338/* nombre galgo */
  339. 339.dog-name{
  340. 340 display:block;
  341. 341 margin-top:6px;
  342. 342 font-size:15px;
  343. 343 font-weight:600;
  344. 344}
  345. 345
  346. 346/* cajón */
  347. 347.dog-trap{
  348. 348 margin-top:6px;
  349. 349 font-size:13px;
  350. 350 font-weight:700;
  351. 351 color:#ef4444;
  352. 352
  353. 353 background:rgba(239,68,68,0.1);
  354. 354 padding:4px 8px;
  355. 355 border-radius:6px;
  356. 356
  357. 357 display:inline-block;
  358. 358}
  359. 359
  360. 360/* hover más elegante */
  361. 361.box:hover .dog-trap{
  362. 362 background:#ef4444;
  363. 363 color:white;
  364. 364}
  365. 365
  366. 366.box{
  367. 367 background:#020617;
  368. 368 padding:20px;
  369. 369 border-radius:12px;
  370. 370 text-align:center;
  371. 371 transition:0.2s;
  372. 372 border:1px solid transparent;
  373. 373}
  374. 374
  375. 375.box:hover{
  376. 376 transform:translateY(-4px);
  377. 377 border:1px solid #22c55e;
  378. 378}
  379. 379
  380. 380/* layout */
  381. 381.container{max-width:1100px;margin:auto;}
  382. 382
  383. 383.card{
  384. 384 background:#111827;
  385. 385 padding:20px;
  386. 386 margin:20px 0;
  387. 387 border-radius:16px;
  388. 388 box-shadow:0 10px 30px rgba(0,0,0,.5);
  389. 389}
  390. 390
  391. 391/* textarea */
  392. 392textarea{
  393. 393 width:100%;
  394. 394 height:220px;
  395. 395 background:#020617;
  396. 396 color:#22c55e;
  397. 397 border-radius:12px;
  398. 398 padding:12px;
  399. 399}
  400. 400
  401. 401/* botones */
  402. 402button{
  403. 403 background:#22c55e;
  404. 404 border:0;
  405. 405 padding:12px 18px;
  406. 406 border-radius:10px;
  407. 407 font-weight:bold;
  408. 408 cursor:pointer;
  409. 409}
  410. 410
  411. 411/* header */
  412. 412.topbar{
  413. 413 display:flex;
  414. 414 justify-content:space-between;
  415. 415 align-items:center;
  416. 416}
  417. 417
  418. 418/* ✅ GRID */
  419. 419.grid{
  420. 420 display:grid;
  421. 421 grid-template-columns:repeat(4,1fr);
  422. 422 gap:15px;
  423. 423}
  424. 424
  425. 425/* ✅ BOX */
  426. 426.box{
  427. 427 background:#020617;
  428. 428 padding:20px;
  429. 429 border-radius:12px;
  430. 430 text-align:center;
  431. 431 transition:0.2s;
  432. 432}
  433. 433.box:hover{
  434. 434 transform:scale(1.05);
  435. 435}
  436. 436
  437. 437/* posiciones */
  438. 438.first{border-top:4px solid gold;}
  439. 439.second{border-top:4px solid silver;}
  440. 440.third{border-top:4px solid #f59e0b;}
  441. 441.normal{border-top:4px solid #334155;}
  442. 442
  443. 443.icon{font-size:32px;margin-bottom:10px;}
  444. 444
  445. 445/* logs */
  446. 446.logsBox{
  447. 447 max-height:200px;
  448. 448 overflow-y:auto;
  449. 449}
  450. 450.log{
  451. 451 font-size:12px;
  452. 452 padding:6px;
  453. 453 border-bottom:1px solid #333;
  454. 454}
  455. 455
  456. 456</style>
  457. 457</head>
  458. 458
  459. 459<body>
  460. 460
  461. 461<div class="container">
  462. 462
  463. 463<div class="topbar">
  464. 464<h1>🐕 GalgoMaster PRO</h1>
  465. 465<button onclick="location.href='historial.php'">📊 Historial</button>
  466. 466</div>
  467. 467
  468. 468<?php if($msg): ?>
  469. 469<div class="card"><?= e($msg) ?></div>
  470. 470<?php endif; ?>
  471. 471
  472. 472<div class="card">
  473. 473<form method="post">
  474. 474<textarea name="html"><?= e($html) ?></textarea>
  475. 475<br><br>
  476. 476<button name="action" value="preview">🔍 Analizar</button>
  477. 477</form>
  478. 478</div>
  479. 479
  480. 480<?php if($data): ?>
  481. 481
  482. 482<div class="card">
  483. 483
  484. 484<h2><?= e($data['evento']) ?></h2>
  485. 485<p>
  486. 486📍 <?= e($data['pista']) ?> — <?= e($data['distancia'] ?? '-') ?> m.
  487. 487🏁 N° Carrera: <?= e($data['carrera'] ?? '-') ?>
  488. 488🕒 Hora de Inicio: <?= e($data['hora'] ?? '-') ?>
  489. 489📊 Estado: - <?= e($data['estado'] ?? '-') ?> -
  490. 490</p>
  491. 491
  492. 492<div class="grid">
  493. 493
  494. 494<?php foreach($data['runners'] as $r):
  495. 495
  496. 496$class="normal";
  497. 497$icon="🐕";
  498. 498
  499. 499if($r['posicion']==1){ $class="first"; $icon="🥇"; }
  500. 500elseif($r['posicion']==2){ $class="second"; $icon="🥈"; }
  501. 501elseif($r['posicion']==3){ $class="third"; $icon="🥉"; }
  502. 502
  503. 503?>
  504. 504
  505. 505<div class="box <?= $class ?>">
  506. 506<div class="icon"><?= $icon ?></div>
  507. 507<b class="dog-name"><?= e($r['nombre']) ?></b>
  508. 508
  509. 509<?php if(!empty($r['trap'])): ?>
  510. 510<div class="dog-trap">
  511. 511 Cajón N° <?= e($r['trap']) ?>
  512. 512</div>
  513. 513<?php endif; ?>
  514. 514</div>
  515. 515
  516. 516<?php endforeach; ?>
  517. 517
  518. 518</div>
  519. 519
  520. 520<br>
  521. 521
  522. 522<form method="post">
  523. 523<input type="hidden" name="action" value="save">
  524. 524<input type="hidden" name="payload" value="<?= base64_encode(json_encode($data)) ?>">
  525. 525<button>💾 Guardar</button>
  526. 526</form>
  527. 527
  528. 528</div>
  529. 529
  530. 530<?php endif; ?>
  531. 531
  532. 532<div class="card">
  533. 533<h3>📜 Logs</h3>
  534. 534
  535. 535<div class="logsBox">
  536. 536
  537. 537<?php foreach($logs as $l): ?>
  538. 538<div class="log">
  539. 539<?= e($l['fecha'] ?? '') ?> → <?= e($l['accion'] ?? '') ?>
  540. 540</div>
  541. 541<?php endforeach; ?>
  542. 542
  543. 543</div>
  544. 544
  545. 545</div>
  546. 546
  547. 547</div>
  548. 548
  549. 549</body>
  550. 550</html>
  551. 551
  552. 552
  553. 553
  554. 554
  555. 555app/Bet365Parser.php:
  556. 556
  557. 557<?php
  558. 558
  559. 559declare(strict_types=1);
  560. 560
  561. 561require_once __DIR__ . '/StyleDetector.php';
  562. 562require_once __DIR__ . '/RatingService.php';
  563. 563
  564. 564final class Bet365Parser
  565. 565{
  566. 566 public function parse(string $html): array
  567. 567 {
  568. 568 $html = html_entity_decode($html, ENT_QUOTES | ENT_HTML5, 'UTF-8');
  569. 569 $html = trim($html);
  570. 570
  571. 571 if ($html === '') {
  572. 572 throw new RuntimeException('El HTML está vacío.');
  573. 573 }
  574. 574
  575. 575 libxml_use_internal_errors(true);
  576. 576
  577. 577 $dom = new DOMDocument('1.0', 'UTF-8');
  578. 578
  579. 579 $loaded = $dom->loadHTML(
  580. 580 '<?xml encoding="UTF-8">' . $html,
  581. 581 LIBXML_NOWARNING | LIBXML_NOERROR | LIBXML_COMPACT
  582. 582 );
  583. 583
  584. 584 if (!$loaded) {
  585. 585 throw new RuntimeException('No se pudo interpretar el HTML.');
  586. 586 }
  587. 587
  588. 588 $xpath = new DOMXPath($dom);
  589. 589
  590. 590 $race = $this->parseRace($xpath);
  591. 591 $runners = $this->parseRunners($xpath, $race);
  592. 592
  593. 593 return [
  594. 594 'html_hash' => hash('sha256', $html),
  595. 595 'race' => $race,
  596. 596 'runners' => $runners,
  597. 597 ];
  598. 598 }
  599. 599
  600. 600 public function parseHistorico(string $html): array
  601. 601 {
  602. 602 libxml_use_internal_errors(true);
  603. 603
  604. 604 $dom = new DOMDocument();
  605. 605 $dom->loadHTML($html);
  606. 606
  607. 607 $xpath = new DOMXPath($dom);
  608. 608
  609. 609 /* ✅ INFO BÁSICA */
  610. 610 $pista = '';
  611. 611 $evento = '';
  612. 612 $hora = null;
  613. 613 $carrera = null;
  614. 614 $distancia = null;
  615. 615
  616. 616 // pista
  617. 617 $n = $xpath->query("//*[contains(@class,'rcr-20')]");
  618. 618 if ($n->length) $pista = trim($n->item(0)->textContent);
  619. 619
  620. 620 // nombre evento
  621. 621 $n = $xpath->query("//*[contains(@class,'rcr-67')]");
  622. 622 if ($n->length) $evento = trim($n->item(0)->textContent);
  623. 623
  624. 624 // info carrera
  625. 625 $info = $xpath->query("//*[contains(@class,'rcr-14')]");
  626. 626 foreach ($info as $i) {
  627. 627 $txt = trim($i->textContent);
  628. 628
  629. 629 if (strpos($txt, 'Carrera') !== false) {
  630. 630 preg_match('/(\d+)/', $txt, $m);
  631. 631 $carrera = $m[1] ?? null;
  632. 632 }
  633. 633
  634. 634 if (strpos($txt, 'Empezada') !== false) {
  635. 635 preg_match('/(\d{2}:\d{2}:\d{2})/', $txt, $m);
  636. 636 $hora = $m[1] ?? null;
  637. 637 }
  638. 638
  639. 639 if (preg_match('/(\d+)m/', $txt, $m)) {
  640. 640 $distancia = (int)$m[1];
  641. 641 }
  642. 642 }
  643. 643
  644. 644 /* ✅ RESULTADOS */
  645. 645 $runners = [];
  646. 646
  647. 647 $rows = $xpath->query("//*[contains(@class,'rrs-610')]");
  648. 648
  649. 649 foreach ($rows as $row) {
  650. 650
  651. 651 $posNode = $xpath->query(".//*[contains(@class,'rrs-1a')]", $row);
  652. 652 $nameNode = $xpath->query(".//*[contains(@class,'rrs-44')]", $row);
  653. 653 $trapNode = $xpath->query(".//img", $row);
  654. 654
  655. 655 if (!$nameNode->length) continue;
  656. 656
  657. 657 $name = trim($nameNode->item(0)->textContent);
  658. 658 if ($name === '') continue;
  659. 659
  660. 660 $pos = $posNode->length ? trim($posNode->item(0)->textContent) : null;
  661. 661
  662. 662 $posNum = null;
  663. 663 if ($pos && preg_match('/(\d+)/', $pos, $m)) {
  664. 664 $posNum = (int)$m[1];
  665. 665 }
  666. 666
  667. 667 $trap = $trapNode->length ? $trapNode->item(0)->getAttribute('alt') : null;
  668. 668
  669. 669 /* ✅ ODDS */
  670. 670 $oddNodes = $xpath->query(".//*[contains(@class,'rrs-763')]", $row);
  671. 671
  672. 672 $odds = [];
  673. 673 foreach ($oddNodes as $o) {
  674. 674 $val = trim($o->textContent);
  675. 675 if ($val !== '-' && $val !== '') {
  676. 676 $odds[] = (float)str_replace(',', '.', $val);
  677. 677 }
  678. 678 }
  679. 679
  680. 680 $avgOdds = count($odds) ? array_sum($odds)/count($odds) : null;
  681. 681
  682. 682 $runners[] = [
  683. 683 'trap' => $trap,
  684. 684 'nombre' => $name,
  685. 685 'posicion' => $pos,
  686. 686 'posicion_num' => $posNum,
  687. 687 'odds_promedio' => $avgOdds,
  688. 688 'odds' => $odds
  689. 689 ];
  690. 690 }
  691. 691
  692. 692 /* ✅ DISTANCIA ENTRE GALGOS */
  693. 693 $gap = null;
  694. 694 $gapNode = $xpath->query("//*[contains(@class,'rrs-035')]");
  695. 695 if ($gapNode->length) {
  696. 696 $gap = trim($gapNode->item(0)->textContent);
  697. 697 }
  698. 698
  699. 699 return [
  700. 700 'html_hash' => hash('sha256', $html),
  701. 701 'race' => [
  702. 702 'pista' => $pista,
  703. 703 'evento' => $evento,
  704. 704 'hora' => $hora,
  705. 705 'carrera' => $carrera,
  706. 706 'distancia' => $distancia,
  707. 707 'gap' => $gap
  708. 708 ],
  709. 709 'runners' => $runners
  710. 710 ];
  711. 711}
  712. 712
  713. 713 private function parseRace(DOMXPath $xpath): array
  714. 714 {
  715. 715 $pista = $this->firstText($xpath, "//*[contains(@class,'rcr-20')]");
  716. 716 $hora = $this->firstText($xpath, "//*[contains(@class,'rcr-31')]");
  717. 717
  718. 718 $carreraNumero = null;
  719. 719 $distancia = null;
  720. 720
  721. 721 $raceLabels = $this->texts($xpath, "//*[contains(@class,'rcr-4b')]");
  722. 722
  723. 723 foreach ($raceLabels as $label) {
  724. 724 if (stripos($label, 'Carrera') !== false) {
  725. 725 $carreraNumero = trim(str_replace('•', '', $label));
  726. 726 }
  727. 727
  728. 728 if (preg_match('/(\d{3,4})\s*m/i', $label, $m)) {
  729. 729 $distancia = (int) $m[1];
  730. 730 }
  731. 731 }
  732. 732
  733. 733 $preview = $this->firstText($xpath, "//*[contains(@class,'sr-0f27e2')]");
  734. 734
  735. 735 $fechaHora = null;
  736. 736
  737. 737 if ($hora && preg_match('/^\d{1,2}:\d{2}$/', $hora)) {
  738. 738 $fechaHora = date('Y-m-d') . ' ' . $hora . ':00';
  739. 739 }
  740. 740
  741. 741 return [
  742. 742 'pista' => $pista ?: 'Unknown',
  743. 743 'hora_texto' => $hora ?: null,
  744. 744 'fecha_texto' => date('Y-m-d'),
  745. 745 'fecha_hora' => $fechaHora,
  746. 746 'carrera_numero' => $carreraNumero,
  747. 747 'distancia' => $distancia,
  748. 748 'grado' => null,
  749. 749 'tipo' => 'Greyhound',
  750. 750 'going' => null,
  751. 751 'preview' => $preview ?: null,
  752. 752 'comentario_experto' => $preview ?: null,
  753. 753 ];
  754. 754 }
  755. 755
  756. 756 private function parseRunners(DOMXPath $xpath, array $race): array
  757. 757 {
  758. 758 $nodes = $xpath->query("//*[contains(concat(' ', normalize-space(@class), ' '), ' rg-cb ')]");
  759. 759
  760. 760 $runners = [];
  761. 761 $seen = [];
  762. 762
  763. 763 foreach ($nodes as $node) {
  764. 764 if (!$node instanceof DOMElement) {
  765. 765 continue;
  766. 766 }
  767. 767
  768. 768 $name = $this->firstTextRelative($xpath, $node, ".//*[contains(@class,'rg-0d')]");
  769. 769 $form = $this->firstAttributeRelative($xpath, $node, ".//*[contains(@class,'rg-0e')]", 'data-content');
  770. 770
  771. 771 if (!$form) {
  772. 772 $form = $this->firstTextRelative($xpath, $node, ".//*[contains(@class,'rg-0e')]");
  773. 773 }
  774. 774
  775. 775 if (!$name || !$form || !preg_match('/^?:-[1-6]{2,}$/', $form)) {
  776. 776 continue;
  777. 777 }
  778. 778
  779. 779 $trap = $this->extractTrap($xpath, $node);
  780. 780 if (!$trap) {
  781. 781 $trap = count($runners) + 1;
  782. 782 }
  783. 783
  784. 784 $oddsFinalText = $this->firstTextRelative($xpath, $node, ".//*[contains(@class,'rul-ce0412')]");
  785. 785 $oddsFinal = RatingService::decimalFromFraction($oddsFinalText);
  786. 786
  787. 787 $oddsHistory = $this->textsRelative($xpath, $node, ".//*[contains(@class,'sr-dc6edd')]//span");
  788. 788 $oddsInicial = null;
  789. 789
  790. 790 if (!empty($oddsHistory[0])) {
  791. 791 $oddsInicial = RatingService::decimalFromFraction($oddsHistory[0]);
  792. 792 }
  793. 793
  794. 794 if ($oddsInicial === null) {
  795. 795 $oddsInicial = $oddsFinal;
  796. 796 }
  797. 797
  798. 798 $history = $this->parseHistory($xpath, $node, $race['pista'] ?? null);
  799. 799
  800. 800 $remarksList = [];
  801. 801
  802. 802 foreach ($history as $row) {
  803. 803 if (!empty($row['remarks'])) {
  804. 804 $remarksList[] = $row['remarks'];
  805. 805 }
  806. 806 }
  807. 807
  808. 808 $style = StyleDetector::detectFromMany($remarksList);
  809. 809
  810. 810 $rating = RatingService::calculate([
  811. 811 'forma_reciente' => $form,
  812. 812 'historial' => $history,
  813. 813 'estilo_detectado' => $style,
  814. 814 ], $race['distancia'] ?? null);
  815. 815
  816. 816 $value = RatingService::valueSignal($rating['rating'], $oddsFinal);
  817. 817
  818. 818 $latest = $history[0] ?? [];
  819. 819
  820. 820 $runner = [
  821. 821 'trap' => (int) $trap,
  822. 822 'nombre' => trim($name),
  823. 823 'forma_reciente' => trim($form),
  824. 824 'odds_inicial' => $oddsInicial,
  825. 825 'odds_final' => $oddsFinal,
  826. 826 'implied_probability' => RatingService::impliedProbability($oddsFinal),
  827. 827 'odds_movimiento' => $oddsHistory,
  828. 828 'cal_tm' => isset($latest['cal_tm']) ? $latest['cal_tm'] : null,
  829. 829 'win_time' => isset($latest['win_tm']) ? $latest['win_tm'] : null,
  830. 830 'splits' => isset($latest['splits']) ? $latest['splits'] : null,
  831. 831 'db' => isset($latest['db']) ? $latest['db'] : null,
  832. 832 'remarks' => implode(' | ', array_slice($remarksList, 0, 3)),
  833. 833 'estilo_detectado' => $style,
  834. 834 'rating_pre' => $rating['rating'],
  835. 835 'score_forma' => $rating['score_forma'],
  836. 836 'score_velocidad' => $rating['score_velocidad'],
  837. 837 'score_estilo' => $rating['score_estilo'],
  838. 838 'value_score' => $value['value_score'],
  839. 839 'value_flag' => $value['value_flag'],
  840. 840 'historial' => $history,
  841. 841 'raw_text' => $this->cleanText($node->textContent),
  842. 842 ];
  843. 843
  844. 844 $key = $runner['trap'] . '-' . mb_strtolower($runner['nombre'], 'UTF-8');
  845. 845
  846. 846 if (!isset($seen[$key])) {
  847. 847 $seen[$key] = true;
  848. 848 $runners[] = $runner;
  849. 849 }
  850. 850 }
  851. 851
  852. 852 usort($runners, fn ($a, $b) => $a['trap'] <=> $b['trap']);
  853. 853
  854. 854 return $runners;
  855. 855 }
  856. 856
  857. 857 private function parseHistory(DOMXPath $xpath, DOMElement $runnerNode, ?string $pista): array
  858. 858 {
  859. 859 $columns = [];
  860. 860
  861. 861 $columnNodes = $xpath->query(
  862. 862 ".//*[contains(concat(' ', normalize-space(@class), ' '), ' rg-a3 ')]",
  863. 863 $runnerNode
  864. 864 );
  865. 865
  866. 866 foreach ($columnNodes as $columnNode) {
  867. 867 if (!$columnNode instanceof DOMElement) {
  868. 868 continue;
  869. 869 }
  870. 870
  871. 871 $header = $this->firstTextRelative($xpath, $columnNode, ".//*[contains(@class,'rg-58')]");
  872. 872 $values = $this->textsRelative($xpath, $columnNode, ".//*[contains(@class,'rg-7f')]");
  873. 873
  874. 874 $header = trim($header);
  875. 875
  876. 876 if ($header !== '') {
  877. 877 $columns[$header] = $values;
  878. 878 }
  879. 879 }
  880. 880
  881. 881 if (!$columns) {
  882. 882 return [];
  883. 883 }
  884. 884
  885. 885 $maxRows = 0;
  886. 886
  887. 887 foreach ($columns as $values) {
  888. 888 $maxRows = max($maxRows, count($values));
  889. 889 }
  890. 890
  891. 891 $history = [];
  892. 892
  893. 893 for ($i = 0; $i < $maxRows; $i++) {
  894. 894 $dateText = $columns['Date'][$i] ?? null;
  895. 895 $disTrp = $columns['Dis / Trp'][$i] ?? null;
  896. 896 $fin = $columns['Fin'][$i] ?? null;
  897. 897 $sp = $columns['SP'][$i] ?? null;
  898. 898 $gr = $columns['Gr'][$i] ?? null;
  899. 899 $calTm = $columns['CalTm'][$i] ?? null;
  900. 900 $splitTm = $columns['SplitTm'][$i] ?? null;
  901. 901 $splits = $columns['Splits'][$i] ?? null;
  902. 902 $db = $columns['DB'][$i] ?? null;
  903. 903 $remarks = $columns['Remarks'][$i] ?? null;
  904. 904 $winTm = $columns['WinTm'][$i] ?? null;
  905. 905 $going = $columns['G'][$i] ?? null;
  906. 906
  907. 907 $distance = null;
  908. 908 $trap = null;
  909. 909
  910. 910 if ($disTrp && preg_match('/(\d{3,4})\s*m\s*\[(\d)\]/i', $disTrp, $m)) {
  911. 911 $distance = (int) $m[1];
  912. 912 $trap = (int) $m[2];
  913. 913 }
  914. 914
  915. 915 $history[] = [
  916. 916 'fecha_texto' => $dateText,
  917. 917 'fecha' => $this->normaliseShortDate($dateText),
  918. 918 'pista' => $pista,
  919. 919 'distancia' => $distance,
  920. 920 'trap' => $trap,
  921. 921 'posicion' => $fin,
  922. 922 'posicion_num' => $this->positionToInt($fin),
  923. 923 'sp' => $sp,
  924. 924 'sp_decimal' => RatingService::decimalFromFraction($sp),
  925. 925 'grade' => $gr ?: null,
  926. 926 'cal_tm' => is_numeric($calTm) ? (float) $calTm : null,
  927. 927 'split_tm' => $splitTm ?: null,
  928. 928 'splits' => $splits ?: null,
  929. 929 'db' => $db ?: null,
  930. 930 'remarks' => $remarks ?: null,
  931. 931 'win_tm' => is_numeric($winTm) ? (float) $winTm : null,
  932. 932 'going' => $going ?: null,
  933. 933 ];
  934. 934 }
  935. 935
  936. 936 return $history;
  937. 937 }
  938. 938
  939. 939 private function extractTrap(DOMXPath $xpath, DOMElement $node): ?int
  940. 940 {
  941. 941 $alt = $this->firstAttributeRelative(
  942. 942 $xpath,
  943. 943 $node,
  944. 944 ".//img[contains(@src,'dog') or contains(@src,'traps_uk')]",
  945. 945 'alt'
  946. 946 );
  947. 947
  948. 948 if ($alt && preg_match('/^[1-6]$/', trim($alt))) {
  949. 949 return (int) $alt;
  950. 950 }
  951. 951
  952. 952 $src = $this->firstAttributeRelative(
  953. 953 $xpath,
  954. 954 $node,
  955. 955 ".//img[contains(@src,'dog') or contains(@src,'traps_uk')]",
  956. 956 'src'
  957. 957 );
  958. 958
  959. 959 if ($src && preg_match('/dog([1-6])\.svg/i', $src, $m)) {
  960. 960 return (int) $m[1];
  961. 961 }
  962. 962
  963. 963 return null;
  964. 964 }
  965. 965
  966. 966 private function normaliseShortDate(?string $date): ?string
  967. 967 {
  968. 968 $date = trim((string) $date);
  969. 969
  970. 970 if (!preg_match('/^(\d{1,2})\/(\d{1,2})$/', $date, $m)) {
  971. 971 return null;
  972. 972 }
  973. 973
  974. 974 $day = (int) $m[1];
  975. 975 $month = (int) $m[2];
  976. 976 $year = (int) date('Y');
  977. 977
  978. 978 if (!checkdate($month, $day, $year)) {
  979. 979 return null;
  980. 980 }
  981. 981
  982. 982 return sprintf('%04d-%02d-%02d', $year, $month, $day);
  983. 983 }
  984. 984
  985. 985 private function positionToInt(?string $position): ?int
  986. 986 {
  987. 987 if (!$position) {
  988. 988 return null;
  989. 989 }
  990. 990
  991. 991 if (preg_match('/(\d+)/', $position, $m)) {
  992. 992 return (int) $m[1];
  993. 993 }
  994. 994
  995. 995 return null;
  996. 996 }
  997. 997
  998. 998 private function firstText(DOMXPath $xpath, string $query): ?string
  999. 999 {
  1000. 1000 $nodes = $xpath->query($query);
  1001. 1001
  1002. 1002 if (!$nodes || $nodes->length === 0) {
  1003. 1003 return null;
  1004. 1004 }
  1005. 1005
  1006. 1006 return $this->cleanText($nodes->item(0)->textContent ?? '');
  1007. 1007 }
  1008. 1008
  1009. 1009 private function firstTextRelative(DOMXPath $xpath, DOMNode $context, string $query): ?string
  1010. 1010 {
  1011. 1011 $nodes = $xpath->query($query, $context);
  1012. 1012
  1013. 1013 if (!$nodes || $nodes->length === 0) {
  1014. 1014 return null;
  1015. 1015 }
  1016. 1016
  1017. 1017 return $this->cleanText($nodes->item(0)->textContent ?? '');
  1018. 1018 }
  1019. 1019
  1020. 1020 private function firstAttributeRelative(DOMXPath $xpath, DOMNode $context, string $query, string $attr): ?string
  1021. 1021 {
  1022. 1022 $nodes = $xpath->query($query, $context);
  1023. 1023
  1024. 1024 if (!$nodes || $nodes->length === 0) {
  1025. 1025 return null;
  1026. 1026 }
  1027. 1027
  1028. 1028 $node = $nodes->item(0);
  1029. 1029
  1030. 1030 if (!$node instanceof DOMElement) {
  1031. 1031 return null;
  1032. 1032 }
  1033. 1033
  1034. 1034 $value = $node->getAttribute($attr);
  1035. 1035
  1036. 1036 return $value !== '' ? trim($value) : null;
  1037. 1037 }
  1038. 1038
  1039. 1039 private function texts(DOMXPath $xpath, string $query): array
  1040. 1040 {
  1041. 1041 $nodes = $xpath->query($query);
  1042. 1042 $out = [];
  1043. 1043
  1044. 1044 if (!$nodes) {
  1045. 1045 return $out;
  1046. 1046 }
  1047. 1047
  1048. 1048 foreach ($nodes as $node) {
  1049. 1049 $text = $this->cleanText($node->textContent ?? '');
  1050. 1050
  1051. 1051 if ($text !== '') {
  1052. 1052 $out[] = $text;
  1053. 1053 }
  1054. 1054 }
  1055. 1055
  1056. 1056 return $out;
  1057. 1057 }
  1058. 1058
  1059. 1059 private function textsRelative(DOMXPath $xpath, DOMNode $context, string $query): array
  1060. 1060 {
  1061. 1061 $nodes = $xpath->query($query, $context);
  1062. 1062 $out = [];
  1063. 1063
  1064. 1064 if (!$nodes) {
  1065. 1065 return $out;
  1066. 1066 }
  1067. 1067
  1068. 1068 foreach ($nodes as $node) {
  1069. 1069 $text = $this->cleanText($node->textContent ?? '');
  1070. 1070 $out[] = $text;
  1071. 1071 }
  1072. 1072
  1073. 1073 return $out;
  1074. 1074 }
  1075. 1075
  1076. 1076 private function cleanText(string $text): string
  1077. 1077 {
  1078. 1078 $text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
  1079. 1079 $text = preg_replace('/\s+/u', ' ', $text);
  1080. 1080
  1081. 1081 return trim((string) $text);
  1082. 1082 }
  1083. 1083}
  1084. 1084
  1085. 1085
  1086. 1086
  1087. 1087app/db.php:
  1088. 1088
  1089. 1089<?php
  1090. 1090
  1091. 1091declare(strict_types=1);
  1092. 1092
  1093. 1093function gm_config(): array
  1094. 1094{
  1095. 1095 static $config = null;
  1096. 1096
  1097. 1097 if ($config === null) {
  1098. 1098 $config = require __DIR__ . '/config.php';
  1099. 1099
  1100. 1100 if (!empty($config['timezone'])) {
  1101. 1101 date_default_timezone_set($config['timezone']);
  1102. 1102 }
  1103. 1103 }
  1104. 1104
  1105. 1105 return $config;
  1106. 1106}
  1107. 1107
  1108. 1108function gm_db(): PDO
  1109. 1109{
  1110. 1110 static $pdo = null;
  1111. 1111
  1112. 1112 if ($pdo instanceof PDO) {
  1113. 1113 return $pdo;
  1114. 1114 }
  1115. 1115
  1116. 1116 $config = gm_config();
  1117. 1117
  1118. 1118 $dsn = sprintf(
  1119. 1119 'mysql:host=%s;dbname=%s;charset=utf8mb4',
  1120. 1120 $config['db_host'],
  1121. 1121 $config['db_name']
  1122. 1122 );
  1123. 1123
  1124. 1124 $pdo = new PDO($dsn, $config['db_user'], $config['db_pass'], [
  1125. 1125 PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
  1126. 1126 PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
  1127. 1127 PDO::ATTR_EMULATE_PREPARES => false,
  1128. 1128 ]);
  1129. 1129
  1130. 1130 return $pdo;
  1131. 1131}
  1132. 1132
  1133. 1133function gm_slug(string $value): string
  1134. 1134{
  1135. 1135 $value = trim(mb_strtolower($value, 'UTF-8'));
  1136. 1136 $ascii = @iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $value);
  1137. 1137
  1138. 1138 if ($ascii !== false) {
  1139. 1139 $value = $ascii;
  1140. 1140 }
  1141. 1141
  1142. 1142 $value = preg_replace('/[^a-z0-9]+/i', '-', $value);
  1143. 1143 $value = trim((string) $value, '-');
  1144. 1144
  1145. 1145 return $value !== '' ? $value : 'galgo-' . bin2hex(random_bytes(4));
  1146. 1146}

Raw Paste

Comments 0
Login to post a comment.
  • No comments yet. Be the first.
Login to post a comment. Login or Register
We use cookies. To comply with GDPR in the EU and the UK we have to show you these.

We use cookies and similar technologies to keep this website functional (including spam protection via Google reCAPTCHA or Cloudflare Turnstile), and — with your consent — to measure usage and show ads. See Privacy.