Skip to content
master
Go to file
Code

Latest commit

 

Git stats

Files

Permalink
Failed to load latest commit information.
Type
Name
Latest commit message
Commit time
 
 
 
 
 
 
 
 

README.org

豆瓣小组顶帖机器人

警告

豆瓣小组顶帖机器人是一个电脑程序,用于豆瓣小组特定作者特定话题的自动化顶帖。该脚本通过约 30 行核心 JavaScript 代码,可以实现无人值守全自动化顶帖,并可以根据个人需要控制发帖频率和发帖内容;利用浏览器的 Cookie/Session,大多数时候可以避免触发豆瓣的验证码;一旦遭遇豆瓣验证码,程序会自动检测并停止顶帖行为,以避免因频繁不当发帖被豆瓣封号。

此程序违反规则,有损公平,通过奇技淫巧用程序模拟人类手工发帖,会严重影响到在豆瓣小组手工发帖顶帖的同学的利益,不符合(社会主义核心)价值观。

因此,脚本虽好,使用需谨慎,后果要自负。

(谁说我是高级黑,站出来,来,咱唠唠。)

缘起

最近在做自己的小产品,PPResume,虽说酒香不怕巷子深,但是商业产品,总会面临一个冷启动的问题,因此会尝试一些初级的推广手段。于是就在豆瓣上各种求职招聘的小组里发了十几个帖子。

/images/ppresume-douban-group-posts.jpg

总体上讲,豆瓣的小组发帖审核并不是很严格,很多小组管理上比较荒芜,但是人气还是有一些,所以多少也可以算是一个免费的广告渠道吧。加上我穷,目前还用不起太贵的付费广告推广渠道,因此前几天就靠着手工顶帖的手段,来获取一些人气。

但是顶帖实际上是个很无聊的事情,无非就是开浏览器窗口输入一些简单文字点击下提交。顶得频率过高还会遭遇豆瓣的验证码,还要手工输入,神烦。每天要耗十几分钟在这种无聊的事情上是很要命的。所以我就“心生邪念”,写了这个顶帖机器人。

是谁说过,“懒惰是程序员的第一美德”。

实现

自动顶帖的实现原理其实非常简单。从用户的角度来说,就是用程序来自动化完全以下的流程:

  • 打开一个浏览器窗口
  • 随机访问一个豆瓣小组话题
  • 在页面底部的表单输入区域输入一些文字
  • 点击提交按钮。

如果你懂一些计算机知识,你会明白,整个过程无非就是自动化地向豆瓣发送一些 HTTP 请求。

具体的程序实现可以有多种方式。列述于下。

后端脚本

稍懂一些计算机知识的就知道,发帖就个行为多数情况下是一个 HTTP Post 请求。通过分析豆瓣的 Post 请求细节,即可写程序模拟,这是整个实现的核心。

通过分析豆瓣页面的源码,并用 Chrome Developer Tools 分析 HTTP 请求包,实现简单的后端脚本程序是不难的。

豆瓣小组话题的提交表单核心 HTML 代码如下:

<form name="comment_form" method="post" action="add_comment#last"
      onsubmit="this.onsubmit=function(){return false}">
  <div style="display:none;">
    <input type="hidden" name="ck" value="uSni">
  </div>
  <textarea id="last" name="rv_comment" rows="8" cols="54"></textarea><br>
  <input type="hidden" name="start" value="0">
  <span class="bn-flat-hot rr">
    <input name="submit_btn" type="submit" value="加上去">
  </span>
</form>

用后端脚本实现需要面临的问题是:

  • 在模拟发帖前需要登录,并保持 session
  • 由于后端脚本发送 HTTP 请求,User-Agent 默认情况下和浏览器是不一样的,很容易被豆瓣服务端识别并封禁,因此需要伪造 User-Agent
  • 除此之外,还需要仔细分析浏览器的实际 HTTP 请求,并在后端请求中加入合适的 HTTP Headers,尽可能模拟浏览器
  • 不像前端 DOM,后端脚本在解析 HTML 方面并不像浏览器内置的 JavaScript 那么方便,我的方式是通过正则匹配提取相应的信息。但是正则表达式其实是个用起来很爽但是调试起来很痛苦的一个工具。各种语言的正则实现多少都会有一些或是写法或是实现上的细节差别,很让人头疼,需要仔细调试。

