CoderCastrov logo
CoderCastrov
Фильмы

Как получить исторические оценки Tomatometer с Rotten Tomatoes

Как получить исторические оценки Tomatometer с Rotten Tomatoes
просмотров
7 мин чтение
#Фильмы

Используйте архивные URL-адреса Wayback Machine для анализа временных рядов обзоров фильмов

Я, как и многие другие, был заинтригован тем, как оценка фильма "Чудо-женщина 1984" на Rotten Tomatoes упала с "сертифицированно свежий" до "гнилой" за несколько недель. Я хотел получить исторические оценки Tomatometer с Rotten Tomatoes, чтобы увидеть, можно ли увидеть подобное падение рейтинга для других фильмов.

Существуют два различных метода для получения этих исторических обзоров, и у каждого из них есть свои плюсы и минусы. Первый метод заключается в том, чтобы получить все обзоры для фильма, а затем использовать дату каждого обзора для расчета оценки Rotten Tomatoes на этот день. Второй метод заключается в использовании Wayback Machine для получения исторических оценок из архивных версий страницы.

Первый метод, конечно, быстрее, но второй более точный (хотя может привести к отсутствию данных в течение нескольких дней). Я выбрал второй метод для своего анализа, но в этом руководстве расскажу о обоих методах.

Я получил эти данные для более общего анализа обзоров Rotten Tomatoes. Видео ниже содержит краткое изложение моих результатов!

Полный код для этого проекта можно увидеть здесь.

Метод #1: Парсинг отзывов с Rotten Tomatoes

Код для первого метода можно увидеть в моем Jupyter Notebook парсера отзывов.

Для повторения, этот первый метод парсит все отзывы критиков для заданного фильма на Rotten Tomatoes и использует эти отзывы для расчета оценки Tomatometer для каждого дня. Этот метод может быть неточным по нескольким причинам:

Тем не менее, я считаю, что этот код полезен для парсинга текста отзывов, особенно если вам интересно провести анализ настроений на основе отзывов о фильмах.

Сначала я определил регулярные выражения для каждого из элементов, которые мне было интересно спарсить (например, номера страниц, отзывы, рейтинги и т.д.), а затем создал функцию make_soup, которая создает объект beautifulsoup из URL-адреса.

# Регулярные выражения
page_pat = re.compile(r'Страница 1 из \d+')
review_pat = re.compile(r'<div class=\"the_review\" data-qa=\"review-text\">[;a-zA-Z\s,-.**\'**\/\?\[\]\":**\'**]*</div>')
rating_pat = re.compile(r'Оригинальный рейтинг:\s([A-Z](\+|-)?|\d(.\d)?(\/\d)?)')
fresh_pat = re.compile(r'small\s(fresh|rotten)\"')
critic_pat = re.compile(r'\/\"\>([A-Z][a-zA-Z]+\s[A-Z][a-zA-Z\-]+)|([A-Z][a-zA-Z.]+\s[A-Z].?\s[A-Z][a-zA-Z]+)|([A-Z][a-zA-Z]+\s[A-Z]+**\'**[A-Z][a-zA-Z]+)')
publisher_pat = re.compile(r'\"subtle\">[a-zA-Z\s,.\(\)**\'**\-&;!\/\d+]+</em>')
date_pat = re.compile(r'[a-zA-Z]+\s\d+,\s\d+')

from bs4 import BeautifulSoup
from requests import TooManyRedirects
import re

def make_soup(url):
    try:
        r = requests.get(url)
        soup = BeautifulSoup(r.content, 'html.parser')
    except TooManyRedirects:
        soup = ''
    return soup

Используя эти регулярные выражения, я затем определил две вспомогательные функции, которые принимают объект beautifulsoup: одну для получения количества страниц отзывов, а другую для получения отзывов с каждой страницы.

def get_num_pages(soup):
    match = re.findall(page_pat,str(list(soup)))
    if len(match) > 0:
        match = match[0]
        match = match.split(' of ')[-1]
        return match
    else:
        return None

