Last active
September 23, 2025 14:14
-
-
Save ijstokes/612483dc4988b93e8d149bd93ff31420 to your computer and use it in GitHub Desktop.
Use vobject to convert VCF file exported from Apple Contacts into CSV format ready for Outlook import
This file contains hidden or 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
| ''' | |
| Couldn't believe there wasn't a (working) tool to do this already. | |
| This was quickly thrown together for my own purposes, did the job. | |
| It has some pieces that are quite specific to my circumstance. | |
| (for example, I worked at BCG and have lots of contacts @bcg.com | |
| where email addresses are last.first) | |
| YMMV, but hopefully this helps someone else out. | |
| I put this in the Public Domain. | |
| Ian Stokes-Rees, 2025 | |
| ''' | |
| import csv | |
| import sys | |
| import vobject | |
| def vcf_to_csv(vcf_file, csv_file): | |
| # open input VCF file | |
| with open(vcf_file, 'r') as vcf: | |
| # open output CSV file | |
| with open(csv_file, 'w', newline='') as csvfile: | |
| # "Just the basics" for the output | |
| fieldnames = ['Last Name', 'First Name', 'Email', 'Phone', 'Address', 'Org'] | |
| # Add the headers | |
| writer = csv.DictWriter(csvfile, fieldnames=fieldnames) | |
| writer.writeheader() | |
| # Get a generator object to iterate over from the VCF | |
| vcards = vobject.readComponents(vcf) | |
| for card in vcards: | |
| # Try to get the lastname & firstname from the N entry | |
| try: | |
| last_name = str(card.n.value).split()[-1] if hasattr(card, 'n') else '' | |
| first_name = str(card.n.value).split()[0] if hasattr(card, 'n') else '' | |
| # If that fails, assume it is a business and use the FN entry | |
| except IndexError as e: | |
| print(f'RETRY: card\n{card.prettyPrint()}\n-------------------') | |
| last_name = card.fn.value if hasattr(card, 'fn') else '' | |
| first_name = '' | |
| email = card.email.value if hasattr(card, 'email') else '' | |
| phone = card.tel.value if hasattr(card, 'tel') else '' | |
| address = card.adr.value if hasattr(card, 'adr') else '' | |
| org = card.org.value[0] if hasattr(card, 'org') else '' | |
| # Some entries are backwards, signaled by a comma in the firstname entry, so remove & swap | |
| if ',' in first_name: | |
| new_last = first_name.replace(',','') | |
| first_name = last_name | |
| last_name = new_last | |
| # Some entries have email as the lastname, so try to extract a name from the email address | |
| if '@' in last_name: | |
| username, host = last_name.split('@') | |
| print(f'DEBUG: host {host} org {org}') | |
| if '.' in username: | |
| if 'bcg' in host.lower(): | |
| last_name = username.split('.')[0] | |
| first_name = username.split('.')[1] | |
| else: | |
| first_name = username.split('.')[0] | |
| last_name = username.split('.')[1] | |
| # If there's no ORG but there is an email address, try to figure out org name, excluding the GMails of the world | |
| if not org and email: | |
| try: | |
| username, domain = email.split('@') | |
| company = domain.split('.')[-2] | |
| # Stupid way to try and deal with 2-part TLDs | |
| if company.lower() in ['org', 'ac', 'uk', 'gov']: | |
| company = domain.split('.')[-3] | |
| except: | |
| print(f'DEBUG: invalid email {email}') | |
| continue | |
| # skip the GMails of the world | |
| if company.lower() not in ['gmail', 'yahoo', 'outlook', 'hotmail', 'googlemail', 'verizon', 'me', 'icloud']: | |
| # Heuristic that 3-5 character names are acronyms, so use UPPER, anything Title | |
| if len(company) < 6: | |
| org = company.upper() | |
| else: | |
| org = company.title() | |
| print(f'UPDATED: org {org} from domain {domain}') | |
| # output a single CSV row entry for the VCard | |
| writer.writerow({'Last Name': last_name.title(), | |
| 'First Name': first_name.title(), | |
| 'Email': email, | |
| 'Phone': phone, | |
| 'Address': address, | |
| 'Org': org}) | |
| # good 'ol sys.argv | |
| vcf_to_csv(sys.argv[1], sys.argv[2]) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment