Як я парсил всю базу даних ігор Metacritic-а

Metacritic — англомовний сайт-агрегатор, який збирає відгуки про музичних альбомах, іграх, фільмах, телевізійних шоу і DVD-дисках. (з вікіпедії).

Використані бібліотеки: lxml, asyncio, aiohttp (lxml — бібліотека розбору HTML-сторінок з допомогою Python, asyncio і aiohttp будемо використовувати для асинхронності та швидкого вилучення даних). Також будемо активно використовувати XPath. Хто не знає, що це таке, відмінний туторіал.

Отримання посилань на сторінки всіх ігор
Спершу доведеться трохи попрацювати ручками. Йдемо на www.metacritic.com/browse/games/genre/metascore/action/all?view=detailed і збираємо всі
URL-и з цього списку:

image

І зберігаємо їх .json файл з іменем genres.json. Ми будемо застосовувати ці посилання, щоб парсити всі ігри з сайту за жанрами.

Трохи подумавши, я вирішив зібрати всі посилання на ігри .csv файли, розділені на жанри. Кожен файл буде мати ім'я відповідного жанру. Заходимо з вищезазначеної посиланню і відразу бачимо сторінку жанру Action. Помічаємо, що присутня пагинация.

Дивимося в html-сторінки:

image

І бачимо, що шуканий елемент а, який містить максимальну кількість сторінок — спадкоємець елемента li з унікальним атрибутів class = page last_page, також помічаємо, що url-и всіх сторінок, крім першого, мають вигляд <url 1-ої сторінки> + <&page=page_number>, і що друга сторінка параметром запиту має номер 1.

Збираємо XPath для отримання номера максимальної сторінки:

//li[@class='page last_page']/a/text()

Тепер потрібно отримати кожну посилання на кожну гру з цього листа.

image

Заглядаємо в розмітку листа і вивчаємо html.

image

По-перше нам в якості кореневого елемента для пошуку потрібно отримати сам список (ol). У нього є унікальний для html-коду сторінки аттрибут class = list_products list_product_summaries. Потім бачимо, що li має дочірній елемент h3 з дочірнім елементом a, в аттрибуте href якого і знаходиться шукана посилання на гру.

Збираємо всі разом:

//ol[@class='list_products list_product_summaries']//h3[@class='product_title']/a/@href

Відмінно! Півсправи зроблено, тепер потрібно програмно організувати цикл по сторінках, збір посилань і збереження їх у файли. Для прискорення распараллелим операцію на всі ядра нашого PC.

# get_games.py
імпортувати csv
import requests
import json
from multiprocessing import Pool
from time import sleep
from lxml import html

# Базовий домен.
root = 'http://www.metacritic.com/'
# При великій кількості запитів Metacritic видає помилку 429 з описом 'Slow down'
# будемо уникати її, зупиняючи потік на певну кількість часу.
SLOW_DOWN = False

def get_html(url):
# Metacritic забороняє запити без заголовка User-Agent.
headers = {"User-Agent": "Mozilla/5.001 (windows; U; NT4.0; en-US; rv:1.0) Gecko/25250101"}
global SLOW_DOWN
try:
# Якщо у нас в якомусь потоці з'явилася помилка 429, то припиняємо всі потоки
# на 15 секунд.
if SLOW_DOWN:
sleep(15)
SLOW_DOWN = False
# Отримуємо html контент сторінки стандартним модулем requests
html = requests.get(url, headers=headers).content.decode('utf-8')
# Якщо помилка в html контент, то присваеваем прапору SLOW_DOWN true.
if '429 Slow down' in html:
SLOW_DOWN = True
print(' - - - SLOW DOWN')
raise TimeoutError
return html
except TimeoutError:
return get_html(url)


def get_pages(genre):
# Складемо всі жанри в папку Games
with open('Games/' + genre.split('/')[-2] + '.csv', 'w') as file:
writer = csv.writer(file, delimiter=',',
quotechar='"', quoting=csv.QUOTE_MINIMAL)
# Структура url сторінок > 1
genre_page_sceleton = genre + '&page=%s'
def scrape():
вміст_сторінки = get_html(genre)
# Отримуємо кореневої lxml елемент з html сторінки.
document = html.fromstring(вміст_сторінки)

try:
# Витягаємо номер останньої сторінки і кастим в int.
lpn_text = document.xpath("//li[@class='page last_page']/a/text()"
last_page_number = int(lpn_text)[0])
pages = [genre_page_sceleton % str(i) for i in range(1, last_page_number)]
# Не забуваємо про першу сторінку.
pages += [genre]
# Для кожної сторінки збираємо і зберігаємо посилання на файл жанру.
for page in pages:
document = html.fromstring(get_html(page))
urls_xpath = "//ol[@class='list_products list_product_summaries']//h3[@class='product_title']/a/@href"
# Посилання кожної гри додаємо url кореня сайту.
games = [root + url for url in document.xpath(urls_xpath)]
print('Page:' + page + " - - - Games: " + str(len(games)))
for game in games:
writer.writerow([game])
except:
# Швидше за все знову 429 помилка. Парсим сторінку заново.
scrape()
scrape()

def main():
# Завантажуємо посилання на сторінки жанрів з нашого .json файлу.
dict = json.load(open('genres.json', 'r'))
p = Pool(4)
# Простий пул. Функцією map віддаємо кожному потоку його порцію жанрів для парсингу.
p.map(get_pages, [dict[key] for key in dict.keys()])
print('Over')

