diff --git a/.venv312/Scripts/Activate.ps1 b/.venv312/Scripts/Activate.ps1
new file mode 100644
index 0000000..918eac3
--- /dev/null
+++ b/.venv312/Scripts/Activate.ps1
@@ -0,0 +1,528 @@
+<#
+.Synopsis
+Activate a Python virtual environment for the current PowerShell session.
+
+.Description
+Pushes the python executable for a virtual environment to the front of the
+$Env:PATH environment variable and sets the prompt to signify that you are
+in a Python virtual environment. Makes use of the command line switches as
+well as the `pyvenv.cfg` file values present in the virtual environment.
+
+.Parameter VenvDir
+Path to the directory that contains the virtual environment to activate. The
+default value for this is the parent of the directory that the Activate.ps1
+script is located within.
+
+.Parameter Prompt
+The prompt prefix to display when this virtual environment is activated. By
+default, this prompt is the name of the virtual environment folder (VenvDir)
+surrounded by parentheses and followed by a single space (ie. '(.venv) ').
+
+.Example
+Activate.ps1
+Activates the Python virtual environment that contains the Activate.ps1 script.
+
+.Example
+Activate.ps1 -Verbose
+Activates the Python virtual environment that contains the Activate.ps1 script,
+and shows extra information about the activation as it executes.
+
+.Example
+Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv
+Activates the Python virtual environment located in the specified location.
+
+.Example
+Activate.ps1 -Prompt "MyPython"
+Activates the Python virtual environment that contains the Activate.ps1 script,
+and prefixes the current prompt with the specified string (surrounded in
+parentheses) while the virtual environment is active.
+
+.Notes
+On Windows, it may be required to enable this Activate.ps1 script by setting the
+execution policy for the user. You can do this by issuing the following PowerShell
+command:
+
+PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
+
+For more information on Execution Policies:
+https://go.microsoft.com/fwlink/?LinkID=135170
+
+#>
+Param(
+ [Parameter(Mandatory = $false)]
+ [String]
+ $VenvDir,
+ [Parameter(Mandatory = $false)]
+ [String]
+ $Prompt
+)
+
+<# Function declarations --------------------------------------------------- #>
+
+<#
+.Synopsis
+Remove all shell session elements added by the Activate script, including the
+addition of the virtual environment's Python executable from the beginning of
+the PATH variable.
+
+.Parameter NonDestructive
+If present, do not remove this function from the global namespace for the
+session.
+
+#>
+function global:deactivate ([switch]$NonDestructive) {
+ # Revert to original values
+
+ # The prior prompt:
+ if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) {
+ Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt
+ Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT
+ }
+
+ # The prior PYTHONHOME:
+ if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) {
+ Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME
+ Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME
+ }
+
+ # The prior PATH:
+ if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) {
+ Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH
+ Remove-Item -Path Env:_OLD_VIRTUAL_PATH
+ }
+
+ # Just remove the VIRTUAL_ENV altogether:
+ if (Test-Path -Path Env:VIRTUAL_ENV) {
+ Remove-Item -Path env:VIRTUAL_ENV
+ }
+
+ # Just remove VIRTUAL_ENV_PROMPT altogether.
+ if (Test-Path -Path Env:VIRTUAL_ENV_PROMPT) {
+ Remove-Item -Path env:VIRTUAL_ENV_PROMPT
+ }
+
+ # Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether:
+ if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) {
+ Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force
+ }
+
+ # Leave deactivate function in the global namespace if requested:
+ if (-not $NonDestructive) {
+ Remove-Item -Path function:deactivate
+ }
+}
+
+<#
+.Description
+Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the
+given folder, and returns them in a map.
+
+For each line in the pyvenv.cfg file, if that line can be parsed into exactly
+two strings separated by `=` (with any amount of whitespace surrounding the =)
+then it is considered a `key = value` line. The left hand string is the key,
+the right hand is the value.
+
+If the value starts with a `'` or a `"` then the first and last character is
+stripped from the value before being captured.
+
+.Parameter ConfigDir
+Path to the directory that contains the `pyvenv.cfg` file.
+#>
+function Get-PyVenvConfig(
+ [String]
+ $ConfigDir
+) {
+ Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg"
+
+ # Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue).
+ $pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue
+
+ # An empty map will be returned if no config file is found.
+ $pyvenvConfig = @{ }
+
+ if ($pyvenvConfigPath) {
+
+ Write-Verbose "File exists, parse `key = value` lines"
+ $pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath
+
+ $pyvenvConfigContent | ForEach-Object {
+ $keyval = $PSItem -split "\s*=\s*", 2
+ if ($keyval[0] -and $keyval[1]) {
+ $val = $keyval[1]
+
+ # Remove extraneous quotations around a string value.
+ if ("'""".Contains($val.Substring(0, 1))) {
+ $val = $val.Substring(1, $val.Length - 2)
+ }
+
+ $pyvenvConfig[$keyval[0]] = $val
+ Write-Verbose "Adding Key: '$($keyval[0])'='$val'"
+ }
+ }
+ }
+ return $pyvenvConfig
+}
+
+
+<# Begin Activate script --------------------------------------------------- #>
+
+# Determine the containing directory of this script
+$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
+$VenvExecDir = Get-Item -Path $VenvExecPath
+
+Write-Verbose "Activation script is located in path: '$VenvExecPath'"
+Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)"
+Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)"
+
+# Set values required in priority: CmdLine, ConfigFile, Default
+# First, get the location of the virtual environment, it might not be
+# VenvExecDir if specified on the command line.
+if ($VenvDir) {
+ Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values"
+}
+else {
+ Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir."
+ $VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/")
+ Write-Verbose "VenvDir=$VenvDir"
+}
+
+# Next, read the `pyvenv.cfg` file to determine any required value such
+# as `prompt`.
+$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir
+
+# Next, set the prompt from the command line, or the config file, or
+# just use the name of the virtual environment folder.
+if ($Prompt) {
+ Write-Verbose "Prompt specified as argument, using '$Prompt'"
+}
+else {
+ Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value"
+ if ($pyvenvCfg -and $pyvenvCfg['prompt']) {
+ Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'"
+ $Prompt = $pyvenvCfg['prompt'];
+ }
+ else {
+ Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virtual environment)"
+ Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'"
+ $Prompt = Split-Path -Path $venvDir -Leaf
+ }
+}
+
+Write-Verbose "Prompt = '$Prompt'"
+Write-Verbose "VenvDir='$VenvDir'"
+
+# Deactivate any currently active virtual environment, but leave the
+# deactivate function in place.
+deactivate -nondestructive
+
+# Now set the environment variable VIRTUAL_ENV, used by many tools to determine
+# that there is an activated venv.
+$env:VIRTUAL_ENV = $VenvDir
+
+if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) {
+
+ Write-Verbose "Setting prompt to '$Prompt'"
+
+ # Set the prompt to include the env name
+ # Make sure _OLD_VIRTUAL_PROMPT is global
+ function global:_OLD_VIRTUAL_PROMPT { "" }
+ Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT
+ New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt
+
+ function global:prompt {
+ Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) "
+ _OLD_VIRTUAL_PROMPT
+ }
+ $env:VIRTUAL_ENV_PROMPT = $Prompt
+}
+
+# Clear PYTHONHOME
+if (Test-Path -Path Env:PYTHONHOME) {
+ Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME
+ Remove-Item -Path Env:PYTHONHOME
+}
+
+# Add the venv to the PATH
+Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH
+$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH"
+
+# SIG # Begin signature block
+# MII0CQYJKoZIhvcNAQcCoIIz+jCCM/YCAQExDzANBglghkgBZQMEAgEFADB5Bgor
+# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
+# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBnL745ElCYk8vk
+# dBtMuQhLeWJ3ZGfzKW4DHCYzAn+QB6CCG9IwggXMMIIDtKADAgECAhBUmNLR1FsZ
+# lUgTecgRwIeZMA0GCSqGSIb3DQEBDAUAMHcxCzAJBgNVBAYTAlVTMR4wHAYDVQQK
+# ExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xSDBGBgNVBAMTP01pY3Jvc29mdCBJZGVu
+# dGl0eSBWZXJpZmljYXRpb24gUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgMjAy
+# MDAeFw0yMDA0MTYxODM2MTZaFw00NTA0MTYxODQ0NDBaMHcxCzAJBgNVBAYTAlVT
+# MR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xSDBGBgNVBAMTP01pY3Jv
+# c29mdCBJZGVudGl0eSBWZXJpZmljYXRpb24gUm9vdCBDZXJ0aWZpY2F0ZSBBdXRo
+# b3JpdHkgMjAyMDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALORKgeD
+# Bmf9np3gx8C3pOZCBH8Ppttf+9Va10Wg+3cL8IDzpm1aTXlT2KCGhFdFIMeiVPvH
+# or+Kx24186IVxC9O40qFlkkN/76Z2BT2vCcH7kKbK/ULkgbk/WkTZaiRcvKYhOuD
+# PQ7k13ESSCHLDe32R0m3m/nJxxe2hE//uKya13NnSYXjhr03QNAlhtTetcJtYmrV
+# qXi8LW9J+eVsFBT9FMfTZRY33stuvF4pjf1imxUs1gXmuYkyM6Nix9fWUmcIxC70
+# ViueC4fM7Ke0pqrrBc0ZV6U6CwQnHJFnni1iLS8evtrAIMsEGcoz+4m+mOJyoHI1
+# vnnhnINv5G0Xb5DzPQCGdTiO0OBJmrvb0/gwytVXiGhNctO/bX9x2P29Da6SZEi3
+# W295JrXNm5UhhNHvDzI9e1eM80UHTHzgXhgONXaLbZ7LNnSrBfjgc10yVpRnlyUK
+# xjU9lJfnwUSLgP3B+PR0GeUw9gb7IVc+BhyLaxWGJ0l7gpPKWeh1R+g/OPTHU3mg
+# trTiXFHvvV84wRPmeAyVWi7FQFkozA8kwOy6CXcjmTimthzax7ogttc32H83rwjj
+# O3HbbnMbfZlysOSGM1l0tRYAe1BtxoYT2v3EOYI9JACaYNq6lMAFUSw0rFCZE4e7
+# swWAsk0wAly4JoNdtGNz764jlU9gKL431VulAgMBAAGjVDBSMA4GA1UdDwEB/wQE
+# AwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTIftJqhSobyhmYBAcnz1AQ
+# T2ioojAQBgkrBgEEAYI3FQEEAwIBADANBgkqhkiG9w0BAQwFAAOCAgEAr2rd5hnn
+# LZRDGU7L6VCVZKUDkQKL4jaAOxWiUsIWGbZqWl10QzD0m/9gdAmxIR6QFm3FJI9c
+# Zohj9E/MffISTEAQiwGf2qnIrvKVG8+dBetJPnSgaFvlVixlHIJ+U9pW2UYXeZJF
+# xBA2CFIpF8svpvJ+1Gkkih6PsHMNzBxKq7Kq7aeRYwFkIqgyuH4yKLNncy2RtNwx
+# AQv3Rwqm8ddK7VZgxCwIo3tAsLx0J1KH1r6I3TeKiW5niB31yV2g/rarOoDXGpc8
+# FzYiQR6sTdWD5jw4vU8w6VSp07YEwzJ2YbuwGMUrGLPAgNW3lbBeUU0i/OxYqujY
+# lLSlLu2S3ucYfCFX3VVj979tzR/SpncocMfiWzpbCNJbTsgAlrPhgzavhgplXHT2
+# 6ux6anSg8Evu75SjrFDyh+3XOjCDyft9V77l4/hByuVkrrOj7FjshZrM77nq81YY
+# uVxzmq/FdxeDWds3GhhyVKVB0rYjdaNDmuV3fJZ5t0GNv+zcgKCf0Xd1WF81E+Al
+# GmcLfc4l+gcK5GEh2NQc5QfGNpn0ltDGFf5Ozdeui53bFv0ExpK91IjmqaOqu/dk
+# ODtfzAzQNb50GQOmxapMomE2gj4d8yu8l13bS3g7LfU772Aj6PXsCyM2la+YZr9T
+# 03u4aUoqlmZpxJTG9F9urJh4iIAGXKKy7aIwggb+MIIE5qADAgECAhMzAAM/y2Wy
+# WWnFfpZcAAAAAz/LMA0GCSqGSIb3DQEBDAUAMFoxCzAJBgNVBAYTAlVTMR4wHAYD
+# VQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKzApBgNVBAMTIk1pY3Jvc29mdCBJ
+# RCBWZXJpZmllZCBDUyBBT0MgQ0EgMDEwHhcNMjUwNDA4MDEwNzI0WhcNMjUwNDEx
+# MDEwNzI0WjB8MQswCQYDVQQGEwJVUzEPMA0GA1UECBMGT3JlZ29uMRIwEAYDVQQH
+# EwlCZWF2ZXJ0b24xIzAhBgNVBAoTGlB5dGhvbiBTb2Z0d2FyZSBGb3VuZGF0aW9u
+# MSMwIQYDVQQDExpQeXRob24gU29mdHdhcmUgRm91bmRhdGlvbjCCAaIwDQYJKoZI
+# hvcNAQEBBQADggGPADCCAYoCggGBAI0elXEcbTdGLOszMU2fzimHGM9Y4EjwFgC2
+# iGPdieHc0dK1DyEIdtnvjKxnG/KICC3J2MrhePGzMEkie3yQjx05B5leG0q8YoGU
+# m9z9K67V6k3DSXX0vQe9FbaNVuyXed31MEf/qek7Zo4ELxu8n/LO3ibURBLRHNoW
+# Dz9zr4DcU+hha0bdIL6SnKMLwHqRj59gtFFEPqXcOVO7kobkzQS3O1T5KNL/zGuW
+# UGQln7fS4YI9bj24bfrSeG/QzLgChVYScxnUgjAANfT1+SnSxrT4/esMtfbcvfID
+# BIvOWk+FPPj9IQWsAMEG/LLG4cF/pQ/TozUXKx362GJBbe6paTM/RCUTcffd83h2
+# bXo9vXO/roZYk6H0ecd2h2FFzLUQn/0i4RQQSOp6zt1eDf28h6F8ev+YYKcChph8
+# iRt32bJPcLQVbUzhehzT4C0pz6oAqPz8s0BGvlj1G6r4CY1Cs2YiMU09/Fl64pWf
+# IsA/ReaYj6yNsgQZNUcvzobK2mTxMwIDAQABo4ICGTCCAhUwDAYDVR0TAQH/BAIw
+# ADAOBgNVHQ8BAf8EBAMCB4AwPAYDVR0lBDUwMwYKKwYBBAGCN2EBAAYIKwYBBQUH
+# AwMGGysGAQQBgjdhgqKNuwqmkohkgZH0oEWCk/3hbzAdBgNVHQ4EFgQU4Y4Xr/Xn
+# zEXblXrNC0ZLdaPEJYUwHwYDVR0jBBgwFoAU6IPEM9fcnwycdpoKptTfh6ZeWO4w
+# ZwYDVR0fBGAwXjBcoFqgWIZWaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9w
+# cy9jcmwvTWljcm9zb2Z0JTIwSUQlMjBWZXJpZmllZCUyMENTJTIwQU9DJTIwQ0El
+# MjAwMS5jcmwwgaUGCCsGAQUFBwEBBIGYMIGVMGQGCCsGAQUFBzAChlhodHRwOi8v
+# d3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL01pY3Jvc29mdCUyMElEJTIw
+# VmVyaWZpZWQlMjBDUyUyMEFPQyUyMENBJTIwMDEuY3J0MC0GCCsGAQUFBzABhiFo
+# dHRwOi8vb25lb2NzcC5taWNyb3NvZnQuY29tL29jc3AwZgYDVR0gBF8wXTBRBgwr
+# BgEEAYI3TIN9AQEwQTA/BggrBgEFBQcCARYzaHR0cDovL3d3dy5taWNyb3NvZnQu
+# Y29tL3BraW9wcy9Eb2NzL1JlcG9zaXRvcnkuaHRtMAgGBmeBDAEEATANBgkqhkiG
+# 9w0BAQwFAAOCAgEAKTeVGPXsDKqQLe1OuKx6K6q711FPxNQyLOOqeenH8zybHwNo
+# k05cMk39HQ7u+R9BQIL0bWexb7wa3XeKaX06p7aY/OQs+ycvUi/fC6RGlaLWmQ9D
+# YhZn2TBz5znimvSf3P+aidCuXeDU5c8GpBFog6fjEa/k+n7TILi0spuYZ4yC9R48
+# R63/VvpLi2SqxfJbx5n92bY6driNzAntjoravF25BSejXVrdzefbnqbQnZPB39g8
+# XHygGPb0912fIuNKPLQa/uCnmYdXJnPb0ZgMxxA8fyxvL2Q30Qf5xpFDssPDElvD
+# DoAbvR24CWvuHbu+CMMr2SJUpX4RRvDioO7JeB6wZb+64MXyPUSSf6QwkKNsHPIa
+# e9tSfREh86sYn5bOA0Wd+Igk0RpA5jDRTu3GgPOPWbm1PU+VoeqThtHt6R3l17pr
+# aQ5wIuuLXgxi1K4ZWgtvXw8BtIXfZz24qCtoo0+3kEGUpEHBgkF1SClbRb8uAzx+
+# 0ROGniLPJRU20Xfn7CgipeKLcNn33JPFwQHk1zpbGS0090mi0erOQCz0S47YdHmm
+# RJcbkNIL9DeNAglTZ/TFxrYUM1NRS1Cp4e63MgBKcWh9VJNokInzzmS+bofZz+u1
+# mm8YNtiJjdT8fmizXdUEk68EXQhOs0+HBNvc9nMRK6R28MZu/J+PaUcPL84wggda
+# MIIFQqADAgECAhMzAAAABzeMW6HZW4zUAAAAAAAHMA0GCSqGSIb3DQEBDAUAMGMx
+# CzAJBgNVBAYTAlVTMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xNDAy
+# BgNVBAMTK01pY3Jvc29mdCBJRCBWZXJpZmllZCBDb2RlIFNpZ25pbmcgUENBIDIw
+# MjEwHhcNMjEwNDEzMTczMTU0WhcNMjYwNDEzMTczMTU0WjBaMQswCQYDVQQGEwJV
+# UzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSswKQYDVQQDEyJNaWNy
+# b3NvZnQgSUQgVmVyaWZpZWQgQ1MgQU9DIENBIDAxMIICIjANBgkqhkiG9w0BAQEF
+# AAOCAg8AMIICCgKCAgEAt/fAAygHxbo+jxA04hNI8bz+EqbWvSu9dRgAawjCZau1
+# Y54IQal5ArpJWi8cIj0WA+mpwix8iTRguq9JELZvTMo2Z1U6AtE1Tn3mvq3mywZ9
+# SexVd+rPOTr+uda6GVgwLA80LhRf82AvrSwxmZpCH/laT08dn7+Gt0cXYVNKJORm
+# 1hSrAjjDQiZ1Jiq/SqiDoHN6PGmT5hXKs22E79MeFWYB4y0UlNqW0Z2LPNua8k0r
+# bERdiNS+nTP/xsESZUnrbmyXZaHvcyEKYK85WBz3Sr6Et8Vlbdid/pjBpcHI+Hyt
+# oaUAGE6rSWqmh7/aEZeDDUkz9uMKOGasIgYnenUk5E0b2U//bQqDv3qdhj9UJYWA
+# DNYC/3i3ixcW1VELaU+wTqXTxLAFelCi/lRHSjaWipDeE/TbBb0zTCiLnc9nmOjZ
+# PKlutMNho91wxo4itcJoIk2bPot9t+AV+UwNaDRIbcEaQaBycl9pcYwWmf0bJ4IF
+# n/CmYMVG1ekCBxByyRNkFkHmuMXLX6PMXcveE46jMr9syC3M8JHRddR4zVjd/FxB
+# nS5HOro3pg6StuEPshrp7I/Kk1cTG8yOWl8aqf6OJeAVyG4lyJ9V+ZxClYmaU5yv
+# tKYKk1FLBnEBfDWw+UAzQV0vcLp6AVx2Fc8n0vpoyudr3SwZmckJuz7R+S79BzMC
+# AwEAAaOCAg4wggIKMA4GA1UdDwEB/wQEAwIBhjAQBgkrBgEEAYI3FQEEAwIBADAd
+# BgNVHQ4EFgQU6IPEM9fcnwycdpoKptTfh6ZeWO4wVAYDVR0gBE0wSzBJBgRVHSAA
+# MEEwPwYIKwYBBQUHAgEWM2h0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMv
+# RG9jcy9SZXBvc2l0b3J5Lmh0bTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTAS
+# BgNVHRMBAf8ECDAGAQH/AgEAMB8GA1UdIwQYMBaAFNlBKbAPD2Ns72nX9c0pnqRI
+# ajDmMHAGA1UdHwRpMGcwZaBjoGGGX2h0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9w
+# a2lvcHMvY3JsL01pY3Jvc29mdCUyMElEJTIwVmVyaWZpZWQlMjBDb2RlJTIwU2ln
+# bmluZyUyMFBDQSUyMDIwMjEuY3JsMIGuBggrBgEFBQcBAQSBoTCBnjBtBggrBgEF
+# BQcwAoZhaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jZXJ0cy9NaWNy
+# b3NvZnQlMjBJRCUyMFZlcmlmaWVkJTIwQ29kZSUyMFNpZ25pbmclMjBQQ0ElMjAy
+# MDIxLmNydDAtBggrBgEFBQcwAYYhaHR0cDovL29uZW9jc3AubWljcm9zb2Z0LmNv
+# bS9vY3NwMA0GCSqGSIb3DQEBDAUAA4ICAQB3/utLItkwLTp4Nfh99vrbpSsL8NwP
+# Ij2+TBnZGL3C8etTGYs+HZUxNG+rNeZa+Rzu9oEcAZJDiGjEWytzMavD6Bih3nEW
+# FsIW4aGh4gB4n/pRPeeVrK4i1LG7jJ3kPLRhNOHZiLUQtmrF4V6IxtUFjvBnijaZ
+# 9oIxsSSQP8iHMjP92pjQrHBFWHGDbkmx+yO6Ian3QN3YmbdfewzSvnQmKbkiTibJ
+# gcJ1L0TZ7BwmsDvm+0XRsPOfFgnzhLVqZdEyWww10bflOeBKqkb3SaCNQTz8nsha
+# UZhrxVU5qNgYjaaDQQm+P2SEpBF7RolEC3lllfuL4AOGCtoNdPOWrx9vBZTXAVdT
+# E2r0IDk8+5y1kLGTLKzmNFn6kVCc5BddM7xoDWQ4aUoCRXcsBeRhsclk7kVXP+zJ
+# GPOXwjUJbnz2Kt9iF/8B6FDO4blGuGrogMpyXkuwCC2Z4XcfyMjPDhqZYAPGGTUI
+# NMtFbau5RtGG1DOWE9edCahtuPMDgByfPixvhy3sn7zUHgIC/YsOTMxVuMQi/bga
+# memo/VNKZrsZaS0nzmOxKpg9qDefj5fJ9gIHXcp2F0OHcVwe3KnEXa8kqzMDfrRl
+# /wwKrNSFn3p7g0b44Ad1ONDmWt61MLQvF54LG62i6ffhTCeoFT9Z9pbUo2gxlyTF
+# g7Bm0fgOlnRfGDCCB54wggWGoAMCAQICEzMAAAAHh6M0o3uljhwAAAAAAAcwDQYJ
+# KoZIhvcNAQEMBQAwdzELMAkGA1UEBhMCVVMxHjAcBgNVBAoTFU1pY3Jvc29mdCBD
+# b3Jwb3JhdGlvbjFIMEYGA1UEAxM/TWljcm9zb2Z0IElkZW50aXR5IFZlcmlmaWNh
+# dGlvbiBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAyMDIwMB4XDTIxMDQwMTIw
+# MDUyMFoXDTM2MDQwMTIwMTUyMFowYzELMAkGA1UEBhMCVVMxHjAcBgNVBAoTFU1p
+# Y3Jvc29mdCBDb3Jwb3JhdGlvbjE0MDIGA1UEAxMrTWljcm9zb2Z0IElEIFZlcmlm
+# aWVkIENvZGUgU2lnbmluZyBQQ0EgMjAyMTCCAiIwDQYJKoZIhvcNAQEBBQADggIP
+# ADCCAgoCggIBALLwwK8ZiCji3VR6TElsaQhVCbRS/3pK+MHrJSj3Zxd3KU3rlfL3
+# qrZilYKJNqztA9OQacr1AwoNcHbKBLbsQAhBnIB34zxf52bDpIO3NJlfIaTE/xrw
+# eLoQ71lzCHkD7A4As1Bs076Iu+mA6cQzsYYH/Cbl1icwQ6C65rU4V9NQhNUwgrx9
+# rGQ//h890Q8JdjLLw0nV+ayQ2Fbkd242o9kH82RZsH3HEyqjAB5a8+Ae2nPIPc8s
+# ZU6ZE7iRrRZywRmrKDp5+TcmJX9MRff241UaOBs4NmHOyke8oU1TYrkxh+YeHgfW
+# o5tTgkoSMoayqoDpHOLJs+qG8Tvh8SnifW2Jj3+ii11TS8/FGngEaNAWrbyfNrC6
+# 9oKpRQXY9bGH6jn9NEJv9weFxhTwyvx9OJLXmRGbAUXN1U9nf4lXezky6Uh/cgjk
+# Vd6CGUAf0K+Jw+GE/5VpIVbcNr9rNE50Sbmy/4RTCEGvOq3GhjITbCa4crCzTTHg
+# YYjHs1NbOc6brH+eKpWLtr+bGecy9CrwQyx7S/BfYJ+ozst7+yZtG2wR461uckFu
+# 0t+gCwLdN0A6cFtSRtR8bvxVFyWwTtgMMFRuBa3vmUOTnfKLsLefRaQcVTgRnzeL
+# zdpt32cdYKp+dhr2ogc+qM6K4CBI5/j4VFyC4QFeUP2YAidLtvpXRRo3AgMBAAGj
+# ggI1MIICMTAOBgNVHQ8BAf8EBAMCAYYwEAYJKwYBBAGCNxUBBAMCAQAwHQYDVR0O
+# BBYEFNlBKbAPD2Ns72nX9c0pnqRIajDmMFQGA1UdIARNMEswSQYEVR0gADBBMD8G
+# CCsGAQUFBwIBFjNodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL0RvY3Mv
+# UmVwb3NpdG9yeS5odG0wGQYJKwYBBAGCNxQCBAweCgBTAHUAYgBDAEEwDwYDVR0T
+# AQH/BAUwAwEB/zAfBgNVHSMEGDAWgBTIftJqhSobyhmYBAcnz1AQT2ioojCBhAYD
+# VR0fBH0wezB5oHegdYZzaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9j
+# cmwvTWljcm9zb2Z0JTIwSWRlbnRpdHklMjBWZXJpZmljYXRpb24lMjBSb290JTIw
+# Q2VydGlmaWNhdGUlMjBBdXRob3JpdHklMjAyMDIwLmNybDCBwwYIKwYBBQUHAQEE
+# gbYwgbMwgYEGCCsGAQUFBzAChnVodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtp
+# b3BzL2NlcnRzL01pY3Jvc29mdCUyMElkZW50aXR5JTIwVmVyaWZpY2F0aW9uJTIw
+# Um9vdCUyMENlcnRpZmljYXRlJTIwQXV0aG9yaXR5JTIwMjAyMC5jcnQwLQYIKwYB
+# BQUHMAGGIWh0dHA6Ly9vbmVvY3NwLm1pY3Jvc29mdC5jb20vb2NzcDANBgkqhkiG
+# 9w0BAQwFAAOCAgEAfyUqnv7Uq+rdZgrbVyNMul5skONbhls5fccPlmIbzi+OwVdP
+# Q4H55v7VOInnmezQEeW4LqK0wja+fBznANbXLB0KrdMCbHQpbLvG6UA/Xv2pfpVI
+# E1CRFfNF4XKO8XYEa3oW8oVH+KZHgIQRIwAbyFKQ9iyj4aOWeAzwk+f9E5StNp5T
+# 8FG7/VEURIVWArbAzPt9ThVN3w1fAZkF7+YU9kbq1bCR2YD+MtunSQ1Rft6XG7b4
+# e0ejRA7mB2IoX5hNh3UEauY0byxNRG+fT2MCEhQl9g2i2fs6VOG19CNep7SquKaB
+# jhWmirYyANb0RJSLWjinMLXNOAga10n8i9jqeprzSMU5ODmrMCJE12xS/NWShg/t
+# uLjAsKP6SzYZ+1Ry358ZTFcx0FS/mx2vSoU8s8HRvy+rnXqyUJ9HBqS0DErVLjQw
+# K8VtsBdekBmdTbQVoCgPCqr+PDPB3xajYnzevs7eidBsM71PINK2BoE2UfMwxCCX
+# 3mccFgx6UsQeRSdVVVNSyALQe6PT12418xon2iDGE81OGCreLzDcMAZnrUAx4XQL
+# Uz6ZTl65yPUiOh3k7Yww94lDf+8oG2oZmDh5O1Qe38E+M3vhKwmzIeoB1dVLlz4i
+# 3IpaDcR+iuGjH2TdaC1ZOmBXiCRKJLj4DT2uhJ04ji+tHD6n58vhavFIrmcxgheN
+# MIIXiQIBATBxMFoxCzAJBgNVBAYTAlVTMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29y
+# cG9yYXRpb24xKzApBgNVBAMTIk1pY3Jvc29mdCBJRCBWZXJpZmllZCBDUyBBT0Mg
+# Q0EgMDECEzMAAz/LZbJZacV+llwAAAADP8swDQYJYIZIAWUDBAIBBQCggcowGQYJ
+# KoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEOMAwGCisGAQQB
+# gjcCARUwLwYJKoZIhvcNAQkEMSIEIGcBno/ti9PCrR9sXrajsTvlHQvGxbk63JiI
+# URJByQuGMF4GCisGAQQBgjcCAQwxUDBOoEiARgBCAHUAaQBsAHQAOgAgAFIAZQBs
+# AGUAYQBzAGUAXwB2ADMALgAxADIALgAxADAAXwAyADAAMgA1ADAANAAwADgALgAw
+# ADKhAoAAMA0GCSqGSIb3DQEBAQUABIIBgE9xMVem4h5iAbvBzmB1pTdA4LYNkvd/
+# hSbYmJRt5oJqBR0RGbUmcfYAgTlhdb/S84aGvI3N62I8qeMApnH89q+UF0i8p6+U
+# Qza6Mu1cAHCq0NkHH6+N8g7nIfe5Cn+BBCBJ6kuYfQm9bx1JwEm5/yVCwG9I6+XV
+# 3WonOeA8djuZFfB9OIW6N9ubX7X+nYqWaeT6w6/lDs8mL+s0Fumy4mJ8B15pd9mr
+# N6dIRFokzhuALq6G0USKFzYf3qJQ4GyCos/Luez3cr8sE/78ds6vah5IlLP6qXMM
+# ETwAdoymIYSm3Dly3lflodd4d7/nkMhfHITOxSUDoBbCP6MO1rhChX591rJy/omK
+# 0RdM9ZpMl6VXHhzZ+lB8U/6j7xJGlxJSJHet7HFEuTnJEjY9dDy2bUgzk0vK1Rs2
+# l7VLOP3X87p9iVz5vDAOQB0fcsMDJvhIzJlmIb5z2uZ6hqD4UZdTDMLIBWe9H7Kv
+# rhmGDPHPRboFKtTrKoKcWaf4fJJ2NUtYlKGCFKAwghScBgorBgEEAYI3AwMBMYIU
+# jDCCFIgGCSqGSIb3DQEHAqCCFHkwghR1AgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFh
+# BgsqhkiG9w0BCRABBKCCAVAEggFMMIIBSAIBAQYKKwYBBAGEWQoDATAxMA0GCWCG
+# SAFlAwQCAQUABCAY3nVyqXzzboHwsVGd+j5FjG9eaMv+O3mJKpX+3EJ43AIGZ9gU
+# uyvYGBMyMDI1MDQwODEyNDEyMi40MTNaMASAAgH0oIHgpIHdMIHaMQswCQYDVQQG
+# EwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwG
+# A1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQg
+# QW1lcmljYSBPcGVyYXRpb25zMSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjozREE1
+# LTk2M0ItRTFGNDE1MDMGA1UEAxMsTWljcm9zb2Z0IFB1YmxpYyBSU0EgVGltZSBT
+# dGFtcGluZyBBdXRob3JpdHmggg8gMIIHgjCCBWqgAwIBAgITMwAAAAXlzw//Zi7J
+# hwAAAAAABTANBgkqhkiG9w0BAQwFADB3MQswCQYDVQQGEwJVUzEeMBwGA1UEChMV
+# TWljcm9zb2Z0IENvcnBvcmF0aW9uMUgwRgYDVQQDEz9NaWNyb3NvZnQgSWRlbnRp
+# dHkgVmVyaWZpY2F0aW9uIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMjAw
+# HhcNMjAxMTE5MjAzMjMxWhcNMzUxMTE5MjA0MjMxWjBhMQswCQYDVQQGEwJVUzEe
+# MBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTIwMAYDVQQDEylNaWNyb3Nv
+# ZnQgUHVibGljIFJTQSBUaW1lc3RhbXBpbmcgQ0EgMjAyMDCCAiIwDQYJKoZIhvcN
+# AQEBBQADggIPADCCAgoCggIBAJ5851Jj/eDFnwV9Y7UGIqMcHtfnlzPREwW9ZUZH
+# d5HBXXBvf7KrQ5cMSqFSHGqg2/qJhYqOQxwuEQXG8kB41wsDJP5d0zmLYKAY8Zxv
+# 3lYkuLDsfMuIEqvGYOPURAH+Ybl4SJEESnt0MbPEoKdNihwM5xGv0rGofJ1qOYST
+# Ncc55EbBT7uq3wx3mXhtVmtcCEr5ZKTkKKE1CxZvNPWdGWJUPC6e4uRfWHIhZcgC
+# sJ+sozf5EeH5KrlFnxpjKKTavwfFP6XaGZGWUG8TZaiTogRoAlqcevbiqioUz1Yt
+# 4FRK53P6ovnUfANjIgM9JDdJ4e0qiDRm5sOTiEQtBLGd9Vhd1MadxoGcHrRCsS5r
+# O9yhv2fjJHrmlQ0EIXmp4DhDBieKUGR+eZ4CNE3ctW4uvSDQVeSp9h1SaPV8UWEf
+# yTxgGjOsRpeexIveR1MPTVf7gt8hY64XNPO6iyUGsEgt8c2PxF87E+CO7A28TpjN
+# q5eLiiunhKbq0XbjkNoU5JhtYUrlmAbpxRjb9tSreDdtACpm3rkpxp7AQndnI0Sh
+# u/fk1/rE3oWsDqMX3jjv40e8KN5YsJBnczyWB4JyeeFMW3JBfdeAKhzohFe8U5w9
+# WuvcP1E8cIxLoKSDzCCBOu0hWdjzKNu8Y5SwB1lt5dQhABYyzR3dxEO/T1K/BVF3
+# rV69AgMBAAGjggIbMIICFzAOBgNVHQ8BAf8EBAMCAYYwEAYJKwYBBAGCNxUBBAMC
+# AQAwHQYDVR0OBBYEFGtpKDo1L0hjQM972K9J6T7ZPdshMFQGA1UdIARNMEswSQYE
+# VR0gADBBMD8GCCsGAQUFBwIBFjNodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtp
+# b3BzL0RvY3MvUmVwb3NpdG9yeS5odG0wEwYDVR0lBAwwCgYIKwYBBQUHAwgwGQYJ
+# KwYBBAGCNxQCBAweCgBTAHUAYgBDAEEwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSME
+# GDAWgBTIftJqhSobyhmYBAcnz1AQT2ioojCBhAYDVR0fBH0wezB5oHegdYZzaHR0
+# cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jcmwvTWljcm9zb2Z0JTIwSWRl
+# bnRpdHklMjBWZXJpZmljYXRpb24lMjBSb290JTIwQ2VydGlmaWNhdGUlMjBBdXRo
+# b3JpdHklMjAyMDIwLmNybDCBlAYIKwYBBQUHAQEEgYcwgYQwgYEGCCsGAQUFBzAC
+# hnVodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL01pY3Jvc29m
+# dCUyMElkZW50aXR5JTIwVmVyaWZpY2F0aW9uJTIwUm9vdCUyMENlcnRpZmljYXRl
+# JTIwQXV0aG9yaXR5JTIwMjAyMC5jcnQwDQYJKoZIhvcNAQEMBQADggIBAF+Idsd+
+# bbVaFXXnTHho+k7h2ESZJRWluLE0Oa/pO+4ge/XEizXvhs0Y7+KVYyb4nHlugBes
+# nFqBGEdC2IWmtKMyS1OWIviwpnK3aL5JedwzbeBF7POyg6IGG/XhhJ3UqWeWTO+C
+# zb1c2NP5zyEh89F72u9UIw+IfvM9lzDmc2O2END7MPnrcjWdQnrLn1Ntday7JSyr
+# DvBdmgbNnCKNZPmhzoa8PccOiQljjTW6GePe5sGFuRHzdFt8y+bN2neF7Zu8hTO1
+# I64XNGqst8S+w+RUdie8fXC1jKu3m9KGIqF4aldrYBamyh3g4nJPj/LR2CBaLyD+
+# 2BuGZCVmoNR/dSpRCxlot0i79dKOChmoONqbMI8m04uLaEHAv4qwKHQ1vBzbV/nG
+# 89LDKbRSSvijmwJwxRxLLpMQ/u4xXxFfR4f/gksSkbJp7oqLwliDm/h+w0aJ/U5c
+# cnYhYb7vPKNMN+SZDWycU5ODIRfyoGl59BsXR/HpRGtiJquOYGmvA/pk5vC1lcnb
+# eMrcWD/26ozePQ/TWfNXKBOmkFpvPE8CH+EeGGWzqTCjdAsno2jzTeNSxlx3glDG
+# Jgcdz5D/AAxw9Sdgq/+rY7jjgs7X6fqPTXPmaCAJKVHAP19oEjJIBwD1LyHbaEgB
+# xFCogYSOiUIr0Xqcr1nJfiWG2GwYe6ZoAF1bMIIHljCCBX6gAwIBAgITMwAAAEYX
+# 5HV6yv3a5QAAAAAARjANBgkqhkiG9w0BAQwFADBhMQswCQYDVQQGEwJVUzEeMBwG
+# A1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTIwMAYDVQQDEylNaWNyb3NvZnQg
+# UHVibGljIFJTQSBUaW1lc3RhbXBpbmcgQ0EgMjAyMDAeFw0yNDExMjYxODQ4NDla
+# Fw0yNTExMTkxODQ4NDlaMIHaMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGlu
+# Z3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBv
+# cmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1lcmljYSBPcGVyYXRpb25zMSYw
+# JAYDVQQLEx1UaGFsZXMgVFNTIEVTTjozREE1LTk2M0ItRTFGNDE1MDMGA1UEAxMs
+# TWljcm9zb2Z0IFB1YmxpYyBSU0EgVGltZSBTdGFtcGluZyBBdXRob3JpdHkwggIi
+# MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCwlXzoj/MNL1BfnV+gg4d0fZum
+# 1HdUJidSNTcDzpHJvmIBqH566zBYcV0TyN7+3qOnJjpoTx6JBMgNYnL5BmTX9Hrm
+# X0WdNMLf74u7NtBSuAD2sf6n2qUUrz7i8f7r0JiZixKJnkvA/1akLHppQMDCug1o
+# C0AYjd753b5vy1vWdrHXE9hL71BZe5DCq5/4LBny8aOQZlzvjewgONkiZm+Sfctk
+# Jjh9LxdkDlq5EvGE6YU0uC37XF7qkHvIksD2+XgBP0lEMfmPJo2fI9FwIA9YMX7K
+# IINEM5OY6nkvKryM9s5bK6LV4z48NYpiI1xvH15YDps+19nHCtKMVTZdB4cYhA0d
+# VqJ7dAu4VcxUwD1AEcMxWbIOR1z6OFkVY9GX5oH8k17d9t35PWfn0XuxW4SG/rim
+# gtFgpE/shRsy5nMCbHyeCdW0He1plrYQqTsSHP2n/lz2DCgIlnx+uvPLVf5+JG/1
+# d1i/LdwbC2WH6UEEJyZIl3a0YwM4rdzoR+P4dO9I/2oWOxXCYqFytYdCy9ljELUw
+# byLjrjRddteR8QTxrCfadKpKfFY6Ak/HNZPUHaAPak3baOIvV7Q8axo3DWQy2ib3
+# zXV6hMPNt1v90pv+q9daQdwUzUrgcbwThdrRhWHwlRIVg2sR668HPn4/8l9ikGok
+# rL6gAmVxNswEZ9awCwIDAQABo4IByzCCAccwHQYDVR0OBBYEFBE20NSvdrC6Z6cm
+# 6RPGP8YbqIrxMB8GA1UdIwQYMBaAFGtpKDo1L0hjQM972K9J6T7ZPdshMGwGA1Ud
+# HwRlMGMwYaBfoF2GW2h0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY3Js
+# L01pY3Jvc29mdCUyMFB1YmxpYyUyMFJTQSUyMFRpbWVzdGFtcGluZyUyMENBJTIw
+# MjAyMC5jcmwweQYIKwYBBQUHAQEEbTBrMGkGCCsGAQUFBzAChl1odHRwOi8vd3d3
+# Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL01pY3Jvc29mdCUyMFB1YmxpYyUy
+# MFJTQSUyMFRpbWVzdGFtcGluZyUyMENBJTIwMjAyMC5jcnQwDAYDVR0TAQH/BAIw
+# ADAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAOBgNVHQ8BAf8EBAMCB4AwZgYDVR0g
+# BF8wXTBRBgwrBgEEAYI3TIN9AQEwQTA/BggrBgEFBQcCARYzaHR0cDovL3d3dy5t
+# aWNyb3NvZnQuY29tL3BraW9wcy9Eb2NzL1JlcG9zaXRvcnkuaHRtMAgGBmeBDAEE
+# AjANBgkqhkiG9w0BAQwFAAOCAgEAFIW5L+gGzX4gyHorS33YKXuK9iC91iZTpm30
+# x/EdHG6U8NAu2qityxjZVq6MDq300gspG0ntzLYqVhjfku7iNzE78k6tNgFCr9wv
+# GkIHeK+Q2RAO9/s5R8rhNC+lywOB+6K5Zi0kfO0agVXf7Nk2O6F6D9AEzNLijG+c
+# Oe5Ef2F5l4ZsVSkLFCI5jELC+r4KnNZjunc+qvjSz2DkNsXfrjFhyk+K7v7U7+JF
+# Z8kZ58yFuxEX0cxDKpJLxiNh/ODCOL2UxYkhyfI3AR0EhfxX9QZHVgxyZwnavR35
+# FxqLSiGTeAJsK7YN3bIxyuP6eCcnkX8TMdpu9kPD97sHnM7po0UQDrjaN7etviLD
+# xnax2nemdvJW3BewOLFrD1nSnd7ZHdPGPB3oWTCaK9/3XwQERLi3Xj+HZc89RP50
+# Nt7h7+3G6oq2kXYNidI9iWd+gL+lvkQZH9YTIfBCLWjvuXvUUUU+AvFI00Utqrvd
+# rIdqCFaqE9HHQgSfXeQ53xLWdMCztUP/YnMXiJxNBkc6UE2px/o6+/LXJDIpwIXR
+# 4HSodLfkfsNQl6FFrJ1xsOYGSHvcFkH8389RmUvrjr1NBbdesc4Bu4kox+3cabOZ
+# c1zm89G+1RRL2tReFzSMlYSGO3iKn3GGXmQiRmFlBb3CpbUVQz+fgxVMfeL0j4Lm
+# KQfT1jIxggPUMIID0AIBATB4MGExCzAJBgNVBAYTAlVTMR4wHAYDVQQKExVNaWNy
+# b3NvZnQgQ29ycG9yYXRpb24xMjAwBgNVBAMTKU1pY3Jvc29mdCBQdWJsaWMgUlNB
+# IFRpbWVzdGFtcGluZyBDQSAyMDIwAhMzAAAARhfkdXrK/drlAAAAAABGMA0GCWCG
+# SAFlAwQCAQUAoIIBLTAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwLwYJKoZI
+# hvcNAQkEMSIEIHgwQkiMhul6IrfEKmPaCFR+R91oZOlPqVgP/9PPcfn+MIHdBgsq
+# hkiG9w0BCRACLzGBzTCByjCBxzCBoAQgEid2SJpUPj5xQm73M4vqDmVh1QR6TiuT
+# UVkL3P8Wis4wfDBlpGMwYTELMAkGA1UEBhMCVVMxHjAcBgNVBAoTFU1pY3Jvc29m
+# dCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFB1YmxpYyBSU0EgVGlt
+# ZXN0YW1waW5nIENBIDIwMjACEzMAAABGF+R1esr92uUAAAAAAEYwIgQgVp6I1YBM
+# Mni0rCuD57vEK/tzWZypHqWFikWLFVY11RwwDQYJKoZIhvcNAQELBQAEggIAnRBH
+# voM5+wbJp+aOwrrL8fi8Rv/eFV820Nhr+jMny73UscN60OWdcdcZDbjDlnDX1KEP
+# sNcEOFvaruHHrF4kDK8N0yemElNz63IgqhUoGoXXQKT2RgVg7T/kiQJH7zuaEjgB
+# YNniAZdXXJJ1C+uv2ZQzkGIEVIEA6pB5/xo4kFhrfkOrdGzqL8HXT/RZQDMn5Uzk
+# W+Sl2JmsyYBS4sgI9Ay3qT5nv+frzngbWlqx1dre21uj37Fgk5mWHJEdmY1nqTTd
+# 25j6oDLGPC8AS9wtgZBXggemKAXwyeOFFahXUFN7X7cbwTALy5aWjE/rqp+N5J7M
+# +YApl3aknUZ13KTXz9pfAF0uhmZimngvBHjijyctleF8HUP2RNAhS/l68OqW7oKi
+# Dqvb7tSHJbcnYkxo7dUq6ppfN51ah61ZsyMVG6SaH015+5QO1k50ohXcFff2GOuZ
+# d3Z9JOoAjIkeiVTNeRlPDlHtS0CSYu4ZKsWsst+0VY2R9rJBeoii9Xa0oiIggkYL
+# 1pHAPH0B1uLlvFcI6B+fAXe0OiCJodbO5lk8ZpvCG5WWYbjzp2c3B8PZGSBgEpSf
+# KYlVavvBAvaJCORUO7j8PyzzDINuzQorP9+i399ORjOnqeC92Cb0V12LcoqqtJaf
+# 7oSB86VOI0lfHnPUlLWvoiLHrFR5PsYkltOuPqU=
+# SIG # End signature block
diff --git a/.venv312/Scripts/activate b/.venv312/Scripts/activate
new file mode 100644
index 0000000..70645b6
--- /dev/null
+++ b/.venv312/Scripts/activate
@@ -0,0 +1,76 @@
+# This file must be used with "source bin/activate" *from bash*
+# You cannot run it directly
+
+deactivate () {
+ # reset old environment variables
+ if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then
+ PATH="${_OLD_VIRTUAL_PATH:-}"
+ export PATH
+ unset _OLD_VIRTUAL_PATH
+ fi
+ if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then
+ PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}"
+ export PYTHONHOME
+ unset _OLD_VIRTUAL_PYTHONHOME
+ fi
+
+ # Call hash to forget past locations. Without forgetting
+ # past locations the $PATH changes we made may not be respected.
+ # See "man bash" for more details. hash is usually a builtin of your shell
+ hash -r 2> /dev/null
+
+ if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then
+ PS1="${_OLD_VIRTUAL_PS1:-}"
+ export PS1
+ unset _OLD_VIRTUAL_PS1
+ fi
+
+ unset VIRTUAL_ENV
+ unset VIRTUAL_ENV_PROMPT
+ if [ ! "${1:-}" = "nondestructive" ] ; then
+ # Self destruct!
+ unset -f deactivate
+ fi
+}
+
+# unset irrelevant variables
+deactivate nondestructive
+
+# on Windows, a path can contain colons and backslashes and has to be converted:
+case "$(uname)" in
+ CYGWIN*|MSYS*|MINGW*)
+ # transform D:\path\to\venv to /d/path/to/venv on MSYS and MINGW
+ # and to /cygdrive/d/path/to/venv on Cygwin
+ VIRTUAL_ENV=$(cygpath 'C:\Users\fmerino\Documents\GitHub\Python-Web-Scraping\.venv312')
+ export VIRTUAL_ENV
+ ;;
+ *)
+ # use the path as-is
+ export VIRTUAL_ENV='C:\Users\fmerino\Documents\GitHub\Python-Web-Scraping\.venv312'
+ ;;
+esac
+
+_OLD_VIRTUAL_PATH="$PATH"
+PATH="$VIRTUAL_ENV/"Scripts":$PATH"
+export PATH
+
+VIRTUAL_ENV_PROMPT='(.venv312) '
+export VIRTUAL_ENV_PROMPT
+
+# unset PYTHONHOME if set
+# this will fail if PYTHONHOME is set to the empty string (which is bad anyway)
+# could use `if (set -u; : $PYTHONHOME) ;` in bash
+if [ -n "${PYTHONHOME:-}" ] ; then
+ _OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}"
+ unset PYTHONHOME
+fi
+
+if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then
+ _OLD_VIRTUAL_PS1="${PS1:-}"
+ PS1="("'(.venv312) '") ${PS1:-}"
+ export PS1
+fi
+
+# Call hash to forget past commands. Without forgetting
+# past commands the $PATH changes we made may not be respected
+hash -r 2> /dev/null
diff --git a/.venv312/Scripts/activate.bat b/.venv312/Scripts/activate.bat
new file mode 100644
index 0000000..eb7fef6
--- /dev/null
+++ b/.venv312/Scripts/activate.bat
@@ -0,0 +1,34 @@
+@echo off
+
+rem This file is UTF-8 encoded, so we need to update the current code page while executing it
+for /f "tokens=2 delims=:." %%a in ('"%SystemRoot%\System32\chcp.com"') do (
+ set _OLD_CODEPAGE=%%a
+)
+if defined _OLD_CODEPAGE (
+ "%SystemRoot%\System32\chcp.com" 65001 > nul
+)
+
+set "VIRTUAL_ENV=C:\Users\fmerino\Documents\GitHub\Python-Web-Scraping\.venv312"
+
+if not defined PROMPT set PROMPT=$P$G
+
+if defined _OLD_VIRTUAL_PROMPT set PROMPT=%_OLD_VIRTUAL_PROMPT%
+if defined _OLD_VIRTUAL_PYTHONHOME set PYTHONHOME=%_OLD_VIRTUAL_PYTHONHOME%
+
+set _OLD_VIRTUAL_PROMPT=%PROMPT%
+set PROMPT=(.venv312) %PROMPT%
+
+if defined PYTHONHOME set _OLD_VIRTUAL_PYTHONHOME=%PYTHONHOME%
+set PYTHONHOME=
+
+if defined _OLD_VIRTUAL_PATH set PATH=%_OLD_VIRTUAL_PATH%
+if not defined _OLD_VIRTUAL_PATH set _OLD_VIRTUAL_PATH=%PATH%
+
+set "PATH=%VIRTUAL_ENV%\Scripts;%PATH%"
+set "VIRTUAL_ENV_PROMPT=(.venv312) "
+
+:END
+if defined _OLD_CODEPAGE (
+ "%SystemRoot%\System32\chcp.com" %_OLD_CODEPAGE% > nul
+ set _OLD_CODEPAGE=
+)
diff --git a/.venv312/Scripts/deactivate.bat b/.venv312/Scripts/deactivate.bat
new file mode 100644
index 0000000..62a39a7
--- /dev/null
+++ b/.venv312/Scripts/deactivate.bat
@@ -0,0 +1,22 @@
+@echo off
+
+if defined _OLD_VIRTUAL_PROMPT (
+ set "PROMPT=%_OLD_VIRTUAL_PROMPT%"
+)
+set _OLD_VIRTUAL_PROMPT=
+
+if defined _OLD_VIRTUAL_PYTHONHOME (
+ set "PYTHONHOME=%_OLD_VIRTUAL_PYTHONHOME%"
+ set _OLD_VIRTUAL_PYTHONHOME=
+)
+
+if defined _OLD_VIRTUAL_PATH (
+ set "PATH=%_OLD_VIRTUAL_PATH%"
+)
+
+set _OLD_VIRTUAL_PATH=
+
+set VIRTUAL_ENV=
+set VIRTUAL_ENV_PROMPT=
+
+:END
diff --git a/.venv312/Scripts/debugpy-adapter.exe b/.venv312/Scripts/debugpy-adapter.exe
new file mode 100644
index 0000000..54567cb
Binary files /dev/null and b/.venv312/Scripts/debugpy-adapter.exe differ
diff --git a/.venv312/Scripts/debugpy.exe b/.venv312/Scripts/debugpy.exe
new file mode 100644
index 0000000..cf73653
Binary files /dev/null and b/.venv312/Scripts/debugpy.exe differ
diff --git a/.venv312/Scripts/f2py.exe b/.venv312/Scripts/f2py.exe
new file mode 100644
index 0000000..c41b53e
Binary files /dev/null and b/.venv312/Scripts/f2py.exe differ
diff --git a/.venv312/Scripts/ipython.exe b/.venv312/Scripts/ipython.exe
new file mode 100644
index 0000000..c89a257
Binary files /dev/null and b/.venv312/Scripts/ipython.exe differ
diff --git a/.venv312/Scripts/ipython3.exe b/.venv312/Scripts/ipython3.exe
new file mode 100644
index 0000000..c89a257
Binary files /dev/null and b/.venv312/Scripts/ipython3.exe differ
diff --git a/.venv312/Scripts/jupyter-kernel.exe b/.venv312/Scripts/jupyter-kernel.exe
new file mode 100644
index 0000000..c1ebe68
Binary files /dev/null and b/.venv312/Scripts/jupyter-kernel.exe differ
diff --git a/.venv312/Scripts/jupyter-kernelspec.exe b/.venv312/Scripts/jupyter-kernelspec.exe
new file mode 100644
index 0000000..fefb091
Binary files /dev/null and b/.venv312/Scripts/jupyter-kernelspec.exe differ
diff --git a/.venv312/Scripts/jupyter-migrate.exe b/.venv312/Scripts/jupyter-migrate.exe
new file mode 100644
index 0000000..7b0187f
Binary files /dev/null and b/.venv312/Scripts/jupyter-migrate.exe differ
diff --git a/.venv312/Scripts/jupyter-run.exe b/.venv312/Scripts/jupyter-run.exe
new file mode 100644
index 0000000..ee83ca2
Binary files /dev/null and b/.venv312/Scripts/jupyter-run.exe differ
diff --git a/.venv312/Scripts/jupyter-troubleshoot.exe b/.venv312/Scripts/jupyter-troubleshoot.exe
new file mode 100644
index 0000000..3ef41ab
Binary files /dev/null and b/.venv312/Scripts/jupyter-troubleshoot.exe differ
diff --git a/.venv312/Scripts/jupyter.exe b/.venv312/Scripts/jupyter.exe
new file mode 100644
index 0000000..5ca9777
Binary files /dev/null and b/.venv312/Scripts/jupyter.exe differ
diff --git a/.venv312/Scripts/normalizer.exe b/.venv312/Scripts/normalizer.exe
new file mode 100644
index 0000000..ddc70cf
Binary files /dev/null and b/.venv312/Scripts/normalizer.exe differ
diff --git a/.venv312/Scripts/numpy-config.exe b/.venv312/Scripts/numpy-config.exe
new file mode 100644
index 0000000..f2df40f
Binary files /dev/null and b/.venv312/Scripts/numpy-config.exe differ
diff --git a/.venv312/Scripts/pip.exe b/.venv312/Scripts/pip.exe
new file mode 100644
index 0000000..7272202
Binary files /dev/null and b/.venv312/Scripts/pip.exe differ
diff --git a/.venv312/Scripts/pip3.12.exe b/.venv312/Scripts/pip3.12.exe
new file mode 100644
index 0000000..7272202
Binary files /dev/null and b/.venv312/Scripts/pip3.12.exe differ
diff --git a/.venv312/Scripts/pip3.exe b/.venv312/Scripts/pip3.exe
new file mode 100644
index 0000000..7272202
Binary files /dev/null and b/.venv312/Scripts/pip3.exe differ
diff --git a/.venv312/Scripts/pygmentize.exe b/.venv312/Scripts/pygmentize.exe
new file mode 100644
index 0000000..752dfd4
Binary files /dev/null and b/.venv312/Scripts/pygmentize.exe differ
diff --git a/.venv312/Scripts/python.exe b/.venv312/Scripts/python.exe
new file mode 100644
index 0000000..ba0cd04
Binary files /dev/null and b/.venv312/Scripts/python.exe differ
diff --git a/.venv312/Scripts/pythonw.exe b/.venv312/Scripts/pythonw.exe
new file mode 100644
index 0000000..68b3cfe
Binary files /dev/null and b/.venv312/Scripts/pythonw.exe differ
diff --git a/.venv312/Scripts/pywin32_postinstall.exe b/.venv312/Scripts/pywin32_postinstall.exe
new file mode 100644
index 0000000..3891ff3
Binary files /dev/null and b/.venv312/Scripts/pywin32_postinstall.exe differ
diff --git a/.venv312/Scripts/pywin32_postinstall.py b/.venv312/Scripts/pywin32_postinstall.py
new file mode 100644
index 0000000..3348b8c
--- /dev/null
+++ b/.venv312/Scripts/pywin32_postinstall.py
@@ -0,0 +1,733 @@
+# postinstall script for pywin32
+#
+# copies pywintypesXX.dll and pythoncomXX.dll into the system directory,
+# and creates a pth file
+import argparse
+import glob
+import os
+import shutil
+import sys
+import sysconfig
+import tempfile
+import winreg
+
+tee_f = open(
+ os.path.join(
+ tempfile.gettempdir(), # Send output somewhere so it can be found if necessary...
+ "pywin32_postinstall.log",
+ ),
+ "w",
+)
+
+
+class Tee:
+ def __init__(self, file):
+ self.f = file
+
+ def write(self, what):
+ if self.f is not None:
+ try:
+ self.f.write(what.replace("\n", "\r\n"))
+ except OSError:
+ pass
+ tee_f.write(what)
+
+ def flush(self):
+ if self.f is not None:
+ try:
+ self.f.flush()
+ except OSError:
+ pass
+ tee_f.flush()
+
+
+sys.stderr = Tee(sys.stderr)
+sys.stdout = Tee(sys.stdout)
+
+com_modules = [
+ # module_name, class_names
+ ("win32com.servers.interp", "Interpreter"),
+ ("win32com.servers.dictionary", "DictionaryPolicy"),
+ ("win32com.axscript.client.pyscript", "PyScript"),
+]
+
+# Is this a 'silent' install - ie, avoid all dialogs.
+# Different than 'verbose'
+silent = 0
+
+# Verbosity of output messages.
+verbose = 1
+
+root_key_name = "Software\\Python\\PythonCore\\" + sys.winver
+
+
+def get_root_hkey():
+ try:
+ winreg.OpenKey(
+ winreg.HKEY_LOCAL_MACHINE, root_key_name, 0, winreg.KEY_CREATE_SUB_KEY
+ )
+ return winreg.HKEY_LOCAL_MACHINE
+ except OSError:
+ # Either not exist, or no permissions to create subkey means
+ # must be HKCU
+ return winreg.HKEY_CURRENT_USER
+
+
+# Create a function with the same signature as create_shortcut
+# previously provided by bdist_wininst
+def create_shortcut(
+ path, description, filename, arguments="", workdir="", iconpath="", iconindex=0
+):
+ import pythoncom
+ from win32com.shell import shell
+
+ ilink = pythoncom.CoCreateInstance(
+ shell.CLSID_ShellLink,
+ None,
+ pythoncom.CLSCTX_INPROC_SERVER,
+ shell.IID_IShellLink,
+ )
+ ilink.SetPath(path)
+ ilink.SetDescription(description)
+ if arguments:
+ ilink.SetArguments(arguments)
+ if workdir:
+ ilink.SetWorkingDirectory(workdir)
+ if iconpath or iconindex:
+ ilink.SetIconLocation(iconpath, iconindex)
+ # now save it.
+ ipf = ilink.QueryInterface(pythoncom.IID_IPersistFile)
+ ipf.Save(filename, 0)
+
+
+# Support the same list of "path names" as bdist_wininst used to
+def get_special_folder_path(path_name):
+ from win32com.shell import shell, shellcon
+
+ for maybe in """
+ CSIDL_COMMON_STARTMENU CSIDL_STARTMENU CSIDL_COMMON_APPDATA
+ CSIDL_LOCAL_APPDATA CSIDL_APPDATA CSIDL_COMMON_DESKTOPDIRECTORY
+ CSIDL_DESKTOPDIRECTORY CSIDL_COMMON_STARTUP CSIDL_STARTUP
+ CSIDL_COMMON_PROGRAMS CSIDL_PROGRAMS CSIDL_PROGRAM_FILES_COMMON
+ CSIDL_PROGRAM_FILES CSIDL_FONTS""".split():
+ if maybe == path_name:
+ csidl = getattr(shellcon, maybe)
+ return shell.SHGetSpecialFolderPath(0, csidl, False)
+ raise ValueError(f"{path_name} is an unknown path ID")
+
+
+def CopyTo(desc, src, dest):
+ import win32api
+ import win32con
+
+ while 1:
+ try:
+ win32api.CopyFile(src, dest, 0)
+ return
+ except win32api.error as details:
+ if details.winerror == 5: # access denied - user not admin.
+ raise
+ if silent:
+ # Running silent mode - just re-raise the error.
+ raise
+ full_desc = (
+ f"Error {desc}\n\n"
+ "If you have any Python applications running, "
+ f"please close them now\nand select 'Retry'\n\n{details.strerror}"
+ )
+ rc = win32api.MessageBox(
+ 0, full_desc, "Installation Error", win32con.MB_ABORTRETRYIGNORE
+ )
+ if rc == win32con.IDABORT:
+ raise
+ elif rc == win32con.IDIGNORE:
+ return
+ # else retry - around we go again.
+
+
+# We need to import win32api to determine the Windows system directory,
+# so we can copy our system files there - but importing win32api will
+# load the pywintypes.dll already in the system directory preventing us
+# from updating them!
+# So, we pull the same trick pywintypes.py does, but it loads from
+# our pywintypes_system32 directory.
+def LoadSystemModule(lib_dir, modname):
+ # See if this is a debug build.
+ import importlib.machinery
+ import importlib.util
+
+ suffix = "_d" if "_d.pyd" in importlib.machinery.EXTENSION_SUFFIXES else ""
+ filename = "%s%d%d%s.dll" % (
+ modname,
+ sys.version_info.major,
+ sys.version_info.minor,
+ suffix,
+ )
+ filename = os.path.join(lib_dir, "pywin32_system32", filename)
+ loader = importlib.machinery.ExtensionFileLoader(modname, filename)
+ spec = importlib.machinery.ModuleSpec(name=modname, loader=loader, origin=filename)
+ mod = importlib.util.module_from_spec(spec)
+ loader.exec_module(mod)
+
+
+def SetPyKeyVal(key_name, value_name, value):
+ root_hkey = get_root_hkey()
+ root_key = winreg.OpenKey(root_hkey, root_key_name)
+ try:
+ my_key = winreg.CreateKey(root_key, key_name)
+ try:
+ winreg.SetValueEx(my_key, value_name, 0, winreg.REG_SZ, value)
+ if verbose:
+ print(f"-> {root_key_name}\\{key_name}[{value_name}]={value!r}")
+ finally:
+ my_key.Close()
+ finally:
+ root_key.Close()
+
+
+def UnsetPyKeyVal(key_name, value_name, delete_key=False):
+ root_hkey = get_root_hkey()
+ root_key = winreg.OpenKey(root_hkey, root_key_name)
+ try:
+ my_key = winreg.OpenKey(root_key, key_name, 0, winreg.KEY_SET_VALUE)
+ try:
+ winreg.DeleteValue(my_key, value_name)
+ if verbose:
+ print(f"-> DELETE {root_key_name}\\{key_name}[{value_name}]")
+ finally:
+ my_key.Close()
+ if delete_key:
+ winreg.DeleteKey(root_key, key_name)
+ if verbose:
+ print(f"-> DELETE {root_key_name}\\{key_name}")
+ except OSError as why:
+ winerror = getattr(why, "winerror", why.errno)
+ if winerror != 2: # file not found
+ raise
+ finally:
+ root_key.Close()
+
+
+def RegisterCOMObjects(register=True):
+ import win32com.server.register
+
+ if register:
+ func = win32com.server.register.RegisterClasses
+ else:
+ func = win32com.server.register.UnregisterClasses
+ flags = {}
+ if not verbose:
+ flags["quiet"] = 1
+ for module, klass_name in com_modules:
+ __import__(module)
+ mod = sys.modules[module]
+ flags["finalize_register"] = getattr(mod, "DllRegisterServer", None)
+ flags["finalize_unregister"] = getattr(mod, "DllUnregisterServer", None)
+ klass = getattr(mod, klass_name)
+ func(klass, **flags)
+
+
+def RegisterHelpFile(register=True, lib_dir=None):
+ if lib_dir is None:
+ lib_dir = sysconfig.get_paths()["platlib"]
+ if register:
+ # Register the .chm help file.
+ chm_file = os.path.join(lib_dir, "PyWin32.chm")
+ if os.path.isfile(chm_file):
+ # This isn't recursive, so if 'Help' doesn't exist, we croak
+ SetPyKeyVal("Help", None, None)
+ SetPyKeyVal("Help\\Pythonwin Reference", None, chm_file)
+ return chm_file
+ else:
+ print("NOTE: PyWin32.chm can not be located, so has not been registered")
+ else:
+ UnsetPyKeyVal("Help\\Pythonwin Reference", None, delete_key=True)
+ return None
+
+
+def RegisterPythonwin(register=True, lib_dir=None):
+ """Add (or remove) Pythonwin to context menu for python scripts.
+ ??? Should probably also add Edit command for pys files also.
+ Also need to remove these keys on uninstall, but there's no function
+ to add registry entries to uninstall log ???
+ """
+ import os
+
+ if lib_dir is None:
+ lib_dir = sysconfig.get_paths()["platlib"]
+ classes_root = get_root_hkey()
+ ## Installer executable doesn't seem to pass anything to postinstall script indicating if it's a debug build
+ pythonwin_exe = os.path.join(lib_dir, "Pythonwin", "Pythonwin.exe")
+ pythonwin_edit_command = pythonwin_exe + ' -edit "%1"'
+
+ keys_vals = [
+ (
+ "Software\\Microsoft\\Windows\\CurrentVersion\\App Paths\\Pythonwin.exe",
+ "",
+ pythonwin_exe,
+ ),
+ (
+ "Software\\Classes\\Python.File\\shell\\Edit with Pythonwin",
+ "command",
+ pythonwin_edit_command,
+ ),
+ (
+ "Software\\Classes\\Python.NoConFile\\shell\\Edit with Pythonwin",
+ "command",
+ pythonwin_edit_command,
+ ),
+ ]
+
+ try:
+ if register:
+ for key, sub_key, val in keys_vals:
+ ## Since winreg only uses the character Api functions, this can fail if Python
+ ## is installed to a path containing non-ascii characters
+ hkey = winreg.CreateKey(classes_root, key)
+ if sub_key:
+ hkey = winreg.CreateKey(hkey, sub_key)
+ winreg.SetValueEx(hkey, None, 0, winreg.REG_SZ, val)
+ hkey.Close()
+ else:
+ for key, sub_key, val in keys_vals:
+ try:
+ if sub_key:
+ hkey = winreg.OpenKey(classes_root, key)
+ winreg.DeleteKey(hkey, sub_key)
+ hkey.Close()
+ winreg.DeleteKey(classes_root, key)
+ except OSError as why:
+ winerror = getattr(why, "winerror", why.errno)
+ if winerror != 2: # file not found
+ raise
+ finally:
+ # tell windows about the change
+ from win32com.shell import shell, shellcon
+
+ shell.SHChangeNotify(
+ shellcon.SHCNE_ASSOCCHANGED, shellcon.SHCNF_IDLIST, None, None
+ )
+
+
+def get_shortcuts_folder():
+ if get_root_hkey() == winreg.HKEY_LOCAL_MACHINE:
+ try:
+ fldr = get_special_folder_path("CSIDL_COMMON_PROGRAMS")
+ except OSError:
+ # No CSIDL_COMMON_PROGRAMS on this platform
+ fldr = get_special_folder_path("CSIDL_PROGRAMS")
+ else:
+ # non-admin install - always goes in this user's start menu.
+ fldr = get_special_folder_path("CSIDL_PROGRAMS")
+
+ try:
+ install_group = winreg.QueryValue(
+ get_root_hkey(), root_key_name + "\\InstallPath\\InstallGroup"
+ )
+ except OSError:
+ install_group = "Python %d.%d" % (
+ sys.version_info.major,
+ sys.version_info.minor,
+ )
+ return os.path.join(fldr, install_group)
+
+
+# Get the system directory, which may be the Wow64 directory if we are a 32bit
+# python on a 64bit OS.
+def get_system_dir():
+ import win32api # we assume this exists.
+
+ try:
+ import pythoncom
+ import win32process
+ from win32com.shell import shell, shellcon
+
+ try:
+ if win32process.IsWow64Process():
+ return shell.SHGetSpecialFolderPath(0, shellcon.CSIDL_SYSTEMX86)
+ return shell.SHGetSpecialFolderPath(0, shellcon.CSIDL_SYSTEM)
+ except (pythoncom.com_error, win32process.error):
+ return win32api.GetSystemDirectory()
+ except ImportError:
+ return win32api.GetSystemDirectory()
+
+
+def fixup_dbi():
+ # We used to have a dbi.pyd with our .pyd files, but now have a .py file.
+ # If the user didn't uninstall, they will find the .pyd which will cause
+ # problems - so handle that.
+ import win32api
+ import win32con
+
+ pyd_name = os.path.join(os.path.dirname(win32api.__file__), "dbi.pyd")
+ pyd_d_name = os.path.join(os.path.dirname(win32api.__file__), "dbi_d.pyd")
+ py_name = os.path.join(os.path.dirname(win32con.__file__), "dbi.py")
+ for this_pyd in (pyd_name, pyd_d_name):
+ this_dest = this_pyd + ".old"
+ if os.path.isfile(this_pyd) and os.path.isfile(py_name):
+ try:
+ if os.path.isfile(this_dest):
+ print(
+ f"Old dbi '{this_dest}' already exists - deleting '{this_pyd}'"
+ )
+ os.remove(this_pyd)
+ else:
+ os.rename(this_pyd, this_dest)
+ print(f"renamed '{this_pyd}'->'{this_pyd}.old'")
+ except OSError as exc:
+ print(f"FAILED to rename '{this_pyd}': {exc}")
+
+
+def install(lib_dir):
+ import traceback
+
+ # The .pth file is now installed as a regular file.
+ # Create the .pth file in the site-packages dir, and use only relative paths
+ # We used to write a .pth directly to sys.prefix - clobber it.
+ if os.path.isfile(os.path.join(sys.prefix, "pywin32.pth")):
+ os.unlink(os.path.join(sys.prefix, "pywin32.pth"))
+ # The .pth may be new and therefore not loaded in this session.
+ # Setup the paths just in case.
+ for name in "win32 win32\\lib Pythonwin".split():
+ sys.path.append(os.path.join(lib_dir, name))
+ # It is possible people with old versions installed with still have
+ # pywintypes and pythoncom registered. We no longer need this, and stale
+ # entries hurt us.
+ for name in "pythoncom pywintypes".split():
+ keyname = "Software\\Python\\PythonCore\\" + sys.winver + "\\Modules\\" + name
+ for root in winreg.HKEY_LOCAL_MACHINE, winreg.HKEY_CURRENT_USER:
+ try:
+ winreg.DeleteKey(root, keyname + "\\Debug")
+ except OSError:
+ pass
+ try:
+ winreg.DeleteKey(root, keyname)
+ except OSError:
+ pass
+ LoadSystemModule(lib_dir, "pywintypes")
+ LoadSystemModule(lib_dir, "pythoncom")
+ import win32api
+
+ # and now we can get the system directory:
+ files = glob.glob(os.path.join(lib_dir, "pywin32_system32\\*.*"))
+ if not files:
+ raise RuntimeError("No system files to copy!!")
+ # Try the system32 directory first - if that fails due to "access denied",
+ # it implies a non-admin user, and we use sys.prefix
+ for dest_dir in [get_system_dir(), sys.prefix]:
+ # and copy some files over there
+ worked = 0
+ try:
+ for fname in files:
+ base = os.path.basename(fname)
+ dst = os.path.join(dest_dir, base)
+ CopyTo("installing %s" % base, fname, dst)
+ if verbose:
+ print(f"Copied {base} to {dst}")
+ worked = 1
+ # Nuke any other versions that may exist - having
+ # duplicates causes major headaches.
+ bad_dest_dirs = [
+ os.path.join(sys.prefix, "Library\\bin"),
+ os.path.join(sys.prefix, "Lib\\site-packages\\win32"),
+ ]
+ if dest_dir != sys.prefix:
+ bad_dest_dirs.append(sys.prefix)
+ for bad_dest_dir in bad_dest_dirs:
+ bad_fname = os.path.join(bad_dest_dir, base)
+ if os.path.exists(bad_fname):
+ # let exceptions go here - delete must succeed
+ os.unlink(bad_fname)
+ if worked:
+ break
+ except win32api.error as details:
+ if details.winerror == 5:
+ # access denied - user not admin - try sys.prefix dir,
+ # but first check that a version doesn't already exist
+ # in that place - otherwise that one will still get used!
+ if os.path.exists(dst):
+ msg = (
+ "The file '%s' exists, but can not be replaced "
+ "due to insufficient permissions. You must "
+ "reinstall this software as an Administrator" % dst
+ )
+ print(msg)
+ raise RuntimeError(msg)
+ continue
+ raise
+ else:
+ raise RuntimeError(
+ "You don't have enough permissions to install the system files"
+ )
+
+ # Register our demo COM objects.
+ try:
+ try:
+ RegisterCOMObjects()
+ except win32api.error as details:
+ if details.winerror != 5: # ERROR_ACCESS_DENIED
+ raise
+ print("You do not have the permissions to install COM objects.")
+ print("The sample COM objects were not registered.")
+ except Exception:
+ print("FAILED to register the Python COM objects")
+ traceback.print_exc()
+
+ # There may be no main Python key in HKCU if, eg, an admin installed
+ # python itself.
+ winreg.CreateKey(get_root_hkey(), root_key_name)
+
+ chm_file = None
+ try:
+ chm_file = RegisterHelpFile(True, lib_dir)
+ except Exception:
+ print("Failed to register help file")
+ traceback.print_exc()
+ else:
+ if verbose:
+ print("Registered help file")
+
+ # misc other fixups.
+ fixup_dbi()
+
+ # Register Pythonwin in context menu
+ try:
+ RegisterPythonwin(True, lib_dir)
+ except Exception:
+ print("Failed to register pythonwin as editor")
+ traceback.print_exc()
+ else:
+ if verbose:
+ print("Pythonwin has been registered in context menu")
+
+ # Create the win32com\gen_py directory.
+ make_dir = os.path.join(lib_dir, "win32com", "gen_py")
+ if not os.path.isdir(make_dir):
+ if verbose:
+ print(f"Creating directory {make_dir}")
+ os.mkdir(make_dir)
+
+ try:
+ # create shortcuts
+ # CSIDL_COMMON_PROGRAMS only available works on NT/2000/XP, and
+ # will fail there if the user has no admin rights.
+ fldr = get_shortcuts_folder()
+ # If the group doesn't exist, then we don't make shortcuts - its
+ # possible that this isn't a "normal" install.
+ if os.path.isdir(fldr):
+ dst = os.path.join(fldr, "PythonWin.lnk")
+ create_shortcut(
+ os.path.join(lib_dir, "Pythonwin\\Pythonwin.exe"),
+ "The Pythonwin IDE",
+ dst,
+ "",
+ sys.prefix,
+ )
+ if verbose:
+ print("Shortcut for Pythonwin created")
+ # And the docs.
+ if chm_file:
+ dst = os.path.join(fldr, "Python for Windows Documentation.lnk")
+ doc = "Documentation for the PyWin32 extensions"
+ create_shortcut(chm_file, doc, dst)
+ if verbose:
+ print("Shortcut to documentation created")
+ else:
+ if verbose:
+ print(f"Can't install shortcuts - {fldr!r} is not a folder")
+ except Exception as details:
+ print(details)
+
+ # importing win32com.client ensures the gen_py dir created - not strictly
+ # necessary to do now, but this makes the installation "complete"
+ try:
+ import win32com.client # noqa
+ except ImportError:
+ # Don't let this error sound fatal
+ pass
+ print("The pywin32 extensions were successfully installed.")
+
+
+def uninstall(lib_dir):
+ # First ensure our system modules are loaded from pywin32_system, so
+ # we can remove the ones we copied...
+ LoadSystemModule(lib_dir, "pywintypes")
+ LoadSystemModule(lib_dir, "pythoncom")
+
+ try:
+ RegisterCOMObjects(False)
+ except Exception as why:
+ print(f"Failed to unregister COM objects: {why}")
+
+ try:
+ RegisterHelpFile(False, lib_dir)
+ except Exception as why:
+ print(f"Failed to unregister help file: {why}")
+ else:
+ if verbose:
+ print("Unregistered help file")
+
+ try:
+ RegisterPythonwin(False, lib_dir)
+ except Exception as why:
+ print(f"Failed to unregister Pythonwin: {why}")
+ else:
+ if verbose:
+ print("Unregistered Pythonwin")
+
+ try:
+ # remove gen_py directory.
+ gen_dir = os.path.join(lib_dir, "win32com", "gen_py")
+ if os.path.isdir(gen_dir):
+ shutil.rmtree(gen_dir)
+ if verbose:
+ print(f"Removed directory {gen_dir}")
+
+ # Remove pythonwin compiled "config" files.
+ pywin_dir = os.path.join(lib_dir, "Pythonwin", "pywin")
+ for fname in glob.glob(os.path.join(pywin_dir, "*.cfc")):
+ os.remove(fname)
+
+ # The dbi.pyd.old files we may have created.
+ try:
+ os.remove(os.path.join(lib_dir, "win32", "dbi.pyd.old"))
+ except OSError:
+ pass
+ try:
+ os.remove(os.path.join(lib_dir, "win32", "dbi_d.pyd.old"))
+ except OSError:
+ pass
+
+ except Exception as why:
+ print(f"Failed to remove misc files: {why}")
+
+ try:
+ fldr = get_shortcuts_folder()
+ for link in ("PythonWin.lnk", "Python for Windows Documentation.lnk"):
+ fqlink = os.path.join(fldr, link)
+ if os.path.isfile(fqlink):
+ os.remove(fqlink)
+ if verbose:
+ print(f"Removed {link}")
+ except Exception as why:
+ print(f"Failed to remove shortcuts: {why}")
+ # Now remove the system32 files.
+ files = glob.glob(os.path.join(lib_dir, "pywin32_system32\\*.*"))
+ # Try the system32 directory first - if that fails due to "access denied",
+ # it implies a non-admin user, and we use sys.prefix
+ try:
+ for dest_dir in [get_system_dir(), sys.prefix]:
+ # and copy some files over there
+ worked = 0
+ for fname in files:
+ base = os.path.basename(fname)
+ dst = os.path.join(dest_dir, base)
+ if os.path.isfile(dst):
+ try:
+ os.remove(dst)
+ worked = 1
+ if verbose:
+ print("Removed file %s" % (dst))
+ except Exception:
+ print(f"FAILED to remove {dst}")
+ if worked:
+ break
+ except Exception as why:
+ print(f"FAILED to remove system files: {why}")
+
+
+# NOTE: This used to be run from inside the bdist_wininst created binary un/installer.
+# From inside the binary installer this script HAD to NOT
+# call sys.exit() or raise SystemExit, otherwise the installer would also terminate!
+# Out of principle, we're still not using system exits.
+
+
+def verify_destination(location: str) -> str:
+ location = os.path.abspath(location)
+ if not os.path.isdir(location):
+ raise argparse.ArgumentTypeError(
+ f'Path "{location}" is not an existing directory!'
+ )
+ return location
+
+
+def main():
+ parser = argparse.ArgumentParser(
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ description="""A post-install script for the pywin32 extensions.
+
+ * Typical usage:
+
+ > python -m pywin32_postinstall -install
+
+ * or (shorter but you don't have control over which python environment is used)
+
+ > pywin32_postinstall -install
+
+ You need to execute this script, with a '-install' parameter,
+ to ensure the environment is setup correctly to install COM objects, services, etc.
+ """,
+ )
+ parser.add_argument(
+ "-install",
+ default=False,
+ action="store_true",
+ help="Configure the Python environment correctly for pywin32.",
+ )
+ parser.add_argument(
+ "-remove",
+ default=False,
+ action="store_true",
+ help="Try and remove everything that was installed or copied.",
+ )
+ parser.add_argument(
+ "-wait",
+ type=int,
+ help="Wait for the specified process to terminate before starting.",
+ )
+ parser.add_argument(
+ "-silent",
+ default=False,
+ action="store_true",
+ help='Don\'t display the "Abort/Retry/Ignore" dialog for files in use.',
+ )
+ parser.add_argument(
+ "-quiet",
+ default=False,
+ action="store_true",
+ help="Don't display progress messages.",
+ )
+ parser.add_argument(
+ "-destination",
+ default=sysconfig.get_paths()["platlib"],
+ type=verify_destination,
+ help="Location of the PyWin32 installation",
+ )
+
+ args = parser.parse_args()
+
+ if not args.quiet:
+ print(f"Parsed arguments are: {args}")
+
+ if not args.install ^ args.remove:
+ parser.error("You need to either choose to -install or -remove!")
+
+ if args.wait is not None:
+ try:
+ os.waitpid(args.wait, 0)
+ except OSError:
+ # child already dead
+ pass
+
+ silent = args.silent
+ verbose = not args.quiet
+
+ if args.install:
+ install(args.destination)
+
+ if args.remove:
+ uninstall(args.destination)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/.venv312/Scripts/pywin32_testall.exe b/.venv312/Scripts/pywin32_testall.exe
new file mode 100644
index 0000000..bf37de6
Binary files /dev/null and b/.venv312/Scripts/pywin32_testall.exe differ
diff --git a/.venv312/Scripts/pywin32_testall.py b/.venv312/Scripts/pywin32_testall.py
new file mode 100644
index 0000000..345d641
--- /dev/null
+++ b/.venv312/Scripts/pywin32_testall.py
@@ -0,0 +1,120 @@
+"""A test runner for pywin32"""
+
+import os
+import site
+import subprocess
+import sys
+
+# locate the dirs based on where this script is - it may be either in the
+# source tree, or in an installed Python 'Scripts' tree.
+project_root = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
+site_packages = [site.getusersitepackages()] + site.getsitepackages()
+
+failures = []
+
+
+# Run a test using subprocess and wait for the result.
+# If we get an returncode != 0, we know that there was an error, but we don't
+# abort immediately - we run as many tests as we can.
+def run_test(script, cmdline_extras):
+ dirname, scriptname = os.path.split(script)
+ # some tests prefer to be run from their directory.
+ cmd = [sys.executable, "-u", scriptname] + cmdline_extras
+ print("--- Running '%s' ---" % script)
+ sys.stdout.flush()
+ result = subprocess.run(cmd, check=False, cwd=dirname)
+ print(f"*** Test script '{script}' exited with {result.returncode}")
+ sys.stdout.flush()
+ if result.returncode:
+ failures.append(script)
+
+
+def find_and_run(possible_locations, extras):
+ for maybe in possible_locations:
+ if os.path.isfile(maybe):
+ run_test(maybe, extras)
+ break
+ else:
+ raise RuntimeError(
+ "Failed to locate a test script in one of %s" % possible_locations
+ )
+
+
+def main():
+ import argparse
+
+ code_directories = [project_root] + site_packages
+
+ parser = argparse.ArgumentParser(
+ description="A script to trigger tests in all subprojects of PyWin32."
+ )
+ parser.add_argument(
+ "-no-user-interaction",
+ default=False,
+ action="store_true",
+ help="(This is now the default - use `-user-interaction` to include them)",
+ )
+
+ parser.add_argument(
+ "-user-interaction",
+ action="store_true",
+ help="Include tests which require user interaction",
+ )
+
+ parser.add_argument(
+ "-skip-adodbapi",
+ default=False,
+ action="store_true",
+ help="Skip the adodbapi tests; useful for CI where there's no provider",
+ )
+
+ args, remains = parser.parse_known_args()
+
+ # win32, win32ui / Pythonwin
+
+ extras = []
+ if args.user_interaction:
+ extras.append("-user-interaction")
+ extras.extend(remains)
+ scripts = [
+ "win32/test/testall.py",
+ "Pythonwin/pywin/test/all.py",
+ ]
+ for script in scripts:
+ maybes = [os.path.join(directory, script) for directory in code_directories]
+ find_and_run(maybes, extras)
+
+ # win32com
+ maybes = [
+ os.path.join(directory, "win32com", "test", "testall.py")
+ for directory in [os.path.join(project_root, "com")] + site_packages
+ ]
+ extras = remains + ["1"] # only run "level 1" tests in CI
+ find_and_run(maybes, extras)
+
+ # adodbapi
+ if not args.skip_adodbapi:
+ maybes = [
+ os.path.join(directory, "adodbapi", "test", "adodbapitest.py")
+ for directory in code_directories
+ ]
+ find_and_run(maybes, remains)
+ # This script has a hard-coded sql server name in it, (and markh typically
+ # doesn't have a different server to test on) but there is now supposed to be a server out there on the Internet
+ # just to run these tests, so try it...
+ maybes = [
+ os.path.join(directory, "adodbapi", "test", "test_adodbapi_dbapi20.py")
+ for directory in code_directories
+ ]
+ find_and_run(maybes, remains)
+
+ if failures:
+ print("The following scripts failed")
+ for failure in failures:
+ print(">", failure)
+ sys.exit(1)
+ print("All tests passed \\o/")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/.venv312/pyvenv.cfg b/.venv312/pyvenv.cfg
new file mode 100644
index 0000000..67a7b53
--- /dev/null
+++ b/.venv312/pyvenv.cfg
@@ -0,0 +1,5 @@
+home = C:\Users\fmerino\AppData\Local\Programs\Python\Python312
+include-system-site-packages = false
+version = 3.12.10
+executable = C:\Users\fmerino\AppData\Local\Programs\Python\Python312\python.exe
+command = C:\Users\fmerino\AppData\Local\Programs\Python\Python312\python.exe -m venv C:\Users\fmerino\Documents\GitHub\Python-Web-Scraping\.venv312
diff --git a/.venv312/share/jupyter/kernels/python3/kernel.json b/.venv312/share/jupyter/kernels/python3/kernel.json
new file mode 100644
index 0000000..cca38a4
--- /dev/null
+++ b/.venv312/share/jupyter/kernels/python3/kernel.json
@@ -0,0 +1,14 @@
+{
+ "argv": [
+ "python",
+ "-m",
+ "ipykernel_launcher",
+ "-f",
+ "{connection_file}"
+ ],
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "metadata": {
+ "debugger": true
+ }
+}
\ No newline at end of file
diff --git a/.venv312/share/jupyter/kernels/python3/logo-32x32.png b/.venv312/share/jupyter/kernels/python3/logo-32x32.png
new file mode 100644
index 0000000..be81330
Binary files /dev/null and b/.venv312/share/jupyter/kernels/python3/logo-32x32.png differ
diff --git a/.venv312/share/jupyter/kernels/python3/logo-64x64.png b/.venv312/share/jupyter/kernels/python3/logo-64x64.png
new file mode 100644
index 0000000..eebbff6
Binary files /dev/null and b/.venv312/share/jupyter/kernels/python3/logo-64x64.png differ
diff --git a/.venv312/share/jupyter/kernels/python3/logo-svg.svg b/.venv312/share/jupyter/kernels/python3/logo-svg.svg
new file mode 100644
index 0000000..467b07b
--- /dev/null
+++ b/.venv312/share/jupyter/kernels/python3/logo-svg.svg
@@ -0,0 +1,265 @@
+
+
+
+
diff --git a/.venv312/share/man/man1/ipython.1 b/.venv312/share/man/man1/ipython.1
new file mode 100644
index 0000000..0f4a191
--- /dev/null
+++ b/.venv312/share/man/man1/ipython.1
@@ -0,0 +1,60 @@
+.\" Hey, EMACS: -*- nroff -*-
+.\" First parameter, NAME, should be all caps
+.\" Second parameter, SECTION, should be 1-8, maybe w/ subsection
+.\" other parameters are allowed: see man(7), man(1)
+.TH IPYTHON 1 "July 15, 2011"
+.\" Please adjust this date whenever revising the manpage.
+.\"
+.\" Some roff macros, for reference:
+.\" .nh disable hyphenation
+.\" .hy enable hyphenation
+.\" .ad l left justify
+.\" .ad b justify to both left and right margins
+.\" .nf disable filling
+.\" .fi enable filling
+.\" .br insert line break
+.\" .sp insert n+1 empty lines
+.\" for manpage-specific macros, see man(7) and groff_man(7)
+.\" .SH section heading
+.\" .SS secondary section heading
+.\"
+.\"
+.\" To preview this page as plain text: nroff -man ipython.1
+.\"
+.SH NAME
+ipython \- Tools for Interactive Computing in Python.
+.SH SYNOPSIS
+.B ipython
+.RI [ options ] " files" ...
+
+.B ipython subcommand
+.RI [ options ] ...
+
+.SH DESCRIPTION
+An interactive Python shell with automatic history (input and output), dynamic
+object introspection, easier configuration, command completion, access to the
+system shell, integration with numerical and scientific computing tools,
+web notebook, Qt console, and more.
+
+For more information on how to use IPython, see 'ipython \-\-help',
+or 'ipython \-\-help\-all' for all available command\(hyline options.
+
+.SH "ENVIRONMENT VARIABLES"
+.sp
+.PP
+\fIIPYTHONDIR\fR
+.RS 4
+This is the location where IPython stores all its configuration files. The default
+is $HOME/.ipython if IPYTHONDIR is not defined.
+
+You can see the computed value of IPYTHONDIR with `ipython locate`.
+
+.SH FILES
+
+IPython uses various configuration files stored in profiles within IPYTHONDIR.
+To generate the default configuration files and start configuring IPython,
+do 'ipython profile create', and edit '*_config.py' files located in
+IPYTHONDIR/profile_default.
+
+.SH AUTHORS
+IPython is written by the IPython Development Team .
diff --git a/README.md b/README.md
index a702c02..822b9bd 100644
--- a/README.md
+++ b/README.md
@@ -1,145 +1,119 @@
-# D-Lab Python Web Scraping Workshop
+# Comentarios del Notebook 02_web_scrapping.ipynb
+**Equipo:** grupo11 · **Integración GitHub:** @anmerinoto
+
+Taller de Web Scraping en Python de D-Lab
[](https://dlab.datahub.berkeley.edu/hub/user-redirect/git-pull?repo=https%3A%2F%2Fgithub.com%2Fdlab-berkeley%2FPython-Web-Scraping&urlpath=lab%2Ftree%2FPython-Web-Scraping%2F&branch=main)
[](https://mybinder.org/v2/gh/dlab-berkeley/Python-Web-Scraping/HEAD)
[](https://creativecommons.org/licenses/by/4.0/)
-This repository contains the materials for D-Lab’s Python Web Scraping Workshop.
-
-## Prerequisites
+Este repositorio contiene los materiales para el taller de Web Scraping de Python de D-Lab.
-We recommend attending [Python Fundamentals](https://github.com/dlab-berkeley/python-fundamentals) and [Python Data Wrangling](https://github.com/dlab-berkeley/Python-Data-Wrangling/) prior to this workshop. We additionally recommend a basic understanding of HTML and CSS.
+## Requisitos previos
-Check out D-Lab’s [Workshop Catalog](https://dlab-berkeley.github.io/dlab-workshops/) to browse all workshops, see what’s running now, and review prerequisites.
+Recomendamos asistir a [Fundamentos de Python](https://github.com/dlab-berkeley/python-fundamentals) y [Organización de datos de Python](https://github.com/dlab-berkeley/Python-Data-Wrangling/) antes de este taller. Además, recomendamos una comprensión básica de HTML y CSS.
+Consulte el [Catálogo de talleres] (https://dlab-berkeley.github.io/dlab-workshops/) de D-Lab para explorar todos los talleres, ver lo que se está ejecutando ahora y revisar los requisitos previos.
-## Workshop Goals
+## Objetivos del taller
-In this workshop, we cover how to scrape data from the web using Python. Web
-scraping involves downloading a webpage's source code and sifting through the
-material to extract desired data.
+En este taller, cubrimos cómo extraer datos de la web usando Python.
+El Web Scraping implica descargar el código fuente de una página web y examinar el archivo material para extraer los datos deseados.
-Web scraping is typically only done when Web APIs are not available. Platforms
-like Twitter, Reddit, or The New York Times offer APIs to retrieve data. If you
-want to learn how to use web APIs in Python, see D-Lab's [Python Web
-APIs](https://github.com/dlab-berkeley/Python-Web-APIs) workshop.
+El Web Scraping generalmente solo se realiza cuando las API web no están disponibles. Plataformas como Twitter, Reddit o The New York Times ofrecen API para recuperar datos. Si usted desea aprender a usar las API web en Python, consulte [Python Web APIs](https://github.com/dlab-berkeley/Python-Web-APIs).
-## Installation Instructions
+## Instrucciones de instalación
-Anaconda is a useful package management software that allows you to run Python
-and Jupyter notebooks easily. Installing Anaconda is the easiest way to make
-sure you have all the necessary software to run the materials for this workshop.
-If you would like to run Python on your own computer, complete the following
-steps prior to the workshop:
+Anaconda es un útil software de gestión de paquetes que permite ejecutar Python
+y cuadernos Jupyter fácilmente. Instalar Anaconda es la forma más fácil de hacer
+Seguro que tienes todo el software necesario para ejecutar los materiales para este taller. Si desea ejecutar Python en su propia computadora, complete lo siguiente
-1. [Download and install Anaconda (Python 3.9
- distribution)](https://www.anaconda.com/products/individual). Click the
- "Download" button.
+Pasos previos al taller:
-2. Download the Python Web Scraping [workshop
- materials](https://github.com/dlab-berkeley/Python-Web-Scraping):
+1. [Descargue e instale Anaconda (Python 3.9
+distribución)](https://www.anaconda.com/products/individual). Haga clic en el icono
+Botón "Descargar".
- - Click the green "Code" button in the top right of the repository
- information.
- - Click "Download Zip".
- - Extract this file to a folder on your computer where you can easily
- access it (we recommend Desktop).
+2. Descargue el [taller] Python Web Scraping
+materiales](https://github.com/dlab-berkeley/Python-Web-Scraping):
-3. Optional: if you're familiar with `git`, you can instead clone this
- repository by opening a terminal and entering the command `git clone
- git@github.com:dlab-berkeley/Python-Web-Scraping.git`.
+ - Haga clic en el botón verde "Código" en la parte superior derecha del repositorio
+información.
+ - Haga clic en "Descargar Zip".
+ - Extraiga este archivo a una carpeta en su computadora donde pueda fácilmente
+acceder a él (recomendamos Escritorio).
+3. Opcional: si estás familiarizado con 'git', puedes clonarlo
+repositorio abriendo una terminal e ingresando el comando 'git clone
+git@github.com:dlab-berkeley/Python-Web-Scraping.git'.
-## Is Python Not Working on Your Computer?
+## ¿Python no funciona en su computadora?
-If you do not have Anaconda installed and the materials loaded on your workshop
-by the time it starts, we *strongly* recommend using the UC Berkeley Datahub to
-run the materials for these lessons. You can access the DataHub by clicking this
-button:
+Si no tiene Anaconda instalada y los materiales cargados en su taller
+para cuando comience, recomendamos usar el centro de datos de UC Berkeley para ejecutar los materiales para estas lecciones. Puede acceder al DataHub haciendo clic en este botón:
-[](https://dlab.datahub.berkeley.edu/hub/user-redirect/git-pull?repo=https%3A%2F%2Fgithub.com%2Fdlab-berkeley%2FPython-Web-Scraping&urlpath=lab%2Ftree%2FPython-Web-Scraping%2F&branch=main)
+[](https://dlab.datahub.berkeley.edu/hub/user-redirect/git-pull?repo=https%3A%2F%2Fgithub.com%2Fdlab-berkeley%2FPython-Web-Scraping&urlpath=lab%2Ftree%2FPython-Web-Scraping%2F&branch=main)
-The DataHub downloads this repository, along with any necessary packages, and
-allows you to run the materials in a Jupyter notebook that is stored on UC
-Berkeley's servers. No installation is necessary from your end - you only need
-an internet browser and a CalNet ID to log in. By using the DataHub, you can
-save your work and come back to it at any time. When you want to return to your
-saved work, just go straight to [DataHub](https://datahub.berkeley.edu), sign
-in, and you click on the `Python-Web-Scraping` folder.
+El DataHub descarga este repositorio, junto con los paquetes necesarios, y
+le permite ejecutar los materiales en un cuaderno de Jupyter que se almacena en UC de los servidores de Berkeley. No es necesaria ninguna instalación por su parte, solo necesita un navegador de Internet y una identificación de CalNet para iniciar sesión. Al usar DataHub, puede Guarde su trabajo y vuelva a él en cualquier momento. Cuando quieras volver a tu trabajo guardado, simplemente vaya directamente a [DataHub] (https://datahub.berkeley.edu), firme y haga clic en la carpeta 'Python-Web-Scraping'.
-If you don't have a Berkeley CalNet ID, you can still run these lessons in the
-cloud, by clicking this button:
+Si no tiene una identificación de Berkeley CalNet, aún puede ejecutar estas lecciones en el cloud, haciendo clic en este botón:
[](https://mybinder.org/v2/gh/dlab-berkeley/Python-Web-Scraping/HEAD)
-By using this button, however, you cannot save your work.
+Sin embargo, al usar este botón, no puede guardar su trabajo.
-## Run the code
+## Ejecuta el código
-1. Open the Anaconda Navigator application. You should see the green snake logo
- appear on your screen. Note that this can take a few minutes to load up the
- first time.
+1. Abra la aplicación Anaconda Navigator. Deberías ver el logotipo de la serpiente verde en tu pantalla. Tenga en cuenta que esto puede tardar unos minutos en cargar el archivo primera vez.
-2. Click the "Launch" button under "Jupyter Notebooks" and navigate through your
- file system to the `Python-Web-Scraping` folder you downloaded above. Note
- that, if you download the materials from GitHub, the folder name may instead
- be `Python-Text-Analysis-main`.
+2. Haga clic en el botón "Iniciar" debajo de "Jupyter Notebooks" y navegue por su
+ sistema de archivos a la carpeta 'Python-Web-Scraping' que descargó anteriormente. Nota que, si descarga los materiales de GitHub, el nombre de la carpeta puede en su lugar ser 'Python-Text-Analysis-main'.
-3. Open the `lessons` folder, and click `01_introduction.md` to begin.
+3. Abra la carpeta 'lecciones' y haga clic en '01_introduction.md' para comenzar.
-4. Press Shift + Enter (or Ctrl + Enter) to run a cell.
+4. Presione Mayús + Entrar (o Ctrl + Entrar) para ejecutar una celda.
-5. By default, the necessary packages for this workshop should already be
- installed. You can install them within the Jupyter notebook by running the
- following line in its own cell:
+5. Por defecto, los paquetes necesarios para este taller ya deberían estar
+Instalado. Puede instalarlos en el cuaderno de Jupyter Notebook ejecutando el comando siguiente línea en su propia celda:
-> ```%pip install -r requirements.txt```
+> '''%pip install -r requirements.txt'''
-Note that all of the above steps can be run from the terminal, if you're
-familiar with how to interact with Anaconda in that fashion. However, using
-Anaconda Navigator is the easiest way to get started if this is your first time
-working with Anaconda.
+Tenga en cuenta que todos los pasos anteriores se pueden ejecutar desde la terminal, si está familiarizado con cómo interactuar con Anaconda de esa manera. Sin embargo, el uso de Anaconda Navigator es la forma más fácil de comenzar si es tu primera vez trabajando con Anaconda.
-# About the UC Berkeley D-Lab
+# Acerca del D-Lab de UC Berkeley
-D-Lab works with Berkeley faculty, research staff, and students to advance
-data-intensive social science and humanities research. Our goal at D-Lab is to
-provide practical training, staff support, resources, and space to enable you to
-use R for your own research applications. Our services cater to all skill levels
-and no programming, statistical, or computer science backgrounds are necessary.
-We offer these services in the form of workshops, one-to-one consulting, and
-working groups that cover a variety of research topics, digital tools, and
-programming languages.
+D-Lab trabaja con profesores, personal de investigación y estudiantes de Berkeley para avanzar investigación intensiva en ciencias sociales y humanidades con uso intensivo de datos. Nuestro objetivo en D-Lab es proporcionar capacitación práctica, apoyo del personal, recursos y espacio para permitirle
+use R para sus propias aplicaciones de investigación. Nuestros servicios se adaptan a todos los niveles de habilidad y no se necesitan antecedentes en programación, estadística o informática.
+Ofrecemos estos servicios en forma de talleres, consultoría personalizada y
+grupos de trabajo que cubren una variedad de temas de investigación, herramientas digitales y lenguajes de programación.
-Visit the [D-Lab homepage](https://dlab.berkeley.edu/) to learn more about us.
-You can view our [calendar](https://dlab.berkeley.edu/events/calendar) for
-upcoming events, learn about how to utilize our
-[consulting](https://dlab.berkeley.edu/consulting) and [data
-services](https://dlab.berkeley.edu/data), and check out upcoming
-[workshops](https://dlab.berkeley.edu/events/workshops). Subscribe to our
-[newsletter](https://dlab.berkeley.edu/news/weekly-newsletter) to stay up to
-date on D-Lab events, services, and opportunities.
+Visite la [página de inicio de D-Lab](https://dlab.berkeley.edu/) para obtener más información sobre nosotros. Puede ver nuestro [calendario](https://dlab.berkeley.edu/events/calendar) para
+próximos eventos, aprenda cómo utilizar nuestros
+[consultoría](https://dlab.berkeley.edu/consulting) y [datos
+servicios](https://dlab.berkeley.edu/data), y echa un vistazo a los próximos
+[talleres](https://dlab.berkeley.edu/events/workshops). Suscríbete a nuestro
+[newsletter](https://dlab.berkeley.edu/news/weekly-newsletter) para mantenerse al día fecha en eventos, servicios y oportunidades de D-Lab.
-# Other D-Lab Python Workshops
+# Otros talleres de D-Lab Python
-D-Lab offers a variety of Python workshops, catered toward different levels of
-expertise.
+D-Lab ofrece una variedad de talleres de Python, dirigidos a diferentes niveles de pericia.
-## Introductory Workshops
+## Talleres introductorios
-- [Python Fundamentals](https://github.com/dlab-berkeley/Python-Fundamentals)
-- [Python Data Wrangling](https://github.com/dlab-berkeley/Python-Data-Wrangling)
-- [Python Data Visualization](https://github.com/dlab-berkeley/Python-Data-Visualization)
+- [Fundamentos de Python](https://github.com/dlab-berkeley/Python-Fundamentals)
+- [Manejo de datos de Python](https://github.com/dlab-berkeley/Python-Data-Wrangling)
+- [Visualización de datos de Python](https://github.com/dlab-berkeley/Python-Data-Visualization)
-## Intermediate and Advanced Workshops
+## Talleres intermedios y avanzados
-- [Python Geospatial Fundamentals](https://github.com/dlab-berkeley/Geospatial-Data-and-Mapping-in-Python)
-- [Python Web Scraping and APIs](https://github.com/dlab-berkeley/Python-Web-Scraping)
-- [Python Machine Learning](https://github.com/dlab-berkeley/Python-Machine-Learning)
-- [Python Text Analysis](https://github.com/dlab-berkeley/Python-Text-Analysis)
-- [Python Deep Learning](https://github.com/dlab-berkeley/Python-Deep-Learning)
+- [Fundamentos geoespaciales de Python](https://github.com/dlab-berkeley/Geospatial-Data-and-Mapping-in-Python)
+- [Raspado web y API de Python](https://github.com/dlab-berkeley/Python-Web-Scraping)
+- [Aprendizaje automático de Python](https://github.com/dlab-berkeley/Python-Machine-Learning)
+- [Análisis de texto de Python](https://github.com/dlab-berkeley/Python-Text-Analysis)
+- [Aprendizaje profundo de Python](https://github.com/dlab-berkeley/Python-Deep-Learning)
-# Contributors
+# Colaboradores
* [Rochelle Terman](https://github.com/rochelleterman)
* [George McIntire](https://github.com/GeorgeMcIntire)
diff --git a/lessons/02_web_scraping.ipynb b/lessons/02_web_scraping.ipynb
index 385806a..525cc44 100644
--- a/lessons/02_web_scraping.ipynb
+++ b/lessons/02_web_scraping.ipynb
@@ -47,19 +47,47 @@
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 140,
"metadata": {},
- "outputs": [],
- "source": [
- "%pip install requests"
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Requirement already satisfied: requests in c:\\users\\jjala\\appdata\\local\\programs\\python\\python313\\lib\\site-packages (2.32.5)\n",
+ "Requirement already satisfied: charset_normalizer<4,>=2 in c:\\users\\jjala\\appdata\\local\\programs\\python\\python313\\lib\\site-packages (from requests) (3.4.3)\n",
+ "Requirement already satisfied: idna<4,>=2.5 in c:\\users\\jjala\\appdata\\local\\programs\\python\\python313\\lib\\site-packages (from requests) (3.10)\n",
+ "Requirement already satisfied: urllib3<3,>=1.21.1 in c:\\users\\jjala\\appdata\\local\\programs\\python\\python313\\lib\\site-packages (from requests) (2.5.0)\n",
+ "Requirement already satisfied: certifi>=2017.4.17 in c:\\users\\jjala\\appdata\\local\\programs\\python\\python313\\lib\\site-packages (from requests) (2025.8.3)\n",
+ "Note: you may need to restart the kernel to use updated packages.\n"
+ ]
+ }
+ ],
+ "source": [
+ "# 🌐 La librería requests es necesaria para hacer solicitudes HTTP y descargar páginas web.\n",
+ "# 🕸️ Esto es fundamental para hacer web scraping (extraer información de páginas web).\n",
+ "%pip install requests "
]
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 141,
"metadata": {},
- "outputs": [],
- "source": [
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Requirement already satisfied: beautifulsoup4 in c:\\users\\jjala\\appdata\\local\\programs\\python\\python313\\lib\\site-packages (4.13.4)\n",
+ "Requirement already satisfied: soupsieve>1.2 in c:\\users\\jjala\\appdata\\local\\programs\\python\\python313\\lib\\site-packages (from beautifulsoup4) (2.7)\n",
+ "Requirement already satisfied: typing-extensions>=4.0.0 in c:\\users\\jjala\\appdata\\local\\programs\\python\\python313\\lib\\site-packages (from beautifulsoup4) (4.14.1)\n",
+ "Note: you may need to restart the kernel to use updated packages.\n"
+ ]
+ }
+ ],
+ "source": [
+ "# 🥣 La instrucción %pip install beautifulsoup4 sirve para instalar la librería Beautiful Soup 4 en tu entorno de Jupyter Notebook.\n",
+ "# 🕸️ Beautiful Soup es esencial para analizar y extraer información de archivos HTML y XML, lo que facilita el web scraping.\n",
"%pip install beautifulsoup4"
]
},
@@ -72,22 +100,33 @@
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 142,
"metadata": {},
- "outputs": [],
- "source": [
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Requirement already satisfied: lxml in c:\\users\\jjala\\appdata\\local\\programs\\python\\python313\\lib\\site-packages (6.0.1)\n",
+ "Note: you may need to restart the kernel to use updated packages.\n"
+ ]
+ }
+ ],
+ "source": [
+ "# 🧩 El comando %pip install lxml instala la librería lxml en tu entorno de Jupyter Notebook.\n",
+ "# ⚡ lxml es un parser rápido y eficiente para analizar y procesar archivos HTML y XML, muy útil para usar con Beautiful Soup en web scraping.\n",
"%pip install lxml"
]
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 143,
"metadata": {
"tags": []
},
"outputs": [],
"source": [
- "# Import required libraries\n",
+ "# importamos las librerías necesarias\n",
"from bs4 import BeautifulSoup\n",
"from datetime import datetime\n",
"import requests\n",
@@ -100,36 +139,63 @@
"source": [
"\n",
"\n",
- "# Extracting and Parsing HTML \n",
+ "# Extracción y análisis de HTML\n",
"\n",
- "In order to succesfully scrape and analyse HTML, we'll be going through the following 4 steps:\n",
- "1. Make a GET request\n",
- "2. Parse the page with Beautiful Soup\n",
- "3. Search for HTML elements\n",
- "4. Get attributes and text of these elements"
+ "Para extraer y analizar correctamente HTML, seguiremos los siguientes 4 pasos:\n",
+ "1. Realizar una solicitud GET\n",
+ "2. Analizar la página con Beautiful Soup\n",
+ "3. Buscar elementos HTML\n",
+ "4. Obtener atributos y texto de estos elementos"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "## Step 1: Make a GET Request to Obtain a Page's HTML\n",
+ "## Paso 1: Realiza una solicitud GET para obtener el HTML de una página\n",
+ "\n",
+ "Podemos usar la librería Requests para:\n",
"\n",
- "We can use the Requests library to:\n",
+ "1. Realizar una solicitud GET a la página, y\n",
"\n",
- "1. Make a GET request to the page, and\n",
- "2. Read in the webpage's HTML code.\n",
+ "2. Leer el código HTML de la página web.\n",
"\n",
- "The process of making a request and obtaining a result resembles that of the Web API workflow. Now, however, we're making a request directly to the website, and we're going to have to parse the HTML ourselves. This is in contrast to being provided data organized into a more straightforward `JSON` or `XML` output."
+ "El proceso de realizar una solicitud y obtener un resultado se asemeja al flujo de trabajo de una API web. Sin embargo, en este caso estamos haciendo la solicitud directamente al sitio web y tendremos que analizar el HTML por nuestra cuenta. Esto es diferente a cuando se nos proporciona la información ya organizada en un formato más sencillo como JSON o XML."
]
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 144,
"metadata": {
"tags": []
},
- "outputs": [],
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "\n",
+ "\n",
+ "\n",
+ " \n",
+ " \n",
+ " \n",
+ " \n",
+ " \n",
+ " \n",
+ " \n",
+ " \n",
+ " \n",
+ " \n",
+ " \n",
+ " \n",
+ " \n",
+ " \n",
+ " \n",
+ " \n",
+ "\n",
+ " \n",
+ " \n",
+ " \n",
+ " \n",
+ " \n",
+ " \n",
+ " \n",
+ " \n",
+ " \n",
+ " \n",
+ " \n",
+ " \n",
+ " \n",
+ " \n",
+ " \n",
+ " \n",
+ " \n",
+ " English\n",
+ " , \n",
+ " Afrikaans\n",
+ " , \n",
+ " Albanian\n",
+ " , \n",
+ " Arabic\n",
+ " , \n",
+ " Armenian\n",
+ " , \n",
+ " Azerbaijani\n",
+ " , \n",
+ " Basque\n",
+ " , \n",
+ " Bengali\n",
+ " , \n",
+ " Bosnian\n",
+ " , \n",
+ " Catalan\n",
+ " ]\n"
+ ]
+ }
+ ],
+ "source": [
+ "# Buscar todos los elementos con una determinada etiqueta\n",
"a_tags = soup.find_all(\"a\")\n",
"print(a_tags[:10])"
]
@@ -203,18 +323,31 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "Because `find_all()` is the most popular method in the Beautiful Soup search API, you can use a shortcut for it. If you treat the BeautifulSoup object as though it were a function, then it’s the same as calling `find_all()` on that object. \n",
+ "Como find_all() es el método más popular en la API de búsqueda de Beautiful Soup, puedes usar un atajo para llamarlo. Si tratas el objeto BeautifulSoup como si fuera una función, es lo mismo que llamar a find_all() sobre ese objeto.\n",
"\n",
- "These two lines of code are equivalent:"
+ "Estas dos líneas de código son equivalentes:"
]
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 147,
"metadata": {
"tags": []
},
- "outputs": [],
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "\n",
+ " English\n",
+ " \n",
+ "\n",
+ " English\n",
+ " \n"
+ ]
+ }
+ ],
"source": [
"a_tags = soup.find_all(\"a\")\n",
"a_tags_alt = soup(\"a\")\n",
@@ -226,14 +359,22 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "How many links did we obtain?"
+ "¿Cuántos enlaces obtuvimos?"
]
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 148,
"metadata": {},
- "outputs": [],
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "270\n"
+ ]
+ }
+ ],
"source": [
"print(len(a_tags))"
]
@@ -242,22 +383,33 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "That's a lot! Many elements on a page will have the same HTML tag. For instance, if you search for everything with the `a` tag, you're likely to get more hits, many of which you might not want. Remember, the `a` tag defines a hyperlink, so you'll usually find many on any given page.\n",
+ "¡Eso es bastante! Muchos elementos en una página tendrán la misma etiqueta HTML. Por ejemplo, si buscas todo lo que tenga la etiqueta a, probablemente obtendrás muchos resultados, muchos de los cuales quizás no te interesen. Recuerda que la etiqueta a define un hipervínculo, por lo que normalmente encontrarás muchos en cualquier página.\n",
"\n",
- "What if we wanted to search for HTML tags with certain attributes, such as particular CSS classes? \n",
+ "¿Qué pasa si queremos buscar etiquetas HTML con ciertos atributos, como clases CSS específicas?\n",
"\n",
- "We can do this by adding an additional argument to the `find_all`. In the example below, we are finding all the `a` tags, and then filtering those with `class_=\"sidemenu\"`."
+ "Podemos hacerlo agregando un argumento adicional a find_all. En el siguiente ejemplo, estamos buscando todas las etiquetas a y luego filtrando aquellas que tengan class_=\"sidemenu\"."
]
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 149,
"metadata": {
"tags": []
},
- "outputs": [],
- "source": [
- "# Get only the 'a' tags in 'sidemenu' class\n",
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "[]"
+ ]
+ },
+ "execution_count": 149,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "# Obtener solo las etiquetas 'a' que tienen la clase 'sidemenu'\n",
"side_menus = soup(\"a\", class_=\"sidemenu\")\n",
"side_menus[:5]"
]
@@ -266,20 +418,31 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "A more efficient way to search for elements on a website is via a **CSS selector**. For this we have to use a different method called `select()`. Just pass a string into the `.select()` to get all elements with that string as a valid CSS selector.\n",
+ "Una forma más eficiente de buscar elementos en un sitio web es mediante un selector CSS. Para esto, debemos usar un método diferente llamado `select()`. Solo tienes que pasar una cadena al método `.select()` para obtener todos los elementos que coincidan con ese selector CSS.\n",
"\n",
- "In the example above, we can use `\"a.sidemenu\"` as a CSS selector, which returns all `a` tags with class `sidemenu`."
+ "En el ejemplo anterior, podemos usar `a.sidemenu` como selector CSS, lo que nos devuelve todas las etiquetas `a` con la clase `sidemenu`."
]
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 150,
"metadata": {
"tags": []
},
- "outputs": [],
- "source": [
- "# Get elements with \"a.sidemenu\" CSS Selector.\n",
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "[]"
+ ]
+ },
+ "execution_count": 150,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "# Obtener elementos con el selector CSS \"a.sidemenu\".\n",
"selected = soup.select(\"a.sidemenu\")\n",
"selected[:5]"
]
@@ -288,51 +451,1391 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "## 🥊 Challenge: Find All\n",
+ "## 🥊Desafío: Buscar Todos\n",
"\n",
- "Use BeautifulSoup to find all the `a` elements with class `mainmenu`."
+ "Usa BeautifulSoup para encontrar todos los elementos `a` con la clase `mainmenu`. Le cambiee a dropdown-item para que se vea los resultados "
]
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 151,
"metadata": {},
- "outputs": [],
- "source": [
- "# YOUR CODE HERE\n"
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "[\n",
+ " English\n",
+ " , \n",
+ " Afrikaans\n",
+ " , \n",
+ " Albanian\n",
+ " , \n",
+ " Arabic\n",
+ " , \n",
+ " Armenian\n",
+ " ]\n"
+ ]
+ }
+ ],
+ "source": [
+ "### TRABAJO PRACTICO \n",
+ "enlaces = soup.find_all(\"a\", class_=\"dropdown-item\")\n",
+ "print(enlaces[:5]) #lo puse hasta 5 para que no se vea tan largo el resultado"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "## Step 4: Get Attributes and Text of Elements\n",
+ "## Paso 4: Obtener atributos y texto de los elementos\n",
"\n",
- "Once we identify elements, we want the access information in that element. Usually, this means two things:\n",
+ "Una vez que identificamos elementos, queremos acceder a la información en ese elemento. Usualmente, esto significa dos cosas:\n",
"\n",
- "1. Text\n",
- "2. Attributes\n",
+ "1. Texto\n",
+ "2. Atributos\n",
"\n",
- "Getting the text inside an element is easy. All we have to do is use the `text` member of a `tag` object:"
+ "Obtener el texto dentro de un elemento es fácil. Todo lo que tenemos que hacer es usar el miembro `text` de un objeto `tag`:"
]
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 152,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Texto: \n",
+ " English\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Afrikaans\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Albanian\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Arabic\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Armenian\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Azerbaijani\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Basque\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Bengali\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Bosnian\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Catalan\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Croatian\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Czech\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Danish\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Dutch\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Esperanto\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Estonian\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Filipino\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Finnish\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " French\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Galician\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Georgian\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " German\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Greek\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Gujarati\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Haitian Creole\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Hausa\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Hawaiian\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Hebrew\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Hindi\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Hungarian\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Icelandic\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Indonesian\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Interlingua\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Interlingue\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Inuktitut\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Irish\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Italian\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Japanese\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Javanese\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Kannada\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Khmer\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Korean\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Latin\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Latvian\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Lithuanian\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Luxembourgish\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Macedonian\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Malagasy\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Malayalam\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Maltese\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Maori\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Marathi\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Myanmar\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Nepali\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Norwegian\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Odia\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Pashto\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Punjabi\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Romanian\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Russian\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Samoan\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Sango\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Sanskrit\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Sardinian\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Sindhi\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Sinhala\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Slovak\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Slovenian\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Somali\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Southern Sotho\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Spanish\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Sundanese\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Swahili\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Swedish\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Tamil\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Telugu\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Thai\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Tigrinya\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Tonga\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Turkish\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Ukrainian\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Urdu\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Vietnamese\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Welsh\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Xhosa\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Yiddish\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Yoruba\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: \n",
+ " Zulu\n",
+ " \n",
+ "href: #\n",
+ "clases: ['dropdown-item']\n",
+ "------\n",
+ "Texto: Translate\n",
+ "href: https://translate.google.com\n",
+ "clases: ['goog-logo-link']\n",
+ "------\n",
+ "Texto: ILGA.GOV\n",
+ "href: /\n",
+ "clases: None\n",
+ "------\n",
+ "Texto: \n",
+ "LEGISLATION & LAWS \n",
+ "\n",
+ "href: /Legislation\n",
+ "clases: None\n",
+ "------\n",
+ "Texto: Bills & Resolutions\n",
+ "href: /Legislation\n",
+ "clases: None\n",
+ "------\n",
+ "Texto: Public Acts\n",
+ "href: /Legislation/PublicActs\n",
+ "clases: None\n",
+ "------\n",
+ "Texto: Illinois Compiled Statutes\n",
+ "href: /Legislation/ILCS/Chapters\n",
+ "clases: None\n",
+ "------\n",
+ "Texto: Illinois Constitution\n",
+ "href: /documents/commission/lrb/conmain.htm\n",
+ "clases: None\n",
+ "------\n",
+ "Texto: Search Legislation\n",
+ "href: /Search?q=&base=Legis\n",
+ "clases: None\n",
+ "------\n",
+ "Texto: Glossary\n",
+ "href: /Legislation/Glossary\n",
+ "clases: None\n",
+ "------\n",
+ "Texto: Guide\n",
+ "href: /Guide\n",
+ "clases: None\n",
+ "------\n",
+ "Texto: \n",
+ "Reports & Inquiry \n",
+ "\n",
+ "href: /Reports\n",
+ "clases: None\n",
+ "------\n",
+ "Texto: Legislative Reports\n",
+ "href: /Reports\n",
+ "clases: None\n",
+ "------\n",
+ "Texto: Special Reports\n",
+ "href: /Reports/SpecialReports\n",
+ "clases: None\n",
+ "------\n",
+ "Texto: FTP Site\n",
+ "href: /ftp/\n",
+ "clases: None\n",
+ "------\n",
+ "Texto: Legislator Lookup\n",
+ "href: /members/FindMyLegislator\n",
+ "clases: None\n",
+ "------\n",
+ "Texto: Capitol Complex Phone Numbers\n",
+ "href: /directory\n",
+ "clases: None\n",
+ "------\n",
+ "Texto: \n",
+ "Rules & Regulations \n",
+ "\n",
+ "href: #\n",
+ "clases: None\n",
+ "------\n",
+ "Texto: Illinois Register\n",
+ "href: http://www.cyberdriveillinois.com/departments/index/register/home.html\n",
+ "clases: None\n",
+ "------\n",
+ "Texto: Administrative Rules\n",
+ "href: /agencies/JCAR/AdminCode\n",
+ "clases: None\n",
+ "------\n",
+ "Texto: \n",
+ "Senate \n",
+ "\n",
+ "href: /Senate/Members\n",
+ "clases: None\n",
+ "------\n",
+ "Texto: Members\n",
+ "href: /Senate/Members\n",
+ "clases: None\n",
+ "------\n",
+ "Texto: Schedules\n",
+ "href: /Senate/Schedules\n",
+ "clases: None\n",
+ "------\n",
+ "Texto: Committees\n",
+ "href: /Senate/Committees\n",
+ "clases: None\n",
+ "------\n",
+ "Texto: Request for Remote Testimony\n",
+ "href: /Uploads/Testimony/Senate/Remote Legislative Hearing Process 104th GA.pdf\n",
+ "clases: None\n",
+ "------\n",
+ "Texto: Journals\n",
+ "href: /Senate/Journals\n",
+ "clases: None\n",
+ "------\n",
+ "Texto: Transcripts\n",
+ "href: /Senate/Transcripts\n",
+ "clases: None\n",
+ "------\n",
+ "Texto: Rules\n",
+ "href: /Senate/Rules\n",
+ "clases: None\n",
+ "------\n",
+ "Texto: Audio/Video\n",
+ "href: /Senate/AudioVideo\n",
+ "clases: None\n",
+ "------\n",
+ "Texto: FOIA Information\n",
+ "href: /Documents/senate/FOIA.pdf\n",
+ "clases: None\n",
+ "------\n",
+ "Texto: Senate Employment Opportunities\n",
+ "href: /EmploymentOpportunities\n",
+ "clases: None\n",
+ "------\n",
+ "Texto: Media Guidelines\n",
+ "href: /Documents/senate/SenateMediaGuidelines.pdf\n",
+ "clases: None\n",
+ "------\n",
+ "Texto: \n",
+ "House \n",
+ "\n",
+ "href: /House/Members\n",
+ "clases: None\n",
+ "------\n",
+ "Texto: Members\n",
+ "href: /House/Members\n",
+ "clases: None\n",
+ "------\n",
+ "Texto: Schedules\n",
+ "href: /House/Schedules\n",
+ "clases: None\n",
+ "------\n",
+ "Texto: Committees\n",
+ "href: /House/Committees\n",
+ "clases: None\n",
+ "------\n",
+ "Texto: Submit testimony for House Committees\n",
+ "href: /Uploads/Testimony/House/Remote_Committee_Hearing_Process_March2023.pdf\n",
+ "clases: None\n",
+ "------\n",
+ "Texto: Journals\n",
+ "href: /House/Journals\n",
+ "clases: None\n",
+ "------\n",
+ "Texto: Transcripts\n",
+ "href: /House/Transcripts\n",
+ "clases: None\n",
+ "------\n",
+ "Texto: Rules\n",
+ "href: /House/Rules\n",
+ "clases: None\n",
+ "------\n",
+ "Texto: Audio/Video\n",
+ "href: /House/AudioVideo\n",
+ "clases: None\n",
+ "------\n",
+ "Texto: FOIA Information\n",
+ "href: /Documents/house/FOIA.pdf\n",
+ "clases: None\n",
+ "------\n",
+ "Texto: House Employment Opportunities\n",
+ "href: /EmploymentOpportunities\n",
+ "clases: None\n",
+ "------\n",
+ "Texto: Log In\n",
+ "href: /Account/Login\n",
+ "clases: ['nav-link']\n",
+ "------\n",
+ "Texto: Home\n",
+ "href: /\n",
+ "clases: ['active']\n",
+ "------\n",
+ "Texto: View List\n",
+ "href: /Senate/Members/List\n",
+ "clases: ['btn', 'btn-primary']\n",
+ "------\n",
+ "Texto: Officers\n",
+ "href: /Documents/Senate/104th_Senate_Officers.pdf\n",
+ "clases: ['btn', 'btn-primary', 'm-1']\n",
+ "------\n",
+ "Texto: Leadership\n",
+ "href: /Documents/Senate/104th_Senate_Leadership.pdf\n",
+ "clases: ['btn', 'btn-primary', 'm-1']\n",
+ "------\n",
+ "Texto: Seating Chart\n",
+ "href: https://www.ilga.gov/Documents/Senate/104th_Senate_Seating_Chart.pdf\n",
+ "clases: ['btn', 'btn-primary', 'm-1']\n",
+ "------\n",
+ "Texto: Report List\n",
+ "href: Members/rptMemberList\n",
+ "clases: ['btn', 'btn-primary', 'm-1']\n",
+ "------\n",
+ "Texto: Neil Anderson\n",
+ "href: /Senate/Members/Details/3312\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Neil Anderson\n",
+ "href: /Senate/Members/Details/3312\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Omar Aquino\n",
+ "href: /Senate/Members/Details/3316\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Omar Aquino\n",
+ "href: /Senate/Members/Details/3316\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Li Arellano, Jr.\n",
+ "href: /Senate/Members/Details/3383\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Li Arellano, Jr.\n",
+ "href: /Senate/Members/Details/3383\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Chris Balkema\n",
+ "href: /Senate/Members/Details/3413\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Chris Balkema\n",
+ "href: /Senate/Members/Details/3413\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Christopher Belt\n",
+ "href: /Senate/Members/Details/3337\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Christopher Belt\n",
+ "href: /Senate/Members/Details/3337\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Terri Bryant\n",
+ "href: /Senate/Members/Details/3386\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Terri Bryant\n",
+ "href: /Senate/Members/Details/3386\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Cristina Castro\n",
+ "href: /Senate/Members/Details/3317\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Cristina Castro\n",
+ "href: /Senate/Members/Details/3317\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Javier L. Cervantes\n",
+ "href: /Senate/Members/Details/3403\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Javier L. Cervantes\n",
+ "href: /Senate/Members/Details/3403\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Andrew S. Chesney\n",
+ "href: /Senate/Members/Details/3410\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Andrew S. Chesney\n",
+ "href: /Senate/Members/Details/3410\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Lakesia Collins\n",
+ "href: /Senate/Members/Details/3443\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Lakesia Collins\n",
+ "href: /Senate/Members/Details/3443\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Bill Cunningham\n",
+ "href: /Senate/Members/Details/3291\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Bill Cunningham\n",
+ "href: /Senate/Members/Details/3291\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: John F. Curran\n",
+ "href: /Senate/Members/Details/3329\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: John F. Curran\n",
+ "href: /Senate/Members/Details/3329\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Donald P. DeWitte\n",
+ "href: /Senate/Members/Details/3334\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Donald P. DeWitte\n",
+ "href: /Senate/Members/Details/3334\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Mary Edly-Allen\n",
+ "href: /Senate/Members/Details/3407\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Mary Edly-Allen\n",
+ "href: /Senate/Members/Details/3407\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Laura Ellman\n",
+ "href: /Senate/Members/Details/3339\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Laura Ellman\n",
+ "href: /Senate/Members/Details/3339\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Paul Faraci\n",
+ "href: /Senate/Members/Details/3412\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Paul Faraci\n",
+ "href: /Senate/Members/Details/3412\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Sara Feigenholtz\n",
+ "href: /Senate/Members/Details/3376\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Sara Feigenholtz\n",
+ "href: /Senate/Members/Details/3376\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Laura Fine\n",
+ "href: /Senate/Members/Details/3338\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Laura Fine\n",
+ "href: /Senate/Members/Details/3338\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Dale Fowler\n",
+ "href: /Senate/Members/Details/3318\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Dale Fowler\n",
+ "href: /Senate/Members/Details/3318\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Suzy Glowiak Hilton\n",
+ "href: /Senate/Members/Details/3341\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Suzy Glowiak Hilton\n",
+ "href: /Senate/Members/Details/3341\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Graciela Guzmán\n",
+ "href: /Senate/Members/Details/3442\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Graciela Guzmán\n",
+ "href: /Senate/Members/Details/3442\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Michael W. Halpin\n",
+ "href: /Senate/Members/Details/3408\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Michael W. Halpin\n",
+ "href: /Senate/Members/Details/3408\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Don Harmon\n",
+ "href: /Senate/Members/Details/3268\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Don Harmon\n",
+ "href: /Senate/Members/Details/3268\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Napoleon Harris, III\n",
+ "href: /Senate/Members/Details/3292\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Napoleon Harris, III\n",
+ "href: /Senate/Members/Details/3292\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Erica Harriss\n",
+ "href: /Senate/Members/Details/3411\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Erica Harriss\n",
+ "href: /Senate/Members/Details/3411\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Michael E. Hastings\n",
+ "href: /Senate/Members/Details/3293\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Michael E. Hastings\n",
+ "href: /Senate/Members/Details/3293\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Darby A. Hills\n",
+ "href: /Senate/Members/Details/3460\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Darby A. Hills\n",
+ "href: /Senate/Members/Details/3460\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Linda Holmes\n",
+ "href: /Senate/Members/Details/3270\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Linda Holmes\n",
+ "href: /Senate/Members/Details/3270\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Mattie Hunter\n",
+ "href: /Senate/Members/Details/3269\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Mattie Hunter\n",
+ "href: /Senate/Members/Details/3269\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Adriane Johnson\n",
+ "href: /Senate/Members/Details/3378\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Adriane Johnson\n",
+ "href: /Senate/Members/Details/3378\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Emil Jones, III\n",
+ "href: /Senate/Members/Details/3276\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Emil Jones, III\n",
+ "href: /Senate/Members/Details/3276\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Patrick J. Joyce\n",
+ "href: /Senate/Members/Details/3372\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Patrick J. Joyce\n",
+ "href: /Senate/Members/Details/3372\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: David Koehler\n",
+ "href: /Senate/Members/Details/3271\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: David Koehler\n",
+ "href: /Senate/Members/Details/3271\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Seth Lewis\n",
+ "href: /Senate/Members/Details/3406\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Seth Lewis\n",
+ "href: /Senate/Members/Details/3406\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Kimberly A. Lightford\n",
+ "href: /Senate/Members/Details/3264\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Kimberly A. Lightford\n",
+ "href: /Senate/Members/Details/3264\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Meg Loughran Cappel\n",
+ "href: /Senate/Members/Details/3380\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Meg Loughran Cappel\n",
+ "href: /Senate/Members/Details/3380\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Robert F. Martwick\n",
+ "href: /Senate/Members/Details/3369\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Robert F. Martwick\n",
+ "href: /Senate/Members/Details/3369\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Steve McClure\n",
+ "href: /Senate/Members/Details/3342\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Steve McClure\n",
+ "href: /Senate/Members/Details/3342\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Julie A. Morrison\n",
+ "href: /Senate/Members/Details/3294\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Julie A. Morrison\n",
+ "href: /Senate/Members/Details/3294\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Laura M. Murphy\n",
+ "href: /Senate/Members/Details/3313\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Laura M. Murphy\n",
+ "href: /Senate/Members/Details/3313\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Robert Peters\n",
+ "href: /Senate/Members/Details/3343\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Robert Peters\n",
+ "href: /Senate/Members/Details/3343\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Jason Plummer\n",
+ "href: /Senate/Members/Details/3344\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Jason Plummer\n",
+ "href: /Senate/Members/Details/3344\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Mike Porfirio\n",
+ "href: /Senate/Members/Details/3404\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Mike Porfirio\n",
+ "href: /Senate/Members/Details/3404\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Willie Preston\n",
+ "href: /Senate/Members/Details/3405\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Willie Preston\n",
+ "href: /Senate/Members/Details/3405\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Sue Rezin\n",
+ "href: /Senate/Members/Details/3281\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Sue Rezin\n",
+ "href: /Senate/Members/Details/3281\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Chapin Rose\n",
+ "href: /Senate/Members/Details/3295\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Chapin Rose\n",
+ "href: /Senate/Members/Details/3295\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Mike Simmons\n",
+ "href: /Senate/Members/Details/3398\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Mike Simmons\n",
+ "href: /Senate/Members/Details/3398\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Elgie R. Sims, Jr.\n",
+ "href: /Senate/Members/Details/3331\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Elgie R. Sims, Jr.\n",
+ "href: /Senate/Members/Details/3331\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Steve Stadelman\n",
+ "href: /Senate/Members/Details/3296\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Steve Stadelman\n",
+ "href: /Senate/Members/Details/3296\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Dave Syverson\n",
+ "href: /Senate/Members/Details/3265\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Dave Syverson\n",
+ "href: /Senate/Members/Details/3265\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Jil Tracy\n",
+ "href: /Senate/Members/Details/3319\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Jil Tracy\n",
+ "href: /Senate/Members/Details/3319\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Doris Turner\n",
+ "href: /Senate/Members/Details/3399\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Doris Turner\n",
+ "href: /Senate/Members/Details/3399\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Sally J. Turner\n",
+ "href: /Senate/Members/Details/3397\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Sally J. Turner\n",
+ "href: /Senate/Members/Details/3397\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Rachel Ventura\n",
+ "href: /Senate/Members/Details/3409\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Rachel Ventura\n",
+ "href: /Senate/Members/Details/3409\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Karina Villa\n",
+ "href: /Senate/Members/Details/3385\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Karina Villa\n",
+ "href: /Senate/Members/Details/3385\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Celina Villanueva\n",
+ "href: /Senate/Members/Details/3375\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Celina Villanueva\n",
+ "href: /Senate/Members/Details/3375\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Ram Villivalam\n",
+ "href: /Senate/Members/Details/3345\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Ram Villivalam\n",
+ "href: /Senate/Members/Details/3345\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Mark L. Walker\n",
+ "href: /Senate/Members/Details/3449\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Mark L. Walker\n",
+ "href: /Senate/Members/Details/3449\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Craig Wilcox\n",
+ "href: /Senate/Members/Details/3336\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Craig Wilcox\n",
+ "href: /Senate/Members/Details/3336\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Dan McConchie\n",
+ "href: /Senate/Members/Details/3315\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: Dan McConchie\n",
+ "href: /Senate/Members/Details/3315\n",
+ "clases: ['notranslate']\n",
+ "------\n",
+ "Texto: \n",
+ " Contact ILGA Webmaster\n",
+ " \n",
+ "href: mailto:webmaster@ilga.gov?subject=Email from ILGA Web\n",
+ "clases: None\n",
+ "------\n",
+ "Texto: \n",
+ "\n",
+ "\n",
+ "href: http://www.amberillinois.com\n",
+ "clases: None\n",
+ "------\n",
+ "Texto: \n",
+ "\n",
+ "\n",
+ "href: https://www.missingkids.org\n",
+ "clases: None\n",
+ "------\n",
+ "Texto: ILGA.GOV\n",
+ "href: /\n",
+ "clases: None\n",
+ "------\n",
+ "Texto: Disclaimers\n",
+ "href: /Disclaimers\n",
+ "clases: None\n",
+ "------\n",
+ "Texto: \n",
+ " ADA\n",
+ " \n",
+ "href: /Accessibility\n",
+ "clases: None\n",
+ "------\n",
+ "Texto: \n",
+ " Contact ILGA Webmaster\n",
+ " \n",
+ "href: mailto:webmaster@ilga.gov?subject=Email from ILGA Web\n",
+ "clases: None\n",
+ "------\n",
+ "Texto: \n",
+ "\n",
+ "\n",
+ "href: http://www.amberillinois.com\n",
+ "clases: None\n",
+ "------\n",
+ "Texto: \n",
+ "\n",
+ "\n",
+ "href: https://www.missingkids.org\n",
+ "clases: None\n",
+ "------\n",
+ "Texto: ILGA.GOV\n",
+ "href: /\n",
+ "clases: None\n",
+ "------\n",
+ "Texto: Disclaimers\n",
+ "href: /Disclaimers\n",
+ "clases: None\n",
+ "------\n",
+ "Texto: \n",
+ " ADA\n",
+ " \n",
+ "href: /Accessibility\n",
+ "clases: None\n",
+ "------\n",
+ "Texto: \n",
+ "href: #\n",
+ "clases: ['back-to-top', 'd-flex', 'align-items-center', 'justify-content-center']\n",
+ "------\n"
+ ]
+ }
+ ],
+ "source": [
+ "# Encuentra todos los enlaces del HTML\n",
+ "links = soup.find_all(\"a\")\n",
+ "\n",
+ "# Imprime el texto y el href de cada enlace\n",
+ "for link in links:\n",
+ " print(\"Texto:\", link.text)\n",
+ " print(\"href:\", link.get('href'))\n",
+ " print(\"clases:\", link.get('class'))\n",
+ " print(\"------\")\n",
+ "\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 153,
"metadata": {
"tags": []
},
- "outputs": [],
- "source": [
- "# Get all sidemenu links as a list\n",
- "side_menu_links = soup.select(\"a.sidemenu\")\n",
- "\n",
- "# Examine the first link\n",
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "\n",
+ " English\n",
+ " \n",
+ "Class: \n"
+ ]
+ }
+ ],
+ "source": [
+ "# obtener elementos con el selector CSS \"a.dropdown-item\"\n",
+ "side_menu_links = soup.select(\"a.dropdown-item\")\n",
+ "\n",
+ "# examinar el primer elemento\n",
"first_link = side_menu_links[0]\n",
- "print(first_link)\n",
"\n",
- "# What class is this variable?\n",
- "print('Class: ', type(first_link))"
+ "# obtener el texto del enlace\n",
+ "print(first_link.text)\n",
+ "\n",
+ "# cuál clase de objeto es?\n",
+ "print('Class: ', type(first_link))\n"
]
},
{
@@ -344,11 +1847,21 @@
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 154,
"metadata": {
"tags": []
},
- "outputs": [],
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "\n",
+ " English\n",
+ " \n"
+ ]
+ }
+ ],
"source": [
"print(first_link.text)"
]
@@ -364,11 +1877,19 @@
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 155,
"metadata": {
"tags": []
},
- "outputs": [],
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "#\n"
+ ]
+ }
+ ],
"source": [
"print(first_link['href'])"
]
@@ -377,18 +1898,23 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "## 🥊 Challenge: Extract specific attributes\n",
+ "## 🥊 Desafío: Extraer atributos específicos\n",
"\n",
- "Extract all `href` attributes for each `mainmenu` URL."
+ "Extrae todos los atributos `href` de cada URL `mainmenu`."
]
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 156,
"metadata": {},
"outputs": [],
"source": [
- "# YOUR CODE HERE\n"
+ "# YOUR CODE HERE\n",
+ "# Extraer todos los atributos href de cada enlace con la clase 'mainmenu'\n",
+ "mainmenu_links = soup.select(\"a.mainmenu\")\n",
+ "\n",
+ "for link in mainmenu_links:\n",
+ " print(link.get('href'))\n"
]
},
{
@@ -397,37 +1923,37 @@
"source": [
"\n",
"\n",
- "# Scraping the Illinois General Assembly\n",
+ "# Análisis de la Asamblea General de Illinois\n",
"\n",
- "Believe it or not, those are really the fundamental tools you need to scrape a website. Once you spend more time familiarizing yourself with HTML and CSS, then it's simply a matter of understanding the structure of a particular website and intelligently applying the tools of Beautiful Soup and Python.\n",
+ "Aunque parezca increíble, estas son las herramientas fundamentales para analizar un sitio web. Una vez que dediques más tiempo a familiarizarte con HTML y CSS, solo tendrás que comprender la estructura de un sitio web específico y aplicar con inteligencia las herramientas de Beautiful Soup y Python.\n",
"\n",
- "Let's apply these skills to scrape the [Illinois 98th General Assembly](http://www.ilga.gov/senate/default.asp?GA=98).\n",
+ "Apliquemos estas habilidades para analizar la [98.ª Asamblea General de Illinois](http://www.ilga.gov/senate/default.asp?GA=98).\n",
"\n",
- "Specifically, our goal is to scrape information on each senator, including their name, district, and party."
+ "En concreto, nuestro objetivo es analizar la información de cada senador, incluyendo su nombre, distrito y partido."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "## Scrape and Soup the Webpage\n",
+ "## Analizar la página web\n",
"\n",
- "Let's scrape and parse the webpage, using the tools we learned in the previous section."
+ "Analicemos la página web usando las herramientas que aprendimos en la sección anterior."
]
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 157,
"metadata": {
"tags": []
},
"outputs": [],
"source": [
- "# Make a GET request\n",
- "req = requests.get('http://www.ilga.gov/senate/default.asp?GA=98')\n",
- "# Read the content of the server’s response\n",
+ "# Hacemos una nueva solicitud a otra página\n",
+ "req = requests.get('https://www.ilga.gov/Senate/Members/rptMemberList')\n",
+ "# leer el contenido de la respuesta del servidor\n",
"src = req.text\n",
- "# Soup it\n",
+ "# analiza la respuesta y conviértela en un árbol HTML.\n",
"soup = BeautifulSoup(src, \"lxml\")"
]
},
@@ -435,18 +1961,29 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "## Search for the Table Elements\n",
+ "## Buscar los elementos de la tabla\n",
"\n",
- "Our goal is to obtain the elements in the table on the webpage. Remember: rows are identified by the `tr` tag. Let's use `find_all` to obtain these elements."
+ "Nuestro objetivo es obtener los elementos de la tabla en la página web. Recuerde: las filas se identifican con la etiqueta `tr`. Usemos `find_all` para obtener estos elementos."
]
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 158,
"metadata": {},
- "outputs": [],
- "source": [
- "# Get all table row elements\n",
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "60"
+ ]
+ },
+ "execution_count": 158,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "# obtener todas las filas de la tabla\n",
"rows = soup.find_all(\"tr\")\n",
"len(rows)"
]
@@ -455,85 +1992,207 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "⚠️ **Warning**: Keep in mind: `find_all` gets *all* the elements with the `tr` tag. We only want some of them. If we use the 'Inspect' function in Google Chrome and look carefully, then we can use some CSS selectors to get just the rows we're interested in. Specifically, we want the inner rows of the table:"
+ "⚠️ **Advertencia**: Ten en cuenta que `find_all` obtiene *todos* los elementos con la etiqueta `tr`. Solo necesitamos algunos. Si usamos la función \"Inspeccionar\" de Google Chrome y observamos con atención, podemos usar selectores CSS para obtener solo las filas que nos interesan. En concreto, queremos las filas internas de la tabla:"
]
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 159,
"metadata": {},
"outputs": [],
"source": [
- "# Returns every ‘tr tr tr’ css selector in the page\n",
- "rows = soup.select('tr tr tr')\n",
+ "# retornar solo las filas que están dentro de otra fila\n",
+ "rows = soup.select('a.dropdown-item')\n",
"\n",
- "for row in rows[:5]:\n",
- " print(row, '\\n')"
+ "for row in rows[:20]:\n",
+ " print(row, '\\n')\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "It looks like we want everything after the first two rows. Let's work with a single row to start, and build our loop from there."
+ "Parece que queremos todo lo que queda después de las dos primeras filas. Empecemos con una sola fila y construyamos nuestro bucle a partir de ahí."
]
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 160,
"metadata": {},
- "outputs": [],
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "0\n",
+ "[]\n"
+ ]
+ }
+ ],
+ "source": [
+ "print(len(rows))\n",
+ "print(rows)\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 161,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "No rows found.\n"
+ ]
+ }
+ ],
"source": [
- "example_row = rows[2]\n",
- "print(example_row.prettify())"
+ "if rows:\n",
+ "\texample_row = rows[0]\n",
+ "\tprint(example_row.prettify())\n",
+ "else:\n",
+ "\tprint(\"No rows found.\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "Let's break this row down into its component cells/columns using the `select` method with CSS selectors. Looking closely at the HTML, there are a couple of ways we could do this.\n",
+ "Desglosemos esta fila en sus celdas/columnas mediante el método `select` con selectores CSS. Si analizamos el HTML con atención, hay un par de maneras de hacerlo.\n",
"\n",
- "* We could identify the cells by their tag `td`.\n",
- "* We could use the the class name `.detail`.\n",
- "* We could combine both and use the selector `td.detail`."
+ "* Podríamos identificar las celdas por su etiqueta `td`.\n",
+ "* Podríamos usar el nombre de clase `.detail`.\n",
+ "* Podríamos combinar ambos y usar el selector `td.detail`."
]
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 162,
"metadata": {},
"outputs": [],
"source": [
- "for cell in example_row.select('td'):\n",
- " print(cell)\n",
- "print()\n",
- "\n",
- "for cell in example_row.select('.detail'):\n",
- " print(cell)\n",
- "print()\n",
+ "# Parse the HTML to get the rows\n",
+ "soup = BeautifulSoup(src, \"lxml\")\n",
+ "rows = soup.select(\"tbody tr\")\n",
"\n",
- "for cell in example_row.select('td.detail'):\n",
- " print(cell)\n",
- "print()"
+ "if rows:\n",
+ "\texample_row = rows[0]\n",
+ "else:\n",
+ "\texample_row = None\n",
+ "\tprint(\"No rows found.\")\n",
+ " "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 163,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "
\n",
+ " 208 A Capitol Building \n",
+ " \n",
+ " Springfield, IL 62706 \n",
+ " (217) 782-5957\n",
+ " \n",
+ "
\n",
+ "
103 North College Avenue \n",
+ " #201 \n",
+ " Aledo IL 61231 \n",
+ " (309) 230-7584\n",
+ "
\n"
+ ]
+ }
+ ],
+ "source": [
+ "if rows:\n",
+ " example_row = rows[0]\n",
+ " print(example_row.prettify())\n",
+ "\n",
+ " # Aquí procesa example_row solo si existe\n",
+ " for cell in example_row.select('td'):\n",
+ " print(cell)\n",
+ " for cell in example_row.select('.detail'):\n",
+ " print(cell)\n",
+ " for cell in example_row.select('td.detail'):\n",
+ " print(cell)\n",
+ "else:\n",
+ " print(\"No rows found.\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "We can confirm that these are all the same."
+ "Podemos confirmar que todos son iguales."
]
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 167,
"metadata": {
"tags": []
},
- "outputs": [],
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "tds: 3\n",
+ "details: 0\n",
+ "td.details: 0\n"
+ ]
+ }
+ ],
"source": [
- "assert example_row.select('td') == example_row.select('.detail') == example_row.select('td.detail')"
+ "# revisamos cuántos elementos encuentra cada método\n",
+ "tds = example_row.select('td')\n",
+ "details = example_row.select('.detail')\n",
+ "td_details = example_row.select('td.detail')\n",
+ "\n",
+ "print(\"tds:\", len(tds))\n",
+ "print(\"details:\", len(details))\n",
+ "print(\"td.details:\", len(td_details))\n",
+ "\n",
+ "# solo para verificar que todos los .detail están en
\n",
+ "assert td_details == details # estos deben de ser iguales"
]
},
{
@@ -988,7 +2647,7 @@
"metadata": {
"anaconda-cloud": {},
"kernelspec": {
- "display_name": "Python 3 (ipykernel)",
+ "display_name": "Python 3",
"language": "python",
"name": "python3"
},
@@ -1002,12 +2661,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.8.13"
- },
- "vscode": {
- "interpreter": {
- "hash": "b6f9fe9f4b7182690503d8ecc2bae97b0ee3ebf54e877167ae4d28c119a56988"
- }
+ "version": "3.13.6"
}
},
"nbformat": 4,
diff --git a/solutions/02_web_scraping_solutions.ipynb b/solutions/02_web_scraping_solutions.ipynb
index a21532e..3d5eaa3 100644
--- a/solutions/02_web_scraping_solutions.ipynb
+++ b/solutions/02_web_scraping_solutions.ipynb
@@ -4,12 +4,13 @@
"cell_type": "markdown",
"metadata": {},
"source": [
+ "\n",
"# Web Scraping with Beautiful Soup: Solutions"
]
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
@@ -20,9 +21,35 @@
"import time"
]
},
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "**Explicación de cada import**\n",
+ "\n",
+ "**from bs4 import BeautifulSoup**\n",
+ "- Trae la clase BeautifulSoup de la librería BeautifulSoup4.\n",
+ "- Sirve para parsear (analizar) el HTML o XML de páginas web y poder extraer información (textos, tablas, enlaces, etc.).\n",
+ "\n",
+ "**from datetime import datetime**\n",
+ "- Importa la clase datetime.\n",
+ "- Te permite manejar fechas y horas: obtener la fecha actual, dar formato, calcular diferencias de tiempo, etc.\n",
+ "\n",
+ "**import requests**\n",
+ "- Carga la librería requests, usada para hacer peticiones HTTP (GET, POST, etc.).\n",
+ "- Es lo que se usa para descargar contenido de páginas web o conectarse a APIs.\n",
+ "\n",
+ "**import time**\n",
+ "- Importa el módulo estándar time.\n",
+ "- Te permite trabajar con funciones relacionadas al tiempo:\n",
+ "\n",
+ " * time.sleep(segundos) para pausar la ejecución.\n",
+ " * time.time() para medir tiempos de ejecución."
+ ]
+ },
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
@@ -34,6 +61,29 @@
"soup = BeautifulSoup(src, 'lxml')"
]
},
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "**Explicacion**\n",
+ "\n",
+ "**req = requests.get('http://www.ilga.gov/senate/default.asp')**\n",
+ "\n",
+ "* Usa requests para hacer una petición HTTP GET a la página del Senado del Estado de Illinois (ilga.gov).\n",
+ "* El servidor responde con el código HTML de esa página.\n",
+ "* Ese resultado queda guardado en req.\n",
+ "* Puedes verificar si la petición fue exitosa con req.status_code (200 significa OK).\n",
+ "\n",
+ "**src = req.text**\n",
+ "* Extrae el contenido de la respuesta en forma de texto (string).\n",
+ "* Aquí estará el HTML crudo de la página web.\n",
+ "\n",
+ "**soup = BeautifulSoup(src, 'lxml')**\n",
+ "* Convierte el HTML en un árbol DOM navegable con la librería BeautifulSoup.\n",
+ "* Usa el parser lxml (más rápido y flexible que html.parser).\n",
+ "* Ahora puedes recorrer el HTML con métodos como .find(), .find_all(), .title, etc."
+ ]
+ },
{
"cell_type": "markdown",
"metadata": {},
@@ -45,13 +95,33 @@
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 3,
"metadata": {},
- "outputs": [],
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "[]"
+ ]
+ },
+ "execution_count": 3,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
"source": [
"soup.select(\"a.mainmenu\")"
]
},
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "**Explicacion del codigo**\n",
+ "\n",
+ " Con soup.select(\"a.mainmenu\") obtendrás una lista de elementos <'a'> solo con clase mainmenu, es decir los dos primeros enlaces."
+ ]
+ },
{
"cell_type": "markdown",
"metadata": {},
@@ -70,6 +140,15 @@
"[link['href'] for link in soup.select(\"a.mainmenu\")]"
]
},
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "**Explicacion del codigo**\n",
+ "\n",
+ " Ese código lista para extraer solo los atributos href (los enlaces) de todas las etiquetas <'a'> con clase mainmenu."
+ ]
+ },
{
"cell_type": "markdown",
"metadata": {},
@@ -109,7 +188,7 @@
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 5,
"metadata": {},
"outputs": [],
"source": [
@@ -150,13 +229,227 @@
" members.append(senator)"
]
},
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Ese bloque de código es un scraper de la página del Senado de Illinois (98ª Asamblea General). Lo que hace es recorrer la tabla de senadores y extraer sus datos principales. Construye una lista de senadores de Illinois (98th GA) con:\n",
+ "\n",
+ "* Nombre\n",
+ "* Distrito\n",
+ "* Partido político\n",
+ "* Enlace a su perfil oficial"
+ ]
+ },
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 84,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Perfiles encontrados en la lista: 60\n",
+ "Total miembros parseados: 60\n",
+ "('Member', 8505, 'D', 'https://www.ilga.gov/Senate/Members/Details/3264')\n",
+ "('Member', 5413, 'R', 'https://www.ilga.gov/Senate/Members/Details/3265')\n",
+ "('Member', 8176, 'D', 'https://www.ilga.gov/Senate/Members/Details/3268')\n",
+ "('Member', 5966, 'D', 'https://www.ilga.gov/Senate/Members/Details/3269')\n",
+ "('Member', 422, 'D', 'https://www.ilga.gov/Senate/Members/Details/3270')\n",
+ "('Member', 8250, 'D', 'https://www.ilga.gov/Senate/Members/Details/3271')\n",
+ "('Member', 9573, 'D', 'https://www.ilga.gov/Senate/Members/Details/3276')\n",
+ "('Member', 3840, 'R', 'https://www.ilga.gov/Senate/Members/Details/3281')\n",
+ "('Member', 5145, 'D', 'https://www.ilga.gov/Senate/Members/Details/3291')\n",
+ "('Member', 8066, 'D', 'https://www.ilga.gov/Senate/Members/Details/3292')\n",
+ "\n",
+ "Primeras 5 filas:\n",
+ " Nombre Distrito Partido Perfil\n",
+ "0 Member 8505 D https://www.ilga.gov/Senate/Members/Details/3264\n",
+ "1 Member 5413 R https://www.ilga.gov/Senate/Members/Details/3265\n",
+ "2 Member 8176 D https://www.ilga.gov/Senate/Members/Details/3268\n",
+ "3 Member 5966 D https://www.ilga.gov/Senate/Members/Details/3269\n",
+ "4 Member 422 D https://www.ilga.gov/Senate/Members/Details/3270\n",
+ "\n",
+ "CSV generado: senado_ilga_members.csv\n"
+ ]
+ }
+ ],
+ "source": [
+ "import re, time\n",
+ "from urllib.parse import urljoin\n",
+ "import requests\n",
+ "from bs4 import BeautifulSoup\n",
+ "\n",
+ "LIST_URLS = [\n",
+ " \"https://www.ilga.gov/Senate/Members/List\",\n",
+ " \"https://www.ilga.gov/Senate/Members\",\n",
+ "]\n",
+ "BASE = \"https://www.ilga.gov\"\n",
+ "\n",
+ "HEADERS = {\n",
+ " \"User-Agent\": \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 \"\n",
+ " \"(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36\",\n",
+ " \"Accept\": \"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\",\n",
+ " \"Accept-Language\": \"es-ES,es;q=0.9,en;q=0.8\",\n",
+ " \"Referer\": \"https://www.ilga.gov/\",\n",
+ " \"Connection\": \"keep-alive\",\n",
+ "}\n",
+ "\n",
+ "def fetch(url):\n",
+ " s = requests.Session()\n",
+ " s.headers.update(HEADERS)\n",
+ " r = s.get(url, timeout=30)\n",
+ " r.raise_for_status()\n",
+ " return r\n",
+ "\n",
+ "def find_profile_links(html, base=BASE):\n",
+ " soup = BeautifulSoup(html, \"lxml\")\n",
+ " # busca anchors a /Senate/Members/Details/... (insensible a mayúsculas)\n",
+ " links = set()\n",
+ " for a in soup.select('a[href]'):\n",
+ " href = a.get(\"href\") or \"\"\n",
+ " if re.search(r\"/Senate/Members/Details/\\d+\", href, flags=re.I):\n",
+ " links.add(urljoin(base, href))\n",
+ " return sorted(links)\n",
+ "\n",
+ "def parse_profile(html):\n",
+ " \"\"\"Devuelve (name, district:int|None, party:str|''), usando varias heurísticas.\"\"\"\n",
+ " soup = BeautifulSoup(html, \"lxml\")\n",
+ "\n",
+ " # Nombre: probar h1, h2, title, aria-labels…\n",
+ " name = \"\"\n",
+ " cand = []\n",
+ " cand += [h.get_text(strip=True) for h in soup.select(\"h1\")]\n",
+ " if not cand: cand += [h.get_text(strip=True) for h in soup.select(\"h2\")]\n",
+ " if not cand and soup.title: cand += [soup.title.get_text(strip=True)]\n",
+ " if cand: name = cand[0]\n",
+ "\n",
+ " # Texto visible para regex\n",
+ " text = soup.get_text(\" \", strip=True)\n",
+ "\n",
+ " # Distrito (varias variantes)\n",
+ " district = None\n",
+ " for pat in [\n",
+ " r\"District\\s*(\\d+)\",\n",
+ " r\"(\\d+)\\s+District\",\n",
+ " r\"Senate\\s+District\\s*(\\d+)\",\n",
+ " r\"(\\d+)\\s+\\(D\\)|(\\d+)\\s+\\(R\\)|(\\d+)\\s+\\(I\\)\"\n",
+ " ]:\n",
+ " m = re.search(pat, text, flags=re.I)\n",
+ " if m:\n",
+ " # toma el primer grupo no vacío\n",
+ " for g in m.groups():\n",
+ " if g and g.isdigit():\n",
+ " district = int(g)\n",
+ " break\n",
+ " if district is not None:\n",
+ " break\n",
+ "\n",
+ " # Partido (D/R/I o completo)\n",
+ " party = \"\"\n",
+ " # primero letra entre paréntesis\n",
+ " m = re.search(r\"\\((D|R|I)\\)\", text)\n",
+ " if m:\n",
+ " party = m.group(1)\n",
+ " else:\n",
+ " # palabras completas\n",
+ " if re.search(r\"\\bDemocrat(ic)?\\b\", text, flags=re.I):\n",
+ " party = \"D\"\n",
+ " elif re.search(r\"\\bRepublican\\b\", text, flags=re.I):\n",
+ " party = \"R\"\n",
+ " elif re.search(r\"\\bIndependent\\b\", text, flags=re.I):\n",
+ " party = \"I\"\n",
+ "\n",
+ " return name, district, party\n",
+ "\n",
+ "# === Flujo principal ===\n",
+ "try:\n",
+ " # 1) Prueba con las URLs de lista y junta todos los perfiles\n",
+ " all_profiles = []\n",
+ " for url in LIST_URLS:\n",
+ " try:\n",
+ " r = fetch(url)\n",
+ " profs = find_profile_links(r.text)\n",
+ " if profs:\n",
+ " all_profiles.extend(profs)\n",
+ " except Exception as e:\n",
+ " print(f\"[WARN] No se pudo leer {url}: {e}\")\n",
+ "\n",
+ " # de-duplicar\n",
+ " all_profiles = sorted(set(all_profiles))\n",
+ " print(\"Perfiles encontrados en la lista:\", len(all_profiles))\n",
+ "\n",
+ " # Diagnóstico si no hay enlaces\n",
+ " if not all_profiles:\n",
+ " print(\"No se hallaron enlaces a perfiles. Guardando debug_list.html…\")\n",
+ " try:\n",
+ " open(\"debug_list.html\", \"w\", encoding=\"utf-8\").write(r.text)\n",
+ " print(\"Revisa debug_list.html para ver el HTML real (¿portal cautivo/bloqueo?).\")\n",
+ " except Exception:\n",
+ " pass\n",
+ "\n",
+ " # 2) Visita cada perfil y extrae datos\n",
+ " members = []\n",
+ " for i, purl in enumerate(all_profiles, 1):\n",
+ " try:\n",
+ " pr = fetch(purl)\n",
+ " name, district, party = parse_profile(pr.text)\n",
+ " if name: # al menos nombre\n",
+ " members.append((name, district, party, purl))\n",
+ " except Exception as e:\n",
+ " print(f\"[WARN] Perfil con error ({purl}): {e}\")\n",
+ " time.sleep(0.5) # pausa cortita para ser amable\n",
+ "\n",
+ " print(\"Total miembros parseados:\", len(members))\n",
+ "\n",
+ " # 3) Mostrar algunas filas\n",
+ " for m in members[:10]:\n",
+ " print(m)\n",
+ "\n",
+ " # 4) (Opcional) DataFrame/CSV\n",
+ " try:\n",
+ " import pandas as pd\n",
+ " df = pd.DataFrame(members, columns=[\"Nombre\", \"Distrito\", \"Partido\", \"Perfil\"])\n",
+ " print(\"\\nPrimeras 5 filas:\")\n",
+ " print(df.head())\n",
+ " df.to_csv(\"senado_ilga_moderno.csv\", index=False, encoding=\"utf-8\")\n",
+ " print(\"\\nCSV generado: senado_ilga_members.csv\")\n",
+ " except ImportError:\n",
+ " print(\"Pandas no está instalado; omitiendo CSV. Instala con: pip install pandas openpyxl\")\n",
+ "\n",
+ "except Exception as e:\n",
+ " print(\"Error general:\", e)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
"metadata": {},
- "outputs": [],
"source": [
- "members[:5]"
+ "**Explicacion del codigo**\n",
+ "\n",
+ "Este script es un scraper a la pagina del Senado de Illinois (ILGA). Hace esto:\n",
+ "\n",
+ "**Configura**\n",
+ "* LIST_URLS: páginas de listado donde buscar enlaces a perfiles.\n",
+ "* HEADERS: cabeceras “de navegador” para evitar bloqueos simples.\n",
+ "\n",
+ "**Funciones clave**\n",
+ "* fetch(url): hace GET con requests.Session + cabeceras y lanza error si no es 200.\n",
+ "* find_profile_links(html): en el HTML del listado, busca todos los enlaces que coinciden con /Senate/Members/Details <'id'> y devuelve las URLs absolutas (evita dependencias del HTML antiguo).\n",
+ "* parse_profile(html): abre cada perfil individual y, con heurísticas (regex):\n",
+ " * extrae name (buscando h1, h2 o <'title'>),\n",
+ " * detecta district (varios patrones: “District 47”, “47 District”, etc.),\n",
+ " * detecta party (letra entre paréntesis (D|R|I) o palabras “Democrat/Republican/Independent”).\n",
+ "\n",
+ "**Flujo principal**\n",
+ "* Visita cada URL en LIST_URLS, junta y de-duplica todas las URLs de perfiles.\n",
+ "* Si no encuentra enlaces, guarda debug_list.html para inspeccionar el HTML real (útil si hay portal cautivo/antibot).\n",
+ "* Recorre cada perfil y arma members con tuplas (Nombre, Distrito, Partido, Perfil), poniendo una pausa de 0.5s por cortesía.\n",
+ "* (Opcional) Crea un DataFrame y exporta senado_ilga_moderno.csv.\n",
+ "\n",
+ "**Salida**\n",
+ "* Imprime cuántos **perfiles encontró** y **miembros parseados**, muestra los primeros, y guarda el CSV si pandas está instalado."
]
},
{
@@ -170,7 +463,7 @@
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 17,
"metadata": {},
"outputs": [],
"source": [
@@ -204,7 +497,7 @@
" # Extract href\n",
" href = row.select('a')[1]['href']\n",
" # Create full path\n",
- " full_path = \"http://www.ilga.gov/senate/\" + href + \"&Primary=True\"\n",
+ " full_path = \"https://www.ilga.gov/Senate/Members/List/\" + href + \"&Primary=True\"\n",
"\n",
" # Store in a tuple\n",
" senator = (name, district, party, full_path)\n",
@@ -213,16 +506,272 @@
" return(members)"
]
},
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "**Explicacion del Codigo**\n",
+ "\n",
+ "Ese código define una función get_members(url) que hace un web scraping sobre una página de la Asamblea Legislativa de Illinois. Hace lo siguiente:\n",
+ "\n",
+ "**Descarga la página web**\n",
+ "* Hace una petición HTTP a la URL.\n",
+ "* Obtiene el HTML como texto.\n",
+ "* Usa BeautifulSoup con el parser lxml para convertirlo en un árbol de etiquetas.\n",
+ "\n",
+ "**Inicializa una lista vacía**\n",
+ "* Aquí se guardarán los senadores extraídos.\n",
+ "\n",
+ "**Busca filas de tabla**\n",
+ "* Selecciona todas las filas <'tr'> anidadas tres niveles (muy específico).\n",
+ "* Filtra solo las que contienen celdas <'td class=\"detail\">, que son las filas con información útil.\n",
+ "\n",
+ "**Extrae los datos de cada fila**\n",
+ "* De cada fila toma solo las celdas con clase detail.\n",
+ "* Convierte su contenido a texto.\n",
+ "* Interpreta la primera celda como nombre, la cuarta como distrito (convertido a entero), y la quinta como partido político.\n",
+ "\n",
+ "**Obtiene el enlace al perfil**\n",
+ "* Busca los enlaces <'a> dentro de la fila.\n",
+ "* Toma el segundo enlace ([1]).\n",
+ "* Construye una URL completa concatenando con una ruta base y agregando &Primary=True.\n",
+ "\n",
+ "**Guarda la información en una tupla**\n",
+ "* Agrupa los datos en una tupla con la forma: (Nombre, Distrito, Partido, URL_perfil)\n",
+ "* Lo agrega a la lista members.\n",
+ "\n",
+ "**Devuelve el resultado**\n",
+ "* Retorna la lista completa de senadores encontrados en la página."
+ ]
+ },
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 96,
"metadata": {},
"outputs": [],
"source": [
- "# Test your code!\n",
- "url = 'http://www.ilga.gov/senate/default.asp?GA=98'\n",
- "senate_members = get_members(url)\n",
- "len(senate_members)"
+ "import re, time\n",
+ "from urllib.parse import urljoin\n",
+ "import requests\n",
+ "from bs4 import BeautifulSoup\n",
+ "\n",
+ "HEADERS = {\"User-Agent\": \"Mozilla/5.0\"}\n",
+ "\n",
+ "# --- util: extraer distrito/partido desde texto plano ---\n",
+ "def _extract_district_party(text: str):\n",
+ " # 1) \"47 R\"\n",
+ " m = re.search(r'\\b(\\d+)\\s+([DRI])\\b', text)\n",
+ " if m:\n",
+ " return int(m.group(1)), m.group(2).upper()\n",
+ " # 2) \"District 47 (R)\" u otras variantes\n",
+ " m = re.search(r'(?:District\\s*)?(\\d+).*?\\b([DRI])\\b', text, flags=re.I)\n",
+ " if m:\n",
+ " return int(m.group(1)), m.group(2).upper()\n",
+ " # 3) Si solo viene el nombre del partido completo\n",
+ " party = \"\"\n",
+ " if re.search(r'\\bDemocrat(ic)?\\b', text, flags=re.I):\n",
+ " party = \"D\"\n",
+ " elif re.search(r'\\bRepublican\\b', text, flags=re.I):\n",
+ " party = \"R\"\n",
+ " elif re.search(r'\\bIndependent\\b', text, flags=re.I):\n",
+ " party = \"I\"\n",
+ " return None, party # distrito desconocido, partido si se detectó\n",
+ "\n",
+ "# --- leer distrito/partido desde el perfil individual ---\n",
+ "def _parse_profile(profile_url: str, session: requests.Session):\n",
+ " r = session.get(profile_url, timeout=30)\n",
+ " r.raise_for_status()\n",
+ " psoup = BeautifulSoup(r.text, \"lxml\")\n",
+ "\n",
+ " # nombre (por si quieres validar)\n",
+ " name = \"\"\n",
+ " h1 = psoup.select_one(\"h1\")\n",
+ " if h1:\n",
+ " name = h1.get_text(strip=True)\n",
+ " elif psoup.title:\n",
+ " name = psoup.title.get_text(strip=True)\n",
+ "\n",
+ " text = psoup.get_text(\" \", strip=True)\n",
+ " district, party = _extract_district_party(text)\n",
+ " return name, district, party\n",
+ "\n",
+ "def get_members(url: str = \"https://www.ilga.gov/Senate/Members\"):\n",
+ " \"\"\"\n",
+ " Devuelve lista de tuplas (Nombre, Distrito:int|None, Partido:str, Perfil:str)\n",
+ " Tomando enlaces a /Senate/Members/Details/... desde /Senate/Members o /Senate/Members/List.\n",
+ " Si el distrito/partido no está cerca del enlace, se visita el perfil para extraerlos.\n",
+ " \"\"\"\n",
+ " s = requests.Session()\n",
+ " s.headers.update(HEADERS)\n",
+ "\n",
+ " r = s.get(url, timeout=30)\n",
+ " r.raise_for_status()\n",
+ " soup = BeautifulSoup(r.text, \"lxml\")\n",
+ "\n",
+ " members = []\n",
+ " seen = set()\n",
+ "\n",
+ " # Enlaces a perfiles\n",
+ " for a in soup.select('a[href*=\"/Senate/Members/Details/\"], a[href*=\"/senate/members/details/\"]'):\n",
+ " name = a.get_text(strip=True)\n",
+ " if not name:\n",
+ " continue\n",
+ "\n",
+ " profile = urljoin(url, a.get(\"href\") or \"\")\n",
+ " if profile in seen:\n",
+ " continue\n",
+ " seen.add(profile)\n",
+ "\n",
+ " # 1) Intentar extraer distrito/partido del contenedor más cercano\n",
+ " container = a.parent\n",
+ " # sube hasta 3 niveles si es necesario (algunas páginas usan divs anidados)\n",
+ " hops = 0\n",
+ " while container and hops < 3 and len(container.get_text(strip=True)) < 10:\n",
+ " container = container.parent\n",
+ " hops += 1\n",
+ "\n",
+ " tail_text = \"\"\n",
+ " if container:\n",
+ " # texto del contenedor sin el nombre, para evitar falsos positivos\n",
+ " ctext = container.get_text(\" \", strip=True)\n",
+ " tail_text = ctext.replace(name, \"\").strip()\n",
+ "\n",
+ " district, party = _extract_district_party(tail_text)\n",
+ "\n",
+ " # 2) Si no encontramos, entramos al perfil\n",
+ " if district is None and not party:\n",
+ " _, district, party = _parse_profile(profile, s)\n",
+ " time.sleep(0.2) # cortesía con el servidor\n",
+ "\n",
+ " members.append((name, district, party, profile))\n",
+ "\n",
+ " return members\n",
+ "\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "**Explicación del Codigo**\n",
+ "\n",
+ "Ese código define una función get_members que hace web scraping sobre la página moderna del Senado de Illinois y devuelve información básica de cada senador. Hace lo siguiente:\n",
+ "\n",
+ "**Definicios de Imports**\n",
+ "* re: para usar expresiones regulares.\n",
+ "* urljoin: para construir URLs absolutas a partir de relativas.\n",
+ "* requests: para hacer la descarga de la página web.\n",
+ "* BeautifulSoup: para parsear el HTML.\n",
+ "\n",
+ "**Definición de la función**\n",
+ "* Toma como parámetro opcional la URL del listado de miembros del Senado.\n",
+ "* Si no le pasas nada, usa la lista oficial de senadores.\n",
+ "\n",
+ "**Descarga y parseo**\n",
+ "* Descarga la página con un User-Agent “realista”.\n",
+ "* raise_for_status() lanza error si la respuesta no fue 200 OK.\n",
+ "* Convierte el HTML a un árbol con BeautifulSoup.\n",
+ "\n",
+ "**Inicializa lista de resultados**\n",
+ "**Encuentra enlaces a perfiles individuales**\n",
+ "* Busca todos los <'a> cuyo href contenga /Senate/Members/Details/.\n",
+ "* Extrae el texto (nombre del senador).\n",
+ "\n",
+ "**Extrae distrito y partido del texto alrededor**\n",
+ "* El contenedor donde está el link suele tener “Nombre 47 R”.\n",
+ "* Usa regex para detectar el número de distrito y la letra del partido:\n",
+ " * D = Demócrata\n",
+ " * R = Republicano\n",
+ " * I = Independiente\n",
+ "* Si no encuentra nada, pone district = None y party = \"\".\n",
+ "\n",
+ "**Construye la URL del perfil**\n",
+ "* profile = urljoin(url, a.get(\"href\"))\n",
+ "\n",
+ "**Guarda la información**\n",
+ "* members.append((name, district, party, profile))\n",
+ "\n",
+ "**Devuelve el resultado**\n",
+ "* Entrega una lista de tuplas de la forma: (Nombre, Distrito, Partido, URL_perfil)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 97,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Total miembros: 60\n",
+ "('Neil Anderson', 2006, 'R', 'https://www.ilga.gov/Senate/Members/Details/3312')\n",
+ "('Omar Aquino', 2016, 'D', 'https://www.ilga.gov/Senate/Members/Details/3316')\n",
+ "('Li Arellano, Jr.', 2025, 'D', 'https://www.ilga.gov/Senate/Members/Details/3383')\n",
+ "('Chris Balkema', 2025, 'R', 'https://www.ilga.gov/Senate/Members/Details/3413')\n",
+ "('Christopher Belt', 2019, 'R', 'https://www.ilga.gov/Senate/Members/Details/3337')\n"
+ ]
+ }
+ ],
+ "source": [
+ "senate_members = get_members() # o get_members(\"https://www.ilga.gov/Senate/Members/List\")\n",
+ "print(\"Total miembros:\", len(senate_members))\n",
+ "for m in senate_members[:5]:\n",
+ " print(m)\n",
+ "\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "\n",
+ "**Explicacion del Codigo**\n",
+ "\n",
+ "Este código ejecuta tu scraper, imprime el número total de senadores encontrados, y luego muestra en consola los primeros 5 registros (nombre, distrito, partido y link al perfil)."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 98,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ " Nombre Distrito Partido \\\n",
+ "0 Neil Anderson 2006.0 R \n",
+ "1 Omar Aquino 2016.0 D \n",
+ "2 Li Arellano, Jr. 2025.0 D \n",
+ "3 Chris Balkema 2025.0 R \n",
+ "4 Christopher Belt 2019.0 R \n",
+ "\n",
+ " Perfil \n",
+ "0 https://www.ilga.gov/Senate/Members/Details/3312 \n",
+ "1 https://www.ilga.gov/Senate/Members/Details/3316 \n",
+ "2 https://www.ilga.gov/Senate/Members/Details/3383 \n",
+ "3 https://www.ilga.gov/Senate/Members/Details/3413 \n",
+ "4 https://www.ilga.gov/Senate/Members/Details/3337 \n"
+ ]
+ }
+ ],
+ "source": [
+ "import pandas as pd\n",
+ "\n",
+ "df = pd.DataFrame(senate_members, columns=[\"Nombre\", \"Distrito\", \"Partido\", \"Perfil\"])\n",
+ "print(df.head())\n",
+ "\n",
+ "df.to_csv(\"senado_ilga_moderno.csv\", index=False, encoding=\"utf-8\")\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "**Explicacion del Codigo**\n",
+ "\n",
+ "* Este código convierte tu lista de senadores en una tabla con pandas y te muestra las primeras 5 filas."
]
},
{
@@ -248,42 +797,116 @@
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 72,
"metadata": {},
"outputs": [],
"source": [
- "def get_bills(url):\n",
- " src = requests.get(url).text\n",
- " soup = BeautifulSoup(src)\n",
- " rows = soup.select('tr tr tr')\n",
- " bills = []\n",
- " # Iterate over rows\n",
- " for row in rows:\n",
- " # Grab all bill list cells\n",
- " cells = row.select('td.billlist')\n",
- " # Keep in mind the name of the senator is not a billlist class!\n",
- " if len(cells) == 5:\n",
- " row_text = [cell.text for cell in cells]\n",
- " # Extract info from row text\n",
- " bill_id = row_text[0]\n",
- " description = row_text[1]\n",
- " chamber = row_text[2]\n",
- " last_action = row_text[3]\n",
- " last_action_date = row_text[4]\n",
- " # Consolidate bill info\n",
- " bill = (bill_id, description, chamber, last_action, last_action_date)\n",
- " bills.append(bill)\n",
- " return bills"
+ "import re, time\n",
+ "from urllib.parse import urljoin\n",
+ "import requests\n",
+ "from bs4 import BeautifulSoup\n",
+ "\n",
+ "HEADERS = {\"User-Agent\": \"Mozilla/5.0\"}\n",
+ "\n",
+ "def get_members():\n",
+ " url = \"https://www.ilga.gov/Senate/Members/List\"\n",
+ " s = requests.Session(); s.headers.update(HEADERS)\n",
+ " r = s.get(url, timeout=30); r.raise_for_status()\n",
+ " soup = BeautifulSoup(r.text, \"lxml\")\n",
+ " \n",
+ " \n",
+ "\n",
+ " # Encuentra todos los enlaces a perfiles\n",
+ " profile_links = []\n",
+ " for a in soup.select('a[href]'):\n",
+ " href = a.get(\"href\") or \"\"\n",
+ " if re.search(r\"/Senate/Members/Details/\\d+\", href, flags=re.I):\n",
+ " profile_links.append(urljoin(url, href))\n",
+ " profile_links = sorted(set(profile_links))\n",
+ "\n",
+ " members = []\n",
+ " # Si hay enlaces, visita cada perfil y extrae datos básicos\n",
+ " for purl in profile_links:\n",
+ " try:\n",
+ " pr = s.get(purl, timeout=30); pr.raise_for_status()\n",
+ " psoup = BeautifulSoup(pr.text, \"lxml\")\n",
+ " # Nombre\n",
+ " name = \"\"\n",
+ " if (h1 := psoup.select_one(\"h1\")): name = h1.get_text(strip=True)\n",
+ " elif (h2 := psoup.select_one(\"h2\")): name = h2.get_text(strip=True)\n",
+ " elif psoup.title: name = psoup.title.get_text(strip=True)\n",
+ "\n",
+ " text = psoup.get_text(\" \", strip=True)\n",
+ " # Distrito\n",
+ " m = re.search(r\"District\\s*(\\d+)\", text, re.I) or re.search(r\"Senate\\s+District\\s*(\\d+)\", text, re.I)\n",
+ " district = int(m.group(1)) if m else None\n",
+ " # Partido\n",
+ " party = \"\"\n",
+ " m = re.search(r\"\\((D|R|I)\\)\", text)\n",
+ " if m: party = m.group(1)\n",
+ " elif re.search(r\"\\bDemocrat(ic)?\\b\", text, re.I): party = \"D\"\n",
+ " elif re.search(r\"\\bRepublican\\b\", text, re.I): party = \"R\"\n",
+ " elif re.search(r\"\\bIndependent\\b\", text, re.I): party = \"I\"\n",
+ "\n",
+ " if name:\n",
+ " members.append((name, district, party, purl))\n",
+ " except Exception:\n",
+ " continue\n",
+ " time.sleep(0.2) # ser amable con el servidor\n",
+ " return members"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "**Explicacion del Codigo**\n",
+ "\n",
+ "* La función get_members() descarga el listado de senadores de Illinois, detecta los enlaces a cada perfil, entra en cada uno, y devuelve una lista con nombre, distrito, partido y URL de perfil de todos los miembros."
]
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 56,
"metadata": {},
- "outputs": [],
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Perfil seleccionado: https://www.ilga.gov/Senate/Members/Details/3264\n",
+ "[]\n"
+ ]
+ }
+ ],
"source": [
+ "senate_members = get_members()\n",
+ "if not senate_members:\n",
+ " raise RuntimeError(\"No se obtuvieron senadores. Revisa debug_list.html y los selectores.\")\n",
+ "\n",
+ "# Ahora es seguro acceder al primero\n",
"test_url = senate_members[0][3]\n",
- "get_bills(test_url)[0:5]"
+ "print(\"Perfil seleccionado:\", test_url)\n",
+ "\n",
+ "# Solo llama get_bills si tienes una URL válida\n",
+ "bills = get_bills(test_url)\n",
+ "print(bills[:5])\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "**Explicacion del Codigo**\n",
+ "\n",
+ "Ese fragmento de código es un bloque de prueba que usa tu función get_members() (la que scrapea la web del Senado) y después llama a otra función get_bills() (seguramente hecha por ti para extraer proyectos de ley de un senador).\n",
+ "\n",
+ "Hace lo siguiente:\n",
+ "* Usa get_members() para obtener senadores.\n",
+ "* Se asegura de que haya resultados.\n",
+ "* Selecciona el perfil del primer senador.\n",
+ "* Llama a get_bills() sobre ese perfil.\n",
+ "* Imprime los primeros 5 proyectos de ley de ese senador."
]
},
{
@@ -299,11 +922,11 @@
},
{
"cell_type": "code",
- "execution_count": 134,
+ "execution_count": 101,
"metadata": {},
"outputs": [],
"source": [
- "bills_dict = {}\n",
+ "bills_dict = {} \n",
"for member in senate_members[:5]:\n",
" bills_dict[member[1]] = get_bills(member[3])\n",
" time.sleep(1)"
@@ -311,18 +934,64 @@
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 104,
"metadata": {},
- "outputs": [],
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Clave encontrada: 2006\n",
+ "Clave encontrada: 2016\n",
+ "Clave encontrada: 2025\n",
+ "Clave encontrada: 2019\n",
+ "Número de proyectos para distrito 2006: 0\n"
+ ]
+ }
+ ],
+ "source": [
+ "# Verificar claves\n",
+ "for key in bills_dict.keys():\n",
+ " print(\"Clave encontrada:\", key)\n",
+ "\n",
+ "# Acceder de forma segura\n",
+ "if 2006 in bills_dict:\n",
+ " print(\"Número de proyectos para distrito 2006:\", len(bills_dict[2006]))\n",
+ "else:\n",
+ " print(\"No hay datos para distrito 2006.\")\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 107,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Claves disponibles en bills_dict: [2006, 2016, 2025, 2019]\n",
+ "Total claves: 4\n",
+ "No hay datos para el distrito 2019.\n"
+ ]
+ }
+ ],
"source": [
- "len(bills_dict[52])"
+ "print(\"Claves disponibles en bills_dict:\", list(bills_dict.keys()))\n",
+ "print(\"Total claves:\", len(bills_dict))\n",
+ "\n",
+ "bills_2019 = bills_dict.get(2019) or bills_dict.get(\"2019\")\n",
+ "if bills_2019 is None:\n",
+ " print(\"No hay datos para el distrito 2019.\")\n",
+ "else:\n",
+ " print(\"Número de proyectos:\", len(bills_2019))\n"
]
}
],
"metadata": {
"anaconda-cloud": {},
"kernelspec": {
- "display_name": "Python 3 (ipykernel)",
+ "display_name": ".venv312",
"language": "python",
"name": "python3"
},
@@ -336,7 +1005,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.8.13"
+ "version": "3.12.10"
}
},
"nbformat": 4,
diff --git a/solutions/debug_ilga.html b/solutions/debug_ilga.html
new file mode 100644
index 0000000..9e629da
--- /dev/null
+++ b/solutions/debug_ilga.html
@@ -0,0 +1,1215 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Illinois General Assembly - Members
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Select Language
+
+
+
+
The Illinois General Assembly offers the Google Translate™ service for visitor convenience. In no way should it be considered accurate as to the translation of any content herein.
+
Visitors of the Illinois General Assembly website are encouraged to use other translation services available on the internet.
+
The English language version is always the official and authoritative version of this website.
+
NOTE: To return to the original English language version, select the "Show Original" button on the Google Translate™ menu bar at the top of the window.