Skip to content

Commit 321c735

Browse files
author
chenghao.zhang
committed
Keep the link content of the image. Remove some dirty data
1 parent 0c77d32 commit 321c735

3 files changed

Lines changed: 106 additions & 26 deletions

File tree

edgar/company_reports.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,10 +259,31 @@ def management_discussion(self):
259259
@property
260260
def directors_officers_and_governance(self):
261261
return self['Item 10']
262+
263+
@property
264+
@lru_cache(maxsize=1)
265+
def chunked_document(self):
266+
return ChunkedDocument(self._filing.html(), prefix_src=self._filing.base_dir)
262267

263268
def __str__(self):
264269
return f"""TenK('{self.company}')"""
265270

271+
def __getitem__(self, item_or_part: str):
272+
# Show the item or part from the filing document. e.g. Item 1 Business from 10-K or Part I from 10-Q
273+
item_text = self.chunked_document[item_or_part]
274+
if item_text:
275+
item_text = item_text.rstrip()
276+
last_line = item_text.split("\n")[-1]
277+
if re.match(r'^\b(PART\s+[IVXLC]+)\b', last_line):
278+
item_text = item_text.rstrip(last_line)
279+
return item_text
280+
281+
def get_item_with_part(self, part: str, item: str):
282+
# Show the item or part from the filing document. e.g. Item 1 Business from 10-K or Part I from 10-Q
283+
item_text = self.chunked_document.get_item_with_part(part, item)
284+
# remove first line or last line (redundant part information)
285+
return item_text
286+
266287
def get_structure(self):
267288
# Create the main tree
268289
tree = Tree("📄 ")
@@ -397,6 +418,11 @@ def get_item_with_part(self, part: str, item: str):
397418
# remove first line or last line (redundant part information)
398419
return item_text
399420

421+
@property
422+
@lru_cache(maxsize=1)
423+
def chunked_document(self):
424+
return ChunkedDocument(self._filing.html(), prefix_src=self._filing.base_dir)
425+
400426
def get_structure(self):
401427
# Create the main tree
402428
tree = Tree("📄 ")

edgar/files/html_documents.py

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,30 @@ def __str__(self):
242242
def __repr__(self):
243243
return self.text
244244

245+
class LinkBlock(Block):
246+
247+
def __init__(self, text: str, tag:str, alt:str, src:str, **tags):
248+
super().__init__(text, **tags)
249+
self.tag = tag
250+
self.alt = alt
251+
self.src = src
252+
self.inline: bool = True
253+
254+
def get_text(self):
255+
return f'<{self.tag} alt="{self.alt}" src="{self.src}">'
256+
# return ''
257+
258+
def to_markdown(self, prefix_src:str=""):
259+
return f"![alt {self.alt}]({prefix_src}/{self.src})\n"
260+
261+
def get_complete_text(self, prefix_src:str):
262+
return f'<{self.tag} alt="{self.alt}" src="{prefix_src}/{self.src}">\n'
263+
264+
def __str__(self):
265+
return "LinkBlock"
266+
267+
def __repr__(self):
268+
return self.text
245269

246270
class TextBlock(Block):
247271

@@ -487,8 +511,8 @@ def generate_chunks(self, ignore_tables: bool = False) -> List[List[Block]]:
487511
is_item_header = bool(re.match(item_pattern, block.text))
488512
is_part_header = bool(re.match(part_pattern, block.text))
489513

490-
if is_item_header:
491-
# Yield the current chunk before starting a new one with the "Item" header
514+
if is_part_header:
515+
# Yield the current chunk before starting a new one with the "Part" header
492516
if current_chunk:
493517
if any(block.text.strip() for block in current_chunk): # Avoid emitting empty chunks
494518
yield current_chunk
@@ -497,11 +521,11 @@ def generate_chunks(self, ignore_tables: bool = False) -> List[List[Block]]:
497521
current_chunk = [block]
498522

499523
# Update flags accordingly
500-
item_header_detected = True
524+
item_header_detected = False
501525
header_detected = True # "Item" headers are considered regular headers for flag purposes
502526
accumulating_regular_text = False # Reset since we're starting a new section
503-
elif is_part_header:
504-
# Yield the current chunk before starting a new one with the "Item" header
527+
elif is_item_header:
528+
# Yield the current chunk before starting a new one with the "Item" header
505529
if current_chunk:
506530
if any(block.text.strip() for block in current_chunk): # Avoid emitting empty chunks
507531
yield current_chunk
@@ -535,6 +559,11 @@ def generate_chunks(self, ignore_tables: bool = False) -> List[List[Block]]:
535559
header_detected = False
536560
item_header_detected = False
537561
current_chunk.append(block)
562+
elif isinstance(block, LinkBlock):
563+
analysis = False
564+
is_regular_text = False
565+
is_item_header = False
566+
yield [block]
538567

