往日微忆
文章目录

Twitter的存档备份包,我们可以用 grailbird_update 更新备份(见 https://www.yiwan.pro/index.php/ltd_documents/384-use-grailbird_update-to-backup-tweets.html)。只要双击存档备份包里的 index.html 就能够在浏览器里浏览全部推文历史。但是,这个存档浏览可能不是我喜欢的风格,我也不想需要太多元素,我只需要其中一部分内容即可,这个存档 tweets 里包含的图片还是在 twitter 上存储,我又想下载到本地,我还想在我的博客里展示我一段时间内的 tweets ,…… 这么多需求怎么办?只能定制,于是有了下面我的探索路程:批量导出 Tweets 为指定格式内容,并下载相关图片和视频资源到本地,真正实现个性化、离线化。

备注:相关文件代码已经存到我的 Github repository :extract-tweets (https://github.com/litanid/extract-tweets)。

相关说明

以下是Twitter存档备份包的文件目录结构:

Tweets_Litanid/
├── css
│   └── application.min.css
├── data
│   └── js
│       ├── payload_details.js
│       ├── tweet_index.js
│       ├── tweets
│       │   ├── 2014_10.js
│       │   ├── 2014_11.js
│       │   ├── 2015_03.js
│       │   ├── …………………………
│       │   ├── …………………………
│       │   ├── …………………………
│       │   ├── …………………………
│       │   ├── 2018_02.js
│       └── user_details.js
├── img
│   ├── bg.png
│   └── sprite.png
├── index.html
├── js
│   ├── application.js
│   └── zh-CN.js
├── lib
│   ├── bootstrap
│   │   ├── bootstrap-dropdown.js
│   │   ├── bootstrap.min.css
│   │   ├── bootstrap-modal.js
│   │   ├── bootstrap-tooltip.js
│   │   ├── bootstrap-transition.js
│   │   ├── glyphicons-halflings.png
│   │   └── glyphicons-halflings-white.png
│   ├── hogan
│   │   └── hogan-2.0.0.min.js
│   ├── jquery
│   │   └── jquery-1.8.3.min.js
│   ├── twt
│   │   ├── sprite.png
│   │   ├── sprite.rtl.png
│   │   ├── twt.all.min.js
│   │   └── twt.min.css
│   └── underscore
│       └── underscore-min.js
├── README.md
├── README.txt
└── tweets.csv

tweets 信息存储有两种格式,一种是 .csv 格式,上述结构目录树中的 tweets.csv 便是,另外一种是 json 格式,上述结构目录树中的 2014_10.js2014_11.js等以年份_月份命名的.js 文件便是。刚从 Twitter 官方下载的存档包,两种数据格式都包含所有的推文信息(当然不包含图片和视频)。用 grailbird_update 更新存档备份包,不更新 tweets.csv ,只更新json 数据格式,默认只更新三个文件,一是更新增加修改上述以年份_月份命名的 .js 文件,再就是更新修改 payload_details.jsstweet_index.js 文件。

如从 tweets.csv 里提取 tweets 信息,可以参考 Tweet Archive Logger 仓库(https://github.com/liam-m/TweetArchiveLogger),相关代码见 extract-tweets (https://github.com/litanid/extract-tweets)下的 tweet_archive_logger_by_liam-m.py

在这里,我们要处理的是以年份_月份命名的 .js文件,如 2014_10.js2014_11.js 等,从其中提取 tweets 信息。每个文件包含其中一个月内的全部推文。 .js 文件内 tweet 存储格式以启用 grailbird_update 更新存档备份包时刻为分界点划分为两部分,之前与之后的格式不一样。我是2017年8月24日启用 grailbird_update 更新存档备份包的,所以我的 2017_08.js 文件里包含两种存储格式的 tweets 。之前的我们以 2017_06.js 为例,之后的我们以 2017_10.js 为例。如下图:

区别主要有三点:

1.created_at时间格式不一样, 2017_06.js 里显示的是:2017-06-22 09:31:07 +00002017_10.js 里显示的是:Tue Oct 31 04:52:14 +0000 2017

2.text 显示格式不一样, 2017_06.js 显示的是诸如\u8F66\u7EC8…… 的 unicode 编码, 2017_10.js 里直接显示汉字。

3.如果有多于一张图片, 2017_06.js 里所有图片信息在entities -> media 里, 2017_10.js 则是在 extended_entities -> media里。

所以,处理 tweets 信息代码也是有针对性的区分。

提取tweets并下载图片和视频

我的 Github repository :extract-tweets (https://github.com/litanid/extract-tweets) 里代码文件 extract-tweets_by_DrDrang.py 是 Dr. Drang 在博文 Completing my Twitter archive(http://www.leancrew.com/all-this/2013/01/completing-my-twitter-archive/) 里的代码。代码文件 twitter-export-image-fill_by_MarcinWichary.py 是 mwichary 在他的 Github repository:twitter-export-image-fill (https://github.com/mwichary/twitter-export-image-fill) 里的代码。我改写完善的文件 extract-tweets-to-md_by_litanid_after.pyextract-tweets-to-md_by_litanid_before.py 主要是参考借鉴这两篇文章。带 before 的是针对启用 grailbird_update 更新前存档备份包的 json 文件。此处主要介绍说明 extract-tweets-to-md_by_litanid_after.py 代码文件,以 2017_10.js 作处理示例。

代码按 Python3 要求编写,Python2 需要少许更改。

from datetime import datetime,timezone,timedelta
import sys
import re
import json
import os
import termios  #实现按任意键退出

if (sys.version_info > (3, 0)):
  from urllib.request import urlretrieve
else:
  from urllib import urlretrieve

导入相关库。

#instrp = '%Y-%m-%d %H:%M:%S +0000'     #对应如  2017-12-31 15:15:15 +0000
instrp = '%a %b %d %H:%M:%S +0000 %Y'   #对应如  Wed Nov 29 15:37:26 +0000 2017

按照 2017_10.js 文件里 tweets 的 created_at 显示时间设置相应格式

outstrf = '%Y-%m-%d %H:%M:%S CST UTC+08:00'
urlprefix = 'https://twitter.com/litanid/status/'
outputFile_extension = '.md'
images_vedio_urlprefix = 'https://pich.yiwan.pro/YiWan/TwitterPictures/'

相应输出,设置输出为北京时间、每条 tweet 链接地址的前缀、输出文件的扩展名、下载的多媒体文件链接地址前缀(即下载的图片和视频保存的服务器目录)。

for month_tweets_filename in sys.argv[1:]:
    #取得输出文件的文件名和扩展名
    outputFile_name = ''        #清空重置文件名
    outputFile_name = os.path.basename(month_tweets_filename)
    outputFile_name_temp = outputFile_name
    outputFile_name = os.path.splitext(outputFile_name)[0]
    outputFile_name = outputFile_name + outputFile_extension

输入命令执行处理时,可以一次性输入好几个.js 文件。获取输入的 .js 文件名,如 2017_10.js ,得到 2017_10.md 的输出文件名。

with open(month_tweets_filename) as data_file:
        data_str = data_file.read()
        first_data_line = re.match(r'Grailbird.data.tweets_(.*) =', data_str).group(0)
        data_str = re.sub(first_data_line, '', data_str)
        data_tweets = json.loads(data_str)

data_tweets.reverse()

读取每一个要处理的.js文件内容,去除首行的 Grailbird.data.tweets_2017_10 = ,并将 tweets 反转,按照时间由前至后。

for tweet in data_tweets :
    #处理日期时间显示格式,原时间格式为格林尼治时间,转为北京东八区时间
    created_at = tweet['created_at']
    dt = datetime.strptime(created_at, instrp)
    dt = dt.replace(tzinfo=timezone.utc)
    tzutc_8 = timezone(timedelta(hours=8))
    local_dt = dt.astimezone(tzutc_8)
    local_dt = local_dt.strftime(outstrf)
    local_datetime = local_dt[:19]
    datetime_number = re.sub(r'(\d+)-(\d+)-(\d+) (\d+):(\d+):(\d+)',r'\1\2\3\4\5\6',local_datetime)

读入每一条 tweet 内容,将 tweet 时间转为北京时间。

try:
    if tweet['entities']['urls']:
        for replaceurl in tweet['entities']['urls']:
            url = replaceurl['url']
            expanded_url = replaceurl['expanded_url']
            display_url = replaceurl['display_url']
            display_url = "<a href='" + expanded_url + "' target=\"_blank\">" + display_url + "</a>"
            tweet_text = re.sub(url, display_url, tweet_text)
except KeyError:
    pass

tweet['text']里包含的网址 url 转为原先内容,twitter 默认将它转为系统短链。

media_directory_name = datetime_number[:6]
if not os.path.isdir(media_directory_name):
    os.mkdir(media_directory_name)

建立以年月为名称的文件夹用来保存当月的图片和视频。

if 'extended_entities' in tweet.keys() and 'media' in tweet['extended_entities'].keys(): 
    medialist.extend(tweet['extended_entities']['media'])
if 'retweeted_status' in tweet.keys() and 'extended_entities' in tweet['retweeted_status'].keys() \
    and 'media' in tweet['retweeted_status']['extended_entities'].keys():
    medialist.extend(tweet['retweeted_status']['extended_entities']['media'])
    
medialist = list(dedupe(medialist, key=lambda d: d['media_url_https']))

tweet 文里包含的图片和视频信息存在 tweet['extended_entities']['media']或者tweet['retweeted_status']['extended_entities']['media']里,if 依次判断是否存在这个主键,保险起见将tweet['extended_entities']['media']tweet['retweeted_status']['extended_entities']['media']内容合并在一起,去除重复内容,得到最终的 medialist 列表,每一个列表元素代表一个多媒体。

for media in medialist:
    if media['type']=="photo" :
        media_url_https = media['media_url_https']
        media_filename = os.path.basename(media_url_https)
        # Download the original/best image size, rather than the default one
        better_media_url_https = media_url_https + ':orig'
        local_media_filename = '%s/%sX%s%sH%s' %\
            (media_directory_name, datetime_number, tweet['id'], tweet_image_count+1, media_filename)
        if os.path.isfile(local_media_filename):
            print("\t*********已经下载过这张图片{0},此处略过!".format(media_url_https))
            local_media_filename = images_vedio_urlprefix + local_media_filename
            print("<br /><a href='{0}'><img class='alignnone size-medium' src='{1}'  /></a>".format(local_media_filename,local_media_filename),file = outputFile)
            tweet_image_count = tweet_image_count + 1
            tweet_image_total_count = tweet_image_total_count + 1
        else:
            print("\t*********下载第{0}张图片:{1}".format(tweet_image_count+1,media_url_https))
            try:
                urlretrieve(better_media_url_https, local_media_filename)
            except:
                print("\t*********第{0}张图片({1})下载失败!!".format(tweet_image_count+1,media_url_https))
                tweet_image_failure_count = tweet_image_failure_count + 1
                pass
            else:
                local_media_filename = images_vedio_urlprefix + local_media_filename
                print("<br /><a href='{0}'><img class='alignnone size-medium' src='{1}'  /></a>".format(local_media_filename,local_media_filename),file = outputFile)
                tweet_image_count = tweet_image_count + 1
                tweet_image_total_count = tweet_image_total_count + 1
                print("\t*********第{0}张图片({1})下载成功!!".format(tweet_image_count,media_url_https))

读入 medialist 列表每一个元素,判断多媒体是图片还是视频,下载到同目录的以 tweet 发布时间年月命名的文件夹下(此文件夹最终要拷贝到图片服务器相应目录),同时设置图片显示代码。

elif media['type']=="video" :
    media_image_url_https = media['media_url_https']
    media_image_filename = os.path.basename(media_image_url_https)
    # Download the original/best image size, rather than the default one
    better_media_image_url_https = media_image_url_https + ':orig'
    local_media_image_filename = '%s/%sX%s%sH%s' %\
        (media_directory_name, datetime_number, tweet['id'], tweet_image_count+1, media_image_filename)
    if os.path.isfile(local_media_image_filename):
        print("\t*********已经下载过这张视频封面图{0},此处略过!".format(media_image_url_https))
        local_media_image_filename = images_vedio_urlprefix + local_media_image_filename
        tweet_image_count = tweet_image_count + 1
        tweet_image_total_count = tweet_image_total_count + 1
    else:
        print("\t*********下载第{0}张图片:{1}".format(tweet_image_count+1,media_image_url_https))
        try:
            urlretrieve(better_media_image_url_https, local_media_image_filename)
        except:
            print("\t*********第{0}张图片({1})下载失败!!".format(tweet_image_count+1,media_image_url_https))
            tweet_image_failure_count = tweet_image_failure_count + 1
            pass
        else:
            local_media_image_filename = images_vedio_urlprefix + local_media_image_filename
            tweet_image_count = tweet_image_count + 1
            tweet_image_total_count = tweet_image_total_count + 1
            print("\t*********第{0}张图片({1})下载成功!!".format(tweet_image_count,media_image_url_https))

    video_quality_temp = []
    for video_quality in media['video_info']['variants'] :
        if "bitrate" in video_quality :
            video_quality_temp.append(video_quality)
        else :
            continue
    aftersorted_video_quality=sorted(video_quality_temp,key=lambda quality:quality['bitrate'],reverse=True)
    better_media_video_url_https = aftersorted_video_quality[0]['url']
    better_media_video_filename = os.path.basename(better_media_video_url_https)
    local_media_video_filename = '%s/%sX%s%sH%s' %\
        (media_directory_name, datetime_number, tweet['id'], tweet_video_count+1, better_media_video_filename)
    if os.path.isfile(local_media_video_filename):
        print("\t*********已经下载过这个视频{0},此处略过!".format(better_media_video_url_https))
        local_media_video_filename = images_vedio_urlprefix + local_media_video_filename
        tweet_video_count = tweet_video_count + 1
        tweet_video_total_count = tweet_video_total_count + 1
        print("\n<video src='{0}' controls autoplay loop>  Your browser does not support the <code>video</code> element.</video>\n"\
                .format(local_media_video_filename),file = outputFile)
    else:
        print("\t*********下载第{0}个视频:{1}".format(tweet_video_count+1,better_media_video_url_https))
        try:
            urlretrieve(better_media_video_url_https, local_media_video_filename)
        except:
            print("\t*********第{0}个视频({1})下载失败!!".format(tweet_video_count+1,better_media_video_url_https))
            tweet_video_failure_count = tweet_video_failure_count + 1
            pass
        else:
            print("\t*********第{0}个视频({1})下载成功!!".format(tweet_video_count+1,better_media_video_url_https))
            local_media_video_filename = images_vedio_urlprefix + local_media_video_filename
            tweet_video_count = tweet_video_count + 1
            tweet_video_total_count = tweet_video_total_count + 1
            print("\n<video src='{0}' controls autoplay loop>  Your browser does not support the <code>video</code> element.</video>\n"\
                .format(local_media_video_filename),file = outputFile)

如果是视频,则首先下载视频封面。视频地址保存在media['video_info']['variants']中,有几种码率,选择分辨率最高的。再下载视频文件,设置视频显示代码。

print("<br /><a href='{0}' target=\"_blank\">{1}</a>".format(tweetlink_url,local_dt),file = outputFile)
        print("---",file = outputFile)
        print("---------------------------------------")
print("\n##############")
print("\n本次归档共计成功下载{0}张图片、{1}个视频,下载失败{2}张图片、{3}个视频!".\
    format(tweet_image_total_count,tweet_video_total_count,tweet_image_failure_count,tweet_video_failure_count))
print("\n##############")
print("\n====================文件 {0} 处理完毕====================\n".format(outputFile_name_temp))

每条 tweet 时间下链接设置为 tweet 链接地址,一个 .js 文件处理完毕显示下载了多少张图片和几个视频。

运行显示

用 python3 运行 extract-tweets-to-md_by_litanid_after.py 后面跟随放置在同目录下的 2017_10.js 文件(可以多个文件),运行结果显示如下:

生成同目录下的 2017_10.md文件,内容如下:

将生成的同目录下的201710文件夹拷贝到服务器目录 https://pich.yiwan.pro/YiWan/TwitterPictures/ 下,再在浏览器预览 2017_10.md 文件,结果显示如下:

至此处理完毕。输出 Markdown 文件,当然也可以按自己需求输出为其他格式文件。 extract-tweets-to-wp_by_litanid_after.py 文件运行输出结果可以直接贴到 wordpress 文章代码编辑窗口里,此处不述,详情请看代码。