使用 Jupyter 提高你的时间管理能力

了解如何在 Jupyter 中使用 Python 解析日历来发现你的时间花费。
79 位读者喜欢这篇文章。
Poll: Upcoming open source conferences

Dafne Cholet,CC BY-SA 2.0。

 

Python 为探索数据提供了令人难以置信的可扩展选项。借助 PandasDask,你可以将 Jupyter 扩展到大数据。但是小数据呢?个人数据呢?私有数据呢?

JupyterLab 和 Jupyter Notebook 为审查我基于笔记本电脑的生活提供了一个很好的环境。

我的探索得益于几乎我使用的每个服务都有 Web 应用程序编程接口 (API) 这一事实。我使用了许多此类服务:待办事项列表、时间跟踪器、习惯跟踪器等等。但几乎每个人都在使用一种服务:日历。相同的想法可以应用于其他服务,但日历有一个很酷的功能:几乎所有 Web 日历都支持的开放标准:CalDAV

在 Jupyter 中使用 Python 解析日历

大多数日历都提供导出为 CalDAV 格式的方法。你可能需要一些身份验证才能访问此私有数据。按照你的服务的说明进行操作应该可以解决问题。你如何获取凭据取决于你的服务,但最终,你应该能够将它们存储在一个文件中。我将我的凭据存储在我的根目录中,一个名为 .caldav 的文件中

import os
with open(os.path.expanduser("~/.caldav")) as fpin:
    username, password = fpin.read().split()

永远不要将用户名和密码直接放在笔记本中!它们很容易通过意外的 git push 泄露。

下一步是使用方便的 PyPI caldav 库。我查找了我的电子邮件服务的 CalDAV 服务器(你的可能不同)

import caldav
client = caldav.DAVClient(url="https://caldav.fastmail.com/dav/", username=username, password=password)

CalDAV 有一个称为 principal 的概念。现在深入了解它并不重要,除非要知道它是你用来访问日历的东西

principal = client.principal()
calendars = principal.calendars()

日历,实际上,都是关于时间的。在访问事件之前,你需要确定一个时间范围。一周应该是一个很好的默认值

from dateutil import tz
import datetime
now = datetime.datetime.now(tz.tzutc())
since = now - datetime.timedelta(days=7)

大多数人使用多个日历,并且大多数人都希望将他们的所有事件放在一起。 itertools.chain.from_iterable 使这变得简单:

import itertools

raw_events = list(
    itertools.chain.from_iterable(
        calendar.date_search(start=since, end=now, expand=True) 
        for calendar in calendars
    )
)

将所有事件读入内存很重要,并且以 API 的原始、本机格式执行此操作是一种重要的实践。这意味着,在微调解析、分析和显示代码时,无需返回 API 服务来刷新数据。

但是“原始”并非轻描淡写。事件以特定格式的字符串形式传递

print(raw_events[12].data)
    BEGIN:VCALENDAR
    VERSION:2.0
    PRODID:-//CyrusIMAP.org/Cyrus 
     3.3.0-232-g4bdb081-fm-20200825.002-g4bdb081a//EN
    BEGIN:VEVENT
    DTEND:20200825T230000Z
    DTSTAMP:20200825T181915Z
    DTSTART:20200825T220000Z
    SUMMARY:Busy
    UID:
     1302728i-040000008200E00074C5B7101A82E00800000000D939773EA578D601000000000
     000000010000000CD71CC3393651B419E9458134FE840F5
    END:VEVENT
    END:VCALENDAR

幸运的是,PyPI 再次使用另一个辅助库 vobject 来救援

import io
import vobject

def parse_event(raw_event):
    data = raw_event.data
    parsed = vobject.readOne(io.StringIO(data))
    contents = parsed.vevent.contents
    return contents
parse_event(raw_events[12])
    {'dtend': [<DTEND{}2020-08-25 23:00:00+00:00>],
     'dtstamp': [<DTSTAMP{}2020-08-25 18:19:15+00:00>],
     'dtstart': [<DTSTART{}2020-08-25 22:00:00+00:00>],
     'summary': [<SUMMARY{}Busy>],
     'uid': [<UID{}1302728i-040000008200E00074C5B7101A82E00800000000D939773EA578D601000000000000000010000000CD71CC3393651B419E9458134FE840F5>]}

好吧,至少稍微好一点。

仍然需要做一些工作才能将其转换为合理的 Python 对象。第一步是拥有一个合理的 Python 对象。attrs 库提供了一个不错的起点

import attr
from __future__ import annotations
@attr.s(auto_attribs=True, frozen=True)
class Event:
    start: datetime.datetime
    end: datetime.datetime
    timezone: Any
    summary: str

是时候编写转换代码了!

第一个抽象从解析的字典中获取值,而没有所有的装饰

def get_piece(contents, name):
    return contents[name][0].value
get_piece(_, "dtstart")
    datetime.datetime(2020, 8, 25, 22, 0, tzinfo=tzutc())

日历事件始终有一个开始,但它们有时有一个“结束”,有时有一个“持续时间”。一些仔细的解析逻辑可以将两者统一到同一个 Python 对象中

def from_calendar_event_and_timezone(event, timezone):
    contents = parse_event(event)
    start = get_piece(contents, "dtstart")
    summary = get_piece(contents, "summary")
    try:
        end = get_piece(contents, "dtend")
    except KeyError:
        end = start + get_piece(contents, "duration")
    return Event(start=start, end=end, summary=summary, timezone=timezone)

由于以你的本地时区而不是 UTC 来显示事件是有用的,因此它使用本地时区

my_timezone = tz.gettz()
from_calendar_event_and_timezone(raw_events[12], my_timezone)
    Event(start=datetime.datetime(2020, 8, 25, 22, 0, tzinfo=tzutc()), end=datetime.datetime(2020, 8, 25, 23, 0, tzinfo=tzutc()), timezone=tzfile('/etc/localtime'), summary='Busy')

既然事件是真正的 Python 对象,它们真的应该有一些额外的信息。幸运的是,可以将方法追溯添加到类中。

但是弄清楚事件发生在哪个日期并不那么明显。你需要本地时区中的日期

def day(self):
    offset = self.timezone.utcoffset(self.start)
    fixed = self.start + offset
    return fixed.date()
Event.day = property(day)
print(_.day)
    2020-08-25

事件始终在内部表示为开始/结束,但了解持续时间是一个有用的属性。持续时间也可以添加到现有类中

def duration(self):
    return self.end - self.start
Event.duration = property(duration)
print(_.duration)
    1:00:00

现在是时候将所有事件转换为有用的 Python 对象了

all_events = [from_calendar_event_and_timezone(raw_event, my_timezone)
              for raw_event in raw_events]

全天事件是一种特殊情况,可能对分析生活没有太大用处。目前,你可以忽略它们

# ignore all-day events
all_events = [event for event in all_events if not type(event.start) == datetime.date] 

事件有一个自然的顺序 - 知道哪个事件先发生可能对分析有用

all_events.sort(key=lambda ev: ev.start)

现在事件已排序,可以将它们分成几天

import collections
events_by_day = collections.defaultdict(list)
for event in all_events:
    events_by_day[event.day].append(event)

有了这个,你就有了带有日期、持续时间和序列的 Python 对象形式的日历事件。

用 Python 报告你的生活

现在是时候编写报告代码了!拥有带有适当的标题、列表、重要事情以粗体显示的引人注目的格式很有趣,等等。

这意味着 HTML 和一些 HTML 模板。我喜欢使用 Chameleon

template_content = """
<html><body>
<div tal:repeat="item items">
<h2 tal:content="item[0]">Day</h2>
<ul>
    <li tal:repeat="event item[1]"><span tal:replace="event">Thing</span></li>
</ul>
</div>
</body></html>"""

Chameleon 的一个很酷的功能是它将使用其 html 方法渲染对象。我将以两种方式使用它

  • 摘要将以粗体显示
  • 对于大多数事件,我将删除摘要(因为这是我的个人信息)
def __html__(self):
    offset = my_timezone.utcoffset(self.start)
    fixed = self.start + offset
    start_str = str(fixed).split("+")[0]
    summary = self.summary
    if summary != "Busy":
        summary = "&lt;REDACTED&gt;"
    return f"<b>{summary[:30]}</b> -- {start_str} ({self.duration})"
Event.__html__ = __html__

为了简洁起见,该报告将被切成一天的内容。

import chameleon
from IPython.display import HTML
template = chameleon.PageTemplate(template_content)
html = template(items=itertools.islice(events_by_day.items(), 3, 4))
HTML(html)

渲染后,它看起来像这样

2020-08-25

  • <已编辑> -- 2020-08-25 08:30:00 (0:45:00)
  • <已编辑> -- 2020-08-25 10:00:00 (1:00:00)
  • <已编辑> -- 2020-08-25 11:30:00 (0:30:00)
  • <已编辑> -- 2020-08-25 13:00:00 (0:25:00)
  • 忙碌 -- 2020-08-25 15:00:00 (1:00:00)
  • <已编辑> -- 2020-08-25 15:00:00 (1:00:00)
  • <已编辑> -- 2020-08-25 19:00:00 (1:00:00)
  • <已编辑> -- 2020-08-25 19:00:12 (1:00:00)

Python 和 Jupyter 的无限选择

这只是触及了你可以通过解析、分析和报告各种 Web 服务拥有的数据所能做的事情的表面。

为什么不尝试使用你最喜欢的服务呢?

接下来读什么
标签
Moshe sitting down, head slightly to the side. His t-shirt has Guardians of the Galaxy silhoutes against a background of sound visualization bars.
自 1998 年以来,Moshe 一直参与 Linux 社区,帮助举办 Linux“安装聚会”。自 1999 年以来,他一直在编写 Python 代码,并为核心 Python 解释器做出了贡献。自从这些术语出现之前,Moshe 一直是 DevOps/SRE,他非常关心软件可靠性、构建可重现性以及其他此类事情。

评论已关闭。

Creative Commons License本作品根据 Creative Commons Attribution-Share Alike 4.0 International License 许可。
© . All rights reserved.