Bet365 uses a custom, proprietary message format for transmitting data instead of standard JSON or XML. The SDK provides robust parsing capabilities through the Bet365MessageParser and Bet365Section classes.
Bet365’s message format is a pipe-delimited, hierarchical structure that encodes sports data, odds, and metadata:
Basic Structure
TYPE;key1=value1;key2=value2|TYPE;key1=value1|...
Format Components:
- Sections: Separated by
| (pipe)
- Section Type: First element before
; (semicolon)
- Properties: Key-value pairs separated by
; and =
- Messages: Multiple sections separated by
\b (backspace character)
Example Message
CL;PD=#AL#B1#R^1#;NA=Football|MG;ID=1234|MA;NA=1X2|PA;OD=2/1;ID=5678
This represents:
- CL (Classification): Football with path descriptor
- MG (Match Group): Group ID 1234
- MA (Market): Market name “1X2”
- PA (Participant): Odds 2/1, ID 5678
Bet365’s format is optimized for size and parsing speed, reducing bandwidth usage for mobile apps.
Bet365Section Class
The Bet365Section class represents a single parsed section:
@dataclass
class Bet365Section:
"""Represents a parsed section from Bet365 message format."""
type: str
properties: dict[str, str]
def __init__(self, section_type: str, properties: dict[str, str]):
self.type = section_type
self.properties = properties
Core Methods
get_property()
Retrieve a property value with an optional default:
def get_property(self, key: str, default: Any = None) -> str:
"""Get a property value with optional default."""
return str(self.properties.get(key, default))
Usage:
section = Bet365Section("CL", {"NA": "Football", "PD": "#AL#B1#"})
name = section.get_property("NA") # "Football"
path = section.get_property("PD") # "#AL#B1#"
missing = section.get_property("XX", "default") # "default"
has_property()
Check if a property exists:
def has_property(self, key: str) -> bool:
"""Check if section has a specific property."""
return key in self.properties
Usage:
if section.has_property("PD"):
path = section.get_property("PD")
Common Section Types
| Type | Description | Common Properties |
|---|
| CL | Classification/Category | NA (name), PD (path descriptor) |
| MG | Match Group | ID (identifier) |
| MA | Market | NA (name), ID (identifier) |
| PA | Participant/Outcome | OD (odds), ID (identifier), FD (description) |
| CO | Competition | NA (name), ID (identifier) |
| EV | Event/Match | NA (name), ID (identifier) |
| F | Frame delimiter | None (separator only) |
Section types are two-letter codes. Understanding these codes is essential for navigating the message structure.
Bet365MessageParser Class
The Bet365MessageParser class handles parsing and searching through message data:
class Bet365MessageParser:
"""Parser for Bet365 message format data."""
def __init__(self, sections: List[List[Bet365Section]]):
self.sections = sections
Parsing Section Strings
parse_section_string()
Parse a single section string into a Bet365Section object:
@staticmethod
def parse_section_string(section_string: str) -> Bet365Section:
"""
Parse a single section string into a Bet365Section object.
Args:
section_string: String in format "TYPE;key1=value1;key2=value2"
Returns:
Bet365Section object
"""
parts = section_string.split(";")
section_type = parts[0]
properties = {}
for part in parts[1:]:
if part and "=" in part:
key, value = part.split("=", 1)
properties[key] = value
return Bet365Section(section_type, properties)
Example:
section_str = "CL;NA=Football;PD=#AL#B1#R^1#"
section = Bet365MessageParser.parse_section_string(section_str)
print(section.type) # "CL"
print(section.get_property("NA")) # "Football"
print(section.get_property("PD")) # "#AL#B1#R^1#"
parse_sections_list()
Parse multiple section strings:
@staticmethod
def parse_sections_list(sections_list: List[str]) -> List[Bet365Section]:
"""Parse a list of section strings into Bet365Section objects."""
return [
Bet365MessageParser.parse_section_string(section)
for section in sections_list
if section
]
Finding Sections
The find_sections() method is the primary way to search for sections:
def find_sections(
self, node_type: str, include_part_index: bool = False, **filters
) -> Iterator[Tuple[int, Bet365Section]]:
"""
Find sections matching specified criteria across all section groups.
Args:
node_type: The section type to search for
include_part_index: Whether to return part index along with section
**filters: Property filters to apply
Yields:
Tuples of (index, section or part)
"""
for section_group in self.sections:
yield from self._find_in_section_group(
section_group, node_type, include_part_index, **filters
)
Filter Matching
@staticmethod
def _matches_filters(section: Bet365Section, filters: Dict[str, Any]) -> bool:
"""Check if a section matches all specified filters."""
for key, expected_value in filters.items():
if key == "include_part_index":
continue
actual_value = section.get_property(key)
if callable(expected_value):
if not expected_value(key, actual_value):
return False
elif actual_value != expected_value:
return False
return True
Filters can be either exact values or callable functions for complex matching logic.
Parsing Complete Messages
The get_parsers() function creates parser instances from raw response data:
def get_parsers(data: str) -> List[Bet365MessageParser]:
parsers = []
parsed_sections = []
for section_group in data.split("\b"):
sections = section_group.split("|")
parsed_sections.append(Bet365MessageParser.parse_sections_list(sections))
for _section in parsed_sections:
for section in split_list_by_delimiter(_section, Bet365Section("F", {})):
if not section:
continue
parsers.append(Bet365MessageParser([section]))
return parsers
Processing Flow:
- Split by
\b (backspace) to separate messages
- Split each message by
| to get sections
- Parse each section string into
Bet365Section objects
- Split by “F” frame delimiters
- Create
Bet365MessageParser for each group
Real-World Examples
From android.py:115-143:
def extract_available_sports(self) -> list[Sport]:
r = self.protected_get(
f"https://{self.host}/leftnavcontentapi/allsportsmenu",
params={
"lid": "30",
"zid": "0",
"pd": "#AL#B1#R^1#",
},
headers={
"User-Agent": "Mozilla (Linux; Android 12 Phone; CPU M2003J15SC OS 12 like Gecko) Chrome/144.0.7559.59 Gen6 bet365/8.0.36.00",
"X-b365App-ID": "8.0.36.00-row",
"Accept-Encoding": "gzip",
},
)
sports = []
for parser in get_parsers(r.text):
for _, cl in parser.find_sections(
"CL", PD=NOT_NULL, NA=NOT_NULL, include_part_index=True
):
pd = cl.get_property("PD", "")
if pd.endswith("K^5#"):
pd = pd[: -len("K^5#")]
sports.append(Sport(cl.get_property("NA"), pd))
return sports
What’s happening:
- Make request to sports menu API
- Parse response with
get_parsers()
- Find all “CL” sections that have PD and NA properties
- Extract sport name (NA) and path descriptor (PD)
- Clean up PD suffix if present
- Create
Sport objects
Custom Filter Function:
def NOT_NULL(_, m):
return m is not None
This ensures both PD and NA properties exist and are not None.
Always check for None values when extracting properties. Missing properties return the default value (None).
Example 2: Finding Match Tables
From android.py:104-113:
for parser in get_parsers(splash_response.text):
for _, _ in parser.find_sections(
"CL",
PV=lambda k, v: v.startswith("podcontentcontentapi"),
include_part_index=True,
):
for idx, _ in parser.find_sections("MG", include_part_index=True):
table = read_table(parser, idx)
pretty_print_table(table)
match_tables.append(fix_data(table))
What’s happening:
- Parse splash response data
- Find “CL” sections where PV property starts with “podcontentcontentapi”
- Within those sections, find all “MG” (Match Group) sections
- Read table data starting at each MG index
- Format and collect match data
Lambda Filter:
PV=lambda k, v: v.startswith("podcontentcontentapi")
Filters accept callable functions for complex matching.
Example 3: Reading Table Data
From message_parser.py:142-177:
def read_table(parser, idx, extra_properties=[]) -> Dict[str, Any]:
assert parser.sections[0][idx + 1].type == "MA"
result = {"title": parser.sections[0][idx].get_property("NA"), "data": []}
idx += 1
def get_key_name(idx) -> Tuple[str, int]:
key_name = parser.sections[0][idx].get_property("NA")
idx += 1
if (
key_name is None
and parser.sections[0][idx].type == "CO"
and parser.sections[0][idx].get_property("NA")
):
key_name = parser.sections[0][idx].get_property("NA")
idx += 1
extra = {
property: parser.sections[0][idx - 1].get_property(property)
for property in extra_properties
if parser.sections[0][idx - 1].get_property(property)
}
return key_name or "No row", idx, extra
while idx < len(parser.sections[0]) and parser.sections[0][idx].type in [
"MA",
"CO",
]:
name, idx, extra = get_key_name(idx)
data = []
while idx < len(parser.sections[0]) and parser.sections[0][idx].type == "PA":
current_pa = parser.sections[0][idx]
data.append(asdict(current_pa))
idx += 1
result["data"].append({"name": name, "values": data, "extra": extra})
return result
Table Structure:
{
"title": "Match Title",
"data": [
{
"name": "Team/Outcome Name",
"values": [
{"type": "PA", "properties": {"OD": "2/1", "ID": "123"}},
...
],
"extra": {"property": "value"}
},
...
]
}
From message_parser.py:188-211:
def fix_data(table):
data = table["data"]
result = []
for i in range(len(data[0]["values"])):
row = [data[0]["values"][i]["properties"].get("FD", "")]
for j in range(1, len(data)):
row.append(
f"{parse_odds(data[j]['values'][i]['properties'].get('OD', '')):0.2f}"
)
result.append(
{
"FD": data[0]["values"][i]["properties"].get("FD", ""),
"ODS": [
data[j]["values"][i]["properties"].get("OD", "")
for j in range(1, len(data))
],
"ODS_IDS": [
data[j]["values"][i]["properties"].get("ID", "")
for j in range(1, len(data))
],
"other_properties": data[0]["values"][i]["properties"],
}
)
return result
Output Format:
[
{
"FD": "Home Win",
"ODS": ["2/1", "5/2"],
"ODS_IDS": ["12345", "12346"],
"other_properties": {"SU": "1", "FI": "0"}
},
...
]
The parse_odds() function converts fractional odds (“2/1”) to decimal odds (3.00).
Common Property Keys
Universal Properties
| Key | Description | Example |
|---|
| NA | Name/Label | ”Football”, “1X2”, “Manchester United” |
| ID | Unique identifier | ”12345678” |
| PD | Path descriptor | ”#AL#B1#R^1#“ |
| FD | Full description | ”Manchester United vs Liverpool” |
Odds Properties
| Key | Description | Example |
|---|
| OD | Odds (fractional) | “2/1”, “5/2”, “11/10” |
| SU | Suspended flag | ”0” (active), “1” (suspended) |
| FI | Fixed odds indicator | ”0” or “1” |
| OR | Odds reference | Internal reference ID |
Event Properties
| Key | Description | Example |
|---|
| BC | Best code | Event classification code |
| CT | Classification type | Sport/league type |
| EI | Event ID | Unique event identifier |
| TU | Time/Date | Timestamp |
Utility Functions
parse_odds()
From utils.py:6-27:
def parse_odds(odds_string: str) -> Union[float, None]:
"""
Parse odds from fraction format to decimal format.
Args:
odds_string: Odds in "numerator/denominator" format
Returns:
Decimal odds value
"""
if not odds_string:
return 0.0
numerator, denominator = odds_string.split("/")
if denominator == "0":
return 0.0
return float(
(Decimal(numerator) / Decimal(denominator) + Decimal(1)).quantize(
Decimal("0.00"), rounding="ROUND_DOWN"
)
)
Conversion:
"2/1" → 3.00 (2÷1 + 1)
"5/2" → 3.50 (5÷2 + 1)
"11/10" → 2.10 (11÷10 + 1)
split_list_by_delimiter()
From utils.py:55-77:
def split_list_by_delimiter(items: List[Any], delimiter: Any) -> List[List[Any]]:
"""
Split a list into sublists based on a delimiter.
Args:
items: The list to split
delimiter: The value to split on
Returns:
List of sublists split by the delimiter
"""
results = []
items_copy = items.copy()
while delimiter in items_copy:
delimiter_index = items_copy.index(delimiter)
results.append(items_copy[:delimiter_index])
items_copy = items_copy[delimiter_index + 1 :]
if items_copy: # Add remaining items
results.append(items_copy)
return results
Used to split sections by “F” (frame) delimiters.
Best Practices
1. Always Use Filters
# Good: Filter to specific sections
for _, section in parser.find_sections("CL", PD=NOT_NULL, NA=NOT_NULL):
process_section(section)
# Bad: Iterate all sections manually
for section in parser.sections[0]:
if section.type == "CL":
process_section(section)
2. Check Property Existence
# Good: Check before accessing
if section.has_property("OD"):
odds = parse_odds(section.get_property("OD"))
# Bad: Assume property exists
odds = parse_odds(section.get_property("OD")) # May fail if OD is None
3. Use Callable Filters for Complex Logic
# Good: Lambda for complex matching
parser.find_sections(
"PA",
OD=lambda k, v: v and parse_odds(v) > 2.0,
include_part_index=True
)
# Bad: Filter after fetching
for _, section in parser.find_sections("PA", include_part_index=True):
if parse_odds(section.get_property("OD")) > 2.0:
process_section(section)
4. Handle Missing Data Gracefully
# Good: Provide defaults
name = section.get_property("NA", "Unknown")
odds = section.get_property("OD", "0/1")
# Bad: No defaults
name = section.get_property("NA") # Could be None
Troubleshooting
Empty Parser Results
Issue: get_parsers() returns empty list
Solutions:
- Check response data format
- Verify
\b delimiters exist
- Inspect raw response text
Section Not Found
Issue: find_sections() yields no results
Solutions:
- Verify section type code is correct
- Check filter criteria aren’t too restrictive
- Print all section types to inspect structure
for section in parser.sections[0]:
print(f"Type: {section.type}, Properties: {section.properties}")
Issue: get_property() returns None or default
Solutions:
- Use
has_property() to verify existence
- Check property key spelling (case-sensitive)
- Inspect section properties dictionary
When debugging, save the raw response to a file and inspect it manually to understand the structure.
Conclusion
Bet365’s proprietary message format requires specialized parsing, but the SDK provides powerful tools to extract data efficiently:
- Bet365Section: Represents individual sections with property access
- Bet365MessageParser: Finds and filters sections across messages
- get_parsers(): Creates parsers from raw response data
- Filters: Support exact matching and callable functions
- Utility functions: Parse odds, split lists, format data
Mastering these tools enables you to extract any data from Bet365 responses with precision and reliability.