@@ -295,62 +295,62 @@ async def fetch_bill(self, congress: int, bill_type: str, number: int) -> Option
295295 if title_elem :
296296 bill_data ['title' ] = self .clean_text (title_elem .get_text ())
297297
298- # Extract long title
299- long_title_elem = soup .find ('span ' , text = re . compile ( 'Long Title:' ) )
300- if long_title_elem and long_title_elem . next_sibling :
301- long_title = self . clean_text ( str ( long_title_elem . next_sibling ) )
302- if long_title and long_title != bill_data . get ( 'title' ):
303- bill_data [ 'longTitle' ] = long_title
304-
305- # Extract scope
306- scope_elem = soup . find ( 'span' , text = re . compile ( 'Scope:' ))
307- if scope_elem and scope_elem . next_sibling :
308- bill_data [ 'scope' ] = self . clean_text ( str ( scope_elem . next_sibling ) )
309-
310- # Extract subject(s )
311- subject_elem = soup . find ( 'span' , text = re . compile ( 'Subject \\ (s \\ ):' ) )
312- if subject_elem and subject_elem . next_sibling :
313- subjects = self .clean_text (str ( subject_elem . next_sibling ))
314- if subjects :
315- bill_data [ 'subject' ] = [ s . strip () for s in subjects . split ( ';' )]
316-
317- # Extract status
318- status_elem = soup . find ( 'span' , text = re . compile ( 'Status:' ))
319- if status_elem and status_elem . next_sibling :
320- status_text = self . clean_text ( str ( status_elem . next_sibling ))
321- bill_data [ 'status' ] = {
322- 'status' : status_text ,
323- 'statusCode' : ''
324- }
325-
326- # Extract committee info with proper structure
327- committee_elem = soup . find ( 'span' , text = re . compile ( 'Committee:' ))
328- if committee_elem and committee_elem . next_sibling :
329- committees_text = self . clean_text ( str ( committee_elem . next_sibling ))
330- if committees_text :
331- # Parse committee text - may contain codes and names
332- committees = []
333- for comm_text in committees_text . split ( ';' ) :
334- comm_text = comm_text . strip ()
335- if comm_text :
336- # Try to extract code if present (e.g., "FINAN - Finance")
337- code_match = re . match ( r'([A-Z]+)\s*-\s*(.+)' , comm_text )
338- if code_match :
339- committees . append ({
340- 'code' : code_match . group ( 1 ),
341- 'name' : code_match . group ( 2 ). strip ( )
342- })
343- else :
344- committees . append ({
345- 'code ' : '' ,
346- 'name' : comm_text
347- })
348-
349- if committees :
350- bill_data ['primaryCommittee ' ] = committees [ 0 ]
351- if len ( committees ) > 1 :
352- bill_data [ 'secondaryCommittees' ] = committees [ 1 :]
353- bill_data [ 'committees' ] = committees
298+ # Extract author from "Filed on" text
299+ content_td = soup .find ('td ' , id = 'content' )
300+ if content_td :
301+ filed_text = content_td . get_text ( )
302+ filed_match = re . search ( r'Filed on ([^\n]+) by ([^\n]+)' , filed_text )
303+ if filed_match :
304+ bill_data [ 'filedDate' ] = filed_match . group ( 1 ). strip ()
305+ bill_data [ 'author' ] = filed_match . group ( 2 ). strip ()
306+
307+ # Extract data from p/blockquote pairs
308+ paragraphs = soup . find_all ( 'p' )
309+ for p in paragraphs :
310+ p_text = self . clean_text ( p . get_text ()). lower ( )
311+ next_elem = p . find_next_sibling ( 'blockquote' )
312+ if next_elem :
313+ content = self .clean_text (next_elem . get_text ( ))
314+
315+ if 'long title' in p_text and content :
316+ bill_data [ 'longTitle' ] = content
317+ elif 'scope' in p_text and content :
318+ bill_data [ 'scope' ] = content
319+ elif 'subject' in p_text :
320+ # Handle multiple subjects separated by <br> tags
321+ # Get the raw HTML to preserve br tags
322+ subjects = []
323+ for br in next_elem . find_all ( 'br' ):
324+ br . replace_with ( '|||' ) # Replace br with delimiter
325+ content = self . clean_text ( next_elem . get_text ())
326+ if '|||' in content :
327+ # Split by our delimiter
328+ subjects = [ s . strip () for s in content . split ( '|||' ) if s . strip ()]
329+ elif ';' in content or '/' in content :
330+ # Fallback to semicolon or slash separation
331+ subjects = re . split ( r'[;/]' , content )
332+ subjects = [s . strip () for s in subjects if s . strip () ]
333+ else :
334+ # Single subject
335+ subjects = [ content . strip ()] if content . strip () else []
336+
337+ if subjects :
338+ bill_data [ 'subject' ] = subjects
339+ elif 'legislative status' in p_text and content :
340+ # Extract status and date
341+ status_match = re . match ( r'(.+?)\s*\((\d+/\d+/\d+)\)' , content )
342+ if status_match :
343+ bill_data [ 'status' ] = {
344+ 'status' : status_match . group ( 1 ). strip (),
345+ 'date ' : status_match . group ( 2 ). strip ()
346+ }
347+ else :
348+ bill_data [ 'status' ] = { 'status' : content }
349+ elif 'primary committee' in p_text and content :
350+ bill_data ['committee ' ] = {
351+ 'name' : content ,
352+ 'type' : 'primary'
353+ }
354354
355355 # Extract abstract
356356 abstract_elem = soup .find ('div' , {'class' : 'lis_billabstract' })
@@ -362,27 +362,17 @@ async def fetch_bill(self, congress: int, bill_type: str, number: int) -> Option
362362 if history :
363363 bill_data ['legislativeHistory' ] = history
364364
365- # Extract filed date and author from first history entry
365+ # Extract additional info from history (e.g., co-authors)
366366 for entry in history :
367367 action = entry .get ('action' , '' )
368- if 'Filed by' in action :
369- # Extract author
370- match = re .search (r'Filed by (.+)' , action )
368+ # Check for introduced by senator (alternative author extraction)
369+ if 'Introduced by Senator' in action and ' author' not in bill_data :
370+ match = re .search (r'Introduced by Senator (.+?)(?:;|$ )' , action )
371371 if match :
372- author_name = match .group (1 ).strip ()
373- bill_data ['principalAuthor' ] = {
374- 'name' : author_name ,
375- 'code' : ''
376- }
377- # Use date from this entry
378- if 'date' in entry :
379- bill_data ['dateFiled' ] = entry ['date' ]
380- break
381-
382- # Extract co-authors
383- for entry in history :
384- if 'Co-Authors:' in entry .get ('action' , '' ):
385- match = re .search (r'Co-Authors?:\s*(.+)' , entry ['action' ])
372+ bill_data ['author' ] = match .group (1 ).strip ()
373+ # Extract co-authors
374+ if 'Co-Author' in action :
375+ match = re .search (r'Co-Authors?:\s*(.+)' , action )
386376 if match :
387377 coauthors = match .group (1 ).strip ()
388378 bill_data ['coAuthors' ] = [a .strip () for a in coauthors .split (',' )]
@@ -392,13 +382,28 @@ async def fetch_bill(self, congress: int, bill_type: str, number: int) -> Option
392382 if related :
393383 bill_data .update (related )
394384
395- # Extract PDF URL
396- pdf_link = soup .find ('a' , href = re .compile (r'\.pdf$' , re .I ))
397- if pdf_link :
398- pdf_url = pdf_link .get ('href' )
399- if not pdf_url .startswith ('http' ):
400- pdf_url = f"https://web.senate.gov.ph{ pdf_url } "
401- bill_data ['pdfUrl' ] = pdf_url
385+ # Extract PDF URL from download section
386+ download_div = soup .find ('div' , id = 'lis_download' )
387+ if download_div :
388+ pdf_links = download_div .find_all ('a' , href = re .compile (r'\.pdf' , re .I ))
389+ if pdf_links :
390+ pdf_url = pdf_links [0 ].get ('href' )
391+ if not pdf_url .startswith ('http' ):
392+ pdf_url = f"https://web.senate.gov.ph{ pdf_url } "
393+ bill_data ['pdfUrl' ] = pdf_url
394+
395+ # Get PDF info (filename, date, size)
396+ pdf_text = pdf_links [0 ].get_text (strip = True )
397+ if pdf_text :
398+ bill_data ['pdfFileName' ] = pdf_text
399+ else :
400+ # Fallback: look for any PDF link
401+ pdf_link = soup .find ('a' , href = re .compile (r'\.pdf$' , re .I ))
402+ if pdf_link :
403+ pdf_url = pdf_link .get ('href' )
404+ if not pdf_url .startswith ('http' ):
405+ pdf_url = f"https://web.senate.gov.ph{ pdf_url } "
406+ bill_data ['pdfUrl' ] = pdf_url
402407
403408 return bill_data
404409
@@ -446,38 +451,40 @@ def extract_legislative_history(self, soup: BeautifulSoup) -> List[Dict]:
446451 """Extract legislative history from bill page."""
447452 history = []
448453
449- # Look for Legislative History table
450- history_section = None
451-
452- # Try to find the section with "Legislative History" header
453- for header in soup .find_all (['h3' , 'h4' , 'span' ], text = re .compile ('Legislative History' )):
454- # Find the next table after this header
455- next_sibling = header .find_next_sibling ('table' )
456- if next_sibling :
457- history_section = next_sibling
458- break
459-
460- # Alternative: look for table that contains legislative history
461- if not history_section :
462- for table in soup .find_all ('table' ):
463- if 'Legislative History' in str (table ) or 'Date' in str (table ) and 'Action' in str (table ):
464- history_section = table
465- break
466-
467- if history_section :
468- rows = history_section .find_all ('tr' )
469- for row in rows :
470- cells = row .find_all ('td' )
471- if len (cells ) >= 2 :
472- date_text = self .clean_text (cells [0 ].get_text ())
473- action_text = self .clean_text (cells [1 ].get_text ())
474-
475- # Skip header row
476- if date_text and action_text and date_text != 'Date' :
477- history .append ({
478- 'date' : date_text ,
479- 'action' : action_text
480- })
454+ # Look for Legislative History in blockquote with table
455+ for p in soup .find_all ('p' ):
456+ if 'legislative history' in p .get_text ().lower ():
457+ blockquote = p .find_next_sibling ('blockquote' )
458+ if blockquote :
459+ table = blockquote .find ('table' , id = 'lis_table' )
460+ if table :
461+ rows = table .find_all ('tr' )
462+ for row in rows :
463+ cells = row .find_all ('td' )
464+
465+ # Handle different row types
466+ if len (cells ) == 2 :
467+ # Date and action row
468+ date_text = self .clean_text (cells [0 ].get_text ())
469+ action_text = self .clean_text (cells [1 ].get_text ())
470+
471+ # Check if it's actually a date row (has date format)
472+ if re .match (r'\d+/\d+/\d+' , date_text ):
473+ history .append ({
474+ 'date' : date_text ,
475+ 'action' : action_text
476+ })
477+ elif len (cells ) == 1 and cells [0 ].get ('colspan' ) == '2' :
478+ # Full width row (like "Entitled:" or session info)
479+ text = self .clean_text (cells [0 ].get_text ())
480+ if text and not text .startswith ('[' ):
481+ # Add as special entry without date
482+ if 'Entitled:' in text :
483+ history .append ({
484+ 'date' : '' ,
485+ 'action' : text
486+ })
487+ break
481488
482489 return history
483490
@@ -486,23 +493,23 @@ def extract_related_bills(self, soup: BeautifulSoup) -> Dict:
486493 related = {}
487494
488495 # Look for consolidated bills
489- consolidated_elem = soup .find ('span' , text = re .compile ('Consolidated.*with' ))
496+ consolidated_elem = soup .find ('span' , string = re .compile ('Consolidated.*with' ))
490497 if consolidated_elem :
491498 text = self .clean_text (consolidated_elem .get_text ())
492499 match = re .findall (r'[SH]BN-\d+' , text )
493500 if match :
494501 related ['consolidatedWith' ] = match
495502
496503 # Look for substitute bills
497- substitute_elem = soup .find ('span' , text = re .compile ('In substitution' ))
504+ substitute_elem = soup .find ('span' , string = re .compile ('In substitution' ))
498505 if substitute_elem :
499506 text = self .clean_text (substitute_elem .get_text ())
500507 match = re .findall (r'[SH]BN-\d+' , text )
501508 if match :
502509 related ['substituteFor' ] = match
503510
504511 # Look for related bills section
505- related_section = soup .find ('span' , text = re .compile ('Related.*Bill' ))
512+ related_section = soup .find ('span' , string = re .compile ('Related.*Bill' ))
506513 if related_section :
507514 text = self .clean_text (related_section .get_text ())
508515 match = re .findall (r'[SH]BN-\d+' , text )
0 commit comments