做爬虫的, 要有自己的工具来发现网络世界的动态, 对信息保持一份敏感, 对数据存有一颗敬畏之心.

项目名称: ClericPy/watchdogs

项目初衷

  1. 统一关注.

    平时可能会追剧追小说追综艺, 关注疫情实况, 订阅一些博客, 但是网站多了太分散, 所以需要一份 Timeline 方便浏览.

  2. 解放双手.

    满足对日常信息变化的订阅, 并提供主动提醒功能, 从不断刷网页判断更新中解放双手. 简而言之, 信息索取由被动转为主动.

  3. 体验新事物.

    而归根结底的初衷, 还是想试试去年就大火的 fastapi, 体验之后, 果然比原生 starlette 要优雅很多.

主要功能

  1. 适用于: 追剧, 追星, 追小说漫画, steam 降价, 博客更新, 服务翻车, 疫情实况, 定向新闻或搜索结果等场景的提醒.
  2. 根据定制解析规则, 关注网页特定数据的变化, 记录更新时间与历史数据, 并提供对应 URL 进行跳转. 所有爬虫执行在协程环境下, 抓取 / 存储 / 回调与整个 Web 服务相互独立不阻塞.
  3. 通过可视化 Web UI 快速配置爬虫的下载器与解析器参数, 目前熟练操作以后, 配置一个抓取规则大约耗时 3 分钟.
    1. 结合前段时间写的解析规则持久化模块 uniparser, 将解析方式存储在数据库中, 避免每次新增爬虫都要发布代码. 该库虽然涵盖的解析方式很多, 但大部分库都是惰性引用, 开销并不算太大.
    2. 总的来说, 就是将大量爬虫解析器的耦合代码归纳起来, 然后把变量部分入库保存, 实现解析规则持久化.
  4. 定制化提醒方式. 默认支持了『Server 酱』和 RSS 的订阅, 以及适配手机的 Lite 页面.
    1. Server 酱 是用来通过微信推送提醒的服务, 具体 Token 获取方式参考其官方网站.
    2. RSS 网页的订阅可以使用专业的 RSS 阅读器应用或扩展. 之前使用的 InoreaderRSS Feed Reader 因为刷新频率太低, 好暂时弃用. 而 Chrome 扩展 feedbro 则支持分钟级提醒.
    3. 至于 Email 等其他提醒方式, 可以凭借继承 watchdogs.callbacks.Callback 抽象类, 并实现 callback(task) 函数来完成.
  5. 快速部署.
  6. 默认配置只需要 pip 安装后 python3.7 -m watchdogs 即可启动 Web App, 配置文件与日志文件存放在 $HOME/watchdogs 目录下, 若没有指定 mysqldb_url, 则使用 sqlite 数据库也存放在该目录.
  7. 内存占用大约 80 MB, Linux 系统下如果安装 uvloop, 性能非常顺畅.
  8. 也可以将 watchdogs 作为第三方库来进行私人订制, 只需要调制好 watchdogs.Config 中的各项原始变量, 然后执行 watchdogs.init_app() 即可拿到 app: Fastapi 对象, 该对象可以进行二次绑定 routes, 不过尽量不要作为 sub_app 使用, 否则会错过 startup 和 shutdown 事件的回调函数, 需要另行调用.

简单上手

  1. 安装 (pip)

    pip install -U watchdogs

  2. 部署 (python3.7+)

    python3.7 -m watchdogs

  3. 使用

    通过浏览器访问相应地址即可

详细使用

启动参数

  • 查看帮助: python -m watchdogs -- -h
  • 以下参数除了命令行启动时使用, 作为第三方库引用时也可以使用, 而实际使用中在通过 watchdogs.init_app 初始化 app 实例前, 直接修改 watchdogs.Config 对应配置更方便.
  • db_url: sqlite / mysql / postgresql 所支持的 db_url, 也就是 databases 所支持的.

若是不指定, 则会使用 sqlite:///$HOME/watchdogs/storage.sqlite