上面的都是小问题,最大的问题还是验证码的问题。因此后端脚本在每次运行后就会退出,因此每次运行时都需要重新登录并建立新的 HTTP session,这很容易触发豆瓣登录的验证码。而验证码识别需要 Computer Vision 领域相关的知识,这方面我所知甚少。这里这里是两篇很好的文章,详细阐述了豆瓣验证码的程序化识别方法。我看了下,不是很难,个人花些时间的话,也可以完成。但是用 Computer Vision 的手段来“顶帖”,还是有些牛刀杀鸡之感,因此最终放弃了这个思路。

附上我最开始写的 Python 程序(需要 pip install requests ),包含豆瓣登录、建立 HTTP session、用正则表达式提取一些表单信息。

import random
import re

import requests

def main():
    urls = ["https://www.douban.com/group/topic/90845242/",
            "https://www.douban.com/group/topic/90845094/",
            "https://www.douban.com/group/topic/90845036/",
            "https://www.douban.com/group/topic/90844973/",
            "https://www.douban.com/group/topic/90844873/",
            "https://www.douban.com/group/topic/90844341/",
            "https://www.douban.com/group/topic/90844012/",
            "https://www.douban.com/group/topic/90841110/",
            "https://www.douban.com/group/topic/90840593/",
            "https://www.douban.com/group/topic/90840457/",
            "https://www.douban.com/group/topic/90840401/",
            "https://www.douban.com/group/topic/90840340/",
            "https://www.douban.com/group/topic/90840228/",
            "https://www.douban.com/group/topic/90840096/",
            "https://www.douban.com/group/topic/90839736/",
            "https://www.douban.com/group/topic/90839654/"]

    words = ['喵', '汪', '嗷', '咩', '哞', '呱',
             '吱', '喔', '叽', '嗡', '咿', '嘶',
             '喳', '嘎', '咕', '哇', '呜', '嗥']

    random_word = words[random.randint(0, len(words) - 1)]

    s = requests.session()
    login_form_data = {'form_email': 'xiaohanyu1988@gmail.com',
                       'form_password': 'some_password'}
    s.post('https://www.douban.com/accounts/login', login_form_data)

    get_url = urls[random.randint(0, len(urls) - 1)]
    post_url = get_url + 'add_comment'
    html = s.get(get_url)

    m = re.match(r"<form name=\"comment_form\".*<input type=\"hidden\" name=\"(.*)\" value=\"(.*)\"", html.text)  # NOQA
    name = m.group(1)
    value = m.group(2)
    # ...

if __name__ == '__main__':
    main()

前端 iframe

第二种方式是通过前端页面嵌入 iframe ,然后通过 JavaScipt 修改 iframe src 来实现自动加载不同的页面。

<html>
  <head>
    <meta charset="utf-8" />
    <meta content="IE=edge,chrome=1" http-equiv="X-UA-Compatible" />
    <meta content="width=1300, maximum-scale=2, user-scalable=yes" name="viewport" />
    <title>Douban-bot</title>
  </head>

  <body>
    <iframe id="douban-group" sandbox="allow-same-origin allow-scripts allow-popups allow-forms" src="" width="1200" height="800">
      <p>Your browser does not support iframes.</p>
    </iframe>
  </body>

  <script>
   (function () {
     // Returns a random integer between min (included) and max (excluded)
     // Using Math.round() will give you a non-uniform distribution!
     function random_int (min, max) {
       min = Math.ceil(min);
       max = Math.floor(max);
       return Math.floor(Math.random() * (max - min)) + min;
     }

     var urls = ["https://www.douban.com/group/topic/90845242/",
                 "https://www.douban.com/group/topic/90845094/",
                 "https://www.douban.com/group/topic/90845036/",
                 "https://www.douban.com/group/topic/90844973/",
                 "https://www.douban.com/group/topic/90844873/",
                 "https://www.douban.com/group/topic/90844341/",
                 "https://www.douban.com/group/topic/90844012/",
                 "https://www.douban.com/group/topic/90841110/",
                 "https://www.douban.com/group/topic/90840593/",
                 "https://www.douban.com/group/topic/90840457/",
                 "https://www.douban.com/group/topic/90840401/",
                 "https://www.douban.com/group/topic/90840340/",
                 "https://www.douban.com/group/topic/90840228/",
                 "https://www.douban.com/group/topic/90840096/",
                 "https://www.douban.com/group/topic/90839736/",
                 "https://www.douban.com/group/topic/90839654/"];

     var douban_iframe = document.getElementById('douban-group');

     douban_iframe.setAttribute('src', urls[random_int(0, urls.length)]);
   })();
  </script>

</html>

但是程序写到后面我发现我犯了致命的错误……

我最初的想法是通过 iframe 加载豆瓣的页面后,通过 JavaScript 来获取 iframe 内部豆瓣页面上的元素,并进行模拟人工的发帖操作。

但这实际上是行不通的。为了安全起见,浏览器默认会禁止跨域请求,所以 iframe 外部的 JavaScript 是无法解析 iframe 内部页面的 DOM 元素的。最终这种方法宣告失败。浏览器与 iframe 之间进行跨域通信的方法似乎只有 postMessage 这一种方法。限于时间和篇幅,不再详述细节。

前端浏览器插件

前两种方法基本上都宣告失败,此时我已经耗了大半天的时间,本已想放弃。灵光突现,忽然想起很久很久很久以前 Firefox 3.x 时代有个备受推崇的插件:Greasemonkey。可惜我当时对计算机程序所知甚少,完全不懂 Web/HTTP/HTML/JavaScript,因此只是简单看了下这个插件,就没有再关注过了。

这次忽然想起,很多浏览器的插件其实也就是一些 JavaScript 脚本,而 Greasemonkey 其实就是个用户端可以自定义的 JavaScript 脚本管理器。通过 Greasemonkey 加上一些自定义脚本,用户几乎可以在客户端对页面做任何事情:

  • 屏蔽网站广告
  • 发送网页布局
  • 替换字体
  • 增强视频播放器

http://userscripts-mirror.org/ 汇集了大量的用户脚本,包罗万象。

Greasemonkey 在 Chrome 浏览器中对应的插件是 Tampermonkey

以此为基础,花了大概三、四个小时,完成了下面的 JavaScript 脚本:

// ==UserScript==
// @name         Douban Group Dingbot
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  Douban Group topics auto comments.
// @author       You
// @match        https://www.douban.com/group/topic/*
// @grant        window.close
// ==/UserScript==

(function() {
  'use strict';

  function random_int (min, max) {
    min = Math.ceil(min);
    max = Math.floor(max);
    return Math.floor(Math.random() * (max - min)) + min;
  }

  var topic = 'PPResume';
  var title = document.getElementsByTagName('h1')[0].textContent;
  var is_ppresume_topic = (title.toUpperCase().indexOf(topic.toUpperCase()) == -1 ? false : true);

  if (! is_ppresume_topic) {
    console.log("This is not a " + topic + " topic, you've made a wrong choice, my master...");
    return;
  }

  var words = ['喵', '汪', '嗷', '咩', '哞', '呱',
               '吱', '喔', '叽', '嗡', '咿', '嘶',
               '喳', '嘎', '咕', '哇', '呜', '嗥'];

  var has_captcha = document.getElementById('captcha_image');
  if (has_captcha) {
    console.log("This page has captcha image, I can't post for you, master, sorry...");
    return;
  }
  var textarea = document.querySelector('textarea[id=last]');
  if (! textarea) {
    console.log("You've just posted a comment already, I'll close the window and exit...");
    setTimeout(function () { window.close(); }, 3000);
    return;
  }

  textarea.innerHTML = words[random_int(0, words.length)];

  var submit_button = document.querySelector('input[name=submit_btn]');
  submit_button.click();
})();

脚本的开头有 Tampermonkey 相关的注释:

  • // @match https://www.douban.com/group/topic/*
    • 此行表示只有就浏览器访问 https://www.douban.com/group/topic/ 开头的网址,才会触发脚本
  • // @grant window.close
    • 此行表示授权脚本关闭浏览器窗口或者标签的权力 window.close() ,这样程序在完成顶帖工作后,可以自行关闭窗口,不会对用户的日常工作有太大的干扰。

整个程序核心只有约 30 行左右,非常简单,逻辑也不复杂,除了回帖功能,程序中还加了一些额外的检测:

  • 根据话题的 title 是否包含特定字符串,来确定此话题是否需要顶帖
  • 根据页面结构判断此话题是否刚刚被顶过
  • 判断页面是否包含验证码,如包含验证码,则直接退出

