Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pandas/io/html.py: 21%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

350 statements  

1""" 

2:mod:`pandas.io.html` is a module containing functionality for dealing with 

3HTML IO. 

4 

5""" 

6 

7from __future__ import annotations 

8 

9from collections import abc 

10import numbers 

11import re 

12from re import Pattern 

13from typing import ( 

14 TYPE_CHECKING, 

15 Literal, 

16 cast, 

17) 

18 

19from pandas._libs import lib 

20from pandas.compat._optional import import_optional_dependency 

21from pandas.errors import ( 

22 AbstractMethodError, 

23 EmptyDataError, 

24) 

25from pandas.util._decorators import set_module 

26from pandas.util._validators import check_dtype_backend 

27 

28from pandas.core.dtypes.common import is_list_like 

29 

30from pandas import isna 

31from pandas.core.indexes.base import Index 

32from pandas.core.indexes.multi import MultiIndex 

33from pandas.core.series import Series 

34 

35from pandas.io.common import ( 

36 get_handle, 

37 is_url, 

38 stringify_path, 

39 validate_header_arg, 

40) 

41from pandas.io.formats.printing import pprint_thing 

42from pandas.io.parsers import TextParser 

43 

44if TYPE_CHECKING: 

45 from collections.abc import ( 

46 Iterable, 

47 Sequence, 

48 ) 

49 

50 from pandas._typing import ( 

51 BaseBuffer, 

52 DtypeBackend, 

53 FilePath, 

54 HTMLFlavors, 

55 ReadBuffer, 

56 StorageOptions, 

57 ) 

58 

59 from pandas import DataFrame 

60 

61############# 

62# READ HTML # 

63############# 

64_RE_WHITESPACE = re.compile(r"[\r\n]+|\s{2,}") 

65 

66 

67def _remove_whitespace(s: str, regex: Pattern = _RE_WHITESPACE) -> str: 

68 """ 

69 Replace extra whitespace inside of a string with a single space. 

70 

71 Parameters 

72 ---------- 

73 s : str or unicode 

74 The string from which to remove extra whitespace. 

75 regex : re.Pattern 

76 The regular expression to use to remove extra whitespace. 

77 

78 Returns 

79 ------- 

80 subd : str or unicode 

81 `s` with all extra whitespace replaced with a single space. 

82 """ 

83 return regex.sub(" ", s.strip()) 

84 

85 

86def _get_skiprows(skiprows: int | Sequence[int] | slice | None) -> int | Sequence[int]: 

87 """ 

88 Get an iterator given an integer, slice or container. 

89 

90 Parameters 

91 ---------- 

92 skiprows : int, slice, container 

93 The iterator to use to skip rows; can also be a slice. 

94 

95 Raises 

96 ------ 

97 TypeError 

98 * If `skiprows` is not a slice, integer, or Container 

99 

100 Returns 

101 ------- 

102 it : iterable 

103 A proper iterator to use to skip rows of a DataFrame. 

104 """ 

105 if isinstance(skiprows, slice): 

106 start, step = skiprows.start or 0, skiprows.step or 1 

107 return list(range(start, skiprows.stop, step)) 

108 elif isinstance(skiprows, numbers.Integral) or is_list_like(skiprows): 

109 return cast("int | Sequence[int]", skiprows) 

110 elif skiprows is None: 

111 return 0 

112 raise TypeError(f"{type(skiprows).__name__} is not a valid type for skipping rows") 

113 

114 

115def _read( 

116 obj: FilePath | BaseBuffer, 

117 encoding: str | None, 

118 storage_options: StorageOptions | None, 

119) -> str | bytes: 

120 """ 

121 Try to read from a url, file or string. 

122 

123 Parameters 

124 ---------- 

125 obj : str, unicode, path object, or file-like object 

126 

127 Returns 

128 ------- 

129 raw_text : str 

130 """ 

131 with get_handle( 

132 obj, "r", encoding=encoding, storage_options=storage_options 

133 ) as handles: 

134 return handles.handle.read() 

135 

136 

137class _HtmlFrameParser: 

138 """ 

139 Base class for parsers that parse HTML into DataFrames. 

140 

141 Parameters 

142 ---------- 

143 io : str or file-like 

144 This can be either a string path, a valid URL using the HTTP, 

145 FTP, or FILE protocols or a file-like object. 

146 

147 match : str or regex 

148 The text to match in the document. 

149 

150 attrs : dict 

151 List of HTML <table> element attributes to match. 

152 

153 encoding : str 

154 Encoding to be used by parser 

155 

156 displayed_only : bool 

157 Whether or not items with "display:none" should be ignored 

158 

159 extract_links : {None, "all", "header", "body", "footer"} 

160 Table elements in the specified section(s) with <a> tags will have their 

161 href extracted. 

162 

163 Attributes 

164 ---------- 

165 io : str or file-like 

166 raw HTML, URL, or file-like object 

167 

168 match : regex 

169 The text to match in the raw HTML 

170 

171 attrs : dict-like 

172 A dictionary of valid table attributes to use to search for table 

173 elements. 

174 

175 encoding : str 

176 Encoding to be used by parser 

177 

178 displayed_only : bool 

179 Whether or not items with "display:none" should be ignored 

180 

181 extract_links : {None, "all", "header", "body", "footer"} 

182 Table elements in the specified section(s) with <a> tags will have their 

183 href extracted. 

184 

185 Notes 

186 ----- 

187 To subclass this class effectively you must override the following methods: 

188 * :func:`_build_doc` 

189 * :func:`_attr_getter` 

190 * :func:`_href_getter` 

191 * :func:`_text_getter` 

192 * :func:`_parse_td` 

193 * :func:`_parse_thead_tr` 

194 * :func:`_parse_tbody_tr` 

195 * :func:`_parse_tfoot_tr` 

196 * :func:`_parse_tables` 

197 * :func:`_equals_tag` 

198 See each method's respective documentation for details on their 

199 functionality. 

200 """ 

