python+selenium,刷问卷星,支持ip+比例,无需改代码,设置ip+比例即可运行

import logging
import random
import re
import traceback
from threading import Thread
import time

import numpy
import requests
from selenium import webdriver
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.by import By

"""
    @Author:鐘
    @Time:2023.11
"""

"""
任何疑问,请加qq群咨询:774326264 || 427847187  我看到了一定会耐心解答的!!!(划掉,不一定耐心了,因为被一些**问题耗尽了耐心,随缘了2023.5.5)
代码前身可能更容易理解一点:https://github.com/Zemelee/wjx/blob/master/wjx.py  ---  使用教程: https://www.bilibili.com/video/BV1qc411T7CG/
除了python,作者还发布了js版脚本在greasy fork上,名字就叫“问卷星脚本”,不带任何前后缀,使用可能比py更方便且支持跳题逻辑;
相关系列教程:https://space.bilibili.com/29109990/channel/collectiondetail?sid=1340503&ctype=0

代码使用规则:
    你需要提前安装python环境,且已具备上述的所有安装包(selenium版本号需要和webdriver匹配)
    还需要下载好chrome的webDriver自动化工具,并将其放在python安装目录下,以便和selenium配套使用,准备工作做好即可直接运行
    按要求填写概率值并替换成自己的问卷链接即可运行。
    虽然但是!!!即使正确填写概率值,不保证100%成功运行,因为代码再强大也强大不过问卷星的灵活性,别问我怎么知道的,都是泪
    如果有疑问欢迎打扰我,如果不会python但确有需要也可以找我帮你刷嗷~(2023.05.04)
"""

"""
获取代理ip,这里要使用到一个叫“品赞ip”的第三方服务: https://www.ipzan.com?pid=ggj6roo98
注册,需要实名认证(这是为了防止你用代理干违法的事,相当于网站的免责声明,属于正常步骤,所有代理网站都会有这一步)
将自己电脑的公网ip添加到网站的白名单中,然后选择地区,时长为1分钟,数据格式为txt,提取数量选1
然后点击生成api,将链接复制到放在zanip函数里
设置完成后,不要问为什么和视频教程有点不一样,因为与时俱进!(其实是因为懒,毕竟代码改起来容易,视频录起来不容易嘿嘿2023.10.29)
如果不需要ip可不设置,也不影响此程序直接运行(悄悄提醒,品赞ip每周可以领3块钱)
"""


def zanip():
    # 这里放你的ip链接,选择你想要的地区,1分钟,ip池无所谓,数据格式txt,提取数量1,其余默认即可
    api = "https://service.ipzan.com/core-extract?num=1&no=???&minute=1&area=all&pool=quality&secret=???"
    ip = requests.get(api).text
    return ip


# 示例问卷,试运行结束后,需要改成你的问卷地址
url = 'https://www.wjx.cn/vm/OM6GYNV.aspx#'

"""
单选题概率参数,"1"表示第一题,0表示不选, [30, 70]表示3:7,-1表示随机
在示例问卷中,第一题有三个选项,"1"后面的概率参数也应该设置三个值才对,否则会报错!!!
同时,题号其实不重要,只是为了填写概率值时方便记录我才加上去的,这个字典在真正使用前会转化为一个列表;(这一行没看懂没关系,下面一行懂了就行)
最重要的其实是保证single_prob的第n个参数对应第n个单选题,比如在示例问卷中第5题是滑块题,但是我single_prob却有“第5题”,因为这个"5"其实对应的是第5个单选题,也就是问卷中的第6题
这个single_prob的"5"可以改成其他任何值,当然我不建议你这么干,因为问卷中只有5个单选题,所以第6个单选题的参数其实是没有用上的,参数只能多不能少!!!(这一点其他类型的概率参数也适用)
"""
single_prob = {"1": [1, 1, 0], "2": -1, "3": -1, "4": -1, "5": -1, "6": [1, 0], }

# 下拉框参数,具体含义参考单选题,如果没有下拉框题也不要删,就让他躺在这儿吧,其他题也是哦,没有就不动他,别删,只改你有的题型的参数就好啦
droplist_prob = {"1": [1, 1, 1]}

# 多选题概率参数,0不选该选项,100必选,[10, 50]表示1:5,-1表示随机,
multiple_prob = {"9": [100, 2, 1, 1]}
# 多选题选择的选项数量(去除必选后的数),这里填1与上面的multiple_prob表示在必选A后,会再从BCD中选1个选项
# 注意!!!如果选项数量比较少,建议多选的数量参数不要太大,因为数量参数值越大,最后刷出来的数据分布误差越大!!!4个选项建议选1-2个即可。
multiple_opts = {"9": 1, }

# 矩阵题概率参数,-1表示随机,其他含义参考单选题;同样的,题号不重要,保证第几个参数对应第几个矩阵小题就可以了;
# 在示例问卷中矩阵题是第10题,每个小题都要设置概率值才行!!以下参数表示第二题随机,其余题全选A
matrix_prob = {"1": [1, 0, 0, 0, 0], "2": -1, "3": [1, 0, 0, 0, 0], "4": [1, 0, 0, 0, 0],
               "5": [1, 0, 0, 0, 0], "6": [1, 0, 0, 0, 0]}

# 量表题概率参数,参考单选题
scale_prob = {"7": [0, 2, 3, 4, 1], "12": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]}

# 填空题参数,在题号后面按该格式填写需要填写的内容,
texts = {"8": ["内容1", "内容2", " 内容3"], }
# 每个内容对应的概率1:1:1,
texts_prob = {"8": [1, 1, 1]}

# --------------到此为止,参数设置完毕,可以直接运行啦!-------------------
# 如果需要设置浏览器窗口数量,请转到最后一个函数(main函数),注意看里面的注释喔!


# 参数归一化,把概率值按比例缩放到概率值和为1,比如某个单选题[1,2,3,4]会被转化成[0.1,0.2,0.3,0.4],[1,1]会转化成[0.5,0.5]
for prob in [single_prob, matrix_prob, droplist_prob, scale_prob, texts_prob]:
    for key in prob:
        if isinstance(prob[key], list) and prob[key] != -1:
            prob_sum = sum(prob[key])
            prob[key] = [x / prob_sum for x in prob[key]]

# 转化为列表,去除题号
single_prob = list(single_prob.values())
droplist_prob = list(droplist_prob.values())
multiple_prob = list(multiple_prob.values())
multiple_opts = list(multiple_opts.values())
matrix_prob = list(matrix_prob.values())
scale_prob = list(scale_prob.values())
texts_prob = list(texts_prob.values())
texts = list(texts.values())

print("单选题参数: ", single_prob)
print("下拉框参数: ", droplist_prob)
print("多选题参数: ", multiple_prob)
print("矩阵题参数: ", matrix_prob)
print("量表题参数: ", scale_prob)
print("所有按照比例刷题的脚本只能让问卷总体数据表面上看起来合理, 并不保证高信效度。")
print("如果对信效度有要求可以进群找作者代刷, 信效度max.")


# 校验IP地址合法性
def validate(ip):
    pattern = r'^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?):(\d{1,5})$'
    if re.match(pattern, ip):
        return True
    return False


# 检测题量和页数的函数,返回一个列表,第一个数表示第一页的题量,第二个数表示第二页的题量;比如示例问卷会返回:[3, 2, 2, 7]
# 虽然但是,我见识过问卷星再没有跳题逻辑的情况下有题被隐藏,我当时就??????
# 这会导致detect返回包含被隐藏的题,数值可能偏高,比如可见题目[3, 2, 2, 7]被detect成[4, 2, 2, 7]。。
def detect(driver):
    q_list = []  # 长度等于页数,数字代表该页的题数
    xpath = '//*[@id="divQuestion"]/fieldset'
    page_num = len(driver.find_elements(By.XPATH, xpath))  # 页数
    qs = driver.find_elements(By.XPATH, f'//*[@id="fieldset1"]/div')  # 每一页的题
    invalid_item = 0  # 无效问题数量
    for qs_item in qs:
        # 判断其topic属性值是否值包含数字
        if qs_item.get_attribute("topic").isdigit() is False:
            invalid_item += 1
    # 如果只有1页
    q_list.append(len(qs) - invalid_item)
    if page_num >= 2:
        for i in range(2, page_num + 1):
            qs = driver.find_elements(By.XPATH, f'//*[@id="fieldset{i}"]/div')
            invalid_item = 0  # 每一页的无效问题初始值为0
            # 遍历每一个div,判断其是否可以回答
            for qs_item in qs:
                # 判断其topic属性值是否值包含数字,因为只有题的div的topic属性才是纯数字
                if qs_item.get_attribute("topic").isdigit() is False:
                    invalid_item += 1
            # [3, 2, 2, 7]
            q_list.append(len(qs) - invalid_item)
    return q_list


# 填空题处理函数
def vacant(driver, current, index):
    content = texts[index]
    # 对应填空题概率参数
    p = texts_prob[index]
    text_index = numpy.random.choice(a=numpy.arange(0, len(p)), p=p)
    driver.find_element(By.CSS_SELECTOR, f'#q{current}').send_keys(content[text_index])


# 单选题处理函数
def single(driver, current, index):
    xpath = f'//*[@id="div{current}"]/div[2]/div'
    a = driver.find_elements(By.XPATH, xpath)
    p = single_prob[index]
    if p == -1:
        r = random.randint(1, len(a))
    else:
        r = numpy.random.choice(a=numpy.arange(1, len(a) + 1), p=p)
    driver.find_element(By.CSS_SELECTOR,
                        f'#div{current} > div.ui-controlgroup > div:nth-child({r})').click()


# 下拉框处理函数
def droplist(driver, current, index):
    # 先点击“请选择”
    driver.find_element(By.CSS_SELECTOR, f"#select2-q{current}-container").click()
    time.sleep(0.5)
    # 选项数量
    options = driver.find_elements(By.XPATH, f"//*[@id='select2-q{current}-results']/li")
    p = droplist_prob[index]  # 对应概率
    r = numpy.random.choice(a=numpy.arange(1, len(options)), p=p)
    driver.find_element(By.XPATH, f"//*[@id='select2-q{current}-results']/li[{r + 1}]").click()


# 多选题处理函数:这个超复杂,要不是chatgpt我一辈子都写不出这代码
def multiple(driver, current, index):
    xpath = f'//*[@id="div{current}"]/div[2]/div'
    options = driver.find_elements(By.XPATH, xpath)
    # 第current题对应的概率值
    probabilities = multiple_prob[index]
    if probabilities == 0:  # 不选
        return
    elif probabilities == -1:  # 随机
        r = random.randint(1, len(options))
        driver.find_element(By.CSS_SELECTOR,
                            f'#div{current} > div.ui-controlgroup > div:nth-child({r})').click()
    else:
        prob_copy = probabilities.copy()
        opts_num = multiple_opts[index]  # 第current题对应的选项数量参数
        for i in prob_copy:  # 如果存在列表中概率为100的项,则直接选择该项
            if i == 100:
                # 找到100元素位置
                sure = prob_copy.index(i)
                driver.find_element(By.CSS_SELECTOR,
                                    f'#div{current} > div.ui-controlgroup > div:nth-child({sure + 1})').click()
                # 将已选的概率修改为0,以便在后面按概率选择其他选项
                prob_copy[sure] = 0
        # 计算不为0的数值总和
        total = sum([num for num in prob_copy])
        if total == 0: return
        # 将不为0的数值归一化
        probabilities_norm = [num / total if num != 0 else 0 for num in prob_copy]
        # 从位置1到列表长度之间随机选择 opts_num - 已选数 相同数量的选项
        selection_indices = numpy.random.choice(
            range(len(options)),
            size=opts_num,
            replace=False,
            p=probabilities_norm)
        # 选择随机选择的选项
        for i in selection_indices:
            driver.find_element(By.CSS_SELECTOR,
                                f'#div{current} > div.ui-controlgroup > div:nth-child({i + 1})').click()


# 矩阵题处理函数
def matrix(driver, current, index):
    xpath1 = f'//*[@id="divRefTab{current}"]/tbody/tr'
    a = driver.find_elements(By.XPATH, xpath1)
    q_num = 0  # 矩阵的题数量
    for tr in a:
        if tr.get_attribute("rowindex") is not None:
            q_num += 1
    # 选项数量
    xpath2 = f'//*[@id="drv{current}_1"]/td'
    b = driver.find_elements(By.XPATH, xpath2)  # 题的选项数量+1 = 6
    # 遍历每一道小题
    for i in range(1, q_num + 1):
        p = matrix_prob[index]
        index += 1
        if p == -1:
            opt = random.randint(2, len(b))
        else:
            opt = numpy.random.choice(a=numpy.arange(2, len(b) + 1), p=p)
        driver.find_element(By.CSS_SELECTOR, f'#drv{current}_{i} > td:nth-child({opt})').click()
    return index


# 排序题处理函数,排序暂时只能随机
def reorder(driver, current):
    xpath = f'//*[@id="div{current}"]/ul/li'
    a = driver.find_elements(By.XPATH, xpath)
    for j in range(1, len(a) + 1):
        b = random.randint(j, len(a))
        driver.find_element(By.CSS_SELECTOR, f'#div{current} > ul > li:nth-child({b})').click()
        time.sleep(0.4)


# 量表题处理函数
def scale(driver, current, index):
    xpath = f'//*[@id="div{current}"]/div[2]/div/ul/li'
    a = driver.find_elements(By.XPATH, xpath)
    p = scale_prob[index]
    if p == -1:
        b = random.randint(1, len(a))
    else:
        b = numpy.random.choice(a=numpy.arange(1, len(a) + 1), p=p)
    driver.find_element(By.CSS_SELECTOR,
                        f"#div{current} > div.scale-div > div > ul > li:nth-child({b})").click()


# 刷题逻辑函数
def brush(driver):
    q_list = detect(driver)  # 检测页数和每一页的题量
    single_num = 0  # 第num个单选题
    vacant_num = 0  # 第num个填空题
    droplist_num = 0  # 第num个下拉框题
    multiple_num = 0  # 第num个多选题
    matrix_num = 0  # 第num个矩阵小题
    scale_num = 0  # 第num个量表题
    current = 0  # 题号
    for j in q_list:  # 遍历每一页
        for k in range(1, j + 1):  # 遍历该页的每一题
            current += 1
            # 判断题型
            q_type = driver.find_element(By.CSS_SELECTOR, f'#div{current}').get_attribute("type")
            if q_type == "1" or q_type == "2":  # 填空题
                vacant(driver, current, vacant_num)
                vacant_num += 1  # 同时将vacant_num+1表示运行vacant函数时该使用texts参数的下一个值
            elif q_type == "3":  # 单选
                single(driver, current, single_num)
                single_num += 1  # single_num+1表示运行single函数时该使用single_prob参数的下一个值
            elif q_type == "4":  # 多选
                multiple(driver, current, multiple_num)
                multiple_num += 1
            elif q_type == "5":  # 量表题
                scale(driver, current, scale_num)
                scale_num += 1
            elif q_type == "6":  # 矩阵题
                matrix_num = matrix(driver, current, matrix_num)
            elif q_type == "7":  # 下拉框
                droplist(driver, current, droplist_num)
                droplist_num += 1
            elif q_type == "8":  # 滑块题
                score = random.randint(1, 100)
                driver.find_element(By.CSS_SELECTOR, f'#q{current}').send_keys(score)
            elif q_type == "11":  # 排序题
                reorder(driver, current)
            else:
                print(f"第{k}题为不支持题型!")
        time.sleep(0.5)
        #  一页结束过后要么点击下一页,要么点击提交
        try:
            driver.find_element(By.CSS_SELECTOR, '#divNext').click()  # 点击下一页
            time.sleep(0.5)
        except:
            # 点击提交
            driver.find_element(By.XPATH, '//*[@id="ctlNext"]').click()
    submit(driver)


# 提交函数
def submit(driver):
    time.sleep(1)
    # 点击对话框的确认按钮
    try:
        driver.find_element(By.XPATH, '//*[@id="layui-layer1"]/div[3]/a').click()
        time.sleep(1)
    except:
        pass
    # 点击智能检测按钮,因为可能点击提交过后直接提交成功的情况,所以智能检测也要try
    try:
        driver.find_element(By.XPATH, '//*[@id="SM_BTN_1"]').click()
        time.sleep(3)
    except:
        pass
    # 滑块验证
    try:
        slider = driver.find_element(By.XPATH, '//*[@id="nc_1__scale_text"]/span')
        if str(slider.text).startswith("请按住滑块"):
            width = slider.size.get('width')
            ActionChains(driver).drag_and_drop_by_offset(slider, width, 0).perform()
    except:
        pass


def run(xx, yy):
    # 躲避智能检测,将webDriver设置为false
    option = webdriver.ChromeOptions()
    option.add_experimental_option('excludeSwitches', ['enable-automation'])
    option.add_experimental_option('useAutomationExtension', False)
    global count
    global stop
    global fail  # 失败次数
    while not stop:
        ip = zanip()
        if validate(ip):
            option.add_argument(f'--proxy-server={ip}')
        driver = webdriver.Chrome(options=option)
        driver.set_window_size(550, 650)
        driver.set_window_position(x=xx, y=yy)
        driver.execute_cdp_cmd('Page.addScriptToEvaluateOnNewDocument',
                               {'source': 'Object.defineProperty(navigator, "webdriver", {get: () => undefined})'})
        try:
            driver.get(url)
            url1 = driver.current_url  # 表示问卷链接
            brush(driver)
            # 刷完后给一定时间让页面跳转
            time.sleep(4)
            url2 = driver.current_url  # 表示问卷填写完成后跳转的链接,一旦跳转说明填写成功
            if url1 != url2:
                count += 1
                print(f"已填写{count}份 - 失败{fail}次 - {time.strftime('%H:%M:%S', time.localtime(time.time()))} ")
                driver.quit()
        except:
            traceback.print_exc()
            fail += 1
            logging.warning(f"已失败{fail}次,失败超过10次(左右)将强制停止------------------------------")
            if fail >= 10:  # 失败阈值
                stop = True
                logging.critical('失败次数过多,为防止耗尽ip余额,程序将强制停止,请检查代码是否正确')
                quit()
            driver.quit()
            continue


# 多线程执行run函数
if __name__ == "__main__":
    count = 0  # 记录已刷份数
    fail = 0  # 失败次数
    stop = False
    if validate(zanip()):
        print("IP设置成功, 将使用代理ip填写")
    else:
        print("IP设置失败, 将使用本机ip填写")
    # 需要几个窗口同时刷就设置几个thread_?,默认两个,args里的数字表示设置浏览器窗口打开时的初始xy坐标
    thread_1 = Thread(target=run, args=(50, 50))
    thread_2 = Thread(target=run, args=(650, 50))
    # thread_3 = Thread(target=run, args=(650, 280))

    thread_1.start()
    thread_2.start()
    # thread_3.start()

    thread_1.join()
    thread_2.join()
    # thread_3.join()

"""
    总结,你需要修改的有: 1 每个题的比例参数(必改)  2 问卷链接(必改)  3 ip链接(可选)  4 浏览器窗口数量(可选)
    有疑问可以加qq群喔: 774326264 || 427847187 ; 
    虽然我不一定回hhh, 但是群友们不一定不回;另外,我不是群主和管理!她们是我拉的不懂代码的工具人xixi
    Presented by 鐘
"""

  • 8
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值