这样写完之后,将脚本安装到 Tampermonkey,然后每次访问特定的豆瓣小组话题页面时,就会触发自动回帖操作。当然,频率不要太高,要不会触发豆瓣的验证码。剩下的问题是,如何让浏览器来定时访问特定的豆瓣页面。

这个很简单。现代操作系统都有定时任务的支持。基于 Unix 的系统,如 Linux 或者 macOS,有一个经典的定时任务工具,Cron,写一个简单的脚本,放入 crontab 即可。我用 ruby 写的脚本如下:

#!/usr/bin/env ruby

def main
  urls = ["https://www.douban.com/group/topic/90845242/",
          "https://www.douban.com/group/topic/90845094/",
          "https://www.douban.com/group/topic/90845036/",
          "https://www.douban.com/group/topic/90844973/",
          "https://www.douban.com/group/topic/90844873/",
          "https://www.douban.com/group/topic/90844341/",
          "https://www.douban.com/group/topic/90844012/",
          "https://www.douban.com/group/topic/90841110/",
          "https://www.douban.com/group/topic/90840593/",
          "https://www.douban.com/group/topic/90840457/",
          "https://www.douban.com/group/topic/90840401/",
          "https://www.douban.com/group/topic/90840340/",
          "https://www.douban.com/group/topic/90840228/",
          "https://www.douban.com/group/topic/90840096/",
          "https://www.douban.com/group/topic/90839736/",
          "https://www.douban.com/group/topic/90839654/"];

  rand_url = urls[rand(urls.length)]

  sleep_seconds = rand(1000)
  puts "Sleep #{sleep_seconds} for a while."
  sleep(sleep_seconds)

  case RUBY_PLATFORM
  when /darwin/
    `open #{rand_url}`
  when /linux/
    `xdg-open #{rand_url}`
  end
end

if __FILE__ == $0
  main
end

我的 crontab 设置是 52 8-23 * * * /usr/bin/ruby /Users/xiao/work/xiaohanyu/douban-group-dingbot/douban-group-dingbot.rb ,这表示在每天 8:00–23:00,每个小时第 52 分钟的时候,运行 ruby 脚本。实际上,我的 ruby 脚本中还加入了一些随机化的 sleep ,因此实际的回帖时间并不是每个小时的第 52 分钟,而是随机性的。这样可以更好地避开豆瓣的验证码限制,真正的 behave like a human。

除了用定时任务定时触发,还可以通过 Tampermonkey 进行手工触发。脚本如下:

// ==UserScript==
// @name         Douban Group Dingbot Helper
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  Douban Group topics auto comments.
// @author       You
// @match        https://www.douban.com/dingbot
// @grant        none
// ==/UserScript==

(function() {
  'use strict';

  function random_int (min, max) {
    min = Math.ceil(min);
    max = Math.floor(max);
    return Math.floor(Math.random() * (max - min)) + min;
  }

  var urls = ["https://www.douban.com/group/topic/90845242/",
              "https://www.douban.com/group/topic/90845094/",
              "https://www.douban.com/group/topic/90845036/",
              "https://www.douban.com/group/topic/90844973/",
              "https://www.douban.com/group/topic/90844873/",
              "https://www.douban.com/group/topic/90844341/",
              "https://www.douban.com/group/topic/90844012/",
              "https://www.douban.com/group/topic/90841110/",
              "https://www.douban.com/group/topic/90840593/",
              "https://www.douban.com/group/topic/90840457/",
              "https://www.douban.com/group/topic/90840401/",
              "https://www.douban.com/group/topic/90840340/",
              "https://www.douban.com/group/topic/90840228/",
              "https://www.douban.com/group/topic/90840096/",
              "https://www.douban.com/group/topic/90839736/",
              "https://www.douban.com/group/topic/90839654/"];

  window.location.href = urls[random_int(0, urls.length)];
})();

注意脚本开头的注释: // @match https://www.douban.com/dingbot 。这表示,当浏览器访问 http://www.douban.com/dingbot 时,会触发这个脚本。而这个脚本做的事情也非常简单,就是随机将浏览器重定向到一个新的页面,而浏览器访问新的页面时会触发自动回帖的操作。