201 

202 def __init__( 

203 self, 

204 io: FilePath | ReadBuffer[str] | ReadBuffer[bytes], 

205 match: str | Pattern, 

206 attrs: dict[str, str] | None, 

207 encoding: str, 

208 displayed_only: bool, 

209 extract_links: Literal["header", "footer", "body", "all"] | None, 

210 storage_options: StorageOptions = None, 

211 ) -> None: 

212 self.io = io 

213 self.match = match 

214 self.attrs = attrs 

215 self.encoding = encoding 

216 self.displayed_only = displayed_only 

217 self.extract_links = extract_links 

218 self.storage_options = storage_options 

219 

220 def parse_tables(self): 

221 """ 

222 Parse and return all tables from the DOM. 

223 

224 Returns 

225 ------- 

226 list of parsed (header, body, footer) tuples from tables. 

227 """ 

228 tables = self._parse_tables(self._build_doc(), self.match, self.attrs) 

229 return (self._parse_thead_tbody_tfoot(table) for table in tables) 

230 

231 def _attr_getter(self, obj, attr): 

232 """ 

233 Return the attribute value of an individual DOM node. 

234 

235 Parameters 

236 ---------- 

237 obj : node-like 

238 A DOM node. 

239 

240 attr : str or unicode 

241 The attribute, such as "colspan" 

242 

243 Returns 

244 ------- 

245 str or unicode 

246 The attribute value. 

247 """ 

248 # Both lxml and BeautifulSoup have the same implementation: 

249 return obj.get(attr) 

250 

251 def _href_getter(self, obj) -> str | None: 

252 """ 

253 Return an href if the DOM node contains a child <a> or None. 

254 

255 Parameters 

256 ---------- 

257 obj : node-like 

258 A DOM node. 

259 

260 Returns 

261 ------- 

262 href : str or unicode 

263 The href from the <a> child of the DOM node. 

264 """ 

265 raise AbstractMethodError(self) 

266 

267 def _text_getter(self, obj): 

268 """ 

269 Return the text of an individual DOM node. 

270 

271 Parameters 

272 ---------- 

273 obj : node-like 

274 A DOM node. 

275 

276 Returns 

277 ------- 

278 text : str or unicode 

279 The text from an individual DOM node. 

280 """ 

281 raise AbstractMethodError(self) 

282 

283 def _parse_td(self, obj): 

284 """ 

285 Return the td elements from a row element. 

286 

287 Parameters 

288 ---------- 

289 obj : node-like 

290 A DOM <tr> node. 

291 

292 Returns 

293 ------- 

294 list of node-like 

295 These are the elements of each row, i.e., the columns. 

296 """ 

297 raise AbstractMethodError(self) 

298 

299 def _parse_thead_tr(self, table): 

300 """ 

301 Return the list of thead row elements from the parsed table element. 

302 

303 Parameters 

304 ---------- 

305 table : a table element that contains zero or more thead elements. 

306 

307 Returns 

308 ------- 

309 list of node-like 

310 These are the <tr> row elements of a table. 

311 """ 

312 raise AbstractMethodError(self) 

313 

314 def _parse_tbody_tr(self, table): 

315 """ 

316 Return the list of tbody row elements from the parsed table element. 

317 

318 HTML5 table bodies consist of either 0 or more <tbody> elements (which 

319 only contain <tr> elements) or 0 or more <tr> elements. This method 

320 checks for both structures. 

321 

322 Parameters 

323 ---------- 

324 table : a table element that contains row elements. 

325 

326 Returns 

327 ------- 

328 list of node-like 

329 These are the <tr> row elements of a table. 

330 """ 

331 raise AbstractMethodError(self) 

332 

333 def _parse_tfoot_tr(self, table): 

334 """ 

335 Return the list of tfoot row elements from the parsed table element. 

336 

337 Parameters 

338 ---------- 

339 table : a table element that contains row elements. 

340 

341 Returns 

342 ------- 

343 list of node-like 

344 These are the <tr> row elements of a table. 

345 """ 

346 raise AbstractMethodError(self) 

347 

348 def _parse_tables(self, document, match, attrs): 

349 """ 

350 Return all tables from the parsed DOM. 

351 

352 Parameters 

353 ---------- 

354 document : the DOM from which to parse the table element. 

355 

356 match : str or regular expression 

357 The text to search for in the DOM tree. 

358 

359 attrs : dict 

360 A dictionary of table attributes that can be used to disambiguate 

361 multiple tables on a page. 

362 

363 Raises 

364 ------ 

365 ValueError : `match` does not match any text in the document. 

366 

367 Returns 

368 ------- 

369 list of node-like 

370 HTML <table> elements to be parsed into raw data. 

371 """ 

372 raise AbstractMethodError(self) 

373 

374 def _equals_tag(self, obj, tag) -> bool: 

