Как получить исторические оценки Tomatometer с Rotten Tomatoes
Используйте архивные 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 должен выглядеть примерно так:
Очевидно, этот 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, который выглядит примерно так:
Метод #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.
И, наконец, посмотрите мое видео, если вам интересно узнать, что я обнаружил при анализе этих данных!