#!/usr/bin/env python3 """ Импорт книг из inpx-файла lib.rus.ec в PostgreSQL. Использование: python parse_inpx.py Пример: python parse_inpx.py \ "/mnt/hdd/share/Torrents/Downloaded/Libruks/Библиотека Либрусек для архивов 18-09-2026.inpx" \ "dbname=books user=postgres password=Karabas@Barabas host=172.20.0.4" \ "lib.rus.ec" """ import zipfile import psycopg2 from psycopg2.extras import execute_values import sys import hashlib def normalize_author_name(author_name): """Нормализует имя автора: убирает лишние запятые и пробелы""" if not author_name: return '' author_name = author_name.strip() # Убираем запятые в конце while author_name.endswith(','): author_name = author_name[:-1] # Убираем двойные запятые while ',,' in author_name: author_name = author_name.replace(',,', ',') # Разбиваем по запятым, убираем пустые части parts = [p.strip() for p in author_name.split(',') if p.strip()] return ','.join(parts) def get_or_create_author(cur, author_name): """Возвращает ID автора, создавая его если нужно""" author_name = normalize_author_name(author_name) if not author_name: return None author_hash = hashlib.md5(author_name.lower().encode()).hexdigest() cur.execute("SELECT id FROM authors WHERE name_hash = %s", (author_hash,)) row = cur.fetchone() if row: return row[0] parts = author_name.split(',') last_name = parts[0].strip() if len(parts) > 0 else '' first_name = parts[1].strip() if len(parts) > 1 and parts[1].strip() else None middle_name = parts[2].strip() if len(parts) > 2 and parts[2].strip() else None first_letter = last_name[0].lower() if last_name else '' cur.execute(""" INSERT INTO authors (full_name, last_name, first_name, middle_name, name_hash, first_letter) VALUES (%s, %s, %s, %s, %s, %s) RETURNING id """, (author_name, last_name, first_name, middle_name, author_hash, first_letter)) return cur.fetchone()[0] def get_or_create_genre(cur, genre_code): """Возвращает ID жанра, создавая его если нужно""" if not genre_code: return None genre_code = genre_code.strip() cur.execute("SELECT id FROM genres WHERE code = %s", (genre_code,)) row = cur.fetchone() if row: return row[0] parent_code = None root_code = None if '_' in genre_code: prefix = genre_code.split('_')[0] cur.execute("SELECT code FROM genres WHERE code = %s AND is_root = TRUE", (prefix,)) row = cur.fetchone() if row: parent_code = prefix root_code = prefix cur.execute(""" INSERT INTO genres (code, title_ru, title_en, parent_code, root_code, is_root, is_custom) VALUES (%s, %s, %s, %s, %s, FALSE, TRUE) RETURNING id """, (genre_code, genre_code, genre_code, parent_code, root_code)) return cur.fetchone()[0] def get_or_create_series(cur, series_name): """Возвращает ID серии, создавая её если нужно""" if not series_name: return None series_name = series_name.strip() if not series_name: return None series_hash = hashlib.md5(series_name.lower().encode()).hexdigest() cur.execute("SELECT id FROM series WHERE name_hash = %s", (series_hash,)) row = cur.fetchone() if row: return row[0] first_letter = series_name[0].lower() if series_name else '' cur.execute(""" INSERT INTO series (name, name_hash, first_letter) VALUES (%s, %s, %s) RETURNING id """, (series_name, series_hash, first_letter)) return cur.fetchone()[0] def parse_inpx_zip(zip_path, db_conn_string, source): print(f"Connecting to database...") conn = psycopg2.connect(db_conn_string) cur = conn.cursor() print(f"Clearing existing books from source: {source}...") cur.execute("DELETE FROM books WHERE source = %s", (source,)) conn.commit() deleted_count = cur.rowcount if deleted_count > 0: print(f"Deleted {deleted_count} existing books from source '{source}'") print("Vacuuming...") conn.autocommit = True try: cur.execute("VACUUM books") print("VACUUM completed") finally: conn.autocommit = False print(f"Opening archive: {zip_path}") with zipfile.ZipFile(zip_path, 'r') as zf: files = [(f, zf.getinfo(f).file_size) for f in zf.namelist()] files.sort(key=lambda x: x[1], reverse=True) target_file = files[0][0] print(f"Processing: {target_file} ({files[0][1]} bytes)") with zf.open(target_file, 'r') as f: count = 0 author_cache = {} genre_cache = {} series_cache = {} for line in f: try: decoded = line.decode('utf-8', errors='ignore') except: continue parts = decoded.strip().split('\x04') if len(parts) < 11: continue authors_str = parts[0].rstrip(',:') if parts[0] else '' genre_str = parts[1] if len(parts) > 1 and parts[1] else '' title = parts[2] if len(parts) > 2 and parts[2] else '' series_name = parts[3].strip() if len(parts) > 3 and parts[3].strip() else None series_number = float(parts[4]) if len(parts) > 4 and parts[4].strip() else None internal_id = int(parts[5]) if len(parts) > 5 and parts[5].strip().isdigit() else None file_size = int(parts[6]) if len(parts) > 6 and parts[6].strip().isdigit() else None internal_id2 = int(parts[7]) if len(parts) > 7 and parts[7].strip().isdigit() else None format_ = parts[9] if len(parts) > 9 and parts[9].strip() else 'fb2' added_date = parts[10] if len(parts) > 10 and parts[10].strip() else None language = parts[11] if len(parts) > 11 and parts[11].strip() else 'ru' title_hash = hashlib.md5(title.lower().encode()).hexdigest() if title else None # Серия (с кэшированием) series_id = None if series_name: if series_name in series_cache: series_id = series_cache[series_name] else: series_id = get_or_create_series(cur, series_name) series_cache[series_name] = series_id # Вставляем книгу cur.execute(""" INSERT INTO books (title, title_hash, internal_id, file_size, internal_id2, format, added_date, language, source, series_id, series_number) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) RETURNING id """, (title, title_hash, internal_id, file_size, internal_id2, format_, added_date, language, source, series_id, series_number)) book_id = cur.fetchone()[0] # Авторы (с кэшированием) if authors_str: author_list = [a.strip() for a in authors_str.split(':') if a.strip()] for position, author_name in enumerate(author_list, 1): normalized = normalize_author_name(author_name) if normalized in author_cache: author_id = author_cache[normalized] else: author_id = get_or_create_author(cur, normalized) author_cache[normalized] = author_id if author_id: cur.execute(""" INSERT INTO book_authors (book_id, author_id, position) VALUES (%s, %s, %s) ON CONFLICT DO NOTHING """, (book_id, author_id, position)) # Жанры (с кэшированием) if genre_str: for genre_code in genre_str.split(':'): genre_code = genre_code.strip() if not genre_code: continue if genre_code in genre_cache: genre_id = genre_cache[genre_code] else: genre_id = get_or_create_genre(cur, genre_code) genre_cache[genre_code] = genre_id if genre_id: cur.execute(""" INSERT INTO book_genres (book_id, genre_id) VALUES (%s, %s) ON CONFLICT DO NOTHING """, (book_id, genre_id)) count += 1 if count % 5000 == 0: conn.commit() print(f"Loaded {count} books... " f"(authors: {len(author_cache)}, " f"series: {len(series_cache)}, " f"genres: {len(genre_cache)})") conn.commit() print("Optimizing...") conn.commit() cur.execute("ANALYZE books") cur.execute("ANALYZE authors") cur.execute("ANALYZE series") cur.execute("ANALYZE genres") conn.close() print(f"Done! Total books loaded: {count}") if __name__ == "__main__": if len(sys.argv) != 4: print("Usage: python parse_inpx.py ") sys.exit(1) parse_inpx_zip(sys.argv[1], sys.argv[2], sys.argv[3])