375 """ 

376 Return whether an individual DOM node matches a tag 

377 

378 Parameters 

379 ---------- 

380 obj : node-like 

381 A DOM node. 

382 

383 tag : str 

384 Tag name to be checked for equality. 

385 

386 Returns 

387 ------- 

388 boolean 

389 Whether `obj`'s tag name is `tag` 

390 """ 

391 raise AbstractMethodError(self) 

392 

393 def _build_doc(self): 

394 """ 

395 Return a tree-like object that can be used to iterate over the DOM. 

396 

397 Returns 

398 ------- 

399 node-like 

400 The DOM from which to parse the table element. 

401 """ 

402 raise AbstractMethodError(self) 

403 

404 def _parse_thead_tbody_tfoot(self, table_html): 

405 """ 

406 Given a table, return parsed header, body, and foot. 

407 

408 Parameters 

409 ---------- 

410 table_html : node-like 

411 

412 Returns 

413 ------- 

414 tuple of (header, body, footer), each a list of list-of-text rows. 

415 

416 Notes 

417 ----- 

418 Header and body are lists-of-lists. Top level list is a list of 

419 rows. Each row is a list of str text. 

420 

421 Logic: Use <thead>, <tbody>, <tfoot> elements to identify 

422 header, body, and footer, otherwise: 

423 - Put all rows into body 

424 - Move rows from top of body to header only if 

425 all elements inside row are <th> 

426 - Move rows from bottom of body to footer only if 

427 all elements inside row are <th> 

428 """ 

429 header_rows = self._parse_thead_tr(table_html) 

430 body_rows = self._parse_tbody_tr(table_html) 

431 footer_rows = self._parse_tfoot_tr(table_html) 

432 

433 def row_is_all_th(row): 

434 return all(self._equals_tag(t, "th") for t in self._parse_td(row)) 

435 

436 if not header_rows: 

437 # The table has no <thead>. Move the top all-<th> rows from 

438 # body_rows to header_rows. (This is a common case because many 

439 # tables in the wild have no <thead> or <tfoot> 

440 while body_rows and row_is_all_th(body_rows[0]): 

441 header_rows.append(body_rows.pop(0)) 

442 

443 header, rem = self._expand_colspan_rowspan(header_rows, section="header") 

444 body, rem = self._expand_colspan_rowspan( 

445 body_rows, 

446 section="body", 

447 remainder=rem, 

448 overflow=len(footer_rows) > 0, 

449 ) 

450 footer, _ = self._expand_colspan_rowspan( 

451 footer_rows, section="footer", remainder=rem, overflow=False 

452 ) 

453 

454 return header, body, footer 

455 

456 def _expand_colspan_rowspan( 

457 self, 

458 rows, 

459 section: Literal["header", "footer", "body"], 

460 remainder: list[tuple[int, str | tuple, int]] | None = None, 

461 overflow: bool = True, 

462 ) -> tuple[list[list], list[tuple[int, str | tuple, int]]]: 

463 """ 

464 Given a list of <tr>s, return a list of text rows. 

465 

466 Parameters 

467 ---------- 

468 rows : list of node-like 

469 List of <tr>s 

470 section : the section that the rows belong to (header, body or footer). 

471 remainder: list[tuple[int, str | tuple, int]] | None 

472 Any remainder from the expansion of previous section 

473 overflow: bool 

474 If true, return any partial rows as 'remainder'. If not, use up any 

475 partial rows. True by default. 

476 

477 Returns 

478 ------- 

479 list of list 

480 Each returned row is a list of str text, or tuple (text, link) 

481 if extract_links is not None. 

482 remainder 

483 Remaining partial rows if any. If overflow is False, an empty list 

484 is returned. 

485 

486 Notes 

487 ----- 

488 Any cell with ``rowspan`` or ``colspan`` will have its contents copied 

489 to subsequent cells. 

490 """ 

491 all_texts = [] # list of rows, each a list of str 

492 text: str | tuple 

493 remainder = remainder if remainder is not None else [] 

494 

495 for tr in rows: 

496 texts = [] # the output for this row 

497 next_remainder = [] 

498 

499 index = 0 

500 tds = self._parse_td(tr) 

501 for td in tds: 

502 # Append texts from previous rows with rowspan>1 that come 

503 # before this <td> 

504 while remainder and remainder[0][0] <= index: 

505 prev_i, prev_text, prev_rowspan = remainder.pop(0) 

506 texts.append(prev_text) 

507 if prev_rowspan > 1: 

508 next_remainder.append((prev_i, prev_text, prev_rowspan - 1)) 

509 index += 1 

510 

511 # Append the text from this <td>, colspan times 

512 text = _remove_whitespace(self._text_getter(td)) 

513 if self.extract_links in ("all", section): 

514 href = self._href_getter(td) 

515 text = (text, href) 

516 rowspan = int(self._attr_getter(td, "rowspan") or 1) 

517 colspan = int(self._attr_getter(td, "colspan") or 1) 

518 

519 for _ in range(colspan): 

520 texts.append(text) 

521 if rowspan > 1: 

522 next_remainder.append((index, text, rowspan - 1)) 

523 index += 1 

524 

525 # Append texts from previous rows at the final position 

526 for prev_i, prev_text, prev_rowspan in remainder: 

527 texts.append(prev_text) 

528 if prev_rowspan > 1: 

529 next_remainder.append((prev_i, prev_text, prev_rowspan - 1)) 

530 

531 all_texts.append(texts) 

532 remainder = next_remainder 

533 

534 if not overflow: 