def get_critic_reviews_from_page(soup):
    reviews = list()
    rating = list()
    fresh = list()
    critic = list()
    top_critic = list()
    publisher = list()
    date = list()
    
    soup = str(soup)
    review_soup = soup.split('="review_table')[1].split('row review_table_row')
    review_soup.pop(0)

    for review in review_soup:
        match = re.findall(review_pat, str(review))
        if len(match) > 0:
            m = match[0]            
            for iden in ['<div class="the_review" data-qa="review-text"> ','</div>']:
                m = m.replace(iden,'')
            reviews.append(m.strip('"'))            
            # Извлечение рейтинга
            match = re.findall(rating_pat, str(review))
            if len(match) > 0:
                m = match[0][0]
                if '/1' in m:
                    sp_m = m.split('/')
                    if sp_m[-1] == '1':
                        sp_m[-1] = '10'
                    m = '/'.join(sp_m)
                rating.append(m)
            else:
                rating.append(None)            
            # Извлечение индикатора fresh
            match = re.findall(fresh_pat, str(review))
            if len(match) > 0:
                fresh.append(match[0])
            else:
                fresh.append(None)            
            # Извлечение критика
            match = re.findall(critic_pat, str(review))
            if len(match) > 0:
                critic.append(''.join(match[0]))
            else:
                critic.append(None)            
            # Проверка, является ли критик топовым
            if '> Top Critic<' in str(review):
                top_critic.append(1)
            else:
                top_critic.append(0)            
            # Извлечение издателя
            match = re.findall(publisher_pat, str(review))
            if len(match) > 0:
                m = match[0]
                m = m.replace('"subtle">', '')
                m = m.replace('</em>','')
                publisher.append(m)
            else:
                publisher.append(None)            
            # Извлечение даты
            match = re.findall(date_pat, str(review))
            if len(match) > 0:
                date.append(match[0].strip('"'))
            else:
                date.append(None)
            
    return [reviews, rating, fresh, critic, top_critic, publisher, date]

