Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5411e26afc | |||
| 36e09fb2a1 | |||
| 05d1ddadc9 | |||
| 79afe02da4 | |||
| ed1eed7d0a | |||
| 7b5ad8d990 |
@@ -19,9 +19,9 @@
|
|||||||
1. Возьмем топ-300 по оценкам критиков и топ-300 по оценкам игроков. Найдем пересечение, назовем базовым набором игр.
|
1. Возьмем топ-300 по оценкам критиков и топ-300 по оценкам игроков. Найдем пересечение, назовем базовым набором игр.
|
||||||
2. Возьмем полный набор игр, измерим для каждой размер образа
|
2. Возьмем полный набор игр, измерим для каждой размер образа
|
||||||
3. Сопоставим базовый набор игр с полным набором игр, найдем результирующий набор игр весом не более 15 Гб, лучший по критериям:
|
3. Сопоставим базовый набор игр с полным набором игр, найдем результирующий набор игр весом не более 15 Гб, лучший по критериям:
|
||||||
1. Выше оценка критиков
|
1. Выше оценка критиков
|
||||||
2. Выше оценка игроков
|
2. Выше оценка игроков
|
||||||
3. Меньше размер
|
3. Меньше размер
|
||||||
4. Результирующий набор игр разобъем на категории по жанрам
|
4. Результирующий набор игр разобъем на категории по жанрам
|
||||||
5. Запишем результирующий набор игр на SD-карту
|
5. Запишем результирующий набор игр на SD-карту
|
||||||
|
|
||||||
@@ -29,26 +29,44 @@
|
|||||||
|
|
||||||
### Построение базового набора игр
|
### Построение базового набора игр
|
||||||
|
|
||||||
*Входные данные:* html-страницы с сайта metacritic
|
- *Входные данные:* html-страницы с сайта metacritic
|
||||||
*Выходные данные:* таблица со столбцами (название, ссылка, оценка критиков, оценка игроков)
|
- *Выходные данные:* таблица со столбцами (название, ссылка, оценка критиков, оценка игроков)
|
||||||
*Решение:* смотри файл make_db.py
|
- *Решение:* смотри файл make_db.py
|
||||||
|
|
||||||
### Измерение размера образов в полном наборе игр
|
### Измерение размера образов в полном наборе игр
|
||||||
|
|
||||||
*Входные данные:* директория с zip-архивами, в каждом nds-образ
|
- *Входные данные:* директория с zip-архивами, в каждом nds-образ
|
||||||
*Выходные данные:* таблица со столбцами (название архива, название файла, размер в Мб)
|
- *Выходные данные:* таблица со столбцами (название архива, название файла, размер в Мб)
|
||||||
*Решение:* смотри файл archive_table.py
|
- *Решение:* смотри файл archive_table.py
|
||||||
|
|
||||||
## Задачи
|
|
||||||
|
|
||||||
### Сопоставление игр с жанрами
|
### Сопоставление игр с жанрами
|
||||||
|
|
||||||
Необходимо для каждой игры из базового набора найти жанры на ее странице на metacritic. Создать таблицу жанров и соединить отношением многие-ко-многим с таблицей базового набора игр.
|
Необходимо для каждой игры из базового набора найти жанры на ее странице на metacritic. Создать таблицу жанров и соединить отношением многие-ко-многим с таблицей базового набора игр.
|
||||||
|
|
||||||
|
- *Входные данные:* html-страницы, по одной на игру из базового набора
|
||||||
|
- *Выходные данные:* таблица жанров, таблица связей со столбцами (игра, жанр)
|
||||||
|
- *Решение:* смотри файл scrape_genres.py
|
||||||
|
|
||||||
### Сопоставление базового с полным набором игр
|
### Сопоставление базового с полным набором игр
|
||||||
|
|
||||||
Необходимо реализовать быстрый неточный поиск из базового набора в полном. Предложение: построить однословные индексы по базовому и полному набору, при сопоставлении использовать расстояние Левенштейна около 3.
|
Необходимо реализовать быстрый неточный поиск из базового набора в полном. Предложение: построить однословные индексы по базовому и полному набору, при сопоставлении использовать расстояние Левенштейна около 3.
|
||||||
|
|
||||||
|
Принято решение сравнивать очищенные названия триграммами. Ошибки присутствуют, для их отбора и исправления можно использовать расстояние Левенштейна.
|
||||||
|
|
||||||
|
- *Входные данные:* таблица игр, таблица архивов
|
||||||
|
- *Выходные данные:* таблица связей со столбцами (игра, архив)
|
||||||
|
- *Решение:* смотри файл fuzzy_search.py
|
||||||
|
|
||||||
|
## Задачи
|
||||||
|
|
||||||
|
### Выделить из базы данных кеш
|
||||||
|
|
||||||
|
html-страницы складывать в отдельную базу данных, не загружать кеш в гитхаб.
|
||||||
|
|
||||||
|
### Проверить базовый набор
|
||||||
|
|
||||||
|
Необходимо просмотреть жанры, маловстречающиеся и повторяющиеся объединить, построить примерное дерево категорий.
|
||||||
|
|
||||||
### Построение результирующего набора игр
|
### Построение результирующего набора игр
|
||||||
|
|
||||||
Необходимо решить задачу минимизации критериальной функции с ограничением в 15 Гб размера на основе критериев:
|
Необходимо решить задачу минимизации критериальной функции с ограничением в 15 Гб размера на основе критериев:
|
||||||
|
|||||||
+4
-4
@@ -3,13 +3,13 @@ import zipfile
|
|||||||
import csv
|
import csv
|
||||||
|
|
||||||
res = []
|
res = []
|
||||||
zips = os.listdir('Nintendo DS')
|
zips = os.listdir("Nintendo DS")
|
||||||
for z in zips:
|
for z in zips:
|
||||||
with zipfile.ZipFile('Nintendo DS/'+z) as myzip:
|
with zipfile.ZipFile("Nintendo DS/" + z) as myzip:
|
||||||
info = myzip.infolist()
|
info = myzip.infolist()
|
||||||
for i in info:
|
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 = csv.writer(f)
|
||||||
writer.writerows(res)
|
writer.writerows(res)
|
||||||
|
|||||||
BIN
Binary file not shown.
@@ -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,))
|
||||||
|
|
||||||
@@ -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)
|
||||||
+13
-5
@@ -28,19 +28,25 @@ def process_pages(db):
|
|||||||
)
|
)
|
||||||
|
|
||||||
types = {
|
types = {
|
||||||
"by_metascore": ("""
|
"by_metascore": (
|
||||||
|
"""
|
||||||
INSERT INTO top_games(name, link, metascore)
|
INSERT INTO top_games(name, link, metascore)
|
||||||
VALUES (?, ?, ?)
|
VALUES (?, ?, ?)
|
||||||
ON CONFLICT(link) DO
|
ON CONFLICT(link) DO
|
||||||
UPDATE SET metascore=excluded.metascore;
|
UPDATE SET metascore=excluded.metascore;
|
||||||
""", int),
|
""",
|
||||||
"by_userscore": ("""
|
int,
|
||||||
|
),
|
||||||
|
"by_userscore": (
|
||||||
|
"""
|
||||||
INSERT INTO top_games(name, link, userscore)
|
INSERT INTO top_games(name, link, userscore)
|
||||||
VALUES (?, ?, ?)
|
VALUES (?, ?, ?)
|
||||||
ON CONFLICT(link) DO
|
ON CONFLICT(link) DO
|
||||||
UPDATE SET userscore=excluded.userscore;
|
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 type, page in db.execute("SELECT type, content FROM pages;"):
|
||||||
for tr in get_all_table_rows(page):
|
for tr in get_all_table_rows(page):
|
||||||
query, score_converter = types[type]
|
query, score_converter = types[type]
|
||||||
@@ -48,6 +54,7 @@ def process_pages(db):
|
|||||||
db.execute(query, (name, link, score))
|
db.execute(query, (name, link, score))
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
|
|
||||||
def get_all_table_rows(page):
|
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]
|
(1, 1): /html/body/div[2]/div/div/div[1]/div[2]/div/div[1]/div/div[2]/table/tr[1]
|
||||||
@@ -69,6 +76,7 @@ def get_all_table_rows(page):
|
|||||||
for tr in table:
|
for tr in table:
|
||||||
yield tr
|
yield tr
|
||||||
|
|
||||||
|
|
||||||
def extract_data(tr, score_converter):
|
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
|
name: /html/body/div[2]/div/div/div[1]/div[2]/div/div[1]/div/div[2]/table/tr[1]/td[2]/a/h3
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
ngram
|
||||||
|
requests
|
||||||
|
lxml
|
||||||
@@ -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)
|
||||||
Reference in New Issue
Block a user