PostgreSQL 没有进行实机测试.

  • password:

初始化的密码, 一般不需要指定, 除非密码被人给破了, 所以要提前指定进行修改, 否则在网页端就可以修改.

  • mute_std_log:

忽略 stdout, stderr 上日志的显示

  • mute_file_log:

日志不再保存到 $HOME/watchdogs 目录.

  • md5_salt:

md5 加盐用的参数, 全局 md5 鉴权时使用, 一般不需要指定, 除非要多处部署分布式, 保证加盐统一.

  • config_dir:

配置文件目录, 用来存放配置文件和日志文件的地方, 如果没有指定数据库, 则也会存放 sqlite 数据文件. 不指定的话, 默认为 $HOME/watchdogs

  • use_default_cdn:

如果 Config.cdn_urls 没有指定, 并且指定了 use_default_cdn, 将使用来自 staticfile.org 的在线 js / css CDN 链接.

  • uvicorn_kwargs:

uvicorn 启动参数, 常用的有 port, host, access_log 等. 例如:

python -m watchdogs --port=9999 --host=127.0.0.1 --access-log=False

Web 界面

  • 通过 python3.7 -m watchdogs 启动时, 默认网页访问的是 http://127.0.0.1:9901
    • 首次访问会有个配置新密码的过程.
  • 先看一下已经配置过的首页样式

[Tab] Tasks

首页, 任务列表.

  1. 也就是首页. 直观展示全部任务列表, 每页默认 30 个.
  2. 新建任务的页面.
  3. 规则列表, 按照 host 分类.
  4. Config.custom_tabs 变量配置的页面, 展示为一个自定义网址的 iframe
  5. Config.custom_tabs 变量配置的页面, 展示的是 fastapi 默认的 /docs
  6. 任务列表的表头
    1. task_id: 自增数字, 用来做主键的.
    2. name: 任务名称, 唯一索引, 可以通过名字找到一个任务.
    3. tag: 任务的标签, 为下面的 RSS 和 lite 页面做的分类过滤, 也为当前任务列表做的过滤, 点击即可过滤.
    4. request_args: 请求参数, 是一个兼容 requests / aiohttp 库的字典, 同时支持 retry, ssl 等参数.
    5. interval: 抓取频率的参数, 类型为整型, 单位是秒.
    6. work_hours: 爬虫工作时间, 支持多种形式, 比如包含标准 strftime 格式的与或非逻辑(==, !=, &, |).
      1. 0, 24: 表示当前小时处于 range(0, 24) 之间.
      2. [18, 19, 20]: 表示每天的 18 19 20 这三个小时内才进行抓取.
      3. %A==Friday: 表示每周五.
      4. %m-%d==03-13: 每年的 3 月 13 日.
      5. %H==05: 每天早上 05:00 ~ 05:59
      6. %w==5;20, 24: 表示每周五的 20:00 ~ 23:59.
      7. [1, 2, 15]&%w==5: 每周五的 1 点 2 点和 15 点.
      8. %w==5|%w==2: 每周二和周五.
      9. %w!=6&%w!=0: 只要不是周六或周日的任意一天.
    7. last_change_time: 上次更新时间. 本链接点击后会展示三个属性: 上次抓取时间, 下次抓取时间, 上次更新时间.
    8. custom_info: 自定义信息. 这个会影响到抓取发现变化时候的回调, 分三种情况.
      1. 内容是无规律没有 : 的:
        1. 如果指定了 name 为 '’ 的 Callback, 则会唤起该回调函数进行处理本任务.
        2. 如果没有指定上述默认回调的话, 则该内容纯粹就是个笔记本记录一点描述.
      2. 内容里有 ::
        1. 会按 : 进行一次 split, 左边是回调名称, 右边则是回调的参数, 具体回调可以通过继承 watchdogs.callbacks.Callback 自动绑定.
        2. 目前默认已经支持了的是 server_chan, 也就是 Server 酱 进行回调, 参数则是回车或空格分割的多个 Token.
    9. latest_result: 最新结果.
      1. 点击链接会跳转采集到的 url 参数指定的网址, 如果未指定, 则会跳转到任务 meta 里的 origin_url 地址.
      2. 点击三个点按钮的话, 则显示的是整个结果列表, 包括抓取到的时间.
    10. 注意: 有一行的任务是橘黄色的, 表示任务抓取出错了, 具体错误可以点击最新结果那列里的感叹号标识, 抓取报错的情况下, 一般不会入库存储算做最新结果, 除非是找不到规则.
  7. 任务操作栏.
    1. 任务开关, 如果是关闭的任务, 除非强制抓取, 否则将不会再被抓取.
    2. 强制抓取. 跳过抓取间隔队列, 强制抓取一次本任务, 会刷新该任务各项 meta, 也会影响下次抓取时间的调度.
    3. 修改任务. 修改该行任务的各项 meta 信息.
      1. 快捷方式: 双击该行.
    4. 删除任务. 会有一次确认弹窗.
  8. 加载下一页.
  9. 四个页面操作的小按钮.
    1. 刷新本页. 会重新从第一页获取任务列表.
    2. RSS 链接. 使用加盐 Token 来避开 Cookie 的验证, 适合给 RSS 阅读器进行订阅.
      1. 由于默认抓取循环的扫描间隔是 60 秒, 所以 RSS 更新的频率不要低于 60 秒.
    3. 适配移动端 Lite 页面链接. 使用加盐 Token 来避开 Cookie 的验证. 点击顶部大标题可以跳回首页.
    4. 自定义链接. 存放于 watchdogs.Config.custom_links 的快捷方式, 方便记录一些经常需要访问的地址. 默认已经放了修改密码与查看日志的页面.

任务信息

  1. 任务的自增 ID, 不可修改.
    1. 任务信息页面如果是新建任务时打开的, 这里为空, 会由后端自动生成.
    2. 任务信息如果是任务列表双击打开的, 这里会是一个整数, 用于定位到数据库中的位置, 方便修改 meta 信息
  2. 错误信息, 出错时会导致正行变成橘红色.
  3. 修改 request_args 的一些方便复制粘贴的常用参数, 比如超时和重试, 以及忽略 ssl 验证避免证书报错.
  4. 当抓取结果里没有 url 的时候, origin_url 会用来跳转.
  5. 快速选择抓取间隔, 省去计算秒数的时间.
  6. 前文提过的 work_hours 文档点击信息图标即可看到了.
  7. 历史列表的最大保存数量, 避免保存太多浪费存储.

[Tab] New

此处是新建任务时使用

  1. 基于 uniparser 子 app 的 iframe 托管, 所以可以直接去其官网查看具体使用文档.
  2. 规则的名称, 一般留空, 等待点击 [Download] 按钮以后会自动获取一个, 到时候再略微修改即可.
  3. 用于识别网址是否匹配该规则的正则表达式, 一般留空, 使用下文所述的 [cURL Parse] 功能即可自动生成, 为了匹配更多网址, 可能要稍微修改其中部分变量为 .*?
  4. 编码, 默认不指定的话会由 requests 或者 aiohttp 自带的 chardet 库进行识别.
  5. 通过此处来设置 request_args 参数会节省很多力气, 点击后填入 URL 或者一段标准 cURL 字符串 ( 比如在 Chrome devtools console -> Network -> Copy as cURL ), 将自动转为 requests 请求的标准字典.
    1. 并且如果 Regex Pattern 参数为空, 则自动填入一段, 然后由用户自行微调.
  6. 可以在此处对请求的字典参数进行细致调整
    1. 注: 因为这里是配置的规则层面的 request_args, 在真正发起请求的时候, 将会被 Task 层面的 request_args所覆盖.
  7. 下载按钮. 点击后如果 Rule Name 没有配置, 则会自动填入请求结果里的 title 或 h1 标签内容, 否则填入一个当前时间戳的字符串.
  8. 发送请求后的响应主体. 会显示请求的状态码和 body.
  9. 具体的解析规则. 配置方式可以参考 uniparser 官网.
    1. 每次点击 Parse 按钮即可查看已经配置的解析结果.
  10. 初始化一份解析规则方便二次修改. 多次点击会在两种 Demo 之间切换.
    1. 一种是 CSS 选择器解析方式的 Demo
    2. 另一种则是 XML 解析的 Demo, 这种主要是用来处理 RSS.
  11. 保存当前配置好的抓取规则.
    1. 一定要确保解析结果符合要求:
      1. 结果里一定要有 text 字段
      2. 结果里可以有 url 字段, 如果没有则会被 Task.origin_url 所替代
      3. 结果可以是字典, 也可以是列表里的字典, 如果是列表会被倒序存入历史列表.
        1. {'text': 'some text', 'url': 'http://example.com'}
        2. [{'text': 'some text', 'url': 'http://example.com'}, {'text': 'some text2', 'url': 'http://example2.com'}]
  12. 创建新任务.
    1. 默认会根据当前规则填入一些空白参数, 略做微调即可.
    2. 提交后可以去 Tasks 页面点强制抓取, 不用等下一次循环.
    3. 具体任务各项 meta 信息在页面上基本都有文档来参考.
    4. 所以, 最简单的创建一个任务来参考的方式就是:
      1. 点击 Init 按钮
      2. 点击 Download 按钮
      3. 点击 Save Crawler Rule 按钮
      4. 点击 Add New Task 按钮
      5. 点击提交即可

