Compare commits

6 Commits

Author SHA1 Message Date
fedor 5411e26afc Solve task 2021-10-14 23:08:50 +03:00
fedor 36e09fb2a1 Import archive game list into database 2021-10-14 21:23:07 +03:00
fedor 05d1ddadc9 Add task 2021-10-13 22:10:30 +03:00
fedor 79afe02da4 Solve task 2021-10-13 22:03:12 +03:00
fedor ed1eed7d0a Fix formatting 2021-10-11 20:50:32 +03:00
Fedor Lyanguzov 7b5ad8d990 Merge pull request #1 from Fedor-Lyanguzov/add-license-1
Create LICENSE
2021-10-11 20:49:15 +03:00
8 changed files with 248 additions and 21 deletions
+29 -11
View File
@@ -19,9 +19,9 @@
1. Возьмем топ-300 по оценкам критиков и топ-300 по оценкам игроков. Найдем пересечение, назовем базовым набором игр.
2. Возьмем полный набор игр, измерим для каждой размер образа
3. Сопоставим базовый набор игр с полным набором игр, найдем результирующий набор игр весом не более 15 Гб, лучший по критериям:
1. Выше оценка критиков
2. Выше оценка игроков
3. Меньше размер
1. Выше оценка критиков
2. Выше оценка игроков
3. Меньше размер
4. Результирующий набор игр разобъем на категории по жанрам
5. Запишем результирующий набор игр на SD-карту
@@ -29,26 +29,44 @@
### Построение базового набора игр
*Входные данные:* html-страницы с сайта metacritic
*Выходные данные:* таблица со столбцами (название, ссылка, оценка критиков, оценка игроков)
*Решение:* смотри файл make_db.py
- *Входные данные:* html-страницы с сайта metacritic
- *Выходные данные:* таблица со столбцами (название, ссылка, оценка критиков, оценка игроков)
- *Решение:* смотри файл make_db.py
### Измерение размера образов в полном наборе игр
*Входные данные:* директория с zip-архивами, в каждом nds-образ
*Выходные данные:* таблица со столбцами (название архива, название файла, размер в Мб)
*Решение:* смотри файл archive_table.py
## Задачи
- *Входные данные:* директория с zip-архивами, в каждом nds-образ
- *Выходные данные:* таблица со столбцами (название архива, название файла, размер в Мб)
- *Решение:* смотри файл archive_table.py
### Сопоставление игр с жанрами
Необходимо для каждой игры из базового набора найти жанры на ее странице на metacritic. Создать таблицу жанров и соединить отношением многие-ко-многим с таблицей базового набора игр.
- *Входные данные:* html-страницы, по одной на игру из базового набора
- *Выходные данные:* таблица жанров, таблица связей со столбцами (игра, жанр)
- *Решение:* смотри файл scrape_genres.py
### Сопоставление базового с полным набором игр
Необходимо реализовать быстрый неточный поиск из базового набора в полном. Предложение: построить однословные индексы по базовому и полному набору, при сопоставлении использовать расстояние Левенштейна около 3.
Принято решение сравнивать очищенные названия триграммами. Ошибки присутствуют, для их отбора и исправления можно использовать расстояние Левенштейна.
- *Входные данные:* таблица игр, таблица архивов
- *Выходные данные:* таблица связей со столбцами (игра, архив)
- *Решение:* смотри файл fuzzy_search.py
## Задачи
### Выделить из базы данных кеш
html-страницы складывать в отдельную базу данных, не загружать кеш в гитхаб.
### Проверить базовый набор
Необходимо просмотреть жанры, маловстречающиеся и повторяющиеся объединить, построить примерное дерево категорий.
### Построение результирующего набора игр
Необходимо решить задачу минимизации критериальной функции с ограничением в 15 Гб размера на основе критериев:
+4 -4
View File
@@ -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)
BIN
View File
Binary file not shown.
+51
View File
@@ -0,0 +1,51 @@
import sqlite3
from ngram import NGram
tr_table = str.maketrans('','',' !$&\'()+,-.=@[]')
def key(s):
"""
!$&'()+,-.0123456789=@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]abcdefghijklmnopqrstuvwxyz
"""
return s.removesuffix('.zip').lower().translate(tr_table)
def test_key():
"""
1 vs 100 (Europe) [b].zip
1 vs 100 (europe) [b]
"""
s = '1 vs 100 (Europe) [b].zip'
assert key(s) == '1vs100europeb', key('1 vs 100 (Europe) [b].zip')
def select(name, xs):
while True:
print(f'?: {name}')
for i, ((_, name), _) in enumerate(xs,1):
print(f'{i}. {name}')
inp = input('1-9,n: ')
if inp=='':
return xs[0]
if inp=='n':
raise ValueError
if inp in set('123456789'):
return xs[int(inp)-1]
if __name__=='__main__':
test_key()
with sqlite3.connect("db.sqlite3") as db:
G = NGram(key=lambda x: key(x[1]))
for game in db.execute("SELECT ROWID, zip_name FROM archive;"):
G.add(game)
db.execute("DROP TABLE IF EXISTS game_to_archive;")
db.execute("CREATE TABLE game_to_archive (id_game INTEGER, id_archive INTEGER);")
for id_game, name in db.execute("SELECT ROWID, name FROM top_games;"):
try:
((id_a1, n1), p1), ((id_a2, n2), p2) = G.search(name)[:2]
if p1/p2<1.05:
(id_a1, n1), p1 = select(name, G.search(name)[:9])
db.execute("INSERT INTO game_to_archive VALUES (?, ?);", (id_game, id_a1))
except:
db.execute("INSERT INTO game_to_archive VALUES (?, NULL);", (id_game,))
+10
View File
@@ -0,0 +1,10 @@
import csv
import sqlite3
with sqlite3.connect("db.sqlite3") as db:
db.execute("DROP TABLE IF EXISTS archive;")
db.execute("CREATE TABLE archive (zip_name TEXT, nds_name TEXT, size INTEGER);")
with open("archive.csv") as inp:
reader = csv.reader(inp)
db.executemany("INSERT INTO archive VALUES (?, ?, ?);", reader)
+14 -6
View File
@@ -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
+3
View File
@@ -0,0 +1,3 @@
ngram
requests
lxml
+137
View File
@@ -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)