I’ve been trying to create a program which allows users to view a text file’s contents and delete some or all of a single entry block.
An example of the text’s file contents can be seen below:
Special Type A Sunflower 2016-10-12 18:10:40 Asteraceae Ingredient in Sunflower Oil Brought to North America by Europeans Requires fertile and moist soil Full sun Pine Tree 2018-12-15 13:30:45 Pinaceae Evergreen Tall and long-lived Temperate climate Tropical Sealion 2019-01-20 12:10:05 Otariidae Found in zoos Likes fish Likes balls Likes zookeepers Big Honey Badger 2015-06-06 10:10:25 Mustelidae Eats anything King of the desert
As such, the entry block refers to all lines without a horizontal space.
Currently, my progress is at:
import time import os global o global dataset global database from datetime import datetime MyFilePath = os.getcwd() ActualFile = "creatures.txt" FinalFilePath = os.path.join(MyFilePath, ActualFile) def get_dataset(): database = [] shown_info = [] with open(FinalFilePath, "r") as textfile: sections = textfile.read().split("nn") for section in sections: lines = section.split("n") database.append({ "Name": lines[0], "Date": lines[1], "Information": lines[2:] }) return database def delete_creature(): dataset = get_dataset() delete_question = str(input("Would you like to 1) delete a creature or 2) only some of its information from the dataset or 3) return to main page? Enter 1, 2 or 3: ")) if delete_question == "1": delete_answer = str(input("Enter the name of the creature: ")) for line in textfile: if delete_answer in line: line.clear() elif delete_question == "2": delete_answer = str(input("Enter the relevant information of the creature: ")) for line in textfile: if delete_answer in line: line.clear() elif delete_question == "3": break else: raise ValueError except ValueError: print("nPlease try again! Your entry is invalid!") while True: try: option = str(input("nGood day, This is a program to save and view creature details.n" + "1) View all creatures.n" + "2) Delete a creature.n" + "3) Close the program.n" + "Please select from the above options: ")) if option == "1": view_all() elif option == "2": delete() elif option == "3": break else: print("nPlease input one of the options 1, 2 or 3.") except: break
The delete_function() is meant to delete the creature by:
- Name, which deletes the entire text block associated with the name
- Information, which deletes only the line of information
I can’t seem to get the delete_creature() function to work, however, and I am unsure of how to get it to work.
Does anyone know how to get it to work?
Many thanks!
Advertisement
Answer
Your problem with removing lines from a section is that you specifically hardcoded which line represents what. Removing a section in your case will be easy, removing a line will, if you do not change your concept, involve setting the line in question to empty or to some character representing the empty string later. Another question here is, do you need your sections to remain ordered as they were entered, or you can have them sorted back in a file in some other order.
What I would do is to change the input file format to e.g. INI file format. Then you can use the configparser module to parse and edit them in an easy manner. The INI file would look like:
[plant1] name="Some plant's English name" species="The plant's Latin species part" subspecies="The plant's subspecies in Latin ofcourse" genus="etc." [animal1] # Same as above for the animal # etc. etc. etc.
configparser.ConfigParser()
will let you load it in an dictionarish manner and edit sections and values. Sections you can name animal1, plant1, or use them as something else, but I prefer to keep the name inside the value, usually under the name key, then use configparser to create a normal dictionary from names, where its value is another dictionary containing key-value pairs as specified in the section. And I reverse the process when saving the results. Either manually, or using configparser again.
The other format you might consider is JSON, using the json module. Using its function dumps() with separators and indentation set correctly, you will get pretty human-readable and editable output format. The nice thing is that you save the data structure you are working with, e.g. dictionary, then you load it and it comes back as you saved it, and you do not need to perform some additional stuff to get it done, as with configparser. The thing is, that an INI file is a bit less confusing for an user not custom to JSON to construct, and results in less errors, while JSON must be strictly formatted, and any errors in opening and closing the scopes or with separators results in whole thing not working or incorrect input. And it easily happens when the file is big.
Both formats allows users to put empty lines wherever they want and they will not change the way the file will be loaded, while your method is strict in regard to empty lines.
If you are expecting your database to be edited only by your program, then use the pickle module to do it and save yourself the mess.
Otherwise you can:
def getdata (stringfromfile): end = {} l = [] # lines in a section for x in stringfromfile.strip().splitlines(): x = x.strip() if not x: # New section encountered end[l[0].lower()] = l[1:] l = [] continue end.append(x) end[l[0].lower()] = l[1:] # Add last section # Connect keys to numbers in the same dict(), so that users can choose by number too for n, key in enumerate(sorted(end)): end[n] = key return end # You define some constants for which line is what in a dict(): values = {"species": 0, "subspecies": 1, "genus": 2} # You load the file and parse the data data = getdata(f.read()) def edit (name_or_number, edit_what, new_value): if isinstance(name_or_number, int): key = data[name_or_number] else: key = name_or_number.lower().strip() if isinstance(edit_what, str): edit_what = values[edit_what.strip().lower()] data[key][edit_what] = new_value.strip() def add (name, list_of_lines): n = len(data)/2 # Number for new entry for numeric getting name = name.strip().lower() data[name] = list_of_lines data[n] = name def remove (name): name = name.lower().strip() del data[name] # Well, this part is bad and clumsy # It would make more sense to keep numeric mappings in separate list # which will do this automatically, especially if the database file is big-big-big... # But I started this way, so, keeping it simple and stupid, just remap everything after removing the item (inefficient as hell itself) for x in data.keys(): if isinstance(x, int): del data[x] for n, key in enumerate(sorted(data)): data[n] = key def getstring (d): # Serialize for saving end = [] for l0, ls in d.items(): if isinstance(l0, int): continue # Skip numeric mappings lines = l0+"n"+"n".join(ls) end.append(lines) return "nn".join(end)
I didn’t test the code. There might be bugs.
If you need no specific lines, you can modify my code easily to search in the lines using the list.index() method, or just use numbers for the lines if they exist when you need to get to them. For doing so with configparser, use generic keys in a section like: answer0, answer1…, or just 0, 1, 2…, Then ignore them and load answers as a list or however. If you are going to use configparser to work on the file, you will get sometimes answer0, answer3… when you remove.
And a warning. If you want to keep the order in which input file gives the creatures, use ordereddict instead of the normal dictionary.
Also, editing the opened file in place is, of course, possible, but complicated and inadvisable, so just don’t. Load and save back. There are very rare situations when you want to change the file directly. And for that you would use the mmap module. Just don’t!