Getting death row inmates' last statement Initial version of the code appeared as an answer to an SO question. I refactored the code a bit and it works pretty well, IMHO. I get a solid .csv file with all the data from Texas Department of Criminal Justice's Death Row page.
What I was especially interested in was getting all offenders' last statement, if there was any, which the codes accomplishes.
What I'd want to get here is some feedback on utilizing pandas, as I'm relatively new to it. Also, some memory efficiency suggestions would be nice too, I guess.
For example, should I save the initial version of the .csv file and then read it so I can append the last statements? Or keeping everything in memory is fine?
If you find any other holes, do point them out!
The code:
import random
import time

import pandas as pd
import requests
from lxml import html

base_url = &quot;https://www.tdcj.texas.gov/death_row&quot;
statement_xpath = '//*[@id=&quot;content_right&quot;]/p[6]/text()'


def get_page() -&gt; str:
    return requests.get(f&quot;{base_url}/dr_executed_offenders.html&quot;).text


def clean(first_and_last_name: list) -&gt; str:
    name = &quot;&quot;.join(first_and_last_name).replace(&quot; &quot;, &quot;&quot;).lower()
    return name.replace(&quot;, Jr.&quot;, &quot;&quot;).replace(&quot;, Sr.&quot;, &quot;&quot;).replace(&quot;'&quot;, &quot;&quot;)


def get_offender_data(page: str) -&gt; pd.DataFrame:
    df = pd.read_html(page, flavor=&quot;bs4&quot;)
    df = pd.concat(df)
    df.rename(
        columns={'Link': &quot;Offender Information&quot;, &quot;Link.1&quot;: &quot;Last Statement URL&quot;},
        inplace=True,
    )

    df[&quot;Offender Information&quot;] = df[
        [&quot;Last Name&quot;, 'First Name']
    ].apply(lambda x: f&quot;{base_url}/dr_info/{clean(x)}.html&quot;, axis=1)

    df[&quot;Last Statement URL&quot;] = df[
        [&quot;Last Name&quot;, 'First Name']
    ].apply(lambda x: f&quot;{base_url}/dr_info/{clean(x)}last.html&quot;, axis=1)
    return df


def get_last_statement(statement_url: str) -&gt; str:
    page = requests.get(statement_url).text
    statement = html.fromstring(page).xpath(statement_xpath)
    text = next(iter(statement), &quot;&quot;)
    return &quot; &quot;.join(text.split())


def get_last_statements(offenders_data: list) -&gt; list:
    statements = []
    for item in offenders_data:
        *names, url = item
        print(f&quot;Fetching statement for {' '.join(names)}...&quot;)
        statements.append(get_last_statement(statement_url=url))
        time.sleep(random.randint(1, 4))
    return statements


if __name__ == &quot;__main__&quot;:
    offenders_df = get_offender_data(get_page())
    names_and_urls = list(
        zip(
            offenders_df[&quot;First Name&quot;],
            offenders_df[&quot;Last Name&quot;],
            offenders_df[&quot;Last Statement URL&quot;],
        )
    )
    offenders_df[&quot;Last Statement&quot;] = get_last_statements(names_and_urls)
    offenders_df.to_csv(&quot;offenders_data.csv&quot;, index=False)

The scraping part is intentionally slow, as I don't want to abuse the server, but I do want to get the job done. So, if you don't have a couple of minutes to spare, you can fetch the offenders_data.csv file from here.
Types
Here:
def clean(first_and_last_name: list) -&gt; str:

list should more specifically be List[str].
Fluent syntax
Consider using fluent syntax for your clean, which - in this case - is basically just a reformat:
    return (
        &quot;&quot;.join(first_and_last_name)
        .replace(&quot; &quot;, &quot;&quot;)
        .replace(&quot;, Jr.&quot;, &quot;&quot;)
        .replace(&quot;, Sr.&quot;, &quot;&quot;)
        .replace(&quot;'&quot;, &quot;&quot;)
        .lower()
    )