Dovahkiin
Dovahkiin

Reputation: 257

What is the best way to modify a text file in-place?

I have a text file, (let's call it 'Potatoes.txt') containing the following info:

Town 1,300,
Town 2,205,
Town 3,600,
Town 4,910,
Town 5,360,

What I want to do is decrease the number for certain towns, and modify the text file accordingly. I did a little research and it appears you can't modify text files, and I need the text file to have the same name, just have different values inside it, so I'm currently doing this instead:

f = open("ModifiedPotatoes.txt","w")
f.close()

with open("Potatoes.txt","r") as file:
    for line in file:
       info = line.split(",")
       if "Town 2" or "Town 4" in line:
           info[1] -= 20
       with open("ModifiedPotatoes.txt","a"):
           infoStr = "\n" + ",".join(str(x) for x in info)
           file.write(infoStr)

f = open("Potatoes.txt","w")
f.close()

with open("ModifedPotatoes.txt","r") as file:
    for line in file:
        with open("Potatoes.txt","a") as potatoesFile:
            potatoesFile.write(line)

So basically I'm just overwriting the old file to a blank one, then copying the value from the modified/temporary file. Is there a better way to do this I'm missing?

Upvotes: 8

Views: 15966

Answers (4)

Jeff K
Jeff K

Reputation: 191

It is possible to open a file for both reading and writing using mode "r+"

data = []
with open("temp", "r+") as inFile:
    for line in inFile:
        ar = line.split(",")
        if ar[0] in ("Town 2", "Town 4"):
            data.append( (ar[0], int(ar[1]) - 20, "\n") )
        else:
            data.append(ar)

    inFile.seek(0)
    for d in data:
        inFile.write(",".join([str(x) for x in d]))
    inFile.truncate()

In order to keep everything clean, I rewind the file after reading it using seek(0), write every line back into it from a buffer, and truncate any remaining part of the file before closing it. I would be interested to know if and when these operations aren't necessary.

This variation doesn't modify (clobber) any other files in the directory, which is a benefit in cases where the code might run simultaneously on different input files. I have no idea if only opening one file one time has any performance benefit, but it probably does to a small degree.

Upvotes: 4

Raymond Hettinger
Raymond Hettinger

Reputation: 226544

I did a little research and it appears you can't modify text files

There is a module that gives you the same effect as modifying text as you loop over it. Try using the fileinput module with the inplace option set to True.

Here is a little Python3.6 code to get you started:

from fileinput import FileInput

with FileInput(files=['Potatoes.txt'], inplace=True) as f:
    for line in f:
        line = line.rstrip()
        info = line.split(",")
        if "Town 2" in line or "Town 4" in line:
            info[1] = int(info[1]) - 20
            line = ",".join(str(x) for x in info))
        print(line)

Upvotes: 6

Paul Rooney
Paul Rooney

Reputation: 21619

You can use the csv module to do the file/string handling.

Just read all the values in and loop over them line by line, making adjustments as required. Then write them back to a new file using a csv.writer object.

import csv
import shutil
import os

with open('potatoes.txt') as f, open('newpotatoes.txt', 'w') as fout:
    rdr = csv.reader(f)
    wrt = csv.writer(fout)

    for line in rdr:
        if line[0] in ('Town 2', 'Town 4'):
            line[1] = str(int(line[1]) - 20)
        wrt.writerow(line)

shutil.copyfile('newpotatoes.txt', 'potatoes.txt')
os.remove('newpotatoes.txt')

The line

line[1] = str(int(line[1]) - 20)

is possibly a little messy. It arises because the values from the csv are all strings. So this is a simple way to convert it to an integer, subtract 20 and convert back to a string.

Looking at your code, there is a mistake in there often made by beginners.

if "Town 2" or "Town 4" in line:

You have to realize that this is combination of two separate statements and is not what you expect. The first statement is just Town 2, which will always evaluate to True. The second statement is "Town 4" in line", which will return True if the string "Town 4" is contained anywhere in the line string.

Your intent was no doubt to test if either string was in line. To do that you need to explicitly test both strings.

if "Town 2" in line or "Town 4" in line:

Will work as expected. You can take it a step further though and cut out some inelegance present in that statement.

You know that string should always occur in the first element of the string, after the split this is info[0] in your code (or line[0] in mine as I let csv do the split).

You can therefore write

if line[0] in ('Town 2', 'Town 4'):

Which I think you'll agree is easier to read and less repetitive typing, especially if you go on to add more Towns.

Upvotes: 1

Jack
Jack

Reputation: 21183

Try:

mod_lines = []

with open("Potatoes.txt", "r") as f:
    for line in f:
        info = line.split(",")
        if info[0] in ("Town 2", "Town 4"):
            info[1] = int(info[1]) - 20
        mod_lines.append(info)

with open("Potatoes.txt", "w") as f:
    for m in mod_lines:
        f.write(",".join([str(x) for x in m]))

This is certainly not the best way, but it's certainly better and works.

Upvotes: 1

Related Questions