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
+26 -8
View File
@@ -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 Гб размера на основе критериев:
+3 -3
View File
@@ -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
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)
+12 -4
View File
@@ -28,18 +28,24 @@ 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):
@@ -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
+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)