Last active
December 17, 2023 21:12
-
-
Save TheAntimist/2524c2070de45b0d8deeb6db200acd45 to your computer and use it in GitHub Desktop.
Convert QIF / MS Money files into CSV. Two Scripts have been provided one in python and another in golang.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"bufio" | |
"encoding/csv" | |
"fmt" | |
"os" | |
"strings" | |
) | |
func parseQifFile(filename string, output string) error { | |
FIELDS := map[string]string{ | |
"D": "date", | |
"T": "amount", | |
"P": "payee", | |
"L": "category", | |
"N": "number", | |
"M": "memo"} | |
transactions := make([]map[string]string, 0) | |
infile, err := os.Open(filename) | |
if err != nil { | |
return err | |
} | |
defer infile.Close() | |
scanner := bufio.NewScanner(infile) | |
scanner.Split(bufio.ScanLines) | |
transaction := make(map[string]string) | |
for i := 0; scanner.Scan(); i++ { | |
line := strings.TrimSpace(scanner.Text()) | |
if len(line) == 0 { | |
continue | |
} | |
field_id := string(line[0]) | |
if field_id == "^" { | |
if len(transaction) > 0 { | |
transactions = append(transactions, transaction) | |
} | |
transaction = make(map[string]string) | |
} else if _, ok := FIELDS[field_id]; ok { | |
transaction[FIELDS[field_id]] = line[1:] | |
} | |
// else if len(line) > 0 { | |
// transaction[fmt.Sprintf("%d", i)] = line | |
// } | |
} | |
outfile, err := os.Create(output) | |
if err != nil { | |
return err | |
} | |
w := csv.NewWriter(outfile) | |
if err := w.Write([]string{"Date Time", "Comment", "Amount", "Ref Number"}); err != nil { | |
return err | |
} | |
for _, val := range transactions { | |
time := strings.Replace(val["memo"], "TXN TIME ", "", 1) | |
date_time := fmt.Sprintf("%s %s", val["date"], time) | |
if len(time) == 0 { | |
date_time = val["date"] | |
} | |
if err := w.Write([]string{date_time, val["payee"], val["amount"], val["number"]}); err != nil { | |
return err | |
} | |
} | |
// Write any buffered data to the underlying writer (standard output). | |
w.Flush() | |
if err := w.Error(); err != nil { | |
return err | |
} | |
fmt.Printf("Wrote %s file with %d transactions\n", output, len(transactions)) | |
return nil | |
} | |
func main() { | |
if len(os.Args) != 3 { | |
fmt.Printf(`Not enough arguments passed. | |
Usage: %s input-file output-file | |
`, os.Args[0]) | |
os.Exit(1) | |
} | |
if err := parseQifFile(os.Args[1], os.Args[2]); err != nil { | |
fmt.Println("Got error during execution ", err.Error()) | |
os.Exit(1) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import argparse | |
import csv | |
import os | |
import sys | |
from collections import OrderedDict | |
def parse_lines(lines, config, options=None): | |
"""Return list of transactions as ordered dicts with fields save in same | |
order as they appear in input file. | |
""" | |
if not options: | |
options = {} | |
res = [] | |
transaction = OrderedDict() | |
for (idx, line) in enumerate(lines): | |
line = line.strip() | |
if not line: | |
continue | |
field_id = line[0] | |
if field_id == '^': | |
if transaction: | |
res.append(transaction) | |
transaction = OrderedDict([]) | |
elif field_id in list(config["FIELDS"].keys()): | |
transaction[config["FIELDS"][field_id]] = line[1:] | |
elif line: | |
transaction['%s' % idx] = line | |
if len(list(transaction.keys())): | |
res.append(transaction) | |
# post-check to not interfere with present keys order | |
for t in res: | |
for field in list(config["FIELDS"].values()): | |
if field not in t: | |
t[field] = None | |
t['filename'] = options.get('src', '') | |
return res | |
if __name__ == "__main__": | |
parser = argparse.ArgumentParser( | |
description='Convert MS Money files to CSV') | |
parser.add_argument("infile", type=argparse.FileType( | |
'r'), help="File to convert to CSV") | |
parser.add_argument("-o", "--output", help="Output file", | |
type=argparse.FileType('w')) | |
args = parser.parse_args() | |
f = args.output if args.output else sys.stdout | |
writer = csv.writer(f, dialect='excel') | |
writer.writerow(['Date Time', 'Comment', 'Amount', 'Ref Number']) | |
config = { | |
"FIELDS": {'D': 'date', 'T': 'amount', 'P': 'payee', 'L': 'category', | |
'N': 'number', 'M': 'memo'}, | |
"EXTRA_FIELDS": {'F': 'filename'} | |
} | |
_, filename = os.path.split(args.infile.name) | |
for transaction in parse_lines(args.infile.readlines(), config, options={'src': filename}): | |
time = transaction.get("memo", "").replace("TXN TIME ", "").strip() | |
writer.writerow([f"{transaction['date']} {time}".strip(), transaction.get( | |
"payee", ""), transaction.get("amount", ""), transaction.get("number", "")]) | |
args.infile.close() | |
if args.output: | |
args.output.close() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Fantastic - thank you for this 👏
Had to make one minor amendment to support HSBC QIF files (remove
time
):then I created the below shell script to iterate over a folder of QIF files:
and run with:
./parse.sh /Volumes/finance/HSBC-Bank-Statements