535 # Append rows that only appear because the previous row had non-1 

536 # rowspan 

537 while remainder: 

538 next_remainder = [] 

539 texts = [] 

540 for prev_i, prev_text, prev_rowspan in remainder: 

541 texts.append(prev_text) 

542 if prev_rowspan > 1: 

543 next_remainder.append((prev_i, prev_text, prev_rowspan - 1)) 

544 all_texts.append(texts) 

545 remainder = next_remainder 

546 

547 return all_texts, remainder 

548 

549 def _handle_hidden_tables(self, tbl_list, attr_name: str): 

550 """ 

551 Return list of tables, potentially removing hidden elements 

552 

553 Parameters 

554 ---------- 

555 tbl_list : list of node-like 

556 Type of list elements will vary depending upon parser used 

557 attr_name : str 

558 Name of the accessor for retrieving HTML attributes 

559 

560 Returns 

561 ------- 

562 list of node-like 

563 Return type matches `tbl_list` 

564 """ 

565 if not self.displayed_only: 

566 return tbl_list 

567 

568 return [ 

569 x 

570 for x in tbl_list 

571 if "display:none" 

572 not in getattr(x, attr_name).get("style", "").replace(" ", "") 

573 ] 

574 

575 

576class _BeautifulSoupHtml5LibFrameParser(_HtmlFrameParser): 

577 """ 

578 HTML to DataFrame parser that uses BeautifulSoup under the hood. 

579 

580 See Also 

581 -------- 

582 pandas.io.html._HtmlFrameParser 

583 pandas.io.html._LxmlFrameParser 

584 

585 Notes 

586 ----- 

587 Documentation strings for this class are in the base class 

588 :class:`pandas.io.html._HtmlFrameParser`. 

589 """ 

590 

591 def _parse_tables(self, document, match, attrs): 

592 element_name = "table" 

593 tables = document.find_all(element_name, attrs=attrs) 

594 if not tables: 

595 raise ValueError("No tables found") 

596 

597 result = [] 

598 unique_tables = set() 

599 tables = self._handle_hidden_tables(tables, "attrs") 

600 

601 for table in tables: 

602 if self.displayed_only: 

603 for elem in table.find_all("style"): 

604 elem.decompose() 

605 

606 for elem in table.find_all(style=re.compile(r"display:\s*none")): 

607 elem.decompose() 

608 

609 if table not in unique_tables and table.find(string=match) is not None: 

610 result.append(table) 

611 unique_tables.add(table) 

612 if not result: 

613 raise ValueError(f"No tables found matching pattern {match.pattern!r}") 

614 return result 

615 

616 def _href_getter(self, obj) -> str | None: 

617 a = obj.find("a", href=True) 

618 return None if not a else a["href"] 

619 

620 def _text_getter(self, obj): 

621 return obj.text 

622 

623 def _equals_tag(self, obj, tag) -> bool: 

624 return obj.name == tag 

625 

626 def _parse_td(self, row): 

627 return row.find_all(("td", "th"), recursive=False) 

628 

629 def _parse_thead_tr(self, table): 

630 return table.select(":scope thead tr") 

631 

632 def _parse_tbody_tr(self, table): 

633 from_tbody = table.select(":scope tbody tr") 

634 from_root = table.find_all("tr", recursive=False) 

635 # HTML spec: at most one of these lists has content 

636 return from_tbody + from_root 

637 

638 def _parse_tfoot_tr(self, table): 

639 return table.select(":scope tfoot tr") 

640 

641 def _setup_build_doc(self): 

642 raw_text = _read(self.io, self.encoding, self.storage_options) 

643 if not raw_text: 

644 raise ValueError(f"No text parsed from document: {self.io}") 

645 return raw_text 

646 

647 def _build_doc(self): 

648 from bs4 import BeautifulSoup 

649 

650 bdoc = self._setup_build_doc() 

651 if isinstance(bdoc, bytes) and self.encoding is not None: 

652 udoc = bdoc.decode(self.encoding) 

653 from_encoding = None 

654 else: 

655 udoc = bdoc 

656 from_encoding = self.encoding 

657 

658 soup = BeautifulSoup(udoc, features="html5lib", from_encoding=from_encoding) 

659 

660 for br in soup.find_all("br"): 

661 br.replace_with("\n" + br.text) 

662 

663 return soup 

664 

665 

666def _build_xpath_expr(attrs) -> str: 

667 """ 

668 Build an xpath expression to simulate bs4's ability to pass in kwargs to 

669 search for attributes when using the lxml parser. 

670 

671 Parameters 

672 ---------- 

673 attrs : dict 

674 A dict of HTML attributes. These are NOT checked for validity. 

675 

676 Returns 

677 ------- 

678 expr : unicode 

679 An XPath expression that checks for the given HTML attributes. 

680 """ 

681 # give class attribute as class_ because class is a python keyword 

682 if "class_" in attrs: 

683 attrs["class"] = attrs.pop("class_") 

684 

685 s = " and ".join([f"@{k}={v!r}" for k, v in attrs.items()]) 

686 return f"[{s}]" 

687 

688 

689_re_namespace = {"re": "http://exslt.org/regular-expressions"} 

690 

691 

692class _LxmlFrameParser(_HtmlFrameParser): 

