Вывести список контактов из файла .vcf (VCARD)

Фрагмент файла с расширением .vcf (VCARD)

BEGIN:VCARD
VERSION:2.1
N;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:;=D0=9C=D0=A2=D0=A1;;;
FN;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:=D0=9C=D0=A2=D0=A1
TEL;CELL;PREF:+78002500123
END:VCARD
BEGIN:VCARD
VERSION:2.1
N;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:;=D0=A1=D0=BB=D1=83=D0=B6=D0=B1=D0=B0=20=D1=81=D0=BF=D0=B0=D1=81=D0=B5=
=D0=BD=D0=B8=D1=8F;;;
FN;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:=D0=A1=D0=BB=D1=83=D0=B6=D0=B1=D0=B0=20=D1=81=D0=BF=D0=B0=D1=81=D0=B5=
=D0=BD=D0=B8=D1=8F
TEL;CELL;PREF:112
END:VCARD

Мой код

import vobject

with open('test.vcf') as source_file:
    vcf_read_components = vobject.readComponents(source_file)
    for item in vcf_read_components:
        print(item)

Происходит ошибка парсинга

Traceback (most recent call last):
  File "/home/xxx/experiments/book.py", line 5, in <module>
    for item in vcf_read_components:
  File "/home/xxx/experiments/venv/lib/python3.11/site-packages/vobject/base.py", line 1166, in readComponents
    vline = textLineToContentLine(line, n)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/xxx/experiments/venv/lib/python3.11/site-packages/vobject/base.py", line 984, in textLineToContentLine
    return ContentLine(*parseLine(text, n), **{"encoded": True, "lineNumber": n})
                        ^^^^^^^^^^^^^^^^^^
  File "/home/xxx/experiments/venv/lib/python3.11/site-packages/vobject/base.py", line 864, in parseLine
    raise ParseError("Failed to parse line: {0!s}".format(line), lineNumber)
vobject.base.ParseError: At line 19: Failed to parse line: =D0=BD=D0=B8=D1=8F;;;
<VCARD| [<VERSION{}2.1>, <FN{'CHARSET': ['UTF-8']}МТС>, <N{'CHARSET': ['UTF-8']} МТС   >, <TEL{}+78002500123>]>

Уточнение от 15.11.24, когда у контакта несколько номеров.

BEGIN:VCARD
VERSION:2.1
N;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:=D0=94=D0=BE=D1=80=D1=84;=D0=9C=D0=B0=D0=BA=D1=81;;;
FN;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:=D0=9C=D0=B0=D0=BA=D1=81=20=D0=94=D0=BE=D1=80=D1=84
TEL;CELL:+79128051234
TEL;CELL:+79097484321
END:VCARD
import vobject

with open('test.vcf') as source_file:
    vcf_read_components = vobject.readComponents(source_file, allowQP=True)
    for item in vcf_read_components:
        print(item)
        print(item.fn.value)
        print(item.tel.value)

item.tel.value возвращает только первый из них

<VCARD| [<VERSION{}2.1>, <FN{'CHARSET': ['UTF-8']}Макс Дорф>, <N{'CHARSET': ['UTF-8']} Макс  Дорф >, <TEL{}+79128051234>, <TEL{}+79097484321>]>
Макс Дорф
+79128051234

Ответы (3 шт):

Автор решения: Oopss

=D0=A1=D0=BB=D1=83=D0=B6=D0=B1=D0=B0=20=D1=81=D0=BF=D0=B0=D1=81=D0=B5 ->= = <-Здесь умирает парсер D0=BD=D0=B8=D1=8F;;;

import vobject
import re

s = '''BEGIN:VCARD
VERSION:2.1
N;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:;=D0=9C=D0=A2=D0=A1;;;
FN;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:=D0=9C=D0=A2=D0=A1
TEL;CELL;PREF:+78002500123
END:VCARD
BEGIN:VCARD
VERSION:2.1
N;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:;=D0=A1=D0=BB=D1=83=D0=B6=D0=B1=D0=B0=20=D1=81=D0=BF=
=D0=B0=D1=81=D0=B5=D0=BD=D0=B8=D1=8F;;;
FN;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:=D0=A1=D0=BB=D1=83=D0=B6=D0=B1=D0=B0=20=D1=81=D0=BF=
=D0=B0=D1=81=D0=B5=D0=BD=D0=B8=D1=8F
TEL;CELL;PREF:112
END:VCARD
BEGIN:VCARD
VERSION:2.1
N:;0423;;;
FN:;0423
TEL;CELL:+79272686770
TEL:+79277767901
X-CLASS:private
X-CATEGORIES:
REV:20121127T052113Z
END:VCARD
BEGIN:VCARD
VERSION:2.1
N;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:=D0=94=D0=BE=D1=80=D1=84;=D0=9C=D0=B0=D0=BA=D1=81;;;
FN;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:=D0=9C=D0=B0=D0=BA=D1=81=20=D0=94=D0=BE=D1=80=D1=84
TEL;CELL:+79128051234
TEL;CELL:+79097484321
END:VCARD'''

# Регулярное выражение для поиска частей между BEGIN:VCARD и END:VCARD
pattern = re.compile(r'(BEGIN:VCARD.*?END:VCARD)', re.DOTALL)
pattern_remove_eq = re.compile(r'=\n=')
s = pattern_remove_eq.sub('=', s)

# Разделение строки на части
vcard_parts = pattern.findall(s)

# Вывод каждой части
for i, part in enumerate(vcard_parts, 1):
    print(i, '------------')
    v = vobject.readOne(part)
    v.prettyPrint()

    print(v.n.value)
    print(v.fn.value)
    if len(v.tel_list) > 1:
        #Списком
        print(f'Список {v.tel_list}')
        #Или так по одному
        for t in v.tel_list:
            print(f'Каждый {t.value}')
    else:
        print(v.tel.value)

1 ------------
 VCARD
    VERSION: 2.1
    N:  МТС   
    params for  N:
       CHARSET ['UTF-8']
    FN: МТС
    params for  FN:
       CHARSET ['UTF-8']
    TEL: +78002500123
 МТС   
МТС
+78002500123
2 ------------
 VCARD
    VERSION: 2.1
    N:  Служба спасения   
    params for  N:
       CHARSET ['UTF-8']
    FN: Служба спасения
    params for  FN:
       CHARSET ['UTF-8']
    TEL: 112
 Служба спасения   
Служба спасения
112
3 ------------
 VCARD
    VERSION: 2.1
    N:  0423   
    FN: ;0423
    TEL: +79272686770
    TEL: +79277767901
    X-CLASS: private
    X-CATEGORIES: 
    REV: 20121127T052113Z
 0423   
;0423
Список [<TEL{}+79272686770>, <TEL{}+79277767901>]
Каждый +79272686770
Каждый +79277767901
4 ------------
 VCARD
    VERSION: 2.1
    N:  Макс  Дорф 
    params for  N:
       CHARSET ['UTF-8']
    FN: Макс Дорф
    params for  FN:
       CHARSET ['UTF-8']
    TEL: +79128051234
    TEL: +79097484321
 Макс  Дорф 
Макс Дорф
Список [<TEL{}+79128051234>, <TEL{}+79097484321>]
Каждый +79128051234
Каждый +79097484321
→ Ссылка
Автор решения: strawdog

Для решения подобной проблемы обычно достаточно разрешить обработку quoted-printable данных:

vobject.readComponents(source_file, allowQP=True)
→ Ссылка
Автор решения: ykoavlil

Учел советы (еще раз всем спасибо!) и немного дополнил. Привожу код с комментариями:

import vobject
import datetime

with open('test.vcf') as source_file:
    contacts = vobject.readComponents(source_file, allowQP=True)
    for contact in contacts:
        # Имя, отчество и фамилия
        print(contact.fn.value)

        # Если у контакта указан день рождения выводим его в российском формате даты (DD.MM.YYYY)
        # Через разницу текущая дата и день рождения определяем сколько лет
        # Обрабатываем исключение если данного поля нет
        try:
            birthday = datetime.date.fromisoformat(contact.bday.value)
            today = datetime.date.today()
            years = (today - birthday).days // 365
            print(f'{birthday.strftime("%d.%m.%Y")} ({years} лет)')
        except AttributeError:
            pass

        # Номеров телефонов может быть несколько выводим их все
        for phone_number in contact.tel_list:
            print(phone_number.value)

        # На основе типа определяем просто его вывести (если строка) или нужно склеивать из строк (список)
        # Обрабатываем исключение если его нет
        try:
            for address in contact.adr_list:
                street = address.value.street
                if isinstance(street, str):
                    print(street)
                else:
                    print(''.join(street))
        except AttributeError:
            pass

        print()

Результат:

Василий Петрович
10.12.1964 (59 лет)
+11111111111

Иван Иванович Иванов
+22222222222
+33333333333
Ленина 1 кв. 1
Октябрьская 1 кв. 1
→ Ссылка