def is_left_service(): if (co.find_first_xpath(element, "child::svg[contains(@class, '--serveHome')]") is not None): return True if (co.find_first_xpath(element, "child::svg[contains(@class, '--serveAway')]") is not None): return False
def ingame_score(): def get_xpath(is_left): return (f"child::div[contains(@class,'event__part--" f"{'home' if is_left else 'away'}') and " f"contains(@class,'event__part--6')]") fst_el = co.find_first_xpath(element, get_xpath(is_left=True)) snd_el = co.find_first_xpath(element, get_xpath(is_left=False)) if fst_el is not None and snd_el is not None: result = fst_el.text, snd_el.text if result != ("", ""): return result
def _make_tourinfo(element, start_idx, skip_levels): def is_skip_key(): return ("DOUBLES" in evt_title_text or "EXHIBITION" in evt_title_text or "Next Gen Finals" in evt_title_text or "Laver Cup" in evt_title_text) def is_skip_obj(tourinfo_fs: TourInfoFlashscore): return (tourinfo_fs.level in skip_levels[tourinfo_fs.sex] or tourinfo_fs.sex == SKIP_SEX or tourinfo_fs.teams) def split_before_after_text(text, part): if part in text: before_including = co.strip_after_find(text, part) after_not_including = text[text.index(part) + len(part):] return before_including, after_not_including def split_sex_doubles_text(text): # now ':' is not in text_content() and we need split text by key words: result = split_before_after_text(text, "SINGLES") if result is None: result = split_before_after_text(text, "TEAMS - WOMEN") if result is None: result = split_before_after_text(text, "TEAMS - MEN") return result if "event__header" not in element.get("class"): log.error( "class != event__header got class: '{}' start_idx {}\n{}".format( element.get("class"), start_idx, etree.tostring(element, encoding="utf8"))) raise Exception( "class != event__header got class: '{}' start_idx {}".format( element.get("class"), start_idx)) evt_title_el = co.find_first_xpath( element, "child::div[contains(@class,'event__title ')]") if evt_title_el is None: raise co.TennisParseError( "not parsed sex_doubles el FS TourInfo from\n{}".format( etree.tostring(element, encoding="utf8"))) evt_title_text = evt_title_el.text_content().strip() if not evt_title_text: raise co.TennisParseError("empty sex_doubles txt FS TourInfo") if is_skip_key(): raise LiveEventToskipError() obj = tourinfo_cache.get(evt_title_text) if obj is not None: return obj evt_title_parts = split_sex_doubles_text(evt_title_text) if evt_title_parts is None: raise co.TennisParseError("fail split sex_doubles: '{}'\n{}".format( evt_title_text, etree.tostring(element, encoding="utf8"))) obj = TourInfoFlashscore(evt_title_parts[0], evt_title_parts[1]) if is_skip_obj(obj): tourinfo_cache.put_skip(evt_title_text) else: tourinfo_cache.put(evt_title_text, obj) return obj
def is_right_serve(): xpath_txt = ( "child::div[starts-with(@class, 'away___')]/" "div[starts-with(@class, 'participantServe___')]/" "div[@title='Serving player']" ) return co.find_first_xpath(entry_elem0, xpath_txt) is not None
def is_final_result_only(): # score (not point by point) will be after match finish. # after finish we can not determinate that is was state FRO. el = co.find_first_xpath( element, "child::div[@class='event__time']/div[@title='Final result only.']" ) return el is not None
def fill_inmatch(): xpath_txt = "child::div[starts-with(@class, 'wrapper___')]" inmatch_el = co.find_first_xpath(entry_elem, xpath_txt) if inmatch_el is not None: txt = inmatch_el.text_content().strip() if txt: fst, snd = txt.split("-") if fst and snd: result.inmatch = int(fst), int(snd)
def get_time(): time_el = co.find_first_xpath(element, "child::div[@class='event__time']") if time_el is not None: time_txt = time_el.text if time_txt: hour_txt, minute_txt = time_txt.split(":") return datetime.time(hour=int(hour_txt), minute=int(minute_txt), second=0)
def _make_current_date(root_elem): i_el = co.find_first_xpath(root_elem, "//div[@class='icon icon--calendar']") if i_el is not None: date_txt = i_el.tail if date_txt is not None: date_txt = date_txt.strip() if len(date_txt) >= 5: day_txt, month_txt = date_txt[:5].split(r"/") return datetime.date( year=datetime.date.today().year, month=int(month_txt), day=int(day_txt), )
def fill_details(): status_el = co.find_first_xpath( entry_elem, "child::div[starts-with(@class, 'status___')]" ) if status_el is not None: detstatus_el = co.find_first_xpath( status_el, "child::span[starts-with(@class, 'detailStatus___')]" ) if detstatus_el is not None: text = detstatus_el.text if text: if "Finished" in text: result.finished = True elif "Set " in text: result.setnum = int(text.strip()[4]) scr_el = co.find_first_xpath( status_el, "child::span[starts-with(@class, 'detailScoreServe___')]" ) if scr_el is not None: text = scr_el.text if text and ":" in text: fst, snd = text.strip().split(":") result.inset = int(fst), int(snd)
def get_name_cou(is_left): inclass_txt = "event__participant--{}".format( "home" if is_left else "away") xpath_txt = "child::div[contains(@class, '{}')]".format(inclass_txt) try: plr_el = co.find_first_xpath(element, xpath_txt) if plr_el is None: raise co.TennisParseError("nofound plr_el in \n{}".format( etree.tostring(element, encoding="utf8"))) txt = co.to_ascii(plr_el.text).strip() bracket_idx = txt.index("(") name = txt[0:bracket_idx].strip() cou = txt[bracket_idx + 1:bracket_idx + 4].strip().upper() return name, cou except ValueError: # maybe it is team's result div without '(cou)': tourinfo_cache.edit_skip(live_event.tour_info)
def build_root(url, fsdriver, verbose: bool = False): wdriver.load_url(fsdriver.drv, url) fsdriver.implicitly_wait(3) page = fsdriver.page() if page is None: if verbose: print(f"not fetched summary page in {url}") return None parser = lxml.html.HTMLParser(encoding="utf8") tree = lxml.html.document_fromstring(page, parser) # lxml.html.HtmlElement if tree is None: if verbose: print(f"fail build root url:{url}") return None content_el = co.find_first_xpath(tree, '//*[@id="detail"]') if content_el is None: if verbose: print(f"fail tennis/detail url:{url}") return None return content_el
def current_status( ) -> Tuple[Optional[MatchStatus], Optional[MatchSubStatus]]: status, sub_status = None, None cls_text = element.get("class") if "event__match--scheduled" in cls_text: status = MatchStatus.scheduled elif "event__match--live" in cls_text: status = MatchStatus.live stage_el = co.find_first_xpath( element, "child::div[contains(@class,'event__stage')]") if stage_el is not None: text = stage_el.text_content().strip().lower() if "finished" in text: status = MatchStatus.finished if "retired" in text or "walkover" in text: sub_status = MatchSubStatus.retired elif "interrupted" in text: sub_status = MatchSubStatus.interrupted elif "cancelled" in text: sub_status = MatchSubStatus.cancelled return status, sub_status
def is_live_active(): qtxt = 'descendant::div[@class="tabs__tab selected" and text() = "Live odds"]' return co.find_first_xpath(root, qtxt) is not None
def is_serve(serve_elem): elem = co.find_first_xpath( serve_elem, "child::div[contains(@title, 'Serving player')]" ) return elem is not None
def is_lost_serve(lostserve_elem): span_elem = co.find_first_xpath( lostserve_elem, "child::span[contains(text(), 'LOST SERVE')]" ) return span_elem is not None
def parse_match_detailed_score(match, fsdriver, verbose: bool = False): """return True if ok, False if logged error. type(match) == LiveMatch""" if not match.href or match.score is None or match.score.retired: return sets_count = match.score.sets_count() best_of_five = match.best_of_five det_score = DetailedScore() try: for setnum in range(1, sets_count + 1): det_set_el = build_root( match.pointbypoint_href(setnum), fsdriver, verbose=verbose ) fsdriver.implicitly_wait(random.randint(4, 7)) entry_elem = co.find_first_xpath( det_set_el, "descendant::div[contains(@class,'matchHistoryWrapper')]" ) if entry_elem is None: if verbose: print(f"not found matchHistoryWrapper in set={setnum} {match}") return False set_scr = match.score[setnum - 1] is_decided_set = setnum == (5 if best_of_five else 3) tie_inf = get_tie_info(match, is_decided_set) is_tie = tie_inf.beg_scr is not None and set_scr in ( (tie_inf.beg_scr[0] + 1, tie_inf.beg_scr[1]), (tie_inf.beg_scr[0], tie_inf.beg_scr[1] + 1), ) mhistos = list( entry_elem.xpath("child::div[contains(@class, 'matchHistory')]") ) fifteens = list(entry_elem.xpath("child::div[contains(@class, 'fifteen')]")) n_usial_games = sum(set_scr) - 1 if is_tie else sum(set_scr) for game_idx in range(min(n_usial_games, len(fifteens))): mhisto = mhistos[game_idx] fifteen = fifteens[game_idx] dg_res = _parse_out_result(mhisto) game_winside = dg_res.winner() if game_winside.is_left(): src_score = (dg_res.x - 1, dg_res.y) else: src_score = (dg_res.x, dg_res.y - 1) point_scores_txt = _parse_point_scores(fifteen) nkey = _make_next_key(det_score, setnum, src_score) det_game = make_detailed_game( dg_res.srv_side, game_winside, make_num_pairs(point_scores_txt), tiebreak=False, is_super_tie=False, ) det_score[nkey] = copy.copy(det_game) if is_tie: mhisto = mhistos[n_usial_games] x_sup, y_sup = _parse_detgame_tiesup_score(mhisto) at_xy = (n_usial_games // 2, n_usial_games // 2) nkey = _make_next_key(det_score, setnum, (min(at_xy), min(at_xy))) det_game = _parse_tie_scores( mhistos[n_usial_games + 1 :], setnum, (x_sup, y_sup), tie_inf.is_super, ) det_score[nkey] = copy.copy(det_game) except co.TennisScoreError as err: if verbose: print("{} in {}".format(err, match)) return False if len(det_score) >= 12: match.detailed_score = det_score return True
def parse_title_score(summary_href, fsdriver, verbose: bool = False) -> TitleScore: """return TitleScore or None.""" def is_left_serve(): xpath_txt = ( "child::div[starts-with(@class, 'home___')]/" "div[starts-with(@class, 'participantServe___')]/" "div[@title='Serving player']" ) return co.find_first_xpath(entry_elem0, xpath_txt) is not None def is_right_serve(): xpath_txt = ( "child::div[starts-with(@class, 'away___')]/" "div[starts-with(@class, 'participantServe___')]/" "div[@title='Serving player']" ) return co.find_first_xpath(entry_elem0, xpath_txt) is not None def fill_inmatch(): xpath_txt = "child::div[starts-with(@class, 'wrapper___')]" inmatch_el = co.find_first_xpath(entry_elem, xpath_txt) if inmatch_el is not None: txt = inmatch_el.text_content().strip() if txt: fst, snd = txt.split("-") if fst and snd: result.inmatch = int(fst), int(snd) def fill_details(): status_el = co.find_first_xpath( entry_elem, "child::div[starts-with(@class, 'status___')]" ) if status_el is not None: detstatus_el = co.find_first_xpath( status_el, "child::span[starts-with(@class, 'detailStatus___')]" ) if detstatus_el is not None: text = detstatus_el.text if text: if "Finished" in text: result.finished = True elif "Set " in text: result.setnum = int(text.strip()[4]) scr_el = co.find_first_xpath( status_el, "child::span[starts-with(@class, 'detailScoreServe___')]" ) if scr_el is not None: text = scr_el.text if text and ":" in text: fst, snd = text.strip().split(":") result.inset = int(fst), int(snd) result = TitleScore() root = build_root(summary_href, fsdriver, verbose=verbose) if root is None: if verbose: print(f"root build failed, title score in {summary_href}") return result entry_elem0 = co.find_first_xpath( root, "child::div[starts-with(@class, 'wrapper___')]" ) if entry_elem0 is None: if verbose: print(f"not found entry0 title score in {summary_href}") return result if is_left_serve(): result.is_left_srv = True elif is_right_serve(): result.is_left_srv = False path = ( "child::div[starts-with(@class, 'score___')]/" "div[starts-with(@class, 'matchInfo___')]" ) entry_elem = co.find_first_xpath(entry_elem0, path) if entry_elem is None: if verbose: print(f"not found entry title score in {summary_href}") return result fill_inmatch() fill_details() return result