├── README.md ├── runthis.php ├── LICENSE ├── http.php ├── example_output.txt └── proto.php /README.md: -------------------------------------------------------------------------------- 1 | # bet365-scraper 2 | Bet365.com scraper, currently scrapes soccer matches but can easily handle data from other games. 3 | 4 | Client didn't pay me, so here it is for free. I hope whatever he was scheming is ruined by you. 5 | -------------------------------------------------------------------------------- /runthis.php: -------------------------------------------------------------------------------- 1 | connect()); 8 | 9 | $timeEnd = microtime(true); 10 | 11 | echo("Script took " . (($timeEnd - $timeStart)) . " seconds!\n"); 12 | ?> 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 s0beit 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /http.php: -------------------------------------------------------------------------------- 1 | $url, 24 | CURLOPT_RETURNTRANSFER => 1, 25 | CURLOPT_FRESH_CONNECT => 1, 26 | CURLOPT_FOLLOWLOCATION => 1, 27 | CURLOPT_SSL_VERIFYHOST => 0, 28 | CURLOPT_SSL_VERIFYPEER => 0, 29 | CURLOPT_USERAGENT => http::$ua, 30 | CURLOPT_CONNECTTIMEOUT => 0, // Disable timeout 31 | CURLOPT_TIMEOUT => 400 32 | )); 33 | 34 | if(http::$cookieJar !== null) { 35 | curl_setopt($ch, CURLOPT_COOKIEJAR, http::$cookieJar); 36 | } 37 | 38 | if(!empty($headers)) { 39 | curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); 40 | } 41 | 42 | $data = curl_exec($ch); 43 | 44 | curl_close($ch); 45 | 46 | return $data; 47 | } 48 | 49 | public static function post($url, $postData = '', $headers = array(), $cookie = '') { 50 | $ch = curl_init(); 51 | 52 | if($ch === FALSE) { 53 | return FALSE; 54 | } 55 | 56 | curl_setopt_array($ch, array( 57 | CURLOPT_URL => $url, 58 | CURLOPT_RETURNTRANSFER => 1, 59 | CURLOPT_FRESH_CONNECT => 1, 60 | CURLOPT_FOLLOWLOCATION => 1, 61 | CURLOPT_SSL_VERIFYHOST => 0, 62 | CURLOPT_SSL_VERIFYPEER => 0, 63 | CURLOPT_USERAGENT => http::$ua, 64 | CURLOPT_CONNECTTIMEOUT => 0, // Disable timeout 65 | CURLOPT_TIMEOUT => 400, 66 | CURLOPT_POST => 1, 67 | CURLOPT_POSTFIELDS => $postData 68 | )); 69 | 70 | if(http::$cookieJar !== null) { 71 | curl_setopt($ch, CURLOPT_COOKIEJAR, http::$cookieJar); 72 | } 73 | 74 | if(!empty($headers)) { 75 | curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); 76 | } 77 | 78 | $data = curl_exec($ch); 79 | 80 | curl_close($ch); 81 | 82 | return $data; 83 | } 84 | }; 85 | ?> 86 | -------------------------------------------------------------------------------- /example_output.txt: -------------------------------------------------------------------------------- 1 | Session ID: B8415D7A5AB94FFFA6D732DE4D092951000003 2 | Pow HTTPS Host: https://premlp-pt11.365lpodds.com:443 3 | Pow Random Number: 3196027854 4 | string(20) "100M39-9D6ZO2nkB/8q" 5 | Constant: 100 6 | Pow Session Id: M39-9D6ZO2nkB/8q 7 | Trying for ID: 15268749882C1_1_3 8 | array(20) { 9 | ["team1"]=> 10 | array(11) { 11 | ["IGoal"]=> 12 | string(1) "0" 13 | ["ICorner"]=> 14 | string(1) "1" 15 | ["IYellowCard"]=> 16 | string(1) "0" 17 | ["IRedCard"]=> 18 | string(1) "0" 19 | ["IThrowIn"]=> 20 | string(1) "-" 21 | ["IFreeKick"]=> 22 | string(1) "-" 23 | ["IGoalKick"]=> 24 | string(1) "-" 25 | ["IPenalty"]=> 26 | string(1) "0" 27 | ["ISubstitution"]=> 28 | string(1) "0" 29 | ["name"]=> 30 | string(10) "Campobasso" 31 | [""]=> 32 | string(2) "18" 33 | } 34 | ["team2"]=> 35 | array(11) { 36 | ["IGoal"]=> 37 | string(1) "0" 38 | ["ICorner"]=> 39 | string(1) "4" 40 | ["IYellowCard"]=> 41 | string(1) "0" 42 | ["IRedCard"]=> 43 | string(1) "0" 44 | ["IThrowIn"]=> 45 | string(1) "-" 46 | ["IFreeKick"]=> 47 | string(1) "-" 48 | ["IGoalKick"]=> 49 | string(1) "-" 50 | ["IPenalty"]=> 51 | string(1) "0" 52 | ["ISubstitution"]=> 53 | string(1) "0" 54 | ["name"]=> 55 | string(8) "Matelica" 56 | [""]=> 57 | string(2) "23" 58 | } 59 | ["Fulltime Result"]=> 60 | array(3) { 61 | ["Campobasso"]=> 62 | string(5) "20/21" 63 | ["Draw"]=> 64 | string(4) "11/5" 65 | ["Matelica"]=> 66 | string(4) "11/4" 67 | } 68 | ["Double Chance"]=> 69 | array(3) { 70 | ["Campobasso or Draw"]=> 71 | string(3) "1/4" 72 | ["Matelica or Draw"]=> 73 | string(4) "8/11" 74 | ["Campobasso or Matelica"]=> 75 | string(3) "1/3" 76 | } 77 | ["Half Time Result"]=> 78 | array(3) { 79 | ["Campobasso"]=> 80 | string(5) "21/10" 81 | ["Draw"]=> 82 | string(3) "4/6" 83 | ["Matelica"]=> 84 | string(3) "9/2" 85 | } 86 | ["1st Goal"]=> 87 | array(3) { 88 | ["Campobasso"]=> 89 | string(3) "4/6" 90 | ["No 1st Goal"]=> 91 | string(3) "5/1" 92 | ["Matelica"]=> 93 | string(3) "7/4" 94 | } 95 | ["Match Goals"]=> 96 | array(2) { 97 | ["Over "]=> 98 | string(4) "11/8" 99 | ["Under "]=> 100 | string(4) "8/15" 101 | } 102 | ["First Half Goals"]=> 103 | array(2) { 104 | ["Over"]=> 105 | string(3) "5/6" 106 | ["Under"]=> 107 | string(3) "5/6" 108 | } 109 | ["Asian Handicap (0-0)"]=> 110 | array(2) { 111 | ["Campobasso -0.5"]=> 112 | string(3) "1/1" 113 | ["Matelica +0.5"]=> 114 | string(3) "4/5" 115 | } 116 | ["Goal Line (0-0)"]=> 117 | array(2) { 118 | ["Over 2"]=> 119 | string(5) "17/20" 120 | ["Under 2"]=> 121 | string(5) "19/20" 122 | } 123 | ["1st Half Asian Handicap (0-0)"]=> 124 | array(2) { 125 | ["Campobasso 0.0,-0.5"]=> 126 | string(4) "11/8" 127 | ["Matelica 0.0,+0.5"]=> 128 | string(5) "11/20" 129 | } 130 | ["1st Half Goal Line (0-0)"]=> 131 | array(2) { 132 | ["Over 0.5"]=> 133 | string(5) "19/20" 134 | ["Under 0.5"]=> 135 | string(5) "17/20" 136 | } 137 | ["Half Time/Full Time"]=> 138 | array(3) { 139 | ["Campobasso - Campobasso"]=> 140 | string(4) "11/4" 141 | ["Campobasso - Draw"]=> 142 | string(4) "18/1" 143 | ["Campobasso - Matelica"]=> 144 | string(4) "40/1" 145 | } 146 | ["Half Time Correct Score"]=> 147 | array(1) { 148 | ["1-0"]=> 149 | string(4) "11/4" 150 | } 151 | ["Final Score"]=> 152 | array(1) { 153 | ["1-0"]=> 154 | string(3) "9/2" 155 | } 156 | ["3-Way Handicap"]=> 157 | array(3) { 158 | ["Campobasso "]=> 159 | string(4) "13/5" 160 | ["Draw (Matelica )"]=> 161 | string(4) "13/5" 162 | ["Matelica "]=> 163 | string(4) "8/11" 164 | } 165 | ["Draw No Bet"]=> 166 | array(2) { 167 | ["Campobasso"]=> 168 | string(3) "4/9" 169 | ["Matelica"]=> 170 | string(4) "13/8" 171 | } 172 | ["Last Team to Score"]=> 173 | array(3) { 174 | ["Campobasso"]=> 175 | string(3) "4/6" 176 | ["No Goals"]=> 177 | string(3) "5/1" 178 | ["Matelica"]=> 179 | string(3) "7/4" 180 | } 181 | ["Goals Odd/Even"]=> 182 | array(2) { 183 | ["Odd"]=> 184 | string(5) "10/11" 185 | ["Even"]=> 186 | string(3) "4/5" 187 | } 188 | ["To Win 2nd Half"]=> 189 | array(3) { 190 | ["Campobasso"]=> 191 | string(3) "6/5" 192 | ["Draw"]=> 193 | string(3) "6/4" 194 | ["Matelica"]=> 195 | string(4) "11/4" 196 | } 197 | } 198 | bool(false) 199 | Script took 24.733845949173 seconds! 200 | -------------------------------------------------------------------------------- /proto.php: -------------------------------------------------------------------------------- 1 | "\x01", 15 | 'FIELD_DELIM' => "\x02", 16 | 'MESSAGE_DELIM' => "\b", 17 | 'CLIENT_CONNECT' => 0, 18 | 'CLIENT_POLL' => 1, 19 | 'CLIENT_SEND' => 2, 20 | 'INITIAL_TOPIC_LOAD' => 20, 21 | 'DELTA' => 21, 22 | 'CLIENT_SUBSCRIBE' => 22, 23 | 'CLIENT_UNSUBSCRIBE' => 23, 24 | 'CLIENT_SWAP_SUBSCRIPTIONS' => 26, 25 | 'NONE_ENCODING' => 0, 26 | 'ENCRYPTED_ENCODING' => 17, 27 | 'COMPRESSED_ENCODING' => 18, 28 | 'BASE64_ENCODING' => 19, 29 | 'SERVER_PING' => 24, 30 | 'CLIENT_PING' => 25, 31 | 'CLIENT_ABORT' => 28, 32 | 'CLIENT_CLOSE' => 29, 33 | 'ACK_ITL' => 30, 34 | 'ACK_DELTA' => 31, 35 | 'ACK_RESPONSE' => 32 36 | ); 37 | 38 | function getPropData($name) { 39 | if(preg_match_all('#"' . $name . '":(\x20|)\{(.*?)\},#ims', $this->homePage, $matches)) { 40 | return json_decode('{' . $matches[2][0] . '}', true); 41 | } 42 | 43 | if(preg_match_all('#"' . $name . '"(\x20|):\[(.*?)\]#ims', $this->homePage, $matches)) { 44 | return json_decode('[' . $matches[2][0] . ']', true); 45 | } 46 | 47 | if(preg_match_all('#"' . $name . '":(.*?),#ims', $this->homePage, $matches)) { 48 | $var = rtrim(ltrim($matches[1][0])); 49 | 50 | if(substr($var, 0, 1) == '"') { 51 | return substr($var, 1, -1); 52 | } 53 | 54 | return $var; 55 | } 56 | 57 | return NULL; 58 | } 59 | 60 | function powRequest($sid, $specialHeaders = array(), $postData = '') { 61 | $defaultHeaders = array( 62 | 'Content-Type: ; charset=UTF-8', 63 | 'Referer: https://mobile.bet365.com/', 64 | 'Origin: https://mobile.bet365.com' 65 | ); 66 | 67 | if(!empty($this->clientId)) { 68 | array_push($defaultHeaders, 'clientid: ' . $this->clientId); 69 | } 70 | 71 | if($sid != 0) { 72 | array_push($defaultHeaders, 's: ' . $this->serverNum); 73 | $this->serverNum++; 74 | } 75 | 76 | $totalHeaders = array_merge($specialHeaders, $defaultHeaders); 77 | 78 | //var_dump($totalHeaders); 79 | 80 | return http::post($this->powConnectionDetails[1]['Host'] . '/pow/?sid=' . $sid . '&rn=' . $this->clientRn, $postData, $totalHeaders); 81 | } 82 | 83 | 84 | function parameterizeLine($line) { 85 | $chunk = explode(';', $line); 86 | 87 | if(empty($chunk)) 88 | return FALSE; 89 | 90 | $cmd = $chunk[0]; 91 | 92 | // Remove cmd element 93 | array_shift($chunk); 94 | 95 | $params = array(); 96 | 97 | foreach($chunk as $pstr) { 98 | $pdata = explode('=', $pstr); 99 | 100 | if(count($pdata) != 2) 101 | continue; 102 | 103 | $params[$pdata[0]] = $pdata[1]; 104 | } 105 | 106 | return array('cmd' => $cmd, 'params' => $params); 107 | } 108 | 109 | function connect() { 110 | http::setCookieJar('cookie.txt'); 111 | 112 | $this->homePage = http::get('http://mobile.bet365.com'); 113 | 114 | if($this->homePage === FALSE || empty($this->homePage)) 115 | return FALSE; 116 | 117 | $this->sessionId = $this->getPropData('sessionId'); 118 | 119 | if($this->sessionId === NULL || empty($this->sessionId)) 120 | return FALSE; 121 | 122 | echo("Session ID: " . $this->sessionId . "\n"); 123 | 124 | $this->powConnectionDetails = $this->getPropData('ConnectionDetails'); 125 | 126 | if($this->powConnectionDetails === NULL || empty($this->powConnectionDetails)) 127 | return FALSE; 128 | 129 | if(!isset($this->powConnectionDetails[0]) || !isset($this->powConnectionDetails[0]['Host'])) 130 | return FALSE; 131 | 132 | echo("Pow HTTPS Host: {$this->powConnectionDetails[1]['Host']}:{$this->powConnectionDetails[1]['Port']}\n"); 133 | 134 | $this->clientRn = substr(str_shuffle("0123456789"), 0, 16); 135 | 136 | echo("Pow Random Number: {$this->clientRn}\n"); 137 | 138 | $requestPow = $this->powRequest(0, array( 139 | 'method: 0', 140 | 'transporttimeout: 20', 141 | 'type: F', 142 | 'topic: S_' . $this->sessionId 143 | )); 144 | 145 | var_dump($requestPow); 146 | 147 | if($requestPow === FALSE || empty($requestPow)) 148 | return FALSE; 149 | 150 | $data = explode($this->readItConstants['FIELD_DELIM'], $requestPow); 151 | 152 | if(empty($data) || count($data) == 0 || count($data) == 1) 153 | return FALSE; 154 | 155 | echo("Constant: {$data[0]}\n"); 156 | echo("Pow Session Id: {$data[1]}\n"); 157 | 158 | $this->clientId = $data[1]; 159 | 160 | $sslStatus = urlencode($this->powConnectionDetails[1]['Host'] . ':' . $this->powConnectionDetails[1]['Port']); 161 | 162 | // Inform the main site of our connection 163 | http::post('https://mobile.bet365.com/pushstatus/logpushstatus.ashx?state=true', 164 | 'sslStatus=' . $sslStatus . '&connectionID=' . $this->clientId . '&uid=' . $this->clientRn . '&connectionStatus=0&stk=' . $this->sessionId, 165 | array( 166 | 'X-Requested-With: XMLHttpRequest', 167 | 'Content-Type: application/x-www-form-urlencoded; charset=UTF-8' 168 | )); 169 | 170 | $requestPow = $this->powRequest(2, array( 171 | 'method: 1' 172 | )); 173 | 174 | // Subscribe to the InPlay list 175 | $this->subscribe('OVInPlay_1_3//'); 176 | 177 | $requestPow = $this->powRequest(2, array( 178 | 'method: 1' 179 | )); 180 | 181 | if(substr($requestPow, 0, 1) != "\x14") { 182 | echo("Unexpected InPlay packet header"); 183 | 184 | return FALSE; 185 | } 186 | 187 | // Here we have some soccer data!!! wow!! 188 | $gameData = explode($this->readItConstants['RECORD_DELIM'], $requestPow); 189 | $gameData = explode("|", $gameData[count($gameData) - 1]); 190 | 191 | array_shift($gameData); // "F" 192 | 193 | $initialCL = $this->parameterizeLine($gameData[0]); 194 | 195 | if($initialCL === FALSE) 196 | return FALSE; 197 | 198 | if($initialCL['cmd'] != 'CL') 199 | return FALSE; 200 | 201 | if($initialCL['params']['NA'] != 'Soccer') 202 | return FALSE; // It isn't soccer!!?? 203 | 204 | // I decided we don't need to know what damn league they're in, holy shit too much trouble 205 | $events = array(); 206 | 207 | // skip the initial CL (soccer) 208 | for($i = 1; $i < count($gameData); $i++) { 209 | $lineData = $this->parameterizeLine($gameData[$i]); 210 | 211 | if($lineData === FALSE) 212 | continue; 213 | 214 | // "EV" == EVENT 215 | // "CT" == COMPETITION_NAME 216 | // "PA" == PARTICIPANT 217 | // "MA" == MARKET 218 | // "CL" == CLASSIFICATION 219 | // "OR" == ORDER 220 | 221 | if($lineData['cmd'] == 'EV') { 222 | //if($lineData['params']['ID'] != '1') 223 | // continue; 224 | 225 | array_push($events, $lineData['params']); 226 | } elseif ($lineData['cmd'] == 'CT') { 227 | if($lineData['params']['NA'] == 'Coupons') { 228 | break; // It adds some kind of coupon stuff... what 229 | } 230 | } elseif ($lineData['cmd'] == 'CL') { 231 | break; // This isn't soccer m8 232 | } 233 | } 234 | 235 | $requestPow = $this->powRequest(2, array( 236 | 'method: 1' 237 | )); 238 | 239 | echo("Trying for ID: {$events[0]['ID']}\n"); 240 | 241 | $this->unsubscribe('OVInPlay_1_3//'); 242 | 243 | // It basically logs the data from the first soccer match found, if you want other matches loop through the array 244 | $soccerEvent = $this->getSoccerEventInformation($events[0]['ID']); 245 | 246 | var_dump($soccerEvent); 247 | 248 | return FALSE; 249 | } 250 | 251 | function getSoccerEventInformation($id) { 252 | $this->subscribe("$id//"); 253 | 254 | // Update 255 | $requestPow = $this->powRequest(2, array( 256 | 'method: 1' 257 | )); 258 | 259 | $eventExpandedData = explode($this->readItConstants['RECORD_DELIM'], $requestPow); 260 | $eventExpandedData = explode('|', $eventExpandedData[count($eventExpandedData) - 1]); 261 | 262 | //var_dump($eventExpandedData); 263 | 264 | $res = array(); 265 | $res['team1'] = array(); 266 | $res['team2'] = array(); 267 | 268 | $evParsedData = array(); 269 | 270 | for($i = 0; $i < count($eventExpandedData); $i++) { 271 | $parsedLine = $this->parameterizeLine($eventExpandedData[$i]); 272 | 273 | if($parsedLine === FALSE) 274 | continue; 275 | 276 | if($parsedLine['cmd'] == 'EV') { // Event 277 | $evParsedData = $parsedLine['params']; 278 | 279 | //var_dump($evParsedData); 280 | } 281 | elseif($parsedLine['cmd'] == 'TE') { // "TE" = TEAM 282 | $currentArrayTeam = 'team' . ($parsedLine['params']['OR'] + 1); 283 | 284 | $res[$currentArrayTeam]['name'] = $parsedLine['params']['NA']; 285 | 286 | for($stat = 1; $stat < 9; $stat++) { 287 | if(array_key_exists('S' . $stat, $evParsedData)) { 288 | if(empty($parsedLine['params']['S' . $stat])) 289 | continue; 290 | 291 | $res[$currentArrayTeam][$evParsedData['S' . $stat]] = $parsedLine['params']['S' . $stat]; 292 | } 293 | } 294 | } 295 | elseif($parsedLine['cmd'] == 'MA') { // Column Data 296 | $res[$parsedLine['params']['NA']] = array(); 297 | 298 | $columnData = $this->parameterizeLine($eventExpandedData[$i + 1]); 299 | 300 | if($columnData['cmd'] == 'CO' && isset($columnData['params']['CN']) && is_numeric($columnData['params']['CN'])) { 301 | for($cn = 1; $cn < ($columnData['params']['CN'] + 1); $cn++) { 302 | $columnFillData = $this->parameterizeLine($eventExpandedData[$i + 1 + $cn]); 303 | 304 | if($columnFillData['cmd'] == 'PA') { 305 | $res[$parsedLine['params']['NA']][$columnFillData['params']['NA']] = $columnFillData['params']['OD']; 306 | } 307 | } 308 | } 309 | } 310 | elseif ($parsedLine['cmd'] == 'SC') { // "SCORES_COLUMN"? 311 | if(empty($parsedLine['params']['NA'])) 312 | continue; // no? 313 | 314 | $dc11 = $this->parameterizeLine($eventExpandedData[$i + 1]); 315 | $dc12 = $this->parameterizeLine($eventExpandedData[$i + 2]); 316 | 317 | $res['team1'][$parsedLine['params']['NA']] = $dc11['params']['D1']; 318 | $res['team2'][$parsedLine['params']['NA']] = $dc12['params']['D1']; 319 | } 320 | } 321 | 322 | return $res; 323 | } 324 | 325 | function unsubscribe($channel) { 326 | $this->powRequest(2, array( 327 | 'method: 23', 328 | "topic: " . $channel 329 | )); 330 | } 331 | 332 | function subscribe($channel) { 333 | $this->powRequest(2, array( 334 | 'method: 22', 335 | "topic: " . $channel 336 | )); 337 | } 338 | }; 339 | 340 | ?> 341 | --------------------------------------------------------------------------------