之前从同事那里二手买了个咕咕机,当玩具玩。享受了几天使用 APP 打印各种沙雕照片和表情包带来的乐趣后,它毫无意外的开始吃灰,就和桌子旁边的树莓派一样染上历史的颜色。
还是拿出来用用吧……
咕咕机 APP 里的一部分订阅号的确有点意思,比如订阅每天天气,早上 8 点自动打印一条天气内容之类的。那么拿来订阅微博更新好像也没什么不对的。
反正一天有 12 小时在公司,闲着也是闲着,不定时的来一段小纸条也不错。
开发者申请
为了做到这些,首先要去咕咕机的开发者页面申请 APPKEY。填写必要的信息然后提交申请就行了。等了一周左右申请就通过了。
拿到 APPKEY 之后,可以查阅官方给的文档,可用的 API 其实不多,更多还是要靠自己去生成内容。
GitHub上也有其他开发者写的 SDK,可以不用自己重复按照官方文档造轮子。
RSS
打个比方,订阅小狐狸的 B 站动态。
熟练的打开小狐狸的空间,F12,刷新,过滤 XHR,找到 API 接口https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/space_history
。B 站的动态有多种类型,需要根据类型判断爬取的内容。
['data']['cards']['desc']['type']
的值表明了动态是何种类型,比如 1 是指转发,2 是带图动态,4 是纯文字动态,8 是投稿视频推送。
在 ['data']['cards']['card']
中是一串 JSON 数据,动态的文本内容根据上面不同的类型存在不同的键值里,比如['dynamic']
、['item']['content']
、['item']['description']
这几个位置。
图片存储
只有文本内容的纸条太单调了,信息量也不够,还是需要能够同时打印图片内容。但图片打印会增加打印时间和下载时间,折衷选择只保存附带图片的第一张。
为了保持目录整洁,图片数据全部用 sqlite3 存储。
内容更新
为了避免重复打印浪费纸张,每次爬取数据时需要判断是否有新的内容。
建数据表,只存储必要的时间、日期、文字内容和图片数据,加上动态的 ID 作为主键来进行去重。
CREATE TABLE
IF
NOT EXISTS 白上吹雪Official (
day VARCHAR,
_id VARCHAR PRIMARY KEY UNIQUE,
msg VARCHAR,
pic BLOB,
title VARCHAR);
然后人工插入一条 id 为 0 的数据,用来记录是否获取到新内容,每次先单独查询这一行,有新数据再取出内容进行打印。
POST 请求打印
利用之前提到的 SDK,将文字和图片的复合内容整合后一起 POST 给服务器。
小花样
为了区分普通的带图动态和视频投稿的推送,仿照动态的样式给视频投稿的图像加上了角标。同时因为咕咕机采用的是 WiFi 打印,为了减小文件体积,POST 数据前对图片尺寸进行缩小。
from pymobird import Content
from pymobird import Content
bird = SimplePymobird(appkey, device_id, user_id)
content = Content()
content.add_text('【{}】'.format(uname)) # 用户名
content.add_text(time.strftime('%Y-%m-%d %H:%M',time.gmtime(int(day)))) # 日期
content.add_text('https://t.bilibili.com/{}'.format(_id)) # 链接
content.add_text(msg) # 文字内容
from PIL import Image
from PIL import ImageDraw
from PIL import ImageFont
img = Image.open('peitu.jpg')
img.thumbnail((400, 400), Image.ANTIALIAS)
font = ImageFont.truetype('SourceHanSansCN-Regular.otf', 22)
draw = ImageDraw.Draw(img)
draw.rectangle(
(268, 15, 368, 47), fill=(250, 115, 150), outline=(250, 140, 160))
draw.text((274, 14), '投稿视频', '#FFFFFF', font=font)
img.save('peitu.png', 'PNG')
content.add_image('peitu.png')
result = bird.print_multi_part_content(content)
Crontab 定时任务
开 SSH 把脚本扔进富有历史感的树莓派。
crontab -e
*/5 * * * * python3 bilibili_fubuki.py
完整代码
#!/usr/bin/python
## -*- coding: utf-8 -*-
import json
import os
import re
import requests
import sqlite3
import time
## bilibili
uname = '白上吹雪 Official'
uid = '332704117'
## memobird
appkey = ''
device_id = ''
user_id = ''
headers = {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:69.0) Gecko/20100101 Firefox/69.0',
'Accept': 'application/json, text/plain, */*',
'Accept-Language':
'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2',
'Origin': 'https://space.bilibili.com',
'DNT': '1',
'Connection': 'keep-alive',
'Referer': 'https://space.bilibili.com/',
}
newfile = not os.path.exists('bilibili_{}.sqlite'.format(uname))
db = sqlite3.connect('bilibili_{}.sqlite'.format(uname), isolation_level=None)
cur = db.cursor()
cur.execute('''
CREATE TABLE
IF
NOT EXISTS {} (
day VARCHAR,
_id VARCHAR PRIMARY KEY UNIQUE,
msg VARCHAR,
pic BLOB,
title VARCHAR);'''.format(uname))
if newfile:
cur.execute('''INSERT INTO {} VALUES ( ?, ?, ?, ?, ? );'''.format(uname),
(None, '0', '1', None, None))
def bili_pic(link):
file = requests.get(link, headers=headers)
return file.content
def main():
params = (
('visitor_uid', '0'),
('host_uid', '{}'.format(uid)),
('offset_dynamic_id', '0'),
)
response = requests.get(
'https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/space_history',
headers=headers,
params=params)
result = json.loads(response.content)
if result.get('code') != 0:
print(result.get('message'))
return False
for card in result['data']['cards']:
card_time = card['desc']['timestamp']
card_id = card['desc']['dynamic_id_str']
card_type = card['desc']['type']
data = json.loads(card['card'])
if card_type == 8:
title = data['title']
pic = data['pic']
text = data['dynamic']
elif card_type == 4:
title = None
pic = None
text = data['item']['content']
elif card_type == 2:
title = None
if data['item'].get('pictures'):
pic = data['item']['pictures'][0]['img_src']
text = data['item']['description']
elif card_type == 1:
title = None
pic = None
text = data['item']['content']
else:
print(card_time, card_id, card_type)
continue
img = bili_pic(pic) if pic else None
cur.execute(
'''INSERT OR REPLACE INTO {} VALUES ( ?, ?, ?, ?, ? );'''.format(
uname), (card_time, card_id, text, img, title))
new = cur.execute(
'''SELECT COUNT(_id) FROM {};'''.format(uname)).fetchone()
old = cur.execute(
'''SELECT msg FROM {} WHERE _id = 0;'''.format(uname)).fetchone()
if int(new[0]) == int(old[0]):
return False
cur.execute('''UPDATE {} SET msg = ? WHERE _id = 0'''.format(uname),
(str(new[0]), ))
limit = int(new[0]) - int(old[0])
if limit < 0:
return False
limit = limit if limit < 1 else 1
from pymobird import Content
from pymobird import SimplePymobird
bird = SimplePymobird(ak=appkey, device_id=device_id, user_id=user_id)
data = cur.execute(
'''SELECT day, _id, msg, pic, title FROM {} ORDER BY _id DESC LIMIT {};'''
.format(uname, limit)).fetchall()
for dynamic in data[::-1]:
day, _id, msg, pic, title = dynamic
content = Content()
content.add_text('【{}】'.format(uname))
content.add_text(time.strftime('%Y-%m-%d %H:%M',time.gmtime(int(day))))
content.add_text('https://t.bilibili.com/{}'.format(_id))
content.add_text(msg)
if pic:
with open('{}.jpg'.format(uname), 'wb') as f:
f.write(pic)
img = Image.open('{}.jpg'.format(uname))
img.thumbnail((400, 400), Image.ANTIALIAS)
if title:
from PIL import Image
from PIL import ImageDraw
from PIL import ImageFont
content.add_text(title)
font = ImageFont.truetype('SourceHanSansCN-Regular.otf', 22)
draw = ImageDraw.Draw(img)
draw.rectangle((268, 15, 368, 47),
fill=(250, 115, 150),
outline=(250, 140, 160))
draw.text((274, 14), '投稿视频', '#FFFFFF', font=font)
img.save('{}.png'.format(uname), 'PNG')
content.add_image('{}.png'.format(uname))
result = bird.print_multi_part_content(content)
print(result)
time.sleep(5)
if __name__ == '__main__':
main()