use itertools.chain:
import itertools, gzip
files = ['path2zipfile1', 'path2zipfile2']
it = (gzip.open(f, 'rt') for f in files)
for line in itertools.chain.from_iterable(it):
print(line)
another version without itertools:
def gen(files):
for f in files:
fo = gzip.open(f, 'rt')
while True:
line = fo.readline()
if not line:
break
yield line
files = ['path2zipfile1', 'path2zipfile2']
for line in gen(files):
print(line)
Answer from georgexsh on Stack Overflowgzip only supports compressing a single file or stream. In your case, the extracted stream is a tar object, so you'd use Python's tarfile library to manipulate the extracted contents. This library actually knows how to cope with .tar.gz so you don't need to explicitly extract the gzip yourself.
Use Python's tarfile to get the contained files, and then Python's gzip again inside the loop to extract the xml.
gzip - Python unzip multiple .gz files - Stack Overflow
linux - How to unzip multiple gz files in python using multi threading? - Stack Overflow
How do i gzip multiple files using python - Stack Overflow
zip - how to read multiple .gz files in a particular directory in python without unzipping them - Stack Overflow
First, get the list of all files.
files = ['/path/to/foo.txt.gz.001', '/path/to/foo.txt.gz.002', '/path/to/foo.txt.gz.003']
Then iterate over each file and append to a result file.
with open('./result.gz', 'ab') as result: # append in binary mode
for f in files:
with open(f, 'rb') as tmpf: # open in binary mode also
result.write(tmpf.read())
Then extract is using zipfile lib. You could use tempfile to avoid handle with temporary zip file.
First you must extract all the zip files sequentially:
import zipfile
paths = ["path_to_1", "path_to_2" ]
extract_paths = ["path_to_extract1", "path_to_extrac2"]
for i in range(0, paths):
zip_ref = zipfile.ZipFile(paths[i], 'r')
zip_ref.extractall(extract_paths[i])
zip_ref.close()
Next you can go to the extracted location and read() individual files with open into a string. Concatenate those strings and save to foo.txt.
You can do this very easily with multiprocessing Pools:
import gzip
import multiprocessing
import shutil
filenames = [
'a.gz',
'b.gz',
'c.gz',
...
]
def uncompress(path):
with gzip.open(path, 'rb') as src, open(path.rstrip('.gz'), 'wb') as dest:
shutil.copyfileobj(src, dest)
with multiprocessing.Pool() as pool:
for _ in pool.imap_unordered(uncompress, filenames, chunksize=1):
pass
This code will spawn a few processes, and each process will extract one file at a time.
Here I've chosen chunksize=1, to avoid stalling processes if some files are bigger than average.
A large segment of of the wall clock time spent decompressing a file with gunzip or gzip -d will be from the I/O operations (reading and writing to disk). It might even be more than the time spent actually decompressing data. You can take advantage of this by having multiple gzip jobs going in the background. As some jobs are blocked on I/O, another job can actually run without having to wait in a queue.
You can speed up the decompressing of the entire file set by having multiple gunzip processes running in the background. Each serving a specific set of files.
You can whip up something easy in BASH. Split the file list into separate commands and use the & to start it as a background job. Then wait for each each job to finish.
I would recommend that you have between 2 to 2*N jobs going at once. Where N is the number of cores or logical processors on your computer. Experiment as appropriate to get the right number.
You can whip something up easy in BASH.
#!/bin/bash
argarray=( "
{#argarray[@]}
#declare 4 empty array sets
set1=()
set2=()
set3=()
set4=()
# enumerate over each argument passed to the script
# and round robin add it to one of the above arrays
i=0
while [
len ]
do
if [
len ]; then
set1+=( "${argarray[$i]}" )
((i++))
fi
if [ $i -lt $len ]; then
set2+=( "${argarray[$i]}" )
((i++))
fi
if [ $i -lt $len ]; then
set3+=( "${argarray[$i]}" )
((i++))
fi
if [ $i -lt $len ]; then
set4+=( "${argarray[$i]}" )
((i++))
fi
done
# for each array, start a background job
gzip -d ${set1[@]} &
gzip -d ${set2[@]} &
gzip -d ${set3[@]} &
gzip -d ${set4[@]} &
# wait for all jobs to finish
wait
In the above example, I picked 4 files per job and started two separate jobs. You can easily extend the script to have more jobs, more files per process, and to take the file names as command line parameters.
Suppose you list the files in a directory and compress each one. Here is code that builds off one of the gzip examples
import pathlib
import gzip
import shutil
target_dir = pathlib.Path('/home/joe/')
for child in target_dir.iterdir():
# Skip directories
if child.is_dir():
continue
# You could also check for just .txt files here if desired
with child.open('rb') as f_in:
with gzip.open(str(child.with_suffix(".txt.gz")), 'wb') as f_out:
shutil.copyfileobj(f_in, f_out)
With a directory structure like
/home/joe/
|-- subdir
|-- a.txt
|-- b.txt
|-- c.txt
This snippet will generate
/home/joe
|-- subdir
|-- a.txt
|-- a.txt.gz
|-- b.txt
|-- b.txt.gz
|-- c.txt
|-- c.txt.gz
If you want to remove the original files you could add child.unlink() after the with context.
I've to convert all the .txt to .gz files.
Take look at glob built-in module, especially glob.glob function. Consider following example
import glob
for filename in glob.glob("*.txt"):
print(filename)
It does print all names of .txt files inside current working directory. You need to add calculating new name to it (e.g. newname = filename + ".gz") and mate it with your code for gzip-ing and it will then apply that action to every .txt file.
Since the split files do not need to be readable text files, I would read & write in chunks of bytes, not in lines. This should be faster than reading and writing line by line. We can go with a chunk size of 4,096 bytes because 4,096 or 8,192 is a typical block size on most file systems.
Although, I should note that when I tested your script on my system with a 9 GB text file which has 81 characters per line, it took about 73 seconds to split it into files of 10,000,000 lines each, or about 772 MB per file. The fact that it takes 12 minutes on your laptop likely confirms @Graipher's suspicion that your disk's write speed is the bottleneck here.
Using a refactored version of your script that reads & writes in byte chunks (provided at the end of this review), when I ran it on the same file with --size 772 (split into files of size 772 MB) it took about 35 seconds. Still a decent improvement, but I'd be curious to know how it runs on your machine.
To implement this, I personally like having a helper method like the following which tells us the chunk sizes to read/write:
from typing import Iterator
def chunk_sizes(total_bytes: int, chunk_size: int) -> Iterator[int]:
for _ in range(total_bytes // chunk_size):
yield chunk_size
if remaining_bytes := total_bytes % chunk_size:
yield remaining_bytes
For example, list(chunk_sizes(10000, 4096)) which returns [4096, 4096, 1808] would tell us that in order to read 10,000 bytes in 4,096 byte chunks, we should read 4,096 bytes, followed by another 4,096 bytes, followed by 1,808 bytes.
General review
There's no reason to have
run()be astaticmethod, and it's weird howFileSplittercreates an instance of itself inside this method.If you change the top-level invocation to
FileSplitter().run()thenrun()could be simplified todef run(self): self.split()But if you do that you might as well remove
run()and makeFileSplitter().split()the top-level call. My recommendation is to move the contents ofrun()under the__name__ == "__main__"guard and then removerun()entirely as it's no longer needed.Always open files with a
withblock to ensure the file is closed properly.I recommend using
argparsebecause you get a lot of stuff for free (input type validations, help/usage message with-h, etc.).I would move the command-line argument parsing logic out of
FileSplitter, because it should ideally only have one responsibility: splitting files.Specify the exact exception you want to handle with a try-except. Handling a bare
exceptis considered a bad practice.I wouldn't use
sys.exitinside methods you might want to test, because it exits the Python interpreter.In Python 3, f-strings are a much more legible way of doing string interpolation.
Before
new_file_name = "%s.%s" % (self.file_base_name, str(file_number))After
new_file_name = f"{self.file_base_name}.{file_number}"
Refactored version
#!/usr/bin/env python3
import argparse
import gzip
import os
import itertools
from typing import BinaryIO, Iterator
CHUNK_SIZE = 4096 # bytes
def megabytes_to_bytes(megabytes: int) -> int:
return megabytes * 1024 * 1024
def chunk_sizes(total_bytes: int, chunk_size: int) -> Iterator[int]:
for _ in range(total_bytes // chunk_size):
yield chunk_size
if remaining_bytes := total_bytes % chunk_size:
yield remaining_bytes
class FileSplitter:
input_file_path: str
output_directory: str
output_file_base_name: str
split_size: int # size of each partition, in MB
def __init__(
self, input_file_path: str, output_directory: str, split_size: int
) -> None:
self.input_file_path = input_file_path
self.output_directory = output_directory
name = os.path.basename(input_file_path)
name_without_file_extension = os.path.splitext(name)[0]
self.output_file_base_name = name_without_file_extension
self.split_size = split_size
def get_new_file(self, file_number: int) -> BinaryIO:
new_file_name = f"{self.output_file_base_name}.{file_number}"
new_file_path = os.path.join(self.output_directory, new_file_name)
return open(new_file_path, "wb")
def split(self) -> None:
bytes_per_file = megabytes_to_bytes(self.split_size)
os.makedirs(self.output_directory, exist_ok=True)
with gzip.open(self.input_file_path, mode="rb") as in_file:
for file_number in itertools.count(start=1, step=1):
with self.get_new_file(file_number) as out_file:
for chunk_size in chunk_sizes(bytes_per_file, CHUNK_SIZE):
chunk = in_file.read(chunk_size)
# chunk == "" means we finished reading the input file
if not chunk:
return
out_file.write(chunk)
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Read a gzip-compressed file "
"and split its uncompressed contents into separate files."
)
parser.add_argument(
"file_name", help="a gzip-compressed file, e.g. foo.gz"
)
parser.add_argument(
"--size",
type=int,
default=500,
help="desired size of each partition, in MB",
)
args = parser.parse_args()
output_directory = os.path.join(os.getcwd(), "pychunks")
splitter = FileSplitter(args.file_name, output_directory, args.size)
splitter.split()
What would you change in this code?
I'd simplify the split function. Not counting lines and files myself and opening/closing the output files at different places without a with statement. Just always try to read another line and if that succeeds, put it and the next up to split_size - 1 lines into another output file.
def split(self):
print("Splitting %s into multiple files with %s lines" % (os.path.join(self.working_dir, self.file_base_name), str(self.split_size)))
with gzip.open(self.in_file, 'rt') as fin:
for file_number, line in enumerate(fin, start=1):
with self.get_new_file(file_number) as fout:
fout.write(line)
fout.writelines(itertools.islice(fin, self.split_size - 1))
print("Created %s files." % (str(file_number)))
While I do frequently use f when I only work on one file, I didn't like the very different names f and out_file. In such cases I use fin and fout (and I think that's rather common), so I changed that as well.
I'd probably also change the inner with to with open(...) as fout and have get_new_file return only the filename, not open the file itself. I'd rather keep the open in the with statement and have get_new_file be a function without such side effects. Or if your only reason for having get_new_file at all was that you had two places for using it, then now you don't need it anymore and could just build the filename inside the split function.
Still unfixed issues in your code:
- While you fixed one
line_number = 1(from your original) toline_number = 0, you forgot the other one. This makes your chunks one line too short. Wouldn't happen if you only had one place :-) - Lacking import of
sys, causingNameError: name 'sys' is not defined. - Lacking creation of folder
pychunks. Without that, the user gets a rather confusing traceback andFileNotFoundError: [Errno 2] No such file or directory: '/home/runner/p/pychunks/main.py.gz.1'. t = next(a_file)loses the first line, as that is never used. Might be what you want, but it's not documented and goes against common sense and the description that is there and gets shown when you run the script without arguments. Perhaps if you do want that, make it a command-line argument for skipping the first number of lines, with default0.
Yes. Use z = zlib.decompressobj(31), and then use z to decompress until z.unused_data is not empty, or you have processed all of the input. If you get z.unused_data as not empty, then it contains the start of the next gzip stream. Create a new y = zlib.decompressobj object, and start decompression with the contents of z.unused_data, continuing with more data from the file.
This prints the uncompressed size of each concatenated gzip component:
#!/usr/bin/python
import sys
import zlib
z = zlib.decompressobj(31)
count = 0
while True:
if z.unused_data == "":
buf = sys.stdin.read(8192)
if buf == "":
break
else:
print count
count = 0
buf = z.unused_data
z = zlib.decompressobj(31)
got = z.decompress(buf)
count += len(got)
print count
@MarkAdler Thank you very much for this answer. It actually helped me quite a bit !
Now I just want to add a tiny detail that can save a lot of your time. The current answer will not detect truncated files such as gzip/zcat would.
zcat file.gz
gzip: file.gz: unexpected end of file
To correct this, check decompress.oef. If False, this means the gzip file is truncated. If you don't do this, you'll never see an error.
Here is the modified code:
#!/usr/bin/python
import sys
import zlib
z = zlib.decompressobj(31)
count = 0
while True:
if z.unused_data == "":
buf = sys.stdin.read(8192)
if buf == "":
# check truncated file
if not z.eof:
raise RuntimeError("unexpected end of file")
break
else:
print count
count = 0
buf = z.unused_data
z = zlib.decompressobj(31)
got = z.decompress(buf)
count += len(got)
print count
I have millions of zip files (.gz extension) that I need EXTRACTED. Each of the zip files has one .txt file in them and I need that .txt file to use in data analysis. Unzipping them manually would take weeks, does anyone have an easily modifiable script out there that they are willing to share? I have very little experience with Python. Thank you in advance.
Edit: should have said "Extract" instead of "unzip."
Why over engineer? Simply gunzip -r a directory of zipped files:
gunzip -r ./directory_of_zipped_files/
EDIT: Sorry ... now noted that you said windows. This is a *nix solution.
Can you install cygwin? If so, you may be able to just do something simple from the commandline using xargs and a commandline extractor.
I believe that's because gzip never operates over directories, it acts as a compression algorithm unlike zip and tar where we could compress directories. python's implementation of gzip is to operate on files. However recursive traversal of a directory tree is easy if we look at the os.walk call.
(I haven't tested this)
def gunzip(file_path,output_path):
with gzip.open(file_path,"rb") as f_in, open(output_path,"wb") as f_out:
shutil.copyfileobj(f_in, f_out)
def recurse_and_gunzip(root):
walker = os.walk(root)
for root,dirs,files in walker:
for f in files:
if fnmatch.fnmatch(f,"*.gz"):
gunzip(f,f.replace(".gz",""))
It may not answer this specific question, but for those looking to extract a gzipped directory structure: that would be a job for shutil.unpack_archive.
For example:
import shutil
shutil.unpack_archive(
filename='path/to/archive.tar.gz', extract_dir='where/to/extract/to'
)
Hi All,
I am a beginner with Python trying to solve a few problems with my daily workflow in a non-tech job. I recently wrote a script that I feel could use some improvement, but I am not knowledgeable enough to know how to make it more efficient.
Background: I sometimes need to download multiple large compressed .txt.gz files from our server and convert them into .csv before I offload them to other business units for review. Instead of extracting manually and pasting into Excel, I wanted to write a script to take care of it. Here is what I came up with after some YouTube and StackOverflow searching (the delimiter in these txt files is a '|' instead of the standard ',':
import csv
import shutil
import gzip
import os
src_dir = 'C:\\Users\\user\\Downloads\\'
dest_dir = 'C:\\Users\\user\\Desktop\\Python\\extractedgzs\\'
f_names = []
# get file names
for files in os.listdir(src_dir):
if files.endswith('.txt.gz'):
f_names.append(files)
#file found confirmation
print('found these files:')
print(f_names)
# unzip gz file to dest dir
for name in f_names:
with gzip.open(src_dir+name, 'rb') as f_in:
with open(dest_dir+name[0:-6], 'wb') as f_out:
shutil.copyfileobj(f_in, f_out)
# extracted file to .csv
with open(dest_dir+name[0:-6], 'r') as in_file:
stripped = (line.strip() for line in in_file)
lines = (line.split("|") for line in stripped if line)
with open(dest_dir+name[0:-6]+'csv', 'w', newline='') as out_file:
writer = csv.writer(out_file)
writer.writerows(lines)I understand that this code is rather rigid but that it is fine since I am the only person who will ever use it and it only needs to serve this one purpose.
The main issue I have with this is that I generate an text file that is used up by the splitter/csv writer for writing the csv. I was wondering if there is any way to eliminate this step, or at the very least automate deleting that file once all the csv's are written.
I appreciate any tips you can offer!
Your iteration over the file is outside of any try... except, so an exception raised here will terminate the program. If you have a single try...except around the whole thing, then it should work:
for file in files:
try:
with gzip.open(file,'rb') as fin:
for line in fin:
temp = line.decode().split(",")
a,b,c,d = temp[0], int(temp[1]), int(temp[2]), int(temp[3])
except (OSError, ValueError):
continue
Note also:
- Only catching the specific exceptions that we would expect to occur with a bad file, not other things that should still terminate the program (e.g.
KeyboardInterrupt). A bareexcept:is usually a bad idea. - It is better to use a
withconstruct withgzip.open
I have splitted the file processing part into a separate function to handles exceptional cases during processing each file.
def proc_file(file):
try:
fin=gzip.open(file,'rb')
except:
return
err_cnt=0
while err_cnt<10:
try:
line=fin.readline()
except:
err_cnt+=1
continue
if not line:
err_cnt+=1
continue
try:
temp=line.decode().split(",")
a,b,c,d=temp[0],int(temp[1]),int(temp[2]),int(temp[3])
except:
continue
return processed_value
for file in files:
result=[]
try:
value=proc_file(file)
except:
continue
result.append(value)