最近给朋友做了一个豆瓣小组自动评论机器人,使用 requests 与 lxml 库,在控制刷新频率的情况下,基本能做到头排评论。除了爬虫的这一部分,还很重要的是要能对帖子回复有趣的内容。GitHub 地址:DouGroupBot 一起讨论:jimxu_work#foxmail.com (把#换成@)
基本功能同时支持 cookie登录以及账号密码登录。建议尽量使用 cookie,密码登录过多会有验证码阻碍,并有异常行为导致封号的危险。支持自动更新 cookie,总是保持使用的 cookie最新。
通过 XPath 快速解析返回的HTML。有心的朋友可以benchmark一下beautiful soup 和 XPath 表达式之间的速度差异。Python 官方的 XPath 文档其实就很清晰了,推荐一读。在获得服务器返回的HTML和准备好回复内容之间的时间,都是不必要的对用户的延迟,速度很重要。
经过实践调试的最佳动态睡眠,在帖子多的情况下开足马力,帖子少时长时间休眠,并在有新帖出现后快速恢复。同时每次评论后随机睡眠,保证功能的情况下尽量降低服务器请求次数,保护账号,保护阿北。(珍惜db 人人有责)
拥有智能图像识别接口,识别验证码,可接入任何OCR模块。目前知名的开源识别库为 Tesseract,然而并不保证效果。
自动回复自动回复单独说一下。自动回复的原理就是发出 post
请求。请求的 URL 地址就是帖子的URL加上 add_comment。带不带最后的 #last 无所谓。注意编码问题,如果回复内容来自外部文件,注意转换为 UTF-8 编码。注意 POST 需要带上所有 hidden
的元素的值。不了解的可以看看HTML中关于 form
元素的知识。
抢头排除了速度以外,还需要记录已经评论过的帖子,避免重复评论。这就需要持久化,最开始可以写入 csv 甚至普通 text 文件。要注意一定在评论完之后才写入本地。这里用来区别的key可以直接用帖子的 href。即使把几千条同时载入内存作为字典,也是绰绰有余的。
帖子的回复内容也很关键,毕竟这不是单纯的爬虫程序,是一个有感情的 bot。至少,要有自己的语录库。更加高级有针对性的自然语言处理聊天是进阶的功能。
经验总结 requests 的 Session 相当好用,可以自动长连接、自动更新 cookie。 session 不足在于,对于请求没有重试的功能。是的你没看错。当网络环境差的时候,会频繁遇到来自底层 urllib 形形色色的报错,requests 统一将其抽象为requests.ConnectionError
。建议从一开始就在项目中使用自己对 requests.Session
的封装,对每次请求封装重试逻辑,并且实现为单例模式。
注意隐私保护,不要将密码上传至 一起讨论:jimxu_work#foxmail.com (把#换成@)
免责声明谨慎使用,遵守网站规定与法律法规,对造成任何后果概不负责。请爱惜网站服务器,也是在爱惜自己的豆瓣账号。
主体代码如下(更多在开头的代码仓库):
import requests
from lxml import etree
import time
import random
from queue import SimpleQueue, Empty
from util import DouUtil
from actions import RespGen
from mySelectors import NewPostSelector
from util import requestsWrapper
log = DouUtil.log
def get_headers(fileName=None):
name = 'headers.txt'
if (fileName is not None):
name = fileName
name = 'resources/' + name
headers = {}
with open(name, "r", encoding='utf-8') as f_headers:
hdrs = f_headers.readlines()
for line in hdrs:
key, value = line.split(": ")
headers[key] = value.strip()
return headers
def login(url, pwd, userName, session):
loginData = {'ck': '', 'name': userName,
'password': pwd, 'remember': 'true'}
loginHeaders = get_headers('login_headers.txt')
l = session.post(url, data=loginData, headers=loginHeaders)
if l.status_code == requests.codes['ok'] or l.status_code == requests.codes['found']:
print("Login Successfully")
return True
else:
print("Failed to Login")
log.error("Failed to Login", l.status_code)
session.close()
return False
def composeCmnt(session, response):
cmntForm = {'ck': '', 'rv_comment': response['ans'],
'start': 0, 'submit_btn': '发送'}
cmntForm['ck'] = DouUtil.getCkFromCookies(session)
return cmntForm
def prepareCaptcha(data, session, postUrl, r=None) -> dict:
pic_url, pic_id = DouUtil.getCaptchaInfo(session, postUrl, r)
verifyCode = ''
pic_path = DouUtil.save_pic_to_disk(pic_url, session)
log.debug(pic_url, pic_path)
verifyCode = DouUtil.getTextFromPic(pic_path)
return data
def postCmnt(session, postUrl, request, response):
data = composeCmnt(session._session, response)
cmntUrl = postUrl + 'add_comment'
r = session.post(cmntUrl, data=data, headers={'Referer': postUrl}, files=response.get('files'))
# r = session.get(postUrl)
code = str(r.status_code)
if (code.startswith('4') or code.startswith('5')) and not code.startswith('404'):
log.error(r.status_code)
raise Exception
elif 0 != len(etree.HTML(r.text).xpath("")):
log.warning(r.status_code)
data = prepareCaptcha(data, session, postUrl, r)
r = session.post(cmntUrl, data=data)
retry = 1
while 0 != len(etree.HTML(r.text).xpath("")):
if retry <= 0:
retry -= 1
break
data = prepareCaptcha(data, session, postUrl, r)
r = session.post(cmntUrl, data=data)
retry -= 1
if retry 0:
tup = q.get(timeout=3)
question, postUrl, dajie = tup[0], tup[1], tup[2]
resp = respGen.getResp(question, dajie)
postCmnt(reqWrapper, postUrl, question, resp)
sleepCmnt = random.randint(20, 30)
log.debug("sleep cmnt: ", sleepCmnt)
recorder.write(postUrl.split('/')[5] + '\n')
record = question + ': ' + resp['ans'] + '\n'
file.write(record)
except Empty:
log.info("Emptied q, one round finished")
finally:
file.close()
recorder.close()
DouUtil.flushCookies(s)
if __name__ == '__main__':
main()
进阶 TODO
多线程
完全的生产者-消费者模式
接入自然语言处理接口
接入OCR接口