diff --git a/README.md b/README.md index c087a1d..23d8ddc 100644 --- a/README.md +++ b/README.md @@ -39,12 +39,20 @@ - *Выходные данные:* таблица со столбцами (название архива, название файла, размер в Мб) - *Решение:* смотри файл archive_table.py -## Задачи - ### Сопоставление игр с жанрами Необходимо для каждой игры из базового набора найти жанры на ее странице на metacritic. Создать таблицу жанров и соединить отношением многие-ко-многим с таблицей базового набора игр. +- *Входные данные:* html-страницы, по одной на игру из базового набора +- *Выходные данные:* таблица жанров, таблица связей со столбцами (игра, жанр) +- *Решение:* смотри файл scrape_genres.py + +## Задачи + +### Проверить базовый набор + +Необходимо просмотреть жанры, маловстречающиеся и повторяющиеся объединить, построить примерное дерево категорий. + ### Сопоставление базового с полным набором игр Необходимо реализовать быстрый неточный поиск из базового набора в полном. Предложение: построить однословные индексы по базовому и полному набору, при сопоставлении использовать расстояние Левенштейна около 3. diff --git a/archive_table.py b/archive_table.py index a562dea..b6efbc9 100644 --- a/archive_table.py +++ b/archive_table.py @@ -3,13 +3,13 @@ import zipfile import csv res = [] -zips = os.listdir('Nintendo DS') +zips = os.listdir("Nintendo DS") for z in zips: - with zipfile.ZipFile('Nintendo DS/'+z) as myzip: + with zipfile.ZipFile("Nintendo DS/" + z) as myzip: info = myzip.infolist() for i in info: - res.append([z, i.filename, int(i.file_size/(1024**2))]) + res.append([z, i.filename, int(i.file_size / (1024 ** 2))]) -with open('archive.csv', 'w', newline='') as f: +with open("archive.csv", "w", newline="") as f: writer = csv.writer(f) writer.writerows(res) diff --git a/db.sqlite3 b/db.sqlite3 index 6d1a46f..8cb4df7 100644 Binary files a/db.sqlite3 and b/db.sqlite3 differ diff --git a/make_db.py b/make_db.py index 46f063d..792d4ca 100644 --- a/make_db.py +++ b/make_db.py @@ -28,19 +28,25 @@ def process_pages(db): ) types = { - "by_metascore": (""" + "by_metascore": ( + """ INSERT INTO top_games(name, link, metascore) VALUES (?, ?, ?) ON CONFLICT(link) DO UPDATE SET metascore=excluded.metascore; - """, int), - "by_userscore": (""" + """, + int, + ), + "by_userscore": ( + """ INSERT INTO top_games(name, link, userscore) VALUES (?, ?, ?) ON CONFLICT(link) DO UPDATE SET userscore=excluded.userscore; - """, lambda x: int(float(x)*10)), - } + """, + lambda x: int(float(x) * 10), + ), + } for type, page in db.execute("SELECT type, content FROM pages;"): for tr in get_all_table_rows(page): query, score_converter = types[type] @@ -48,6 +54,7 @@ def process_pages(db): db.execute(query, (name, link, score)) db.commit() + def get_all_table_rows(page): """ (1, 1): /html/body/div[2]/div/div/div[1]/div[2]/div/div[1]/div/div[2]/table/tr[1] @@ -69,10 +76,11 @@ def get_all_table_rows(page): for tr in table: yield tr + def extract_data(tr, score_converter): """ name: /html/body/div[2]/div/div/div[1]/div[2]/div/div[1]/div/div[2]/table/tr[1]/td[2]/a/h3 - link: /html/body/div[2]/div/div/div[1]/div[2]/div/div[1]/div/div[2]/table/tr[1]/td[2]/a + link: /html/body/div[2]/div/div/div[1]/div[2]/div/div[1]/div/div[2]/table/tr[1]/td[2]/a score: /html/body/div[2]/div/div/div[1]/div[2]/div/div[1]/div/div[2]/table/tr[1]/td[2]/div[1]/a/div """ name = tr.xpath("td[2]/a/h3")[0].text diff --git a/scrape_genres.py b/scrape_genres.py new file mode 100644 index 0000000..808b276 --- /dev/null +++ b/scrape_genres.py @@ -0,0 +1,137 @@ +""" +### Сопоставление игр с жанрами + +Необходимо для каждой игры из базового набора найти жанры на ее странице на metacritic. Создать таблицу жанров и соединить отношением многие-ко-многим с таблицей базового набора игр. +""" +import sqlite3 +import requests +import lxml.html +import time +import traceback + + +def load_page(base_url, link): + return requests.get( + base_url + link, + headers={ + "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:92.0) Gecko/20100101 Firefox/92.0" + }, + ).text + + +def parse_page(page): + """ + metascore: + /html/body/div[1]/div[2]/div[1]/div[1]/div/div/div/div/div/div/div/div/div[1]/div[1]/div[3]/div/div[2]/div[1]/div[1]/div/div/a/div/span + /html/body/div[1]/div[2]/div[1]/div[1]/div/div/div/div/div/div/div/div/div[1]/div[1]/div[3]/div/div/div[2]/div[1]/div[1]/div/div/a/div/span + /html/body/div[1]/div[2]/div[1]/div[1]/div/div/div/div/div/div/div/div/div[1]/div[1]/div[3]/div/div/div[2]/div[1]/div[1]/div/div/a/div/span + userscore: + /html/body/div[1]/div[2]/div[1]/div[1]/div/div/div/div/div/div/div/div/div[1]/div[1]/div[3]/div/div[2]/div[1]/div[2]/div[1]/div/a/div + /html/body/div[1]/div[2]/div[1]/div[1]/div/div/div/div/div/div/div/div/div[1]/div[1]/div[3]/div/div/div[2]/div[1]/div[2]/div[1]/div/a/div + genres: + /html/body/div[1]/div[2]/div[1]/div[1]/div/div/div/div/div/div/div/div/div[1]/div[1]/div[3]/div/div[2]/div[2]/div[2]/ul/li[2]/span[@class="data"] + + """ + tree = lxml.html.fromstring(page) + try: + metascore = int( + tree.xpath( + "/html/body/div[1]/div[2]/div[1]/div[1]/div/div/div/div/div/div/div/div/div[1]/div[1]/div[3]//div[2]/div[1]/div[1]/div/div/a/div/span" + )[0].text + ) + except ValueError: + metascore = None + except IndexError: + metascore = None + try: + userscore = int( + float( + tree.xpath( + "/html/body/div[1]/div[2]/div[1]/div[1]/div/div/div/div/div/div/div/div/div[1]/div[1]/div[3]//div[2]/div[1]/div[2]/div[1]/div/a/div" + )[0].text + ) + * 10 + ) + except ValueError: + userscore = None + except IndexError: + print("Invalid xpath for userscore") + raise + genres = [ + x.text + for x in tree.xpath( + '/html/body/div[1]/div[2]/div[1]/div[1]/div/div/div/div/div/div/div/div/div[1]/div[1]/div[3]//div[2]/div[2]/div[2]/ul/li[2]/span[@class="data"]' + ) + ] + return metascore, userscore, genres + + +def update_score(db, game_id, metascore, userscore): + db.execute( + "UPDATE top_games SET metascore=?, userscore=? WHERE ROWID=?;", + (metascore, userscore, game_id), + ) + + +def update_genres(db, genres): + db.executemany("INSERT OR IGNORE INTO genres VALUES (?);", map(lambda x: (x,), genres)) + + +def link_game_to_genres(db, game_id, genres): + query = """ + INSERT INTO game_to_genre + SELECT ?, genres.ROWID FROM genres + WHERE genres.name=?; + """ + db.executemany(query, map(lambda x: (game_id, x), set(genres))) + + +def load_pages(db, base_url): + db.execute("DROP TABLE IF EXISTS games_pages;") + db.execute("CREATE TABLE games_pages (id_game INTEGER UNIQUE, content TEXT);") + for i, link in db.execute("SELECT ROWID, link FROM top_games ORDER BY ROWID;"): + page = load_page(base_url, link) + db.execute("INSERT INTO games_pages VALUES (?, ?);", (i, page)) + time.sleep(0.5) + if i % 100 == 0: + print(i) + + +def main(db_name, base_url): + with sqlite3.connect(db_name) as db: + db.execute("DROP TABLE IF EXISTS genres;") + db.execute("DROP TABLE IF EXISTS game_to_genre;") + db.execute("CREATE TABLE genres (name TEXT UNIQUE);") + db.execute("CREATE TABLE game_to_genre (id_game INTEGER, id_genre INTEGER);") + for i, name, link in db.execute("SELECT ROWID, name, link FROM top_games ORDER BY ROWID;"): + (page,) = db.execute( + "SELECT content FROM games_pages WHERE id_game=?;", (i,) + ).fetchone() + metascore, userscore, genres = parse_page(page) + update_score(db, i, metascore, userscore) + update_genres(db, genres) + link_game_to_genres(db, i, genres) + + +def test_parse_page(): + with sqlite3.connect(db_name) as db: + for i, link in db.execute("SELECT ROWID, link FROM top_games WHERE ROWID IN (1,2,3);"): + page = load_page(base_url, link) + metascore, userscore, genres = parse_page(page) + print(metascore, userscore, genres) + breakpoint() + + +def test_insert_not_unique(): + with sqlite3.connect(":memory:") as db: + db.execute("CREATE TABLE genres (name TEXT UNIQUE);") + genres = ["a", "b", "a", "b"] + update_genres(db, genres) + assert db.execute("SELECT ROWID, * FROM genres;").fetchall() == [(1, "a"), (2, "b")] + breakpoint() + + +if __name__ == "__main__": + db_name = "db.sqlite3" + base_url = "https://www.metacritic.com" + main(db_name, base_url)