539568
# Check to yield the remaining chunk if it's the last block
540569
if i == len(self.blocks) - 1 and current_chunk:
@@ -547,12 +576,20 @@ def extract_and_format_content(element) -> List[Block]:
547576
Recursively extract and format content from an element,
548577
applying special formatting to tables and concatenating text for other elements.
549578
"""
550-
551579
if element.name == 'table':
552580
table_block = TableBlock(table_element=element, rows=len(element.find_all("tr")))
553581
return [table_block]
554582
elif element.name in ['ul', 'ol']:
555583
return [TextBlock(text=fixup(element.text), element=element.name, text_type='list')]
584+
elif element.name in ["img", ]:
585+
return [
586+
LinkBlock(text=str(element),
587+
tag=element.name,
588+
element=element.name,
589+
alt=element.get('alt'),
590+
src=element.get('src'),
591+
text_type='string')
592+
]
556593
else:
557594
inline = is_inline(element)
558595
blocks: List[Block] = []

edgar/files/htmltools.py

Lines changed: 37 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from edgar.core import pandas_version
1616

1717
from edgar.datatools import compress_dataframe
18-
from edgar.files.html_documents import HtmlDocument, Block, TableBlock, table_to_markdown
18+
from edgar.files.html_documents import HtmlDocument, Block, TableBlock, LinkBlock, table_to_markdown
1919
from edgar.richtools import repr_rich
2020

2121
__all__ = [
@@ -285,7 +285,6 @@ def chunks2df(chunks: List[List[Block]],
285285
chunk_df.loc[chunk_df.Toc.notnull() & chunk_df.Toc, 'Item'] = ""
286286
# if item_adjuster:
287287
# chunk_df = item_adjuster(chunk_df, **{'item_structure': item_structure, 'item_detector': item_detector})
288-
289288
# Foward fill item and parts
290289
# Handle deprecation warning in fillna(method='ffill')
291290
if pandas_version >= (2, 1, 0):
@@ -330,14 +329,17 @@ class ChunkedDocument:
330329

331330
def __init__(self,
332331
html: str,
333-
chunk_fn: Callable[[List], pd.DataFrame] = chunks2df):
332+
chunk_fn: Callable[[List], pd.DataFrame] = chunks2df,
333+
prefix_src: str = ""):
334334
"""
335335
:param html: The filing html
336336
:param chunk_fn: A function that converts the chunks to a dataframe
337+
:param file_path: The path to the filing
337338
"""
338339
self.chunks = chunk(html)
339340
self._chunked_data = chunk_fn(self.chunks)
340341
self.chunk_fn = chunk_fn
342+
self.prefix_src = prefix_src
341343

342344
@lru_cache(maxsize=4)
343345
def as_dataframe(self):
@@ -445,21 +447,42 @@ def tables(self):
445447
for block in chunk:
446448
if isinstance(block, TableBlock):
447449
yield block
450+
451+
def assemble_block_text(self, chunks: List[Block]):
452+
453+
if self.prefix_src:
454+
for chunk in chunks:
455+
for block in chunk:
456+
if isinstance(block, LinkBlock):
457+
yield block.to_markdown(prefix_src=self.prefix_src)
458+
else:
459+
yield block.get_text()
460+
else:
461+
for chunk in chunks:
462+
yield "".join([block.get_text() for block in chunk])
448463

449464
def get_item_with_part(self, part: str, item: str):
450465
if isinstance(part, str):
451466
chunks = list(self._chunks_mul_for(part, item))
452-
return "".join(["".join(block.get_text()
453-
for block in chunk)
454-
for chunk in chunks])
467+
return self.clean_part_line("".join([text for text in self.assemble_block_text(chunks)]))
455468
return ""
456469

470+
@staticmethod
471+
def clean_part_line(text:str):
472+
# clean last line dity data, example 'PART I — FINANCIAL INFORMATION'
473+
res = text.rstrip("\n")
474+
last_line = res.split("\n")[-1]
475+
if re.match(r'^\b(PART\s+[IVXLC]+)\b', last_line):
476+
res = res.rstrip(last_line)
477+
return res
478+
457479
def get_signature(self):
458480
res = self.get_item_with_part("Signature", "Signature")
459481
last_line = res.split("\n")[-1]
460482
if re.match(r'^\b(PART\s+[IVXLC]+)\b', last_line):
461483
res = res.rstrip(last_line)
462-
return res
484+
return self.clean_part_line(res)
485+
463486

464487
def get_introduction(self):
465488
"""
@@ -487,16 +510,12 @@ def get_introduction(self):
487510
return ""
488511

489512
# Reuse __getitem__ to extract chunks up to min_index
490-
res = "".join([
491-
"".join(block.get_text() for block in self.chunks[idx])
492-
for idx in range(intro_index)
493-
])
494-
# clean last line dity data, example 'PART I — FINANCIAL INFORMATION'
495-
res = res.rstrip("\n")
496-
last_line = res.split("\n")[-1]
497-
if re.match(r'^\b(PART\s+[IVXLC]+)\b', last_line):
498-
res = res.rstrip(last_line)
499-
return res
513+
res = "".join(
514+
[text for text in
515+
self.assemble_block_text(
516+
[self.chunks[idx] for idx in range(intro_index)]
517+
)])
518+
return self.clean_part_line(res)
500519

501520
def __len__(self):
502521
return len(self.chunks)
@@ -511,9 +530,7 @@ def __getitem__(self, item):
511530
if len(chunks) == 0:
512531
return None
513532
# render the nested List of List [str]
514-
return "".join(["".join(block.get_text()
515-
for block in chunk)
516-
for chunk in chunks])
533+
return "".join([text for text in self.assemble_block_text(chunks)])
517534

518535
def __iter__(self):
519536
return iter(self.chunks)

0 commit comments

Comments
 (0)