Skip to content

Instantly share code, notes, and snippets.

@TheAntimist
Last active December 17, 2023 21:12
Show Gist options
  • Save TheAntimist/2524c2070de45b0d8deeb6db200acd45 to your computer and use it in GitHub Desktop.
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.
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)
}
}
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()
@leonroy
Copy link

leonroy commented Dec 17, 2023

Fantastic - thank you for this 👏

Had to make one minor amendment to support HSBC QIF files (remove time):

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')

    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}):
        writer.writerow([f"{transaction['date']}".strip(), transaction.get(
            "payee", ""), transaction.get("amount", "")])

    args.infile.close()
    if args.output:
        args.output.close()

then I created the below shell script to iterate over a folder of QIF files:

#!/bin/sh

for f in $(find $1 -name "*.qif"); do
  dirpath=$(dirname "$f")
  filename=$(basename "$f" .qif)
  if [ -f "$dirpath/$filename.csv" ]; then
    echo "Skipping $dirpath/$filename.csv file..."
    continue
  fi
  echo "Processing $f file..."
  python qif2csv.py -o "$dirpath/$filename.csv" $f
done

and run with:

./parse.sh /Volumes/finance/HSBC-Bank-Statements

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment