写在前面的话:
之前写过在豆瓣上更新旧贴和回复顶贴的脚本. 毕竟已经是几年前的事情了, 一方面N多功能不能用了.,另一方面看当时的代码就跟一砣那啥似的. 所以更新了下, 虽然还是很像那啥
下面这个脚本 主要是学习研究技术点使用的, 顺便完成了发新贴
和回复顶贴
两个功能:
直接上码
import os
import sys
import inspect
import arrow
import requests
from requests.cookies import RequestsCookieJar
import math
from datetime import datetime
import json
import time
import pickle
from selenium import webdriver
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from lxml import etree
import re
import random
common_dir = os.path.realpath(os.path.abspath(os.path.join(
os.path.split(inspect.getfile(inspect.currentframe()))[0],
"../../../")))
if common_dir not in sys.path:
sys.path.insert(0, common_dir)
import logging
logging.basicConfig(level=logging.DEBUG,
format='%(asctime)s - %(filename)s[line:%(lineno)d] - %(levelname)s: %(message)s')
# 回帖内容集合
comment_list = [
"为了把自家的房子租出去, 都上劲了",
"发个回复, 表示房子还在",
"房子还在, 若房子不在了, 我会删除所有发贴记录, 不留一丝丝痕迹",
"房子还在, 你我都怕加了骗子微信, 那先私信聊",
"发个状态, 表示房子还在 ",
"666",
]
# 标题集合
subject2300 = [
'合租出租|6号线青年路站周边|南向次卧|月付2300全包|朝阳大悦城|达美中心',
'合租出租|朝阳大悦城周边|正规南向次卧|非隔断|月付2300全包',
]
subject1000=[
'合租出租|6号线青年路站周边|单人小暗间|月付1000全包|朝阳大悦城|达美中心',
'合租出租|朝阳大悦城周边|单人小暗间|月付1000全包',
]
# 内容集合 自定义的内容, 非真实内容
content2300 = [
"""
【出租,不限男女】 出租南向次卧(非隔断间), 适合情侣或单男单女居住 (抽烟或带宠物请勿扰, 谢谢)
""",
"""
价格是2300元每月,全包,可以月付,再也没有其他费用了。
其他没啥了,随时看房,和我签约,我也在里面住,有事找我, 不用担心跑路啥的,哈哈
"""
]
content1000 = [
"""
【出租,限男生】 出租单人小暗间, 适合一个男生. (抽烟或带宠物请勿扰, 谢谢)
""",
"""
爪机码字不方便,先报电话:17896035021,让我加你微信那就算了吧,因为我已经被很多冒充找房子的骗加微信,加了之后是微商广告啊什用了。
其他没啥了,随时看房,和我签约,我也在里面住,有事找我, 不用担心跑路啥的,哈哈
"""
]
content_dict = {
"type1000": content1000,
"type2300": content2300,
}
subject_dict = {
"type1000": subject1000,
"type2300": subject2300,
}
class MainPage():
"""
页面
"""
base_url = 'https://www.douban.com/' # 首页
accounts_url = 'https://accounts.douban.com/passport/login?source=group' # 登录页面
group_url = 'https://www.douban.com/group/' # 我的小组 页面
publish = 'https://www.douban.com/group/people/188207245/publish' # 我发布的 列表面
class MainPageLocators():
"""
页面元素定位器
"""
# 第一个页面
usepassword_lable = (By.XPATH, '//*[@id="account"]/div[2]/div[2]/div/div[1]/ul[1]/li[2]') # 登录页面的 <密码登录> 标签
username = (By.ID, 'username') # 用户名
password = (By.ID, 'password') # 密码
# signin_btn = (By.XPATH, '//*[@id="account"]/div[2]/div[2]/div/div[2]/div[1]/div[4]/a') # 登录页面的 <登录豆瓣> 按钮
signin_btn = (By.LINK_TEXT, '登录豆瓣') # 登陆页面的 <登录豆瓣> 按钮
group_lable = (By.LINK_TEXT, '小组') # 登陆后的 <小组> 标签
# 进入二级页面
mygroup_lable = (By.LINK_TEXT, '我的小组主页') # 新页面的 <我的小组主页> 标签
## 进入发起列表页, 进行回复顶贴
faqi_lable = (By.LINK_TEXT, '发起') # 新页面导航栏的 <发起> 标签
element_tbody = (By.XPATH, '//*[@id="content"]/div/div[1]/div[2]/table/tbody') # 找到 tbody 元素
element_tr = (By.TAG_NAME, 'tr') # 找到tr元素
### 进入三级页面
reply_textarea = (By.ID, 'last') # <你的回应>文本框, 可以填写回复的内容
captcha_image = (By.ID, 'captcha_image') # 验证码图片
captcha_textarea = (By.ID, 'captcha_field') # 验证码的输入框
submit_btn = (By.NAME, 'submit_btn') # <发送> 按键
## 进入 <加入的小组> 页面, 进行发新贴操作
join_group_lable = (By.LINK_TEXT, '加入的小组') # 新页面导航栏的 <加入的小组> 标签
fayan_lable = (By.XPATH, '//*[@id="group-new-topic-bar"]/div[1]/a') # 新页面导航栏的 <加入的小组> 标签
subject_textarea = (By.XPATH, '//*[@id="group-editor-root"]/div/div[2]/div[1]/span/textarea') # 新贴的标题文本框
tijiao_btn = (By.LINK_TEXT, '提交') # <提交> 按键
class Douban():
def __init__(self, *args, **kwargs):
self.chromedriver='D:\\work\\spider\\chromedriver.exe'
self.pages = MainPage()
self.locators = MainPageLocators()
self.driver = webdriver.Chrome(executable_path=self.chromedriver)
self.wait = WebDriverWait(self.driver, 10)
self.headers = {
"user-agent":'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.75 Safari/537.36',
}
self.session = requests.Session()
self.session.headers = self.headers
self.session.verify = False
self.jar = RequestsCookieJar()
def sign_in(self, url):
"""
登录
打开登录网址-> 定位到密码登录标签->输入用户名和密码-> 点击登录
返回 self.driver
"""
self.driver.get(url)
# 最大化窗口
# self.driver.maximize_window()
# 选择帐户密码标签
account_method = self.wait.until(EC.element_to_be_clickable(self.locators.usepassword_lable)).click()
# 定位用户名和密码
username = self.driver.find_element(*self.locators.username)
username.send_keys('yourname')
password = self.driver.find_element(*self.locators.password)
password.send_keys('password')
# 登录 TODO: 这里可能会有验证码登录的情况 ,暂时未判断出
self.wait.until(EC.element_to_be_clickable(self.locators.signin_btn)).click()
return self.driver
def cookies_writeto_local(self):
"""
将 selenium 获取到的 cookies 写入本地
"""
self.driver = self.sign_in(self.pages.accounts_url)
cookies = self.driver.get_cookies()
with open('doubancookies.txt', 'wb') as f:
f.write(pickle.dumps(cookies))
print("Done")
self.driver.quit()
def transform_jar(self, cookies=None):
"""
若没有传入新的cookies,则读本地的cookies文件,
否则更新本地cookies文件,
再 将 selenium 获取到的cookies 转为适合 requests 模块适用的cookies
"""
self.session.get(self.pages.base_url) # 这一步是有必要存在的,
if cookies is None:
with open('doubancookies.txt', 'rb') as f:
r = f.read()
if r:
cookies = pickle.loads(r)
else:
with open('doubancookies.txt', 'wb') as f:
f.write(pickle.dumps(cookies))
for cookie in cookies:
self.jar.set(cookie['name'], cookie['value'])
return self.jar
def prompt(self):
"""
提示框 处理, 没有用到
"""
self.wait.until(expected_conditions.alert_is_present())
alert = Alert(self.driver)
alert.send_keys("Selenium")
alert.accept()
def run(self):
"""
自动登录 并进入<我的小组主页>
"""
# 1. 优先使用的方案: 读取本地的cookies,(经验证, 弃用, 原因: 被网站拦截了)
# with open('doubancookies.txt', 'rb') as f:
# r = f.read()
# if r:
# cookies = pickle.loads(r)
# else:
# raise ValueError('doubancookies.txt 中没有内容')
# self.driver.get(self.pages.base_url)
# for cookie in cookies:
# self.driver.add_cookie(cookie)
# self.driver.get(self.pages.base_url)
# 2. 备用方案: 模拟登录(在用)
try:
self.driver.get(self.pages.accounts_url)
self.driver.maximize_window()
self.driver = self.sign_in(self.pages.accounts_url)
self.wait.until(EC.element_to_be_clickable(self.locators.group_lable)).click() # 此时会产生一个新的页面
for handle in self.driver.window_handles:
self.driver.switch_to_window(handle)
self.wait.until(EC.element_to_be_clickable(self.locators.mygroup_lable)).click()
# self.auto_new_topic()
self.auto_reply()
except Exception as e:
print(e)
finally:
self.driver.quit()
def auto_new_topic(self):
"""
自动发新贴, 规则: 以小组人数为权重, 随机选择5个小组发新贴
"""
self.wait.until(EC.element_to_be_clickable(self.locators.join_group_lable)).click()
with open('my_group_info.txt', 'r') as f:
groups = json.loads(f.read())
# 以每个小组的人数作为本小组的权重
weights = [int(g.get('num')) for g in groups]
# 以权重随机选出5个小组发贴
groups = random.choices(groups, weights=weights, k=5)
for g in groups:
name = g.get('name')
print(name)
url = g.get('url')
self.driver.get(url)
self.wait.until(EC.element_to_be_clickable(self.locators.fayan_lable)).click()
# 由于豆瓣的特殊设计, 无法使用xpath等定位到的内容录入的文本框, 但发现页面的焦点是在内容录入的位置, 因此, 采用的焦点激活定位的方法
content = self.driver.switch_to_active_element()
# 这随机选择内容和标题进行发新贴
type = random.choice(list(content_dict.keys()))
content.send_keys(random.choice(content_dict.get(type)))
subject = self.driver.find_element(*self.locators.subject_textarea)
subject.clear()
subject.send_keys(random.choice(subject_dict.get(type)))
self.wait.until(EC.element_to_be_clickable(self.locators.tijiao_btn)).click()
time.sleep(10)
def auto_reply(self):
"""
进入已发贴的列表, 根据回复权重, 随机选择4个, 进行自动回复顶贴
"""
self.wait.until(EC.element_to_be_clickable(self.locators.faqi_lable)).click()
table_body = self.driver.find_element(*self.locators.element_tbody)
tr_list = table_body.find_elements(*self.locators.element_tr)
result = []
for tr in tr_list:
ele = {}
title_url = tr.find_element(By.XPATH, "td[1]/a").get_attribute('href')
title_name = tr.find_element(By.XPATH, 'td[1]/a').text
reply_num = tr.find_element(By.XPATH, 'td[2]').text
closest_reply_time = tr.find_element(By.XPATH, 'td[3]').text
group_url=tr.find_element(By.XPATH, 'td[4]/a').get_attribute('href')
ele.setdefault('title_url', title_url)
ele.setdefault('title_name', title_name)
ele.setdefault('reply_num', re.sub(r'\s|回应', '', reply_num))
ele.setdefault('closest_reply_time', closest_reply_time)
ele.setdefault('group_url', group_url)
result.append(ele)
random_choices = random.choices(result, weights=[int(d.get('reply_num')) for d in result], k=4)
for choice in random_choices:
url = choice.get('title_url')
self.driver.get(url)
reply = self.driver.find_element(*self.locators.reply_textarea)
reply.clear()
reply.send_keys(random.choice(comment_list))
try:
# 以下两个还未实现, 原因是捕捉验证码图片较为困难.
test1 = self.wait.until(EC.visibility_of(self.driver.find_element(*self.locators.captcha_image))) # 判断验证码图片是否存在, 若存在, 则接受键盘录入验证码
print(test1)
test2 = self.wait.until(EC.visibility_of(*self.locators.captcha_image)) # 判断验证码图片是否存在, 若存在, 则接受键盘录入验证码
print(test2)
captcha = self.driver.find_element(*self.locators.captcha_textarea) #
captcha.send_keys(Keys.RETURN) # 在这里接受键盘的录入
except Exception as e:
print(e)
self.wait.until(EC.element_to_be_clickable(self.locators.submit_btn)).click()
time.sleep(10)
def main():
obj = Douban()
# obj.cookies_writeto_local()
obj.run()
if __name__ == "__main__":
main()
总结技术点:
- 依据权重, 随机选择样本,
import random
l = ['a', 'b', 'c']
weights = [1, 2, 3]
res = random.choices(l, weights=weights, k=1)
# c被选出的概率高于a
-
selenium 元素定位的多种尝试,
-
参考学习的文档:
selenium 官方文档1
selenium 官方文档2
参考的selenium 博客