这也是我最终选择的方案。其优点在于:

  • 只依赖本机环境,只要本机浏览器登录了网站,控制好顶帖频率,那么脚本几乎不太可能会触发豆瓣的验证码
  • 和后端脚本不一样,此方案触发的 HTTP 请求在豆瓣服务端看来,看手工回帖触发的 HTTP 请求是没有区别的,因此应该不会导致因账号不当使用而被封

其他方案

除了以上,我还想到两套方案,限于时间,没有实践,写下思路备忘。

类浏览器测试工具

在前端开发中,经常其实对前端页面进行一些测试,简单的测试可以靠手工肉眼点击按钮观察结果来进行,项目上了规模,成百上千个测试用例,全靠人工点鼠标,极易出错,既不可能亦无必要实施。

好在前端界有一些工具,可以通过程序控制的手段模拟人工操作,来实现自动化的测试。这方面的工具我知道的有两个:

  • Selenium 功能强大,需要一个完整的图形界面
  • PhantomJS 不需要图形界面,可以在服务器上远行

其原理也是在开源浏览器的内核(比如 Webkit)上做一层包装,然后通过浏览器的 JavaScript 来操控,进而完成对浏览器的模拟。

我觉得用这两个工具也有可能会碰到每次自动回帖都需要重新登录豆瓣进而触发豆瓣验证码。纯属猜测,还望指教。

自动化图像识别

以上四种多多少少都属于要写“传统”的程序的方案,其实浏览器相关的自动化操作还有一个软件可以考虑:Sikuli

引用官网介绍:

Sikuli automates anything you see on the screen. It uses image recognition to identify and control GUI components. It is useful when there is no easy access to a GUI’s internal or source code.

http://www.sikuli.org/

我个人认为 Sikuli 是一个非常有前途的程序,它的威力还远没有被大众所认知。

BTW,Sikuli 的发明人,vgod,台湾人,MIT PHD,是个非常厉害的程序员。

使用

使用方法?教程?如果你看了上面我写的文字一头雾水,我劝你还是放弃吧,别折腾了;如果你能看懂 50%,相信你已经知道使用方法了,记得修改下 urls 参数,别折腾了半天,最后帖子顶到我这边,为我做嫁;如果你实在是想用这个方案自己又搞不定,欢迎发邮件给我进行付费咨询,喵。

我个人在本机的使用效果(可以关注这个帖子):

/images/douban-group-dingbot-result.png

技巧

  • 可以在 这里 配置顶帖时是否需要按主题过滤,从而进行无差别顶帖。
  • Ruby 脚本 中加入了一些 frequency 的配置, frequency 值越大,这个帖子被顶的概率也会越高。

后记

2016 年 9 月 12 日中秋佳节,阿里巴巴几位程序员因为写脚本抢月饼触发系统 bug 导致多抢了几盒被公司以违反价值观为由开除(也有说是劝退)。

这让我痛心疾首。

网上讨论分成两派,程序员们多认为员工无错,公司做得太过,并且以违反价值观为由开人,实在是太过牵强。

而不懂网络编程技术的普通人则多认为公司处理得当。

而事实上,技术细节对整个事件的定性至关重要。

引用知乎一们匿名用户的回答:

为什么要解释,相信业内人士不是应该的么?

“利用漏洞” 这种需要专业人士来鉴定的事,怎么变的人人都懂了。怎么鉴定疾病有专业人士,鉴定古董有专业人士,tmd 鉴定是否利用漏洞就不要专业人士了?今天 “利用漏洞” 抢月饼,明天抢我支付宝,这种论调是谁带的

知乎

尽管大量程序员通过各种各样的方式解释了事情的来龙去脉并用尽了各种办法让外行人士理解技术细节,但是多数“吃瓜群众”还是以一种“我不听我不听我就是这么认为”的态度来看待、评判此事,什么“小偷针大偷金”,什么“今日写程序抢了月饼,明天就可以写程序改支付宝余额,后天就可以偷看马云的聊天记录”等等,诸如此类,荒谬无止。

也许我们每个人,都自己不了解不熟悉的领域都要多一些敬畏,在肆意评判之前,在肆意地给别人扣上价值观道德的大帽子之前,先了解下什么叫漏洞,会么叫 bug,什么叫自动化程序。

用机器代替人工,是刻在程序员骨子里的工作和生活方式。

以此为记。

About

[Fun] A greasemonkey/tampermonkey user script for commenting douban groups' topics automatically.

Resources

License

Packages

No packages published
You can’t perform that action at this time.