if __name__ == "__main__":
main()

Зливаємо всі файли з посиланнями в один файл. Якщо ви під лінуксом, то просто використовуємо cat і перенаправляємо STDOUT в новий файл. Під віндою напишемо невеликий скрипт і запустимо його в папці з файлами жанрів.

from import os listdir
from os.path import isfile, join
onlyfiles = [f for f in listdir('.') if isfile(join(mypath, f))]
fout=open("all_games.csv","a")
for path in onlyfiles:
f = open(path)
f.next()
for line in f:
fout.write(line)
f.close()
fout.close()

Тепер у нас є один великий .csv файл з посиланнями на всі ігри на Metacritic. Досить великий, 25 тисяч записів. Плюс є дупликаты, так як одна гра може мати декілька жанрів.

Витяг інформації про всіх іграх
Який наш план далі? Пройтися по кожному посиланні і отримати інформацію про кожній грі.
Заходимо, наприклад, на сторінку Portal 2.

Будемо витягати:

  • Назва гри
  • Платформи
  • Опис
  • Metascore
  • Жанр
  • Дата випуску
Щоб скоротити посаду, відразу наведу xpath-и, якими витягував дану інформацію.

Назва гри:

//h1[@class='product_title']//span[@itemprop='name']//text()

Платформ у нас може бути кілька, тому знадобляться два запиту:

//span[@itemprop='device']//text()
//li[@class='summary_detail product_platforms']//a//text()

Опис у нас знаходиться в Summary:

//span[@itemprop='description']//text()

Metascore:

//span[@itemprop='ratingValue']//text()

Жанр:

//span[@itemprop='description']//text()

Дата випуску:

//span[@itemprop='datePublished']//text()

Згадуємо, що у нас 25 тисяч сторінок і хапаємося за голову. Що робити? Навіть з декількома потоками буде долговато. Вихід є — асинхронність і неблокирующие корутины. Ось відмінне відео від PyCon. Async-await спрощують асинхронне програмування на python 3.5.2. Туторіал на Хабре.

Пишемо код для нашого парсера.

from time import sleep
import asyncio
from aiohttp import ClientSession
from lxml import html

# Зчитуємо посилання на всі ігри з нашого файлу, прибираємо дупликаты.
games_urls = list(set([line for line in open('Games/all_games.csv', 'r')]))
# Сюди будемо складати результат.
result = []
# Скільки всього сторінок наша програма запарсила на даний момент.
total_checked = 0

async def get_one(url, session):
global total_checked
async with session.get(url) as response:
# Очікуємо відповіді і блокуємо таск.
вміст_сторінки = await response.read()
# Отримуємо інформацію про гру і зберігаємо в лист.
item = get_item(вміст_сторінки, url)
result.append(item)
total_checked += 1
print('Inserted:' + url + '- - - Total checked: '+ str(total_checked))

async def bound_fetch(sm, url, session):
try:
async with sm:
await get_one(url, session)
except as Exception e:
print(e)
# Блокуємо всі таски на 30 секунд в разі помилки 429.
sleep(30)

async def run(urls):
tasks = []
# Вибрав лок від балди. Можете погратися.
sm = asyncio.Semaphore(50)
headers = {"User-Agent": "Mozilla/5.001 (windows; U; NT4.0; en-US; rv:1.0) Gecko/25250101"}
# Знову ж залишаємо User-Agent, щоб не отримати від помилку Metacritic
async with ClientSession(
headers=headers) as session:
for url in urls:
# Збираємо таски і додаємо в лист для подальшого очікування.
task = asyncio.ensure_future(bound_fetch(sm, url, session))
tasks.append(task)
# Очікуємо завершення всіх наших завдань.
await asyncio.gather(*tasks)


def get_item(вміст_сторінки, url):
# Отримуємо кореневої lxml елемент з html сторінки.
document = html.fromstring(вміст_сторінки)

def get(xpath):
item = document.xpath(xpath)
if item:
return item[-1]
# Якщо раптом якась частина інформації на сторінці не знайдена, то повертаємо None
return None

name = get("//h1[@class='product_title']//span[@itemprop='name']//text()")
if name:
name = name.replace('\n', ").strip()
genre = get("//span[@itemprop='genre']//text()")
date = get("//span[@itemprop='datePublished']//text()")
main_platform = get("//span[@itemprop='device']//text()")
if main_platform:
main_platform = main_platform.replace('\n', ").strip()
else:
main_platform = "
other_platforms = document.xpath("//li[@class='summary_detail product_platforms']//a//text()")
other_platforms = '/'.join(other_platforms)
platforms = main_platform + '/' + other_platforms
score = get("//span[@itemprop='ratingValue']//text()")
desc = get("//span[@itemprop='description']//text()")
#Повертаємо словник з інформацією про гру.
return {'url': url,
'name': name,
'genre': genre,
'date': date,
'platforms': platforms,
'score': score,
'desc': desc}


def main():
#Запускаємо наш парсер.
loop = asyncio.get_event_loop()
future = asyncio.ensure_future(run(games_urls))
loop.run_until_complete(future)
# Виводимо результат. Можете зберегти його куди-небудь файл.
print(result)
print('Over')


if __name__ == "__main__":
main()

Получаеся дуже навіть непогано, на моєму комп'ютері Intel i5 6600K, 16 GB RAM, lan 10 mb/s виходило десь 10 ігор/с. Можна підправити код і адаптувати скрипт під музику\фільми.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.