693 """ 

694 HTML to DataFrame parser that uses lxml under the hood. 

695 

696 Warning 

697 ------- 

698 This parser can only handle HTTP, FTP, and FILE urls. 

699 

700 See Also 

701 -------- 

702 _HtmlFrameParser 

703 _BeautifulSoupLxmlFrameParser 

704 

705 Notes 

706 ----- 

707 Documentation strings for this class are in the base class 

708 :class:`_HtmlFrameParser`. 

709 """ 

710 

711 def _href_getter(self, obj) -> str | None: 

712 href = obj.xpath(".//a/@href") 

713 return None if not href else href[0] 

714 

715 def _text_getter(self, obj): 

716 return obj.text_content() 

717 

718 def _parse_td(self, row): 

719 # Look for direct children only: the "row" element here may be a 

720 # <thead> or <tfoot> (see _parse_thead_tr). 

721 return row.xpath("./td|./th") 

722 

723 def _parse_tables(self, document, match, kwargs): 

724 pattern = match.pattern 

725 

726 # 1. check all descendants for the given pattern and only search tables 

727 # GH 49929 

728 xpath_expr = f"//table[.//text()[re:test(., {pattern!r})]]" 

729 

730 # if any table attributes were given build an xpath expression to 

731 # search for them 

732 if kwargs: 

733 xpath_expr += _build_xpath_expr(kwargs) 

734 

735 tables = document.xpath(xpath_expr, namespaces=_re_namespace) 

736 

737 tables = self._handle_hidden_tables(tables, "attrib") 

738 if self.displayed_only: 

739 for table in tables: 

740 # lxml utilizes XPATH 1.0 which does not have regex 

741 # support. As a result, we find all elements with a style 

742 # attribute and iterate them to check for display:none 

743 for elem in table.xpath(".//style"): 

744 elem.drop_tree() 

745 for elem in table.xpath(".//*[@style]"): 

746 if "display:none" in elem.attrib.get("style", "").replace(" ", ""): 

747 elem.drop_tree() 

748 if not tables: 

749 raise ValueError(f"No tables found matching regex {pattern!r}") 

750 return tables 

751 

752 def _equals_tag(self, obj, tag) -> bool: 

753 return obj.tag == tag 

754 

755 def _raise_if_unreadable(self) -> None: 

756 # lxml does not report an unreadable local file consistently -- some 

757 # builds raise an errno-less OSError, others parse it as an empty 

758 # document -- so reopen the path to let the real error escape. GH#29125 

759 if isinstance(self.io, (str, bytes)) and not is_url(self.io): 

760 with open(self.io, "rb"): 

761 pass 

762 

763 def _build_doc(self): 

764 """ 

765 Raises 

766 ------ 

767 ValueError 

768 * If a URL that lxml cannot parse is passed. 

769 

770 Exception 

771 * Any other ``Exception`` thrown. For example, trying to parse a 

772 URL that is syntactically correct on a machine with no internet 

773 connection will fail. 

774 

775 See Also 

776 -------- 

777 pandas.io.html._HtmlFrameParser._build_doc 

778 """ 

779 from lxml.etree import XMLSyntaxError 

780 from lxml.html import ( 

781 HTMLParser, 

782 parse, 

783 ) 

784 

785 parser = HTMLParser(recover=True, encoding=self.encoding) 

786 

787 if is_url(self.io): 

788 with get_handle(self.io, "r", storage_options=self.storage_options) as f: 

789 r = parse(f.handle, parser=parser) 

790 else: 

791 # try to parse the input in the simplest way 

792 try: 

793 r = parse(self.io, parser=parser) 

794 except OSError: 

795 self._raise_if_unreadable() 

796 raise 

797 try: 

798 r = r.getroot() 

799 except AttributeError: 

800 pass 

801 else: 

802 if not hasattr(r, "text_content"): 

803 self._raise_if_unreadable() 

804 raise XMLSyntaxError("no text parsed from document", 0, 0, 0) 

805 

806 for br in r.xpath("*//br"): 

807 br.tail = "\n" + (br.tail or "") 

808 

809 return r 

810 

811 def _parse_thead_tr(self, table): 

812 rows = [] 

813 

814 for thead in table.xpath(".//thead"): 

815 rows.extend(thead.xpath("./tr")) 

816 

817 # HACK: lxml does not clean up the clearly-erroneous 

818 # <thead><th>foo</th><th>bar</th></thead>. (Missing <tr>). Add 

819 # the <thead> and _pretend_ it's a <tr>; _parse_td() will find its 

820 # children as though it's a <tr>. 

821 # 

822 # Better solution would be to use html5lib. 

823 elements_at_root = thead.xpath("./td|./th") 

824 if elements_at_root: 

825 rows.append(thead) 

826 

827 return rows 

828 

829 def _parse_tbody_tr(self, table): 

830 from_tbody = table.xpath(".//tbody//tr") 

831 from_root = table.xpath("./tr") 

832 # HTML spec: at most one of these lists has content 

833 return from_tbody + from_root 

834 

835 def _parse_tfoot_tr(self, table): 

836 return table.xpath(".//tfoot//tr") 

837 

838 

839def _expand_elements(body) -> None: 

840 data = [len(elem) for elem in body] 

841 lens = Series(data) 

842 lens_max = lens.max() 

843 not_max = lens[lens != lens_max] 

844 

845 empty = [""] 

846 for ind, length in not_max.items(): 

847 body[ind] += empty * (lens_max - length) 

848 

849 

850def _data_to_frame(**kwargs): 

851 head, body, foot = kwargs.pop("data") 

852 header = kwargs.pop("header") 