[Tab] Rule

规则列表页面, 按照 host name 进行归类

  1. 可以输入 host 片段对所有 host 进行过滤, 按下回车或点击搜索按钮则会重新去服务器获取.
  2. 每个 host 是一个可以点击的彩色 Tag, 点击后出现详细信息的弹窗
  3. 规则信息弹窗, 在数据库中也是按 host name 索引的
  4. 删除整个 HostRule, 整行删除
  5. 控制该 host 下的所有请求抓取频率, 避免因为抓取频率过快而触发反爬
    1. 默认不限制并发数和抓取间隔
    2. 举个例子, n 设置为 3, interval 设置为 1, 表示 1 秒里最多发起 3 次请求
  6. 之前新建任务时候起的倒霉名字, 如果想修改, 点击这里就会跳转过去, 修改完再次 Save 即可.
    1. 注意: 因为 CrawlerRuleHostRule 里面是拿 name 做 key 的, 所以会有一次覆盖确认.
  7. 和上面一样, 点击后跳转到该条 CrawlerRule 的修改页面
  8. 将该条抓取规则直接当成 JSON 修改, 适合较熟悉的使用, 在 uniparser 里, 所有规则都是可持久化的, 因为它们都继承了 dict
  9. 删除该抓取规则, 因为是二次弹出的页面, 所以不太需要二次确认了, 点击就会删除

自定义使用

from fastapi import FastAPI, Request
from uvicorn import run
from watchdogs import Config, init_app

# 更新默认的 Config 为自定义参数
Config.db_url = 'mysql+pymysql://root:password@172.1.2.3:3306/db?charset=utf8mb4'

app: FastAPI = init_app()


@app.get('/new_page')
def new_page(request: Request):
    return {'method': request.method}


run(app, **Config.uvicorn_kwargs)

结语

​ 整个项目框架搭起来用了大概半个月, 然而后续小 bug 层出不穷, 再加上很多功能优化, 断断续续发了很多版本改了很长时间. 其实之前也尝试过做了一个 onwebchange 的库, 但是各种设计都太稚嫩, 所以想了想本着 2.0 的思路重新从 uniparser 开始搞, 也算把很久以前存放在 todoist 里的想法清理了一遍.

​ 英文文档远没有中文文档详细, 遇到问题可以通过 Github issues.

​ 截止到 2020-03-31, 功能基本已经完整, 之后不再计划太频繁的非 bug 更新.