Как только у меня были эти функции определены, я мог приступить к парсингу. Я определил функцию get_critic_reviews, которая вызывает эти вспомогательные функции и принимает URL-адрес Rotten Tomatoes (например, https://www.rottentomatoes.com/m/wonder_woman_1984/)

def get_critic_reviews(page):
    info = [[],[],[],[],[],[],[]]
    soup = make_soup(page + "reviews")
    pages = get_num_pages(soup)
    if pages is not None:
        for page_num in range(1,int(pages)+1):
            soup = make_soup(page + "reviews?page=" + str(page_num) + "&sort=")
            c_info = get_critic_reviews_from_page(soup)
            
            # Накопление информации об отзывах
            for i in range(len(c_info)):
                info[i] = info[i] + c_info[i]
        
        c_info = dict()
        keys = ['reviews', 'rating', 'fresh', 'critic', 'top_critic', 'publisher', 'date']
        for k in range(len(keys)):
            c_info[keys[k]] = info[k]        
    else:
        c_info = None        
    return c_info

Эта функция должна вернуть словарь, который затем можно преобразовать в dataframe с помощью pd.DataFrame.from_dict(). Ваш dataframe должен выглядеть примерно так:

Image by author.

Очевидно, этот dataframe нужно немного очистить (особенно столбец с отзывами). Меня не интересовал анализ текста, но если бы мне было интересно, я бы просто добавил ".text" к элементу div в парсере.

Меня интересует как-то преобразовать столбец "fresh" в целые числа 0, чтобы я мог рассчитать накопительную оценку Rotten Tomatoes для каждого дня. И это довольно просто сделать в pandas.

all_films['score'] = all_films['fresh'].apply(lambda x: 1 if x == 'fresh' else 0)

Теперь мы можем использовать этот столбец "score", чтобы наконец-то рассчитать оценку Rotten Tomatoes! Сначала я сгруппировал отзывы по дате и использовал две агрегирующие функции для столбца "score" (сумма и подсчет). Таким образом, мы можем взять накопительные суммы оценок и разделить их на накопительные суммы количества оценок.

По сути, мы рассчитываем, какой процент от общего числа критиков дал положительную оценку фильму в каждый день, что и является способом расчета оценки Tomatometer.

df = all_films[all_films['Film'] == "Wonder Woman 1984"]
grouped_1 = df[['date', 'score']].groupby('date').agg([sum, 'count'])
grouped_1.columns = grouped_1.columns.droplevel(0)
grouped_1.cumsum()['sum']/grouped_1.cumsum()['count']

И вуаля! Вы должны получить объект series, который выглядит примерно так:

Image by author.

Метод #2: Парсинг оценок Rotten Tomatoes с помощью Wayback Machine

Код для второго метода можно увидеть в моем Jupyter Notebook с парсером оценок.

Второй метод может показаться более очевидным, но он намного сложнее. Веб-сайт Rotten Tomatoes сильно изменился за последние 3 года, и из-за этого мне пришлось написать несколько парсеров, чтобы учесть различные версии сайта.

Изображение автора.

Я проверил, что эти парсеры работают для архивированных веб-сайтов с 2018 года и позже. Если вы выбираете этот метод, убедитесь, что вы используете правильный парсер для правильного года!

Эти парсеры используют пакет waybackpy для Python, который позволяет вам программно получать доступ к архивным версиям URL. Я написал функции для парсинга количества отзывов критиков и зрителей, а также оценок критиков и зрителей с каждого URL.

Хорошая новость заключается в том, что код для этого метода гораздо короче! Вот функция, которая парсит количество отзывов для критиков и зрителей:

_#получить количество отзывов критиков и зрителей_
**def** getNumReviews(soup):        
    critic = soup.find_all("small", class_="mop-ratings-wrap__text--small")
    audience = soup.find_all("strong", class_="mop-ratings-wrap__text--small")
    **if** critic:
        critic = critic[0].text.replace("**\n**", '').strip()
    **else**:
        critic = soup.find_all("a", class_='scoreboard__link scoreboard__link--tomatometer')
        critic = critic[0].text
    **if** len(audience) > 1:
        audience = audience[1].text.replace("Verified Ratings: ", '')
    **else**:
        audience = soup.find_all("a", class_='scoreboard__link scoreboard__link--audience')
        **if** audience:
            audience = audience[0].text
        **else**:    
            audience = "Coming"
    **return** [critic, audience]

И вот функция(и) для парсинга оценок зрителей и критиков:

_#парсер 2018 года_
**def** get_score(soup):
    critic = soup.find('span', {'class': "meter-value superPageFontColor"})
    audience = soup.find('div', {'class': "audience-score meter"}).find('span', {'class': "superPageFontColor"})
    **return** [critic.text, audience.text]_#парсер 2019-2021 годов_**def** getScore(soup):
    temp = soup.find_all('div', class_='mop-ratings-wrap__half')
    **try**:
        critic = temp[0].text.strip().replace('**\n**', '').split(' ')[0] _#парсер 2020 года        _
        **if** len(temp) > 1:
            audience = temp[1].text.strip().replace('**\n**', '').split(' ')[0]
        **else**:
            audience = "Coming"
    **except**:
        scores = soup.find("score-board") _#парсер 2021 года_
        **return** [scores["tomatometerscore"], scores["audiencescore"]]
    **return** [critic, audience]

Немного раздражает то, что формат оценок немного отличается в разных версиях веб-сайта Rotten Tomatoes. Иногда строке может быть прикреплен символ «%», иногда она будет содержать слова «score» или другие связанные слова. Обратите внимание на эти крайние случаи при очистке данных!

В любом случае, после определения вспомогательных функций парсинга, мы можем получить данные! Я написал функцию build, которая принимает URL, начальную и конечную даты для парсинга данных.

**def** build(key, date1, date2):
    dates = pd.date_range(date1, date2).tolist()
    wayback = waybackpy.Url(movie_urls[key], user_agent)
    scores = []
    **for** date **in** dates:
        **try**:
            archived = wayback.near(year=date.year, month=date.month, day=date.day).archive_url
        **except**:
            print(date)
            **continue**
        page = requests.get(archived).text
        soup = BeautifulSoup(page, 'lxml')
        scores.append(getScore(soup) + getNumReviews(soup) + [date, key])
    **return** scores

Результатом этой функции будет массив массивов, как на изображении ниже.

Изображение автора.

Опять же, мы легко можем преобразовать этот вывод в DataFrame для нашего анализа!

pd.DataFrame(df, columns=["critic", "audience", "criticNum", "audienceNum", "date", "film"]))

Недостаток использования этого метода заключается в том, что если Wayback Machine не архивировал URL для определенного дня, вы не сможете получить данные за этот день. Я использовал этот метод для более чем 20 фильмов, и обычно это не было проблемой. Может быть 1–5 дней с пропущенными данными в зависимости от вашего диапазона дат, но если вы хотите получить оценки для менее известного фильма, который, вероятно, не будет архивирован, вам придется использовать метод #1.

И, наконец, посмотрите мое видео, если вам интересно узнать, что я обнаружил при анализе этих данных!