853 kwargs["skiprows"] = _get_skiprows(kwargs["skiprows"]) 

854 if head: 

855 body = head + body 

856 

857 # Infer header when there is a <thead> or top <th>-only rows 

858 if header is None: 

859 if len(head) == 1: 

860 header = 0 

861 else: 

862 # ignore all-empty-text rows 

863 header = [i for i, row in enumerate(head) if any(text for text in row)] 

864 

865 if foot: 

866 body += foot 

867 

868 # fill out elements of body that are "ragged" 

869 _expand_elements(body) 

870 with TextParser(body, header=header, **kwargs) as tp: 

871 return tp.read() 

872 

873 

874_valid_parsers = { 

875 "lxml": _LxmlFrameParser, 

876 None: _LxmlFrameParser, 

877 "html5lib": _BeautifulSoupHtml5LibFrameParser, 

878 "bs4": _BeautifulSoupHtml5LibFrameParser, 

879} 

880 

881 

882def _parser_dispatch(flavor: HTMLFlavors | None) -> type[_HtmlFrameParser]: 

883 """ 

884 Choose the parser based on the input flavor. 

885 

886 Parameters 

887 ---------- 

888 flavor : {"lxml", "html5lib", "bs4"} or None 

889 The type of parser to use. This must be a valid backend. 

890 

891 Returns 

892 ------- 

893 cls : _HtmlFrameParser subclass 

894 The parser class based on the requested input flavor. 

895 

896 Raises 

897 ------ 

898 ValueError 

899 * If `flavor` is not a valid backend. 

900 ImportError 

901 * If you do not have the requested `flavor` 

902 """ 

903 valid_parsers = list(_valid_parsers.keys()) 

904 if flavor not in valid_parsers: 

905 raise ValueError( 

906 f"{flavor!r} is not a valid flavor, valid flavors are {valid_parsers}" 

907 ) 

908 

909 if flavor in ("bs4", "html5lib"): 

910 import_optional_dependency("html5lib") 

911 import_optional_dependency("bs4") 

912 else: 

913 import_optional_dependency("lxml.etree") 

914 return _valid_parsers[flavor] 

915 

916 

917def _print_as_set(s) -> str: 

918 arg = ", ".join([pprint_thing(el) for el in s]) 

919 return f"{{{arg}}}" 

920 

921 

922def _validate_flavor(flavor): 

923 if flavor is None: 

924 flavor = "lxml", "bs4" 

925 elif isinstance(flavor, str): 

926 flavor = (flavor,) 

927 elif isinstance(flavor, abc.Iterable): 

928 if not all(isinstance(flav, str) for flav in flavor): 

929 raise TypeError( 

930 f"Object of type {type(flavor).__name__!r} " 

931 f"is not an iterable of strings" 

932 ) 

933 else: 

934 msg = repr(flavor) if isinstance(flavor, str) else str(flavor) 

935 msg += " is not a valid flavor" 

936 raise ValueError(msg) 

937 

938 flavor = tuple(flavor) 

939 valid_flavors = set(_valid_parsers) 

940 flavor_set = set(flavor) 

941 

942 if not flavor_set & valid_flavors: 

943 raise ValueError( 

944 f"{_print_as_set(flavor_set)} is not a valid set of flavors, valid " 

945 f"flavors are {_print_as_set(valid_flavors)}" 

946 ) 

947 return flavor 

948 

949 

950def _parse( 

951 flavor, 

952 io, 

953 match, 

954 attrs, 

955 encoding, 

956 displayed_only, 

957 extract_links, 

958 storage_options, 

959 **kwargs, 

960): 

961 flavor = _validate_flavor(flavor) 

962 compiled_match = re.compile(match) # you can pass a compiled regex here 

963 

964 retained = None 

965 for flav in flavor: 

966 parser = _parser_dispatch(flav) 

967 p = parser( 

968 io, 

969 compiled_match, 

970 attrs, 

971 encoding, 

972 displayed_only, 

973 extract_links, 

974 storage_options, 

975 ) 

976 

977 try: 

978 tables = p.parse_tables() 

979 except ValueError as caught: 

980 # if `io` is an io-like object, check if it's seekable 

981 # and try to rewind it before trying the next parser 

982 if hasattr(io, "seekable") and io.seekable(): 

983 io.seek(0) 

984 elif hasattr(io, "seekable") and not io.seekable(): 

985 # if we couldn't rewind it, let the user know 

986 raise ValueError( 

987 f"The flavor {flav} failed to parse your input. " 

988 "Since you passed a non-rewindable file " 

989 "object, we can't rewind it to try " 

990 "another parser. Try read_html() with a different flavor." 

991 ) from caught 

992 

993 retained = caught 

994 else: 

995 break 

996 else: 

997 assert retained is not None # for mypy 

998 raise retained 

999 

1000 ret = [] 

1001 for table in tables: 

1002 try: 

1003 df = _data_to_frame(data=table, **kwargs) 

1004 # Cast MultiIndex header to an Index of tuples when extracting header 

1005 # links and replace nan with None (therefore can't use mi.to_flat_index()). 

1006 # This maintains consistency of selection (e.g. df.columns.str[1]) 

1007 if extract_links in ("all", "header") and isinstance( 

1008 df.columns, MultiIndex 

1009 ): 

1010 df.columns = Index( 

1011 ((col[0], None if isna(col[1]) else col[1]) for col in df.columns), 

1012 tupleize_cols=False, 

1013 ) 

