| #!/usr/bin/env python |
| # |
| # Copyright (C) 2018 The Android Open Source Project |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| """Merge multiple CSV files, possibly with different columns. |
| """ |
| |
| import argparse |
| import csv |
| import io |
| import heapq |
| import itertools |
| import operator |
| |
| from zipfile import ZipFile |
| |
| args_parser = argparse.ArgumentParser( |
| description='Merge given CSV files into a single one.' |
| ) |
| args_parser.add_argument( |
| '--header', |
| help='Comma separated field names; ' |
| 'if missing determines the header from input files.', |
| ) |
| args_parser.add_argument( |
| '--zip_input', |
| help='Treat files as ZIP archives containing CSV files to merge.', |
| action="store_true", |
| ) |
| args_parser.add_argument( |
| '--key_field', |
| help='The name of the field by which the rows should be sorted. ' |
| 'Must be in the field names. ' |
| 'Will be the first field in the output. ' |
| 'All input files must be sorted by that field.', |
| ) |
| args_parser.add_argument( |
| '--output', |
| help='Output file for merged CSV.', |
| default='-', |
| type=argparse.FileType('w'), |
| ) |
| args_parser.add_argument('files', nargs=argparse.REMAINDER) |
| args = args_parser.parse_args() |
| |
| |
| def dict_reader(csvfile): |
| return csv.DictReader(csvfile, delimiter=',', quotechar='|') |
| |
| |
| csv_readers = [] |
| if not args.zip_input: |
| for file in args.files: |
| csv_readers.append(dict_reader(open(file, 'r'))) |
| else: |
| for file in args.files: |
| with ZipFile(file) as zipfile: |
| for entry in zipfile.namelist(): |
| if entry.endswith('.uau'): |
| csv_readers.append( |
| dict_reader(io.TextIOWrapper(zipfile.open(entry, 'r'))) |
| ) |
| |
| if args.header: |
| fieldnames = args.header.split(',') |
| else: |
| headers = {} |
| # Build union of all columns from source files: |
| for reader in csv_readers: |
| for fieldname in reader.fieldnames: |
| headers[fieldname] = "" |
| fieldnames = list(headers.keys()) |
| |
| # By default chain the csv readers together so that the resulting output is |
| # the concatenation of the rows from each of them: |
| all_rows = itertools.chain.from_iterable(csv_readers) |
| |
| if len(csv_readers) > 0: |
| keyField = args.key_field |
| if keyField: |
| assert keyField in fieldnames, ( |
| "--key_field {} not found, must be one of {}\n" |
| ).format(keyField, ",".join(fieldnames)) |
| # Make the key field the first field in the output |
| keyFieldIndex = fieldnames.index(args.key_field) |
| fieldnames.insert(0, fieldnames.pop(keyFieldIndex)) |
| # Create an iterable that performs a lazy merge sort on the csv readers |
| # sorting the rows by the key field. |
| all_rows = heapq.merge(*csv_readers, key=operator.itemgetter(keyField)) |
| |
| # Write all rows from the input files to the output: |
| writer = csv.DictWriter( |
| args.output, |
| delimiter=',', |
| quotechar='|', |
| quoting=csv.QUOTE_MINIMAL, |
| dialect='unix', |
| fieldnames=fieldnames, |
| ) |
| writer.writeheader() |
| |
| # Read all the rows from the input and write them to the output in the correct |
| # order: |
| for row in all_rows: |
| writer.writerow(row) |