diff --git a/beautifulsoup/main.py b/beautifulsoup/main.py deleted file mode 100644 index 96db2e1..0000000 --- a/beautifulsoup/main.py +++ /dev/null @@ -1,25 +0,0 @@ -from urllib.request import urlopen, Request -from bs4 import BeautifulSoup -import re - -URL = "https://www.vinissimus.com/es/vinos/tinto/?cursor=0" - -def main(): - req = Request( - URL, - headers={ - "User-Agent": "Mozilla/5.0 (compatible; Konqueror/3.5.8; Linux)" - } - ) - - doc = BeautifulSoup( - urlopen(req), - "lxml" - ) - - for child in doc.find_all("div", class_="list large"): - name = child.find("h2", class_=["title"]) - print(name) - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/ejercicios/beautifulsoup/ej1/src/__ssl.py b/ejercicios/beautifulsoup/ej1/src/__ssl.py new file mode 100644 index 0000000..475f052 --- /dev/null +++ b/ejercicios/beautifulsoup/ej1/src/__ssl.py @@ -0,0 +1,5 @@ +def init_ssl(): + import os, ssl + if (not os.environ.get('PYTHONHTTPSVERIFY', '') and + getattr(ssl, '_create_unverified_context', None)): + ssl._create_default_https_context = ssl._create_unverified_context diff --git a/ejercicios/beautifulsoup/ej1/src/config.py b/ejercicios/beautifulsoup/ej1/src/config.py new file mode 100644 index 0000000..d70ae38 --- /dev/null +++ b/ejercicios/beautifulsoup/ej1/src/config.py @@ -0,0 +1,6 @@ +from pathlib import Path + +URL = "https://www.vinissimus.com/es/vinos/tinto/?cursor=0" +DATA_DIR = Path(__file__).parent.parent / "data" +CSV_PATH = DATA_DIR / "books.csv" +DB_PATH = DATA_DIR / "books.bd" \ No newline at end of file diff --git a/ejercicios/beautifulsoup/ej1/src/db.py b/ejercicios/beautifulsoup/ej1/src/db.py new file mode 100644 index 0000000..31bf2cb --- /dev/null +++ b/ejercicios/beautifulsoup/ej1/src/db.py @@ -0,0 +1,124 @@ +import sqlite3 +from pathlib import Path + + +class DBAttr: + def __init__(self, name, type_, modifier=""): + self.name = name + self.type_ = type_ + self.modifier = modifier + + def sql(self): + parts = [self.name, self.type_] + if self.modifier: + parts.append(self.modifier) + return " ".join(parts) + + +class DBManager: + _instance = None + + def __new__(cls, *args, **kwargs): + if cls._instance is None: + cls._instance = super().__new__(cls) + return cls._instance + + def __init__(self, path): + self.path = Path(path) + self.conn = sqlite3.connect(self.path) + self.conn.row_factory = sqlite3.Row + + def create_table(self, table_name, attributes: list[DBAttr]): + columns_sql = ",\n ".join(attr.sql() for attr in attributes) + + query = f""" + CREATE TABLE IF NOT EXISTS {table_name} ( + {columns_sql} + ); + """ + + try: + with self.conn: + self.conn.execute(query) + except Exception as e: + print("Error creating table:", e) + + def get_all(self, table_name): + try: + cursor = self.conn.execute(f"SELECT * FROM {table_name};") + return [dict(row) for row in cursor.fetchall()] + except Exception as e: + print("Error selecting:", e) + return [] + + def get_by(self, table_name, column, value): + try: + query = f"SELECT * FROM {table_name} WHERE {column} = ?;" + cursor = self.conn.execute(query, (value,)) + return [dict(row) for row in cursor.fetchall()] + except Exception as e: + print("Error selecting:", e) + return [] + + def insert(self, table_name, data: dict): + keys = ", ".join(data.keys()) + placeholders = ", ".join("?" for _ in data) + values = tuple(data.values()) + + query = f""" + INSERT INTO {table_name} ({keys}) + VALUES ({placeholders}); + """ + + try: + with self.conn: + self.conn.execute(query, values) + except Exception as e: + print("Error inserting:", e) + + def update(self, table_name, data: dict, where_column, where_value): + set_clause = ", ".join(f"{key} = ?" for key in data.keys()) + values = list(data.values()) + values.append(where_value) + + query = f""" + UPDATE {table_name} + SET {set_clause} + WHERE {where_column} = ?; + """ + + try: + with self.conn: + self.conn.execute(query, tuple(values)) + except Exception as e: + print("Error updating:", e) + + def delete(self, table_name, where_column, where_value): + query = f"DELETE FROM {table_name} WHERE {where_column} = ?;" + + try: + with self.conn: + self.conn.execute(query, (where_value,)) + except Exception as e: + print("Error deleting:", e) + + def exists(self, table_name, where_column, where_value): + query = f"SELECT 1 FROM {table_name} WHERE {where_column} = ? LIMIT 1;" + + try: + cursor = self.conn.execute(query, (where_value,)) + return cursor.fetchone() is not None + except Exception as e: + print("Error checking existence:", e) + return False + + def count(self, table_name): + try: + cursor = self.conn.execute(f"SELECT COUNT(*) as total FROM {table_name};") + return cursor.fetchone()["total"] + except Exception as e: + print("Error counting:", e) + return 0 + + def close(self): + self.conn.close() \ No newline at end of file diff --git a/ejercicios/beautifulsoup/ej1/src/main.py b/ejercicios/beautifulsoup/ej1/src/main.py new file mode 100644 index 0000000..af37ad5 --- /dev/null +++ b/ejercicios/beautifulsoup/ej1/src/main.py @@ -0,0 +1,35 @@ +from bs4 import BeautifulSoup +import re + +from db import DBManager, DBAttr +from ui import WinesUI +from req import Requester +from __ssl import init_ssl +from config import * + +init_ssl() + +dbm = DBManager(DB_PATH) +req = Requester() + +def create_tables(): + wines_attr = [ + DBAttr("name", "TEXT", "NOT NULL"), + DBAttr("price", "INTEGER", "NOT NULL"), + DBAttr("origin", "TEXT", "NOT NULL"), + DBAttr("cellar", "TEXT", "NOT NULL"), + DBAttr("type", "TEXT", "NOT NULL") + ] + + types_attr = [ + DBAttr("type", "TEXT") + ] + + dbm.create_table("wines", wines_attr) + dbm.create_table("types", types_attr) + +def main(): + pass + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/ejercicios/beautifulsoup/ej1/src/req.py b/ejercicios/beautifulsoup/ej1/src/req.py new file mode 100644 index 0000000..4ac21cb --- /dev/null +++ b/ejercicios/beautifulsoup/ej1/src/req.py @@ -0,0 +1,10 @@ +from urllib.request import urlopen, Request + +class Requester(): + def __init__(self): + self.headers = { + "User-Agent": "Mozilla/5.0 (compatible; Konqueror/3.5.8; Linux)" + } + + def get(self, url): + return urlopen(Request(url, self.headers)) \ No newline at end of file diff --git a/ejercicios/beautifulsoup/ej1/src/ui.py b/ejercicios/beautifulsoup/ej1/src/ui.py new file mode 100644 index 0000000..f2e7384 --- /dev/null +++ b/ejercicios/beautifulsoup/ej1/src/ui.py @@ -0,0 +1,80 @@ +import tkinter as tk +from tkinter import ttk, messagebox +from tkinter.scrolledtext import ScrolledText + +class WinesUI(): + def __init__(self, root, title = "AII"): + self.root = root + self.root.title(title) + self.root.geometry("900x600") + + # Menu Principal + self.menu = tk.Menu(self.root) + self.root.config(menu=self.menu) + + # Menu Datos + datos_menu = tk.Menu(self.menu, tearoff=0) + datos_menu.add_command(label="Cargar", command=lambda: self.callback("cargar")) + datos_menu.add_separator() + datos_menu.add_command(label="Salir", command=self.root.quit) + self.menu.add_cascade(label="Datos", menu=datos_menu) + + # Menu Listar + listar_menu = tk.Menu(self.menu, tearoff=0) + listar_menu.add_command(label="Completo", command=lambda: self.callback("listar_completo")) + listar_menu.add_command(label="Ordenado", command=lambda: self.callback("listar_ordenado")) + self.menu.add_cascade(label="Listar", menu=listar_menu) + + # Menu Buscar + buscar_menu = tk.Menu(self.menu, tearoff=0) + buscar_menu.add_command(label="Título", command=lambda: self.callback("buscar_titulo")) + buscar_menu.add_command(label="Editorial", command=lambda: self.callback("buscar_editorial")) + self.menu.add_cascade(label="Buscar", menu=buscar_menu) + + # Callback externo desde el punto de entrada + self.callback = None + + def show_list(self, books, fields, title="Listado"): + mw = tk.Toplevel(self.root) + mw.title(title) + listbox = tk.Listbox(mw, width=80, height=20) + listbox.pack(side="left", fill="both", expand=True) + scrollbar = tk.Scrollbar(mw) + scrollbar.pack(side="right", fill="y") + listbox.config(yscrollcommand=scrollbar.set) + scrollbar.config(command=listbox.yview) + + for book in books: + row = " | ".join(str(book[field]) for field in fields) + listbox.insert("end", row) + + def ask_text(self, label, callback): + mw = tk.Toplevel(self.root) + mw.title(label) + tk.Label(mw, text=label).pack(pady=5) + entry = ttk.Entry(mw) + entry.pack(pady=5) + ttk.Button(mw, text="Aceptar", command= + lambda: [callback(entry.get()), mw.destroy()]).pack(pady=10) + + def ask_spinbox(self, label, options, callback): + mw = tk.Toplevel(self.root) + mw.title(label) + tk.Label(mw, text=label).pack(pady=5) + spinbox = ttk.Spinbox(mw, values=options, state="readonly", width=40) + spinbox.pack(pady=5) + ttk.Button(mw, text="Aceptar", command= + lambda: [callback(spinbox.get()), mw.destroy()]).pack(pady=10) + + def ask_radiobutton(self, label, options, callback): + mw = tk.Toplevel(self.root) + mw.title(label) + tk.Label(mw, text=label).pack(pady=5) + sv = tk.StringVar(value=options[0]) + for option in options: + tk.Radiobutton(mw, text=option, variable=sv, value=option).pack(anchor="w") + ttk.Button(mw, text="Aceptar", command= + lambda: [callback(sv.get()), mw.destroy()]).pack(pady=10) + + def info(slef, message): + messagebox.showinfo("Información", message) \ No newline at end of file diff --git a/regex-sqlite/ejercicios/ej1/src/main.py b/ejercicios/python/ej1/src/main.py similarity index 100% rename from regex-sqlite/ejercicios/ej1/src/main.py rename to ejercicios/python/ej1/src/main.py diff --git a/regex-sqlite/ejercicios/ej2/data/books.bd b/ejercicios/python/ej2/data/books.bd similarity index 99% rename from regex-sqlite/ejercicios/ej2/data/books.bd rename to ejercicios/python/ej2/data/books.bd index 6280dfd..2694a7d 100644 Binary files a/regex-sqlite/ejercicios/ej2/data/books.bd and b/ejercicios/python/ej2/data/books.bd differ diff --git a/regex-sqlite/ejercicios/ej2/data/books.csv b/ejercicios/python/ej2/data/books.csv similarity index 98% rename from regex-sqlite/ejercicios/ej2/data/books.csv rename to ejercicios/python/ej2/data/books.csv index a4cafd3..00dc95b 100644 --- a/regex-sqlite/ejercicios/ej2/data/books.csv +++ b/ejercicios/python/ej2/data/books.csv @@ -1,4 +1,4 @@ -ISBN;title;author;year;publisher +isbn;title;author;year;publisher 913154;The Way Things Work: An Illustrated Encyclopedia of Technology;C. van Amerongen (translator);1967;"Simon & Schuster" 1010565;Mog's Christmas;Judith Kerr;1992;Collins 1046438;Liar;Stephen Fry;Unknown;Harpercollins Uk diff --git a/ejercicios/python/ej2/src/db.py b/ejercicios/python/ej2/src/db.py new file mode 100644 index 0000000..31bf2cb --- /dev/null +++ b/ejercicios/python/ej2/src/db.py @@ -0,0 +1,124 @@ +import sqlite3 +from pathlib import Path + + +class DBAttr: + def __init__(self, name, type_, modifier=""): + self.name = name + self.type_ = type_ + self.modifier = modifier + + def sql(self): + parts = [self.name, self.type_] + if self.modifier: + parts.append(self.modifier) + return " ".join(parts) + + +class DBManager: + _instance = None + + def __new__(cls, *args, **kwargs): + if cls._instance is None: + cls._instance = super().__new__(cls) + return cls._instance + + def __init__(self, path): + self.path = Path(path) + self.conn = sqlite3.connect(self.path) + self.conn.row_factory = sqlite3.Row + + def create_table(self, table_name, attributes: list[DBAttr]): + columns_sql = ",\n ".join(attr.sql() for attr in attributes) + + query = f""" + CREATE TABLE IF NOT EXISTS {table_name} ( + {columns_sql} + ); + """ + + try: + with self.conn: + self.conn.execute(query) + except Exception as e: + print("Error creating table:", e) + + def get_all(self, table_name): + try: + cursor = self.conn.execute(f"SELECT * FROM {table_name};") + return [dict(row) for row in cursor.fetchall()] + except Exception as e: + print("Error selecting:", e) + return [] + + def get_by(self, table_name, column, value): + try: + query = f"SELECT * FROM {table_name} WHERE {column} = ?;" + cursor = self.conn.execute(query, (value,)) + return [dict(row) for row in cursor.fetchall()] + except Exception as e: + print("Error selecting:", e) + return [] + + def insert(self, table_name, data: dict): + keys = ", ".join(data.keys()) + placeholders = ", ".join("?" for _ in data) + values = tuple(data.values()) + + query = f""" + INSERT INTO {table_name} ({keys}) + VALUES ({placeholders}); + """ + + try: + with self.conn: + self.conn.execute(query, values) + except Exception as e: + print("Error inserting:", e) + + def update(self, table_name, data: dict, where_column, where_value): + set_clause = ", ".join(f"{key} = ?" for key in data.keys()) + values = list(data.values()) + values.append(where_value) + + query = f""" + UPDATE {table_name} + SET {set_clause} + WHERE {where_column} = ?; + """ + + try: + with self.conn: + self.conn.execute(query, tuple(values)) + except Exception as e: + print("Error updating:", e) + + def delete(self, table_name, where_column, where_value): + query = f"DELETE FROM {table_name} WHERE {where_column} = ?;" + + try: + with self.conn: + self.conn.execute(query, (where_value,)) + except Exception as e: + print("Error deleting:", e) + + def exists(self, table_name, where_column, where_value): + query = f"SELECT 1 FROM {table_name} WHERE {where_column} = ? LIMIT 1;" + + try: + cursor = self.conn.execute(query, (where_value,)) + return cursor.fetchone() is not None + except Exception as e: + print("Error checking existence:", e) + return False + + def count(self, table_name): + try: + cursor = self.conn.execute(f"SELECT COUNT(*) as total FROM {table_name};") + return cursor.fetchone()["total"] + except Exception as e: + print("Error counting:", e) + return 0 + + def close(self): + self.conn.close() \ No newline at end of file diff --git a/ejercicios/python/ej2/src/files.py b/ejercicios/python/ej2/src/files.py new file mode 100644 index 0000000..1b66710 --- /dev/null +++ b/ejercicios/python/ej2/src/files.py @@ -0,0 +1,23 @@ +import csv + +class FileReader: + _instance = None + + def __new__(cls, *args, **kwargs): + if cls._instance is None: + cls._instance = super().__new__(cls) + return cls._instance + + def __init__(self, delimiter=";"): + self.delimiter = delimiter + + def read(self, file): + results = [] + try: + with open(file, encoding="utf-8-sig") as f: + reader = csv.DictReader(f, delimiter=self.delimiter) + for row in reader: + results.append(dict(row)) + except Exception as e: + print(f"Error leyendo archivo: {e}") + return results \ No newline at end of file diff --git a/ejercicios/python/ej2/src/main.py b/ejercicios/python/ej2/src/main.py new file mode 100644 index 0000000..8d65ec1 --- /dev/null +++ b/ejercicios/python/ej2/src/main.py @@ -0,0 +1,82 @@ +from pathlib import Path +from tkinter import Tk + +from files import FileReader +from db import DBManager, DBAttr +from ui import BooksUI + +DATA_DIR = Path(__file__).parent.parent / "data" +CSV_PATH = DATA_DIR / "books.csv" +DB_PATH = DATA_DIR / "books.bd" + +dbm = DBManager(DB_PATH) +fr = FileReader() + +def create_tables(): + book_attrs = [ + DBAttr("isbn", "INTEGER", "PRIMARY KEY"), + DBAttr("title", "TEXT", "NOT NULL"), + DBAttr("author", "TEXT"), + DBAttr("year", "DATE"), + DBAttr("publisher", "TEXT") + ] + + dbm.create_table("books", book_attrs) + +def main(): + create_tables() + root = Tk() + ui = BooksUI(root) + + books = fr.read(CSV_PATH) + for book in books: + print(book) + + + def handle_action(action): + match(action): + case "cargar": + books = fr.read(CSV_PATH) + count = 0 + for book in books: + book["isbn"] = int(book["isbn"]) + if not dbm.exists("books", "isbn", book["isbn"]): + dbm.insert("books", book) + count += 1 + ui.info(f"{count} libros almacenados.") + case "listar_todo": + books = dbm.get_all("books") + ui.show_list(books, ["isbn", "title", "author", "year"]) + case "listar_ordenado": + def sort(attr): + books = dbm.get_all("books") + def key_fn(x): + v = x[attr] + if isinstance(v, int): + return v + elif isinstance(v, str) and v.isdigit(): + return int(v) + else: + return float('inf') + books.sort(key=key_fn) + ui.show_list(books, ["isbn", "title", "author", "year"]) + ui.ask_radiobutton("Ordenar por: ", ["isbn", "year"], sort) + case "buscar_titulo": + def search_title(title): + books = [book for book in dbm.get_all("books") if title.lower() in book["title"].lower()] + ui.show_list(books, ["isbn", "title", "author", "year"]) + ui.ask_text("Buscar por título: ", search_title) + case "buscar_editorial": + publishers = list({book["publisher"] for book in dbm.get_all("books")}) + publishers.sort() + def search_publisher(publisher): + books = [book for book in dbm.get_all("books") if book["publisher"] == publisher] + ui.show_list(books, ["title", "author", "publisher"]) + ui.ask_spinbox("Selecciona editorial: ", publishers, search_publisher) + + ui.callback = handle_action + root.mainloop() + dbm.close() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/ejercicios/python/ej2/src/ui.py b/ejercicios/python/ej2/src/ui.py new file mode 100644 index 0000000..c0358cc --- /dev/null +++ b/ejercicios/python/ej2/src/ui.py @@ -0,0 +1,80 @@ +import tkinter as tk +from tkinter import ttk, messagebox +from tkinter.scrolledtext import ScrolledText + +class BooksUI(): + def __init__(self, root, title = "AII"): + self.root = root + self.root.title(title) + self.root.geometry("900x600") + + # Menu Principal + self.menu = tk.Menu(self.root) + self.root.config(menu=self.menu) + + # Menu Datos + datos_menu = tk.Menu(self.menu, tearoff=0) + datos_menu.add_command(label="Cargar", command=lambda: self.callback("cargar")) + datos_menu.add_separator() + datos_menu.add_command(label="Salir", command=self.root.quit) + self.menu.add_cascade(label="Datos", menu=datos_menu) + + # Menu Listar + listar_menu = tk.Menu(self.menu, tearoff=0) + listar_menu.add_command(label="Completo", command=lambda: self.callback("listar_completo")) + listar_menu.add_command(label="Ordenado", command=lambda: self.callback("listar_ordenado")) + self.menu.add_cascade(label="Listar", menu=listar_menu) + + # Menu Buscar + buscar_menu = tk.Menu(self.menu, tearoff=0) + buscar_menu.add_command(label="Título", command=lambda: self.callback("buscar_titulo")) + buscar_menu.add_command(label="Editorial", command=lambda: self.callback("buscar_editorial")) + self.menu.add_cascade(label="Buscar", menu=buscar_menu) + + # Callback externo desde el punto de entrada + self.callback = None + + def show_list(self, books, fields, title="Listado"): + mw = tk.Toplevel(self.root) + mw.title(title) + listbox = tk.Listbox(mw, width=80, height=20) + listbox.pack(side="left", fill="both", expand=True) + scrollbar = tk.Scrollbar(mw) + scrollbar.pack(side="right", fill="y") + listbox.config(yscrollcommand=scrollbar.set) + scrollbar.config(command=listbox.yview) + + for book in books: + row = " | ".join(str(book[field]) for field in fields) + listbox.insert("end", row) + + def ask_text(self, label, callback): + mw = tk.Toplevel(self.root) + mw.title(label) + tk.Label(mw, text=label).pack(pady=5) + entry = ttk.Entry(mw) + entry.pack(pady=5) + ttk.Button(mw, text="Aceptar", command= + lambda: [callback(entry.get()), mw.destroy()]).pack(pady=10) + + def ask_spinbox(self, label, options, callback): + mw = tk.Toplevel(self.root) + mw.title(label) + tk.Label(mw, text=label).pack(pady=5) + spinbox = ttk.Spinbox(mw, values=options, state="readonly", width=40) + spinbox.pack(pady=5) + ttk.Button(mw, text="Aceptar", command= + lambda: [callback(spinbox.get()), mw.destroy()]).pack(pady=10) + + def ask_radiobutton(self, label, options, callback): + mw = tk.Toplevel(self.root) + mw.title(label) + tk.Label(mw, text=label).pack(pady=5) + sv = tk.StringVar(value=options[0]) + for option in options: + tk.Radiobutton(mw, text=option, variable=sv, value=option).pack(anchor="w") + ttk.Button(mw, text="Aceptar", command= + lambda: [callback(sv.get()), mw.destroy()]).pack(pady=10) + + def info(slef, message): + messagebox.showinfo("Información", message) \ No newline at end of file diff --git a/regex-sqlite/ejercicios/ej2/src/db.py b/regex-sqlite/ejercicios/ej2/src/db.py deleted file mode 100644 index aee14b5..0000000 --- a/regex-sqlite/ejercicios/ej2/src/db.py +++ /dev/null @@ -1,39 +0,0 @@ -import sqlite3 -from pathlib import Path - -class DBManager: - def __init__(self, path): - self.conn = sqlite3.connect(path) - - def init(self): - try: - with self.conn: - self.conn.execute( - """ - CREATE TABLE IF NOT EXISTS books ( - isbn INTEGER PRIMARY KEY, - title TEXT, - author TEXT, - year DATE, - publisher TEXT - ); - """ - ) - except Exception as e: - print("Error creating table:", e) - - def insert(self, item): - try: - with self.conn: - self.conn.execute( - """ - INSERT INTO books (isbn, title, author, year, publisher) - VALUES (?, ?, ?, ?, ?); - """, - (item.isbn, item.title, item.author, item.year, item.publisher) - ) - except Exception as e: - print("Error inserting book:", e) - - def close(self): - self.conn.close() \ No newline at end of file diff --git a/regex-sqlite/ejercicios/ej2/src/files.py b/regex-sqlite/ejercicios/ej2/src/files.py deleted file mode 100644 index 1c4dc14..0000000 --- a/regex-sqlite/ejercicios/ej2/src/files.py +++ /dev/null @@ -1,10 +0,0 @@ -import csv -from collections import namedtuple - -nt = namedtuple("Book", ["isbn", "title", "author", "year", "publisher"]) - -def read_file(file): - with open(file, encoding="utf-8") as f: - reader = csv.reader(f, delimiter=";") - next(reader) - return [nt(r[0], r[1], r[2], r[3], r[4]) for r in reader] \ No newline at end of file diff --git a/regex-sqlite/ejercicios/ej2/src/main.py b/regex-sqlite/ejercicios/ej2/src/main.py deleted file mode 100644 index 5d51024..0000000 --- a/regex-sqlite/ejercicios/ej2/src/main.py +++ /dev/null @@ -1,16 +0,0 @@ -from files import read_file -from db import DBManager -from pathlib import Path - -DATA = Path(__file__).parent.parent / "data" - -def main(): - dbm = DBManager(DATA / "books.bd") - dbm.init() - - file_path = DATA / "books.csv" - for book in read_file(file_path): - dbm.insert(book) - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..7175645 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +lxml +bs4 +requests \ No newline at end of file