1014 

1015 ret.append(df) 

1016 except EmptyDataError: # empty table 

1017 continue 

1018 return ret 

1019 

1020 

1021@set_module("pandas") 

1022def read_html( 

1023 io: FilePath | ReadBuffer[str], 

1024 *, 

1025 match: str | Pattern = ".+", 

1026 flavor: HTMLFlavors | Sequence[HTMLFlavors] | None = None, 

1027 header: int | Sequence[int] | None = None, 

1028 index_col: int | Sequence[int] | None = None, 

1029 skiprows: int | Sequence[int] | slice | None = None, 

1030 attrs: dict[str, str] | None = None, 

1031 parse_dates: bool = False, 

1032 thousands: str | None = ",", 

1033 encoding: str | None = None, 

1034 decimal: str = ".", 

1035 converters: dict | None = None, 

1036 na_values: Iterable[object] | None = None, 

1037 keep_default_na: bool = True, 

1038 displayed_only: bool = True, 

1039 extract_links: Literal["header", "footer", "body", "all"] | None = None, 

1040 dtype_backend: DtypeBackend | lib.NoDefault = lib.no_default, 

1041 storage_options: StorageOptions = None, 

1042) -> list[DataFrame]: 

1043 r""" 

1044 Read HTML tables into a ``list`` of ``DataFrame`` objects. 

1045 

1046 This function requires one of the following libraries: 

1047 `lxml <https://lxml.de/>`_, `html5lib <https://github.com/html5lib/html5lib-python>`_, 

1048 or `beautifulsoup4 <https://www.crummy.com/software/BeautifulSoup/>`_. 

1049 

1050 This function searches for ``<table>`` elements within an HTML document 

1051 and parses their rows and columns into DataFrames. It can read from a URL, 

1052 a file path, or a raw HTML string, and supports filtering tables by 

1053 matching text content via a regular expression. 

1054 

1055 Parameters 

1056 ---------- 

1057 io : str, path object, or file-like object 

1058 String path, path object (implementing ``os.PathLike[str]``), or file-like 

1059 object implementing a string ``read()`` function. 

1060 The string can represent a URL. Note that 

1061 lxml only accepts the http, ftp and file url protocols. If you have a 

1062 URL that starts with ``'https'`` you might try removing the ``'s'``. 

1063 

1064 match : str or compiled regular expression, optional 

1065 The set of tables containing text matching this regex or string will be 

1066 returned. Unless the HTML is extremely simple you will probably need to 

1067 pass a non-empty string here. Defaults to '.+' (match any non-empty 

1068 string). The default value will return all tables contained on a page. 

1069 This value is converted to a regular expression so that there is 

1070 consistent behavior between Beautiful Soup and lxml. 

1071 

1072 flavor : {"lxml", "html5lib", "bs4"} or list-like, optional 

1073 The parsing engine (or list of parsing engines) to use. 'bs4' and 

1074 'html5lib' are synonymous with each other, they are both there for 

1075 backwards compatibility. The default of ``None`` tries to use ``lxml`` 

1076 to parse and if that fails it falls back on ``bs4`` + ``html5lib``. 

1077 

1078 header : int or list-like, optional 

1079 The row (or list of rows for a :class:`~pandas.MultiIndex`) to use to 

1080 make the columns headers. 

1081 

1082 index_col : int or list-like, optional 

1083 The column (or list of columns) to use to create the index. 

1084 

1085 skiprows : int, list-like or slice, optional 

1086 Number of rows to skip after parsing the header. 0-based. If a 

1087 sequence of integers or a slice is given, will skip the rows indexed by 

1088 that sequence. Note that a single element sequence means 'skip the nth 

1089 row' whereas an integer means 'skip n rows'. 

1090 

1091 attrs : dict, optional 

1092 This is a dictionary of attributes that you can pass to use to identify 

1093 the table in the HTML. These are not checked for validity before being 

1094 passed to lxml or Beautiful Soup. However, these attributes must be 

1095 valid HTML table attributes to work correctly. For example, :: 

1096 

1097 attrs = {"id": "table"} 

1098 

1099 is a valid attribute dictionary because the 'id' HTML tag attribute is 

1100 a valid HTML attribute for *any* HTML tag as per `this document 

1101 <https://html.spec.whatwg.org/multipage/dom.html#global-attributes>`__. :: 

1102 

1103 attrs = {"asdf": "table"} 

1104 

1105 is *not* a valid attribute dictionary because 'asdf' is not a valid 

1106 HTML attribute even if it is a valid XML attribute. Valid HTML 4.01 

1107 table attributes can be found `here 

1108 <http://www.w3.org/TR/REC-html40/struct/tables.html#h-11.2>`__. A 

1109 working draft of the HTML 5 spec can be found `here 

1110 <https://html.spec.whatwg.org/multipage/tables.html>`__. It contains the 

1111 latest information on table attributes for the modern web. 

1112 

1113 parse_dates : bool, optional 

1114 See :func:`~read_csv` for more details. 

1115 

1116 thousands : str, optional 

1117 Separator to use to parse thousands. Defaults to ``','``. 

1118 

1119 encoding : str, optional 

1120 The encoding used to decode the web page. Defaults to ``None``.``None`` 

1121 preserves the previous encoding behavior, which depends on the 

1122 underlying parser library (e.g., the parser library will try to use 

1123 the encoding provided by the document). 

1124 

1125 decimal : str, default '.' 

1126 Character to recognize as decimal point (e.g. use ',' for European 

1127 data). 

1128 

1129 converters : dict, default None 

1130 Dict of functions for converting values in certain columns. Keys can 

1131 either be integers or column labels, values are functions that take one 

1132 input argument, the cell (not column) content, and return the 

1133 transformed content. 

1134 

1135 na_values : iterable, default None 

1136 Custom NA values. 

1137 

1138 keep_default_na : bool, default True 

1139 If na_values are specified and keep_default_na is False the default NaN 

1140 values are overridden, otherwise they're appended to. 

1141 

1142 displayed_only : bool, default True 

1143 Whether elements with "display: none" should be parsed. 

1144 

1145 extract_links : {None, "all", "header", "body", "footer"} 

1146 Table elements in the specified section(s) with <a> tags will have their 

1147 href extracted. 

1148 

1149 dtype_backend : {'numpy_nullable', 'pyarrow'} 

1150 Back-end data type applied to the resultant :class:`DataFrame` 

1151 (still experimental). If not specified, the default behavior 

1152 is to not use nullable data types. If specified, the behavior 

1153 is as follows: 

1154 

1155 * ``"numpy_nullable"``: returns nullable-dtype-backed :class:`DataFrame` 

1156 * ``"pyarrow"``: returns pyarrow-backed nullable 

1157 :class:`ArrowDtype` :class:`DataFrame` 

1158 

1159 .. versionadded:: 2.0 

1160 

1161 storage_options : dict, optional 

1162 Extra options that make sense for a particular storage connection, e.g. 

1163 host, port, username, password, etc. For HTTP(S) URLs the key-value pairs 

1164 are forwarded to ``urllib.request.Request`` as header options. For other 

1165 URLs (e.g. starting with "s3://", and "gcs://") the key-value pairs are 

1166 forwarded to ``fsspec.open``. Please see ``fsspec`` and ``urllib`` for more 

1167 details, and for more examples on storage options refer `here 

1168 <https://pandas.pydata.org/docs/user_guide/io.html? 

1169 highlight=storage_options#reading-writing-remote-files>`_. 

1170 

1171 .. versionadded:: 2.1.0 

1172 

1173 Returns 

1174 ------- 

1175 dfs 

1176 A list of DataFrames. 

1177 

1178 See Also 

1179 -------- 

1180 read_csv : Read a comma-separated values (csv) file into DataFrame. 

1181 

1182 Notes 

1183 ----- 

1184 Before using this function you should read the :ref:`gotchas about the 

1185 HTML parsing libraries <io.html.gotchas>`. 

1186 

1187 Expect to do some cleanup after you call this function. For example, you 

1188 might need to manually assign column names if the column names are 

1189 converted to NaN when you pass the `header=0` argument. We try to assume as 

1190 little as possible about the structure of the table and push the 

1191 idiosyncrasies of the HTML contained in the table to the user. 

1192 

1193 This function searches for ``<table>`` elements and only for ``<tr>`` 

1194 and ``<th>`` rows and ``<td>`` elements within each ``<tr>`` or ``<th>`` 

1195 element in the table. ``<td>`` stands for "table data". This function 

1196 attempts to properly handle ``colspan`` and ``rowspan`` attributes. 

1197 If the function has a ``<thead>`` argument, it is used to construct 

1198 the header, otherwise the function attempts to find the header within 

1199 the body (by putting rows with only ``<th>`` elements into the header). 

1200 

1201 Similar to :func:`~read_csv` the `header` argument is applied 

1202 **after** `skiprows` is applied. 

1203 

1204 This function will *always* return a list of :class:`DataFrame` *or* 

1205 it will fail, i.e., it will *not* return an empty list, save for some 

1206 rare cases. 

1207 It might return an empty list in case of inputs with single row and 

1208 ``<td>`` containing only whitespaces. 

1209 

1210 Examples 

1211 -------- 

1212 See the :ref:`read_html documentation in the IO section of the docs 

1213 <io.read_html>` for some examples of reading in HTML tables. 

1214 """ 

1215 # Type check here. We don't want to parse only to fail because of an 

1216 # invalid value of an integer skiprows. 

1217 if isinstance(skiprows, numbers.Integral) and skiprows < 0: 

1218 raise ValueError( 

1219 "cannot skip rows starting from the end of the " 

1220 "data (you passed a negative value)" 

1221 ) 

1222 if extract_links not in [None, "header", "footer", "body", "all"]: 

1223 raise ValueError( 

1224 "`extract_links` must be one of " 

1225 '{None, "header", "footer", "body", "all"}, got ' 

1226 f'"{extract_links}"' 

1227 ) 

1228 

1229 validate_header_arg(header) 

1230 check_dtype_backend(dtype_backend) 

1231 

1232 io = stringify_path(io) 

1233 

1234 return _parse( 

1235 flavor=flavor, 

1236 io=io, 

1237 match=match, 

1238 header=header, 

1239 index_col=index_col, 

1240 skiprows=skiprows, 

1241 parse_dates=parse_dates, 

1242 thousands=thousands, 

1243 attrs=attrs, 

1244 encoding=encoding, 

1245 decimal=decimal, 

1246 converters=converters, 

1247 na_values=na_values, 

1248 keep_default_na=keep_default_na, 

1249 displayed_only=displayed_only, 

1250 extract_links=extract_links, 

1251 dtype_backend=dtype_backend, 

1252 storage_options=storage_options, 

1253 )