此文已由作者张耕源授权网易云社区发布。
欢迎访问网易云社区,了解更多网易技术产品运营经验。
自从公司的易信公众服务号有了查询今日菜单的功能,自己慢慢养成了每次去吃饭前查一 下各个窗口的菜谱,再决定去哪吃饭的习惯。
不过这个功能使用的越多,越来越觉得它不方便。目前在易信公众号查询菜单的步骤是:
打开易信
打开网易精灵公众号
点击便捷服务
点击今日菜单
等待返回今日菜单的入口链接
点击入口链接查看今日菜单
作为固定的每天至少要操作两次的动作,整个流程是被动的、并且有点复杂了。特别是 第五步、第六步都需要网络访问,如果手机网络访问不稳定(WiFi、4G信号不好等,在 坐、等电梯时很容易碰到这个情况),其中任何一步都会卡住导致无法查询;还有一些 同学由于种种原因根本就没有关注网易精灵公共号,无从查看今日菜单。
所以我就想,有没有更简单的办法,直接把每日菜单内容直接主动推送到手机,只需要 简单的点一下就能查看菜单了呢:
点击推送消息
查看今日菜单
有了这个想法就动手做了。
这件事可以拆成三步:
数据抓取
数据处理
数据推送
下面详细说。
数据抓取
要想爬菜谱的数据,首先需要知道这些信息是从哪里查询出来的。我没有做过易信公众号 的开发,但是根据一般的经验,不管是微信还是易信的公众号发布的文章一般就是一个 简单的 HTTP 页面。想要找出每日菜单数据的来源,找出这些 HTTP 页面地址的 pattern 一般就搞定了。
抓取手机网络请求的方法很多,最方便的办法应该是在手机后台跑一个类似 tcpdump 功能 的工具的同时,访问易信的今日菜单,就可以抓到想要的结果。不过由于我的手机是没有 越狱的 iOS 系统,由于沙盒机制的限制无法做到。
最后用的办法是,在相同网络的电脑上跑一个 mitmproxy1 服务,将手机的 HTTP Proxy 地址指定为电脑的地址,在易信里打开今日菜单的链接,就可以在 mitmproxy 里看到一串 手机的 HTTP 访问记录,其中就包含我们要抓取今日菜单的 HTTP URL 。
可以发现今日菜单的 HTTP URL 链接就是下面的 pattern:
http://numenplus.yixin.im/singleNewsWap.do?companyId=1&materialId=${id}
其中只有一个变量 ${id} ,是个正整数,应该就是文章的ID。我们要爬的每日菜单内容, 都在这些链接里面,还包含网易精灵公众号发布的其他一些广告文章,这个页面就是简单 的 HTTP GET 就可以爬到内容,不需要做额外的处理。
自己研究了一下没有找出今日菜单的文章的 ID 生成的规律,推测应该是在易信后端生成 的,手机客户端无法直接拿到这个 ID。于是干脆就每个ID都爬一遍,检查内容是今日菜单 的文章就处理,不是就忽略。这样就搞定今日菜单的数据来源了。
比较熟悉 Python,就用 Python 实现了:
import requestsdef http_get(url, timeout=3): try: res = requests.get(url, timeout) except: LOG.exception("Failed to GET: %s" % url) else: if res.status_code != 200: return None else: return resdef fetch(start, step=300): last_id = start for i in iter(range(start, start + step)): url = ("http://numenplus.yixin.im/singleNewsWap.do?" "companyId=1&materialId=%d" % i) response = http_get(url) if not response: continue # handle menu data here
数据处理
数据处理,主要有两个任务:
上面也提到了,需要检查爬取的文章内容是不是今日菜单
解析 HTML 内容,获得我们想要的菜单信息
第一个问题比较简单,我们可以直接通过简单的关键词正则表达式匹配来检查。比如文章 内容含有“今日菜单”这四个字,我们就认为这篇文章内容是今日菜单。
第二个问题稍微复杂一些,我们需要从爬取的 HTML 源数据提取出其中的文本数据,然后 从中生成这份菜单的日期、早餐、午餐、晚餐信息。这个用稍微复杂一点的正则表达式也 可以搞定。
Python 中有一个比较有名的处理 HTML 格式内容的第三方库 BeautifulSoup , 使用非常方便:
获取菜单正文内容
def _parse(self, content): try: bs = BS(content, "html.parser") if bs.find_all(class_="m-error"): return None else: return bs except: LOG.exception("Failed to Parse content: %s" % content)def _handle_menu(bs): try: content = bs.find(id="divCNT") except: LOG.warn("Failed to get content") return None else: return content
HTML 解析前是这个样子
解析后就是这个样子,已经把 HTML 的 tag 都脱掉了
判断是否是今日菜单
def _is_menu(text): # \u4eca\u65e5\u83dc\u5355 => 今日菜单 if re.findall(ur"\u4eca\u65e5\u83dc\u5355", text, re.UNICODE): return True else: return False
提取菜单日期
def _handle_date(content): # \u6708 => 月 \u65e5 => 日 res = re.findall(ur"(\d+)\u6708(\d+)\u65e5", content.text, re.UNICODE) if not res: LOG.warn("Failed to parse date") return None else: month, day = tuple([int(i) for i in res[0]]) year = datetime.datetime.now().year return datetime.datetime(year, month, day)
提取菜单早餐、午餐、晚餐内容
def _menu_to_text(content): # \u65e9\u9910 => 早餐 # \u4e2d\u9910 => 中餐 # \u665a\u9910 => 晚餐 # \u591c\u5bb5 => 夜宵 text = content.get_text() res = re.findall(ur"\u65e9\u9910([\s\S]+)\u4e2d\u9910([\s\S]+)" ur"\u665a\u9910([\s\S]+)\u591c\u5bb5", text, re.UNICODE | re.MULTILINE) if not res: LOG.warn("Failed to match menu") return None else: menu = {} menu[BREAKFAST] = res[0][0] menu[LUNCH] = res[0][1] menu[SUPPER] = res[0][2] return menu
数据推送
现在已经解决了今日菜单的数据爬取、处理,就差如何把菜单内容推送到手机了。
经过调研,iOS 平台上比较好用的第三方消息推送服务有 Pushover、Pushbullet、 Boxcar、Amazon SNS 等。
Amazon SNS 没有提供现成的客户端首先否决掉;Pushover 综合看起来是最好的选择, 不过每个手机客户端使用需要付 5$ 的一次性授权费用,穷,也否决掉;综合看起来, Pushbullet 功能较全、免费、文档清晰、全平台支持,最后选择 Pushbullet 推送消息。
按照 Pushbullet 提供的 API 文档写一个 HTTP POST 请求就可以实现推送功能了:
def send_notification(subject, content, channel=PUSHBULLET_CHANNEL): try: res = requests.post( "%s/pushes" % PUSHBULLET_API, headers={"Access-Token": PUSHBULLET_TOKEN}, data={"title": subject, "body": content, "type": "note", "channel_tag": channel}, timeout=30) except: LOG.exception("Failed to send notification") else: if res.status_code != 200: LOG.warn("Error when pushing notification")
推送过来的菜单就是这样了:
PC/Mac 端同样支持:
把上面这些代码片段拼起来,就是一个可以抓取、推送今日菜单的小项目了,最后能跑的 代码放在这里(代码里还包含之前写的把菜单内容发邮件通知的功能):
https://g.hz.netease.com/hzzhanggy/what2eat2day_ntes
自动化
整个数据爬取、推送的流程都写好了,最后剩下的需要做的事情就是让整个流程自动化 运行,我们只需要每天饭点看手机推送消息就可以了。
其实就是将数据爬取、推送做成定时任务就可以了。我通过 systemd timer 实现:
在 virtualenv 中运行脚本的 wrapper run.sh
#!/bin/bashBASE=/home/stanzgy/workspace/what2eat2day_ntes$BASE/.venv/bin/python $BASE/fetch.py $@
今日菜单抓取 service 文件 menu_fetch.service
[Unit]Description=Fetch NetEase menu today[Service]Type=oneshotExecStart=/home/stanzgy/workspace/what2eat2day_ntes/run.sh f[Install]WantedBy=multi-user.target
今日菜单抓取 timer 文件 menu_fetch.timer
[Unit]Description=Fetch NetEase menu everyday[Timer]OnCalendar=Mon-Fri *-*-* 10:00:00Unit=menu_fetch.service[Install]WantedBy=multi-user.target
推送今日菜单的 timer 配置类似上面,仅仅是命令行传入的参数不一样,这里就省略了。 最后效果如下
更多网易技术、产品、运营经验分享请点击。
相关文章:
【推荐】 手把手带你打造一个 Android 热修复框架
【推荐】 git 常用命令
【推荐】 从golang的垃圾回收说起(上篇)