├── utils_ └── utils.py ├── .gitignore ├── formatdata.py ├── README.md ├── browser ├── bitbrowser.py └── adspower.py └── twitter └── twitter.py /utils_/utils.py: -------------------------------------------------------------------------------- 1 | import sys, traceback, requests 2 | 3 | def try_except_code(function): 4 | """处理python的异常,以及requests请求异常。在需要的函数名前面使用装饰器语法@try_except_code调用 5 | 捕获requests异常时,需要在使用地方的响应后面加一句response.raise_for_status()来检查响应的状态码,如果状态码表明请求失败,则抛出一个HTTPError异常,然后就可以用此函数捕获异常。如果状态码表明请求成功,则什么也不会发生,函数会直接返回 6 | """ 7 | def wrapper(*args, **kwargs): 8 | try: 9 | result = function(*args, **kwargs) 10 | except Exception as e: 11 | # 获取异常信息 12 | exc_type, exc_obj, exc_tb = sys.exc_info() 13 | # 获取文件名和行号 14 | file_name = traceback.extract_tb(exc_tb)[-1][0] 15 | line_number = traceback.extract_tb(exc_tb)[-1][1] 16 | # 输出错误信息 17 | print("出错文件:", file_name) 18 | print("出错位置:", line_number, '行') 19 | print("出错类型:", exc_type.__name__) 20 | print("错误信息:", str(e)) 21 | result = None 22 | except requests.exceptions.RequestException: 23 | # 处理 requests 异常 24 | exc_type, exc_obj, exc_tb = sys.exc_info() 25 | # 获取文件名和行号 26 | file_name = traceback.extract_tb(exc_tb)[-1][0] 27 | line_number = traceback.extract_tb(exc_tb)[-1][1] 28 | # 输出错误信息 29 | print("出错文件:", file_name) 30 | print("出错位置:", line_number, '行') 31 | print("出错类型:", exc_type.__name__) 32 | print("错误信息:", str(exc_obj)) 33 | result = None 34 | except requests.exceptions.HTTPError as e: 35 | # 处理 HTTP 异常 36 | exc_type, exc_obj, exc_tb = sys.exc_info() 37 | # 获取文件名和行号 38 | file_name = traceback.extract_tb(exc_tb)[-1][0] 39 | line_number = traceback.extract_tb(exc_tb)[-1][1] 40 | # 输出错误信息 41 | print("出错文件:", file_name) 42 | print("出错位置:", line_number, '行') 43 | print("出错类型:", exc_type.__name__) 44 | print("错误信息:", str(e)) 45 | result = None 46 | return result 47 | return wrapper -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | .DS_Store 6 | 7 | # 敏感数据 8 | /data 9 | config.py 10 | refresh_tokens.json 11 | 12 | # C extensions 13 | *.so 14 | 15 | # Distribution / packaging 16 | .Python 17 | build/ 18 | develop-eggs/ 19 | dist/ 20 | downloads/ 21 | eggs/ 22 | .eggs/ 23 | lib/ 24 | lib64/ 25 | parts/ 26 | sdist/ 27 | var/ 28 | wheels/ 29 | pip-wheel-metadata/ 30 | share/python-wheels/ 31 | *.egg-info/ 32 | .installed.cfg 33 | *.egg 34 | MANIFEST 35 | 36 | # PyInstaller 37 | # Usually these files are written by a python script from a template 38 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 39 | *.manifest 40 | *.spec 41 | 42 | # Installer logs 43 | pip-log.txt 44 | pip-delete-this-directory.txt 45 | 46 | # Unit test / coverage reports 47 | htmlcov/ 48 | .tox/ 49 | .nox/ 50 | .coverage 51 | .coverage.* 52 | .cache 53 | nosetests.xml 54 | coverage.xml 55 | *.cover 56 | *.py,cover 57 | .hypothesis/ 58 | .pytest_cache/ 59 | 60 | # Translations 61 | *.mo 62 | *.pot 63 | 64 | # Django stuff: 65 | *.log 66 | local_settings.py 67 | db.sqlite3 68 | db.sqlite3-journal 69 | 70 | # Flask stuff: 71 | instance/ 72 | .webassets-cache 73 | 74 | # Scrapy stuff: 75 | .scrapy 76 | 77 | # Sphinx documentation 78 | docs/_build/ 79 | 80 | # PyBuilder 81 | target/ 82 | 83 | # Jupyter Notebook 84 | .ipynb_checkpoints 85 | 86 | # IPython 87 | profile_default/ 88 | ipython_config.py 89 | 90 | # pyenv 91 | .python-version 92 | 93 | # pipenv 94 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 95 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 96 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 97 | # install all needed dependencies. 98 | #Pipfile.lock 99 | 100 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 101 | __pypackages__/ 102 | 103 | # Celery stuff 104 | celerybeat-schedule 105 | celerybeat.pid 106 | 107 | # SageMath parsed files 108 | *.sage.py 109 | 110 | # Environments 111 | .env 112 | .venv 113 | env/ 114 | venv/ 115 | ENV/ 116 | env.bak/ 117 | venv.bak/ 118 | 119 | # Spyder project settings 120 | .spyderproject 121 | .spyproject 122 | 123 | # Rope project settings 124 | .ropeproject 125 | 126 | # mkdocs documentation 127 | /site 128 | 129 | # mypy 130 | .mypy_cache/ 131 | .dmypy.json 132 | dmypy.json 133 | 134 | # Pyre type checker 135 | .pyre/ 136 | -------------------------------------------------------------------------------- /formatdata.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import os,sys 3 | sys.path.append(os.getcwd()) # 工作目录 4 | from config import * 5 | from utils_.utils import try_except_code 6 | 7 | # 组装数据 8 | @try_except_code 9 | def my_format_data(start_num, end_num, is_bitbrowser=True): 10 | """组装数据 11 | 12 | Attributes: 13 | start_num:开始账号 14 | end_num:结束账号 15 | is_bitbrowser:是否为比特浏览器数据 True:比特浏览器 False:ads浏览器 16 | """ 17 | if int(start_num) <= 0 or int(end_num) <= 0: 18 | print('账号必须大于0') 19 | return 20 | if int(start_num) > int(end_num): 21 | print('开始账号必须小于或等于结束账号') 22 | return 23 | # 浏览器数据 24 | if is_bitbrowser == True: 25 | all_browser = pd.read_excel(bitbrowser_file) 26 | all_browser = all_browser.sort_values(by='序号', ascending=True) # 按序号升序排列 27 | all_browser = all_browser.reset_index(drop=True) # 放弃原来datafarm序号 28 | all_browser = all_browser[['序号', 'ID', 'User Agent']] 29 | all_browser = all_browser.rename(columns={'序号': 'index_id', 'ID': 'browser_id', 'User Agent': 'user_agent'}) 30 | else: 31 | all_browser = pd.read_excel(adspower_file) 32 | all_browser = all_browser.sort_values(by='acc_id', ascending=True) # 按acc_id升序排列 33 | all_browser = all_browser.reset_index(drop=True) # 放弃原来datafarm序号 34 | all_browser = all_browser[['acc_id', 'id', 'ua']] 35 | all_browser = all_browser.rename(columns={'acc_id': 'index_id', 'id': 'browser_id', 'ua': 'user_agent'}) 36 | # ip数据 37 | all_ip = pd.read_csv(ip_file, sep=':', engine='python') 38 | all_ip['proxy'] = "socks5://" + all_ip['proxy_username'] +":" + all_ip['proxy_password'] +"@" + all_ip['proxy_ip'] +":" + all_ip['proxy_port'].map(str) 39 | # twitter数据 40 | all_twitter = pd.read_csv(twitter_file, sep='|', engine='python') 41 | 42 | data = pd.merge(left=all_browser,right=all_ip,left_index=True,right_index=True,how='inner') 43 | data = pd.merge(left=data,right=all_twitter,left_index=True,right_index=True,how='inner') 44 | 45 | data = data.iloc[int(start_num)-1:int(end_num),:].reset_index(drop=True) 46 | data = data.to_dict('records') 47 | return data 48 | 49 | @try_except_code 50 | def my_twitter_data(): 51 | """所有twitter账号数据。数组 52 | """ 53 | all_twitter = pd.read_csv(twitter_file, sep='|', engine='python') 54 | # 将DataFrame数据转换为数组 55 | my_twitter_data = all_twitter['twitter_username'].tolist() 56 | return my_twitter_data 57 | 58 | if __name__ == '__main__': 59 | 60 | my_format_data = my_format_data(1, 20, is_bitbrowser=True) 61 | print(my_format_data) 62 | 63 | my_twitter_data = my_twitter_data() 64 | print(my_twitter_data) 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 功能 2 | 3 | twitter 4 | - 关注(follow) 5 | - 点赞(like) 6 | - 转推(retweet) 7 | - 评论(reply) 8 | - 根据条件随机选择推文,点赞转推评论 9 | - 随机选择一个互关贴,转推评论关注(作者及评论者) 10 | - 回关 11 | 12 | 指纹浏览器 13 | - 批量创建浏览器 14 | - 批量修改代理 15 | 16 | # 前置条件 17 | - python3,可通过anaconda安装 18 | - 编辑器,如vscode 19 | - 指纹浏览器,如bitbrowser、adspower 20 | - 独立ip 21 | 22 | # 说明 23 | - python、编辑器问题需要自行搜索答案。 24 | - 本程序在mac下测试,windows需要自行测试 25 | - 本程序默认使用比特浏览器。如需使用ads浏览器,twitter_.py文件需要如下修改 26 | ``` 27 | # 导入adspower文件 28 | from browser.adspower import * 29 | 30 | # OAuth2ForTwitterUtil类改为继承AdspowerUtil类 31 | class OAuth2ForTwitterUtil(AdspowerUtil): 32 | ``` 33 | 34 | # 准备 35 | 36 | ## 创建twitter应用程序,设置OAuth2,获取key 37 | 38 | twitter开发者平台网址:https://developer.twitter.com/en/portal/dashboard 39 | 40 | 1、创建项目生成key 41 | ![twitter创建项目生成key](https://s2.loli.net/2022/07/13/zslLJa5TkdRmAuG.jpg) 42 | 43 | 2、设置OAuth2并生成client_id和client_secret(只有机密客户,才需要client_secret) 44 | ![twitter设置OAuth2并生成clientID](https://s2.loli.net/2022/07/13/7X1mVTicu8ABIC5.jpg) 45 | 46 | ## 准备数据文件 47 | 48 | 完整文件结构 49 | ``` 50 | twitter_script 51 | twitter 52 | twitter.py # 主文件 53 | refresh_tokens.json # 各账号refresh_token文件 54 | browser 55 | bitbrowser.py # 比特浏览器文件 56 | adspower.py # ads浏览器文件 57 | data 58 | twitter.csv # twitter账号文件 59 | ip.csv # 独立ip文件 60 | bitbrowser.xlsx # 比特浏览器数据文件 61 | adspower.xlsx # ads浏览器数据文件 62 | tweet_texts.txt # 推文内容文件 63 | reply_texts.txt # 回复话术文件 64 | config.py # 配置文件 65 | formatdata.py # 数据组装文件 66 | README.md # 说明文档 67 | .gitignore # git版本控制系统文件,敏感数据不上传到github 68 | ``` 69 | 70 | 由于数据属于敏感文件,不能上传。需要自行补齐数据文件。xxxxx的地方换成你自己的数据 71 | 72 | 1、config.py 73 | ``` 74 | client_id = "xxxxxxxxxx" 75 | client_secret = "xxxxxxxxxx" 76 | redirect_uri = "https://twitter.com/home" 77 | 78 | # AdsPower Local API 接口 79 | adspower_url = 'http://local.adspower.com:xxxxx' 80 | # BitBrowser Local API 接口 81 | bitbrowser_url = 'http://127.0.0.1:xxxxx' 82 | 83 | refresh_tokens_file = './twitter_/refresh_tokens.json' 84 | twitter_file = './data/twitter.csv' 85 | ip_file = './data/ip.csv' 86 | tweet_texts_file = './data/tweet_texts.txt' 87 | reply_texts_file = './data/reply_texts.txt' 88 | bitbrowser_file = './data/bitbrowser.xlsx' 89 | adspower_file = './data/adspower.xlsx' 90 | ``` 91 | 92 | 2、refresh_tokens.json 93 | 94 | 各推特账号的刷新令牌。留空即可,程序会自动更新 95 | ``` 96 | {} 97 | ``` 98 | 99 | 3、twitter.csv 100 | 101 | 你的twitter账号数据。只列出了username和password。可以自行添加其他字段 102 | ``` 103 | twitter_username|twitter_password|... 104 | xxxxx|xxxxx|... 105 | xxxxx|xxxxx|... 106 | ...... 107 | ``` 108 | 109 | 4、ip.csv 110 | 111 | 你的独立ip数据 112 | ``` 113 | proxy_ip:proxy_port:proxy_username:proxy_password 114 | xxxxx:xxxxx:xxxxx:xxxxx 115 | xxxxx:xxxxx:xxxxx:xxxxx 116 | ...... 117 | ``` 118 | 119 | 5、tweet_texts.txt 120 | 121 | 要发布的推文内容文件。找点鸡汤文之类的,一行一个。调用时程序会随机选择一条发推。 122 | ``` 123 | Never evade problems, time will not give the weak anything in return. 124 | You can‘t do it — that’s the biggest lie on earth。 125 | ...... 126 | ``` 127 | 128 | 6、reply_texts.txt 129 | 130 | 要评论的内容文件。搜集点马屁话,一行一个。调用时程序会随机选择一条发评论。 131 | ``` 132 | good job 133 | great 134 | ...... 135 | ``` 136 | 137 | 7、bitbroswer.xlsx / adspower.xlsx 138 | 139 | 比特浏览器 / ads浏览器数据文件。需要创建完浏览器后导出数据文件,并重命名。 140 | 141 | # 组装数据 142 | 143 | `formatdata.py`文件用来组装数据。将散落在各文件的数据,指纹浏览器的数据、twitter账号的数据、ip数据等组装起来备用。 144 | 145 | 格式是`[{formatadata1},{formatadata2},...]`。每条数据如下所示 146 | ``` 147 | {'index_id': 1, 'browser_id': xxxxx, 'user_agent': xxxxx, 'proxy_ip': xxxxx, 'proxy_port': xxxxx, 'proxy_username': xxxxx, 'proxy_password': xxxxx, 'proxy': 'socks5://account:password@ip:port', 'twitter_username': xxxxx, 'twitter_password': xxxxx} 148 | ``` 149 | 150 | 这里一定要做好数据的对应。一个浏览器对应一个ip一个twitter,一一对应好后就不要变了。如果出现twitter被封或者ip不干净的情况,及时更换相关文件的对应数据。 151 | 152 | # 示例 153 | 154 | 1. 批量创建指纹浏览器(程序创建没有设置代理)并导出浏览器数据文件 155 | 2. 批量修改代理ip 156 | 3. 批量授权(会自动打开浏览器。唯一一次需要在浏览器里操作的) 157 | 4. 批量处理业务(如批量关注、转推等) 158 | 5. 择时重复4 159 | 160 | 示例详见`bitbrowser.py`、`twitter_py`文件(底部) 161 | -------------------------------------------------------------------------------- /browser/bitbrowser.py: -------------------------------------------------------------------------------- 1 | """ 2 | bitbrowser api : https://doc.bitbrowser.cn/api-jie-kou-wen-dang/liu-lan-qi-jie-kou 3 | """ 4 | from selenium import webdriver 5 | from selenium.webdriver.chrome.service import Service 6 | from selenium.webdriver.chrome.options import Options 7 | from selenium.webdriver.support.wait import WebDriverWait 8 | from selenium.webdriver.support import expected_conditions as EC 9 | from selenium.webdriver.common.by import By 10 | from selenium.webdriver.common.keys import Keys 11 | from selenium.webdriver.common.action_chains import ActionChains 12 | from selenium.webdriver.common.desired_capabilities import DesiredCapabilities 13 | import requests,os,sys,time,math,json 14 | sys.path.append(os.getcwd()) # 工作目录 15 | from config import * 16 | from formatdata import * 17 | from utils_.utils import try_except_code 18 | 19 | @try_except_code 20 | def create_or_update_browser(browser_os='mac', browser_id=''): 21 | """创建或者修改浏览器窗口 22 | 23 | Attributes: 24 | browser_os:操作系统,影响ua值。最好跟本机操作系统相同。 25 | browser_id:bitbrowser浏览器id。此参数空值时代表创建浏览器。有值时代表修改浏览器信息 26 | returns: 27 | browser_id:浏览器id 28 | """ 29 | # 操作系统 30 | if browser_os in ['mac','macos']: 31 | browser_os = 'MacIntel' 32 | elif browser_os in ['win','windows']: 33 | browser_os = 'Win32' 34 | 35 | body = { 36 | 'id': browser_id, # 有值时为修改,无值是添加 37 | 'platform': 'https://www.google.com', # 账号平台 38 | 'platformIcon': 'other', # 取账号平台的 hostname 或者设置为other 39 | 'workbench': 'localServer', # 浏览器窗口工作台页面。chuhai2345(默认)|localServer|disable 40 | 'proxyMethod': 2, # 代理类型 1平台 2自定义 41 | # 'agentId': '', # proxyMethod为1时,平台代理IP的id 42 | # 自定义代理类型 ['noproxy', 'http', 'https', 'socks5', '911s5'] 43 | 'proxyType': 'noproxy', # 先不设置代理。可在修改代理接口去设置 44 | "browserFingerPrint": { 45 | 'coreVersion': '104', # 内核版本,默认104,可选92 46 | 'ostype': 'PC', # 操作系统平台 PC | Android | IOS 47 | 'version': '106', # 浏览器版本,建议92以上,不填则会从92以上版本随机。目前106最高 48 | 'os': browser_os, # 为navigator.platform值 Win32 | Linux i686 | Linux armv7l | MacIntel 49 | 'userAgent': '', # ua,不填则自动生成 50 | 'isIpCreateTimeZone': False, # 基于IP生成对应的时区 51 | 'timeZone': 'GMT+08:00', # 时区,isIpCreateTimeZone 为false时,参考附录中的时区列表 52 | 'position': '1', # 网站请求获取您当前位置时,是否允许 0询问|1允许|2禁止。 53 | 'isIpCreatePosition': True, # 是否基于IP生成对应的地理位置 54 | 'lat': '', # 经度 isIpCreatePosition 为false时设置 55 | 'lng': '', # 纬度 isIpCreatePosition 为false时设置 56 | 'precisionData': '', # 精度米 isIpCreatePosition 为false时设置 57 | 'isIpCreateLanguage': False, # 是否基于IP生成对应国家的浏览器语言 58 | 'languages': 'zh-CN', # isIpCreateLanguage 为false时设置,值参考附录 59 | 'isIpCreateDisplayLanguage': False, # 是否基于IP生成对应国家的浏览器界面语言 60 | 'displayLanguages': '', # isIpCreateDisplayLanguage 为false时设置,默认为空,即跟随系统,值参考附录 61 | 'WebRTC': 0, # 0替换(默认)|1允许|2禁止。开启WebRTC,将公网ip替换为代理ip,同时掩盖本地ip 62 | } 63 | } 64 | 65 | response = requests.post(f"{bitbrowser_url}/browser/update", json=body) 66 | response.raise_for_status() 67 | response = response.json() 68 | browser_id = response['data']['id'] 69 | print('创建或修改浏览器成功,浏览器id为:', browser_id) 70 | 71 | @try_except_code 72 | def update_proxy(browser_id, index_id, proxy_ip, proxy_port, proxy_username, proxy_password): 73 | """修改浏览器代理 74 | 75 | Attributes: 76 | browser_id: 浏览器id 77 | index_id: 序号 78 | proxy_ip: 代理主机 79 | proxy_port: 代理端口 80 | proxy_username: 代理账号 81 | proxy_password: 代理密码 82 | """ 83 | body = { 84 | 'ids': [browser_id], 85 | 'ipCheckService': '', # IP查询渠道,默认ip-api 86 | 'proxyMethod': 2, # 代理类型 1平台 2自定义 87 | 'proxyType': 'socks5', 88 | 'host': proxy_ip, # 代理主机 89 | 'port': proxy_port, # 代理端口 90 | 'proxyUserName': proxy_username, # 代理账号 91 | 'proxyPassword': proxy_password, # 代理密码 92 | 'isIpv6': False, # 默认false 93 | } 94 | response = requests.post(f"{bitbrowser_url}/browser/proxy/update", json=body) 95 | response.raise_for_status() 96 | print('第',index_id,'个账号修改代理成功') 97 | 98 | class BitBrowserUtil(): 99 | """selenium操作adspower指纹浏览器 100 | """ 101 | 102 | @try_except_code 103 | def __init__(self, browser_id): 104 | """启动浏览器(webdriver为自带的,不必单独下载) 105 | 106 | Attributes: 107 | browser_id:bitbrowser浏览器id 108 | """ 109 | self.browser_id = browser_id 110 | # 打开浏览器 111 | self.driver = self.open() 112 | # 全屏 113 | self.driver.maximize_window() 114 | # 关闭其他窗口 115 | self.close_other_windows() 116 | 117 | @try_except_code 118 | def open(self): 119 | body = {'id': self.browser_id} 120 | response = requests.post(f"{bitbrowser_url}/browser/open", json=body) 121 | response.raise_for_status() 122 | response = response.json() 123 | # 启动浏览器后在返回值中拿到对应的driver的路径 124 | chrome_driver = Service(str(response["data"]["driver"])) 125 | # selenium启动的chrome浏览器是一个空白的浏览器。chromeOptions是一个配置chrome启动属性的类,用来配置参数。 126 | chrome_options = webdriver.ChromeOptions() 127 | # adspower提供的debug接口,用于执行selenium自动化 128 | chrome_options.add_experimental_option("debuggerAddress", response["data"]["http"]) 129 | driver = webdriver.Chrome(service=chrome_driver, options=chrome_options) 130 | return driver 131 | 132 | @try_except_code 133 | def close_other_windows(self): 134 | """关闭无关窗口,只留当前窗口 135 | 理论上下面代码会保留当前窗口,句柄没错。但是实际窗口却没有达到预期。不清楚具体原因。后续再研究。目前能做到的就是只保留一个窗口 136 | """ 137 | current_handle = self.driver.current_window_handle 138 | all_handles = self.driver.window_handles 139 | for handle in all_handles: 140 | self.driver.switch_to.window(handle) 141 | if handle != current_handle: 142 | self.driver.close() 143 | self.driver.switch_to.window(current_handle) 144 | 145 | @try_except_code 146 | def quit(self): 147 | """关闭浏览器 148 | """ 149 | body = {'id': self.browser_id} 150 | response = requests.post(f"{bitbrowser_url}/browser/close", json=body) 151 | response.raise_for_status() 152 | 153 | if __name__ == '__main__': 154 | 155 | # # 创建浏览器 156 | # for i in range(20): 157 | # create_or_update_browser(browser_os='mac', browser_id='') 158 | # exit() 159 | 160 | 161 | 162 | data = my_format_data(start_num=1, end_num=20, is_bitbrowser=True) 163 | # print(data) 164 | 165 | 166 | 167 | # # 修改代理ip(socks5) 168 | # for d in data: 169 | # # 参数:browser_id, index_id, proxy_ip, proxy_port, proxy_username, proxy_password) 170 | # update_proxy(browser_id=d['browser_id'], index_id=d['index_id'], proxy_ip=d['proxy_ip'], proxy_port=d['proxy_port'], proxy_username=d['proxy_username'], proxy_password=d['proxy_password']) 171 | # exit() 172 | 173 | 174 | 175 | # for d in data: 176 | # bitbrowser = BitBrowserUtil(d['browser_id']) 177 | -------------------------------------------------------------------------------- /browser/adspower.py: -------------------------------------------------------------------------------- 1 | """ 2 | adspower api : http://apidoc.adspower.net/localapi/local-api-v1.html 3 | """ 4 | from selenium import webdriver 5 | from selenium.webdriver.chrome.service import Service 6 | from selenium.webdriver.chrome.options import Options 7 | from selenium.webdriver.support.wait import WebDriverWait 8 | from selenium.webdriver.support import expected_conditions as EC 9 | from selenium.webdriver.common.by import By 10 | from selenium.webdriver.common.keys import Keys 11 | from selenium.webdriver.common.action_chains import ActionChains 12 | from selenium.webdriver.common.desired_capabilities import DesiredCapabilities 13 | import requests,os,sys,time,math,json 14 | sys.path.append(os.getcwd()) # 工作目录 15 | from config import * 16 | from formatdata import * 17 | from utils_.utils import try_except_code 18 | 19 | @try_except_code 20 | def create_or_update_browser(browser_os='mac', is_create=True, browser_id=''): 21 | """创建或者修改浏览器信息 22 | 23 | Attributes: 24 | browser_os: 基于什么系统生成或修改浏览器。最好跟自己主机一致。 mac | windows 25 | is_create: 是否为创建 True创建 | False修改 26 | browser_id: 浏览器id。is_create为False时设置。默认空 27 | """ 28 | # 操作系统 29 | if browser_os in ['mac','macos']: 30 | browser_os = ['All macOS'] 31 | elif browser_os in ['win','windows']: 32 | browser_os = ['All Windows'] 33 | 34 | body = { 35 | 'group_id': '0', # 添加到对应分组的ID,未分配分组则可以传0 36 | 'user_proxy_config': { # 账号代理配置 37 | "proxy_soft":"no_proxy", # 先不设置代理。可在修改代理接口去设置 38 | }, 39 | 'fingerprint_config': { # 账号指纹配置 40 | 'browser_kernel_config': { # version:内核版本 41 | "version": "latest", # ”92“为92版内核、”99“为99版内核、”latest“为智能匹配; 42 | "type":"chrome", # 浏览器类型,chrome | firefox 43 | }, 44 | 'random_ua': { # 支持指定类型、系统、版本设置ua。若同时传入了自定义ua,则优先使用自定义的ua。 45 | 'ua_system_version': browser_os, # 非必填,不填默认在所有系统中随机,支持 Android, iOS, Windows, Mac OS X , Linux。 46 | 'ua_version': [], # 非必填,支持当前主流版本,不填默认在所有版本中随机 100 101 ...。 47 | }, 48 | 'ua': '', # user-agent用户信息,默认不传使用随机ua库,自定义需要确保ua格式与内容符合标准 49 | 'automatic_timezone': 0, # 1 基于IP自动生成对应的时区(默认);0 指定时区' 50 | 'timezone': 'Asia/Shanghai', # 上海 51 | 'webrtc': 'proxy', # proxy 替换,使用代理IP覆盖真实IP,代理场景使用 | local真实,网站会获取真实IP | disabled禁用(默认),网站会拿不到IP 52 | 'location': 'ask', # 网站请求获取您当前地理位置时的选择.询问ask(默认),与普通浏览器的提示一样 | 允许allow,始终允许网站获取位置 | 禁止block,始终禁止网站获取位置 53 | 'location_switch': 1, # 1 基于IP自动生成对应的位置(默认) | 0 指定位置 54 | 'longitude': '', # 指定位置的经度,指定位置时必填,范围是-180到180,支持小数点后六位 55 | 'latitude': '', # 指定位置的纬度,指定位置时必填,范围是-90到90,支持小数点后六位 56 | 'accuracy': '', # 指定位置的精度(米) ,指定位置时必填,范围10-5000,整数 57 | 'language': ["en-US","en","zh-CN","zh"], # 浏览器的语言(默认["en-US","en"]),支持传多个语言,格式为字符串数组 58 | 'language_switch': 0, # 基于IP国家设置语言,0:关闭 | 1:启用 59 | } 60 | } 61 | if is_create == False: 62 | if browser_id == '': 63 | print('请传入browser_id') 64 | return 65 | data = json.loads(body) 66 | data['user_id'] = browser_id 67 | body = json.dumps(data) 68 | url = f"{adspower_url}/api/v1/user/create" if is_create == True else f"{adspower_url}/api/v1/user/update" 69 | response = requests.post(url=url, json=body) 70 | # 检查响应的状态码,如果状态码表明请求失败,则抛出一个HTTPError异常。用try_except_code捕获错误。如果状态码表明请求成功,则什么也不会发生,函数会直接返回 71 | response.raise_for_status() 72 | response = response.json() 73 | print(response['data']) 74 | time.sleep(1) # api速率限制。添加等待 75 | 76 | @try_except_code 77 | def update_proxy(browser_id, index_id, proxy_ip, proxy_port, proxy_username, proxy_password): 78 | """修改浏览器代理 79 | Attributes: 80 | browser_id: 浏览器id 81 | index_id: 序号 82 | proxy_ip: 代理主机 83 | proxy_port: 代理端口 84 | proxy_username: 代理账号 85 | proxy_password: 代理密码 86 | """ 87 | data = { 88 | 'user_id': browser_id, 89 | 'user_proxy_config': { 90 | "proxy_soft":"other", 91 | "proxy_type":"socks5", 92 | "proxy_host":proxy_ip, 93 | "proxy_port":proxy_port, 94 | "proxy_user":proxy_username, 95 | "proxy_password":proxy_password 96 | } 97 | } 98 | response = requests.post(url=f"{adspower_url}/api/v1/user/update", json=data) 99 | response.raise_for_status() 100 | print('第',index_id,'个账号修改代理成功') 101 | time.sleep(1) # api速率限制。添加等待 102 | 103 | class AdsPowerUtil(): 104 | """selenium操作adspower指纹浏览器 105 | """ 106 | 107 | @try_except_code 108 | def __init__(self, browser_id): 109 | """启动浏览器(webdriver为adspower自带的,不必单独下载) 110 | 111 | Attributes: 112 | browser_id:adspower浏览器id 113 | """ 114 | self.browser_id = browser_id 115 | # 打开浏览器 116 | self.driver = self.open() 117 | # 全屏 118 | self.driver.maximize_window() 119 | # 关闭其他窗口 120 | self.close_other_windows() 121 | 122 | @try_except_code 123 | def open(self): 124 | response = requests.get(f"{adspower_url}/api/v1/browser/start?user_id={self.browser_id}") 125 | response.raise_for_status() 126 | response = response.json() 127 | # 启动浏览器后在返回值中拿到对应的Webdriver的路径response["data"]["webdriver"] 128 | chrome_driver = Service(str(response["data"]["webdriver"])) 129 | # selenium启动的chrome浏览器是一个空白的浏览器。chromeOptions是一个配置chrome启动属性的类,用来配置参数。 130 | chrome_options = webdriver.ChromeOptions() 131 | # adspower提供的debug接口,用于执行selenium自动化 132 | chrome_options.add_experimental_option("debuggerAddress", response["data"]["ws"]["selenium"]) 133 | driver = webdriver.Chrome(service=chrome_driver, options=chrome_options) 134 | return driver 135 | 136 | @try_except_code 137 | def close_other_windows(self): 138 | """关闭无关窗口,只留当前窗口 139 | 理论上下面代码会保留当前窗口,句柄没错。但是实际窗口却没有达到预期。不清楚具体原因。后续再研究。目前能做到的就是只保留一个窗口 140 | """ 141 | current_handle = self.driver.current_window_handle 142 | all_handles = self.driver.window_handles 143 | for handle in all_handles: 144 | self.driver.switch_to.window(handle) 145 | if handle != current_handle: 146 | self.driver.close() 147 | self.driver.switch_to.window(current_handle) 148 | 149 | @try_except_code 150 | def quit(self): 151 | """关闭浏览器 152 | """ 153 | response = requests.get(f"{adspower_url}/api/v1/browser/stop?user_id={self.browser_id}") 154 | response.raise_for_status() 155 | 156 | if __name__ == '__main__': 157 | 158 | # # 创建或修改浏览器信息 159 | # for i in range(20): 160 | # # 参数:browser_os='mac', is_create=True, browser_id='' 161 | # create_or_update_browser(browser_os='mac', is_create=True, browser_id='') 162 | # exit() 163 | 164 | 165 | 166 | data = my_format_data(start_num=1, end_num=20, is_bitbrowser=False) 167 | # print(data) 168 | 169 | 170 | 171 | 172 | # # 修改代理ip(socks5) 173 | # for d in data: 174 | # # 参数:browser_id, proxy_ip, proxy_port, proxy_username, proxy_password 175 | # update_proxy(browser_id=d['browser_id'], index_id=d['index_id'], proxy_ip=d['proxy_ip'], proxy_port=d['proxy_port'], proxy_username=d['proxy_username'], proxy_password=d['proxy_password']) 176 | # exit() 177 | 178 | 179 | 180 | # for d in data: 181 | # print(d) 182 | # adspower = AdsPowerUtil(d['browser_id']) 183 | 184 | -------------------------------------------------------------------------------- /twitter/twitter.py: -------------------------------------------------------------------------------- 1 | import tweepy 2 | import urllib.parse 3 | import json, requests, random, datetime, time, pytz 4 | import sys, os 5 | from dateutil.parser import parse as date_parse 6 | sys.path.append(os.getcwd()) # 工作目录 7 | from browser.bitbrowser import * 8 | # from browser.adspower import * 9 | from config import * 10 | from formatdata import * 11 | from utils_.utils import try_except_code 12 | 13 | class OAuth2ForTwitterUtil(BitBrowserUtil): 14 | """身份验证,获取refresh_token备用 15 | """ 16 | 17 | @try_except_code 18 | def __init__(self, browser_id): 19 | super(OAuth2ForTwitterUtil, self).__init__(browser_id) 20 | 21 | @try_except_code 22 | def create_refresh_token(self, account): 23 | oauth2_user_handler = tweepy.OAuth2UserHandler( 24 | client_id=client_id, 25 | redirect_uri=redirect_uri, 26 | # 只有在使用机密客户时,才需要client_secret 27 | # client_secret="data['client_secret']", 28 | # 加offline.access会生成refresh_token 29 | scope=[ 30 | "offline.access", 31 | "tweet.read", 32 | "tweet.write", 33 | "users.read", 34 | "follows.read", 35 | "follows.write", 36 | "like.read", 37 | "like.write", 38 | "list.read", 39 | "list.write"], 40 | ) 41 | auth_url = oauth2_user_handler.get_authorization_url() 42 | self.driver.get(auth_url) 43 | try: 44 | # 不知为啥click()不生效,用send_keys(Keys.ENTER)代替 45 | WebDriverWait(self.driver,10).until(EC.visibility_of_element_located((By.XPATH, "/html/body/div[1]/div/div/div[2]/main/div/div/div[2]/div/div/div[1]/div[3]/div"))).send_keys(Keys.ENTER) 46 | time.sleep(5) 47 | response_url = self.driver.current_url 48 | # print(response_url) 49 | except Exception as e: 50 | print(e) 51 | response = oauth2_user_handler.fetch_token(response_url) 52 | new_refresh_token = {account: response ['refresh_token']} 53 | with open(refresh_tokens_file, 'r',encoding="utf-8") as f: 54 | data = json.load(f) 55 | refresh_token = {**data, **new_refresh_token} 56 | data.update(refresh_token) 57 | with open(refresh_tokens_file, 'w',encoding="utf-8") as f: 58 | json.dump(data, f, indent=4, ensure_ascii=False) 59 | 60 | class TwitterUtil(): 61 | 62 | @try_except_code 63 | def __init__(self, account, user_agent, proxy): 64 | """获取授权,调用twitter api 65 | """ 66 | proxies = {"http": proxy, "https": proxy} 67 | access_token = self.get_new_access_token(account, user_agent, proxies) 68 | # 使用新的 access_token 生成clien, 进行 API 调用 69 | self.client = tweepy.Client(bearer_token=access_token) 70 | # 设置代理 71 | self.client.session.proxies = proxies 72 | self.account = account 73 | 74 | @try_except_code 75 | def get_new_access_token(self, account, user_agent, proxies): 76 | """ 通过refresh_token请求新的refresh_token和新的access_token 77 | 提示: tweepy通过access_token而不是通过refresh_token内部处理成access_token来初始化。access_token有效期2小时,不可能过期了再通过用户授权的方式来请求,效率低.另外refresh_token不会过期, 78 | 所以还是需要存refresh_token.twitter奇怪的点是通过refresh_token获取access_token时refresh_token也会变,所以存储的refreshtoken得跟着修改,google的refresh_token就不会变 79 | 通过refresh_token获得access_token和新的refresh_token是用twitter的api实现的不是tweepy的库实现的 80 | """ 81 | with open(refresh_tokens_file, 'r',encoding="utf-8") as f: 82 | data = json.load(f) 83 | # tweepy目前没有方法通过refresh_token来刷新access_token。 84 | # 通过refresh_token跟twitter API端点交互,获取新的refresh_token和access_token。 85 | url = 'https://api.twitter.com/2/oauth2/token' 86 | headers ={ 87 | "Content-Type": "application/x-www-form-urlencoded", 88 | "User-Agent": user_agent 89 | } 90 | playlod = urllib.parse.urlencode({ 91 | "grant_type": "refresh_token", 92 | "refresh_token": data[account], 93 | "client_id": client_id 94 | }) 95 | response = requests.post(url=url, headers=headers, data=playlod, proxies=proxies) 96 | # 检查响应的状态码,如果状态码表明请求失败,则抛出一个HTTPError异常。用try_except_code捕获错误。如果状态码表明请求成功,则什么也不会发生,函数会直接返回 97 | response.raise_for_status() 98 | # byte.decode('UTF-8')将byte转换成str,json.load将str转换成dict 99 | result = json.loads((response.content).decode('UTF-8')) 100 | # 用新的refresh_token替换refresh_tokens_file中对应账号的旧的refresh_token 101 | new_refresh_token = {account: result['refresh_token']} 102 | with open(refresh_tokens_file, 'r',encoding="utf-8") as f: 103 | data = json.load(f) 104 | refresh_token = {**data, **new_refresh_token} 105 | data.update(refresh_token) 106 | with open(refresh_tokens_file, 'w',encoding="utf-8") as f: 107 | json.dump(data, f, indent=4, ensure_ascii=False) 108 | access_token = result['access_token'] 109 | return access_token 110 | 111 | @try_except_code 112 | def get_account(self): 113 | """获取验证账号的信息 114 | """ 115 | account_info = self.client.get_me(user_auth=False) 116 | account_id = account_info.data.id 117 | account_username = account_info.data.username 118 | account = {'account_id':account_id, 'account_username':account_username} 119 | return account 120 | 121 | @try_except_code 122 | def get_user_id_from_username(self, username): 123 | """通过用户名获取用户唯一id 124 | """ 125 | data = self.client.get_user(username=username) 126 | # 获取用户的twitter id 127 | user_id = data.data.id 128 | return user_id 129 | 130 | @try_except_code 131 | def get_user_followers(self, user_id, num=None): 132 | """获取用户的关注者 133 | 134 | Attributes: 135 | user_id:用户id 136 | num:获取用户跟随者的数量。默认None,获取所有的跟随者 137 | """ 138 | if num is not None and type(num) != int: 139 | print('数量必须是整数') 140 | return 141 | followers = [] 142 | args = { 143 | "id": user_id, 144 | "max_results": 100 #一页最大返回数量。1-1000,默认100 145 | } 146 | for page in tweepy.Paginator(self.client.get_users_followers, **args): 147 | if not page.data: 148 | print('此用户没有关注别人') 149 | break 150 | if num != None: 151 | if len(followers) >= num: 152 | break 153 | for user in page.data: 154 | if num != None: 155 | if len(followers) >= num: 156 | break 157 | user_id = user.id 158 | user_name = user.username 159 | followers.append({'user_id':user_id, 'user_name':user_name}) 160 | # print(followers) 161 | # print(len(followers)) 162 | return followers 163 | 164 | @try_except_code 165 | def get_user_followings(self, user_id, num=None): 166 | """获取用户关注的人 167 | 168 | Attributes: 169 | user_id:用户id 170 | num:获取用户跟随的人的数量。默认None,获取所有的跟随的人 171 | """ 172 | if num is not None and type(num) != int: 173 | print('数量必须是整数') 174 | return 175 | followings = [] 176 | for page in tweepy.Paginator(self.client.get_users_following, id=user_id): 177 | if not page.data: 178 | print('此用户没有关注者') 179 | break 180 | if num != None: 181 | if len(followings) >= num: 182 | break 183 | for user in page.data: 184 | if num != None: 185 | if len(followings) >= num: 186 | break 187 | user_id = user.id 188 | user_name = user.username 189 | followings.append({'user_id':user_id, 'user_name':user_name}) 190 | # print(followings) 191 | # print(len(followings)) 192 | return followings 193 | 194 | @try_except_code 195 | def follow(self, user_id): 196 | """关注别人 197 | """ 198 | self.client.follow_user(user_id, user_auth=False) 199 | 200 | @try_except_code 201 | def unfollow(self, user_id): 202 | """取消关注别人 203 | """ 204 | self.client.unfollow_user(user_id, user_auth=False) 205 | 206 | @try_except_code 207 | def create_tweet(self): 208 | """创建推文 209 | """ 210 | # 获取推文话术 211 | with open(tweet_texts_file) as f: 212 | tweet_texts = f.read().splitlines() 213 | tweet_text = random.choice(tweet_texts) 214 | self.client.create_tweet(text=tweet_text, user_auth=False) 215 | print('推文已发') 216 | 217 | @try_except_code 218 | def reply(self, tweet_id, tem_reply_text='', is_use_reply_file=True): 219 | """评论推文。 220 | 221 | Attributes: 222 | tweet_id: 回复的推文id 223 | tem_reply_text: 临时回复内容。比如需要@几个好友;互关贴回复#互关标签等。 224 | is_use_reply_file:是否使用回复话术文件。平时养号没有啥特殊要求,从文件中随机选取一条回复即可 225 | """ 226 | if type(tem_reply_text) != str: 227 | print("回复话术或文件路径必须是字符串") 228 | return 229 | if is_use_reply_file == True: 230 | with open(reply_texts_file) as f: 231 | reply_texts = f.read().splitlines() 232 | random_reply_text = random.choice(reply_texts) 233 | if len(tem_reply_text) != 0: 234 | reply_text = random_reply_text + '\n' + tem_reply_text 235 | else: 236 | reply_text = random_reply_text 237 | else: 238 | if len(tem_reply_text) != 0: 239 | reply_text = tem_reply_text 240 | else: 241 | print('请输入回复话术') 242 | 243 | self.client.create_tweet(in_reply_to_tweet_id=tweet_id, text=reply_text, user_auth=False) 244 | 245 | @try_except_code 246 | def delete_tweet(self, tweet_id): 247 | """删除推文 248 | """ 249 | self.client.delete_tweet(id=tweet_id, user_auth=False) 250 | 251 | @try_except_code 252 | def like(self, tweet_id): 253 | """喜欢推文 254 | """ 255 | self.client.like(tweet_id, user_auth=False) 256 | 257 | @try_except_code 258 | def unlike(self, tweet_id): 259 | """取消喜欢推文 260 | """ 261 | self.client.unlike(tweet_id, user_auth=False) 262 | 263 | @try_except_code 264 | def retweet(self, tweet_id): 265 | """转发推文 266 | """ 267 | self.client.retweet(tweet_id, user_auth=False) 268 | 269 | @try_except_code 270 | def unretweet(self, tweet_id): 271 | """取消转发推文 272 | """ 273 | self.client.unretweet(source_tweet_id=tweet_id, user_auth=False) 274 | 275 | @try_except_code 276 | def parse_time(self, start_time, end_time): 277 | """将时间解析为twitter要求的iso时间。注意:tweepy接口时间为utc时间。为了方便此函数传递的时间为utc+8时间 278 | 279 | Attributes: 280 | start_time: 格式:YYYY-MM-DD HH:mm:ss 例:2023-02-25 13:00:00。开始日期,返回此日期之后的推文 281 | end_time: 格式:YYYY-MM-DD HH:mm:ss 例:2023-02-25 13:00:00。结束日期,返回此日期之前的推文。默认值None,表示当前时间 282 | """ 283 | try: 284 | tz = pytz.timezone('Asia/Shanghai') 285 | # 构造开始时间 286 | if start_time is None: # 如果start_time为None,则代表从无限早时间开始查询。 287 | start_time_iso = date_parse('1977-01-01 00:00:00').strftime("%Y-%m-%dT%H:%M:%SZ") 288 | elif type(start_time) == int: # 如果start_time为整数,则代表从start_time天前开始查询。 289 | start_time_iso = (datetime.datetime.utcnow() - datetime.timedelta(days=start_time)).strftime("%Y-%m-%dT%H:%M:%SZ") 290 | else: # 如果start_time为时间类型,则代表从此时间结束查询。 291 | start_time_utc8 = tz.localize(date_parse(start_time)) # 将字符串解析为datetime对象,并将其设置为UTC+8时间 292 | start_time_utc = start_time_utc8.astimezone(pytz.utc) # 将UTC+8时间转换为UTC时间 293 | start_time_iso = start_time_utc.strftime("%Y-%m-%dT%H:%M:%SZ") 294 | 295 | # 构造结束时 296 | if end_time is None: # 如果end_time为None,则代表当前时间结束查询。 297 | end_time_iso = datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ") 298 | elif type(end_time) == int: # 如果end_time为整数,则代表从start_time天前结束查询。 299 | end_time_iso = (datetime.datetime.utcnow() - datetime.timedelta(days=end_time)).strftime("%Y-%m-%dT%H:%M:%SZ") 300 | else: # 如果end_time为时间类型,则代表从此时间结束查询。 301 | end_time_utc8 = tz.localize(date_parse(end_time)) # 将字符串解析为datetime对象,并将其设置为UTC+8时间 302 | end_time_utc = end_time_utc8.astimezone(pytz.utc) # 将UTC+8时间转换为UTC时间 303 | end_time_iso = end_time_utc.strftime("%Y-%m-%dT%H:%M:%SZ") 304 | 305 | # 开始时间必须比结束时间要早 306 | if start_time_iso >= end_time_iso: 307 | print('开始时间必须小于结束时间') 308 | return 309 | return start_time_iso, end_time_iso 310 | except Exception as e: 311 | print('错误信息',e,'\n时间只允许整数类型或者YYYY-MM-DD HH:mm:ss类型') 312 | return 313 | 314 | @try_except_code 315 | def get_home_timeline(self, start_time, end_time=None): 316 | """允许您检索您和您关注的用户发布的最新推文和转推的集合。此端点最多返回最后 3200 条推文。 317 | 318 | Attributes: 319 | start_time: 格式:YYYY-MM-DD HH:mm:ss 例:2023-02-25 13:00:00。开始日期,返回此日期之后的推文 320 | end_time: 格式:YYYY-MM-DD HH:mm:ss 例:2023-02-25 13:00:00。结束日期,返回此日期之前的推文。默认值None,表示当前时间 321 | 322 | return: 323 | tweets: 推文信息,包含id、text、author_id、author_username、follows_count、llike_count 324 | """ 325 | tweets = [] 326 | start_time_iso, end_time_iso = self.parse_time(start_time, end_time) 327 | args = { 328 | 'start_time': start_time_iso, 329 | 'end_time': end_time_iso, 330 | 'expansions': ['author_id'], 331 | 'tweet_fields': ['author_id', 'created_at', 'public_metrics', 'entities', 'conversation_id'], 332 | 'user_fields': ['public_metrics', 'entities', 'username'], 333 | 'user_auth': False 334 | } 335 | for page in tweepy.Paginator(self.client.get_home_timeline, **args): 336 | if not page.data: 337 | break 338 | for tweet in page.data: 339 | tweet_id = tweet.id 340 | tweet_author_id = tweet.author_id 341 | tweet_text = tweet.text 342 | for user in page.includes['users']: 343 | if user.id == tweet_author_id: 344 | tweet_author_username = user.username 345 | tweets.append({'tweet_id': tweet_id, 'tweet_author_id': tweet_author_id, 'tweet_author_username': tweet_author_username, 'tweet_text': tweet_text}) 346 | # print(tweets) 347 | return tweets 348 | 349 | @try_except_code 350 | def get_user_tweets(self, username, start_time, end_time=None): 351 | """允许您检索指定的单个用户组成的推文。最多返回最近的 3200 条推文。 352 | 353 | Attributes: 354 | username: 用户名 355 | start_time: 格式:YYYY-MM-DD HH:mm:ss 例:2023-02-25 13:00:00。开始日期,返回此日期之后的推文 356 | end_time: 格式:YYYY-MM-DD HH:mm:ss 例:2023-02-25 13:00:00。结束日期,返回此日期之前的推文。默认值None,表示当前时间 357 | return: 358 | tweets: 推文信息 359 | """ 360 | tweets = [] 361 | start_time_iso, end_time_iso = self.parse_time(start_time, end_time) 362 | user_id = self.get_user_id_from_username(username) 363 | args = { 364 | 'id': user_id, 365 | 'start_time': start_time_iso, 366 | 'end_time': end_time_iso, 367 | 'max_results': 50, #一页最大返回数量。5-100,默认10 368 | 'expansions': ['author_id'], 369 | 'tweet_fields': ['author_id', 'created_at', 'public_metrics', 'entities', 'conversation_id'], 370 | 'user_auth': False 371 | } 372 | for page in tweepy.Paginator(self.client.get_users_tweets, **args): 373 | if not page.data: 374 | break 375 | for tweet in page.data: 376 | tweet_id = tweet.id 377 | tweet_author_id = tweet.author_id 378 | tweet_text = tweet.text 379 | tweets.append({'tweet_id': tweet_id, 'tweet_author_id': tweet_author_id, 'tweet_text': tweet_text}) 380 | # print(tweets) 381 | return tweets 382 | 383 | @try_except_code 384 | def get_tweet(self, tweet_id): 385 | """检索指定推文信息 386 | 387 | Attributes: 388 | tweet_id: 推文id 389 | return: 390 | tweets: 推文信息 391 | """ 392 | tweets = [] 393 | args = { 394 | 'id': tweet_id, 395 | 'expansions': ['author_id'], 396 | 'tweet_fields': ['author_id', 'created_at', 'public_metrics', 'entities', 'conversation_id'], 397 | 'user_fields': ['username', 'created_at'], 398 | 'user_auth': False 399 | } 400 | response = self.client.get_tweet(**args) 401 | tweet_author_id = response.data.author_id 402 | tweet_text = response.data.text 403 | # 推文提及的人 404 | tweet_mentions_id = [] 405 | if 'mentions' in response.data.entities.keys(): 406 | for mention in response.data.entities['mentions']: 407 | mention_id = mention['id'] 408 | tweet_mentions_id.append(mention_id) 409 | for user in response.includes['users']: 410 | tweet_author_username = user.username 411 | tweet = { 412 | 'tweet_id': tweet_id, 413 | 'tweet_author_id': tweet_author_id, 414 | 'tweet_author_username': tweet_author_username, 415 | 'tweet_mentions_id': tweet_mentions_id, 416 | 'tweet_text': tweet_text 417 | } 418 | # print(tweet) 419 | return tweet 420 | 421 | @try_except_code 422 | def get_tweet_replyers(self, tweet_id, replyer_amount=10, my_twitter_data=None): 423 | """检索指定推文的评论者 424 | 425 | Attributes: 426 | tweet_id: 推文id 427 | replyer_amount: 获取的评论者数量 428 | my_twitter_data: 所有的账号信息。用于排除评论者中自己的小号 429 | return: 430 | tweets: 评论者信息 431 | """ 432 | replyers = [] 433 | tweet = self.get_tweet(tweet_id) 434 | tweet_author_username = tweet['tweet_author_username'] 435 | args = { 436 | 'query': f'conversation_id:{tweet_id}', 437 | 'max_results': 50, #一页最大返回数量。10-100,默认10 438 | 'expansions': ['author_id', 'in_reply_to_user_id', 'referenced_tweets.id'], 439 | 'tweet_fields': ['author_id', 'created_at', 'public_metrics', 'entities', 'conversation_id', 'in_reply_to_user_id', 'referenced_tweets'], 440 | 'user_fields': ['username', 'created_at'], 441 | 'user_auth': False 442 | } 443 | for page in tweepy.Paginator(self.client.search_recent_tweets, **args): 444 | # print(page) 445 | if len(replyers) >= replyer_amount: 446 | break 447 | if 'users' in page.includes: 448 | for replyer in page.includes['users']: 449 | if len(replyers) >= replyer_amount: 450 | break 451 | # 排除推文作者、根据my_twitter_data决定是否排除自己的小号 452 | replyer_id = replyer.id 453 | replyer_username = replyer.username 454 | if my_twitter_data is None: 455 | if replyer_username != tweet_author_username and replyer_username != self.account: 456 | replyers.append({'replyer_id': replyer_id, 'replyer_username': replyer_username}) 457 | else: 458 | if replyer_username != tweet_author_username and (replyer_username not in my_twitter_data): 459 | replyers.append({'replyer_id': replyer_id, 'replyer_username': replyer_username}) 460 | else: 461 | print('此推文没有评论') 462 | # print(replyers) 463 | return replyers 464 | 465 | @try_except_code 466 | def search_recent_tweets(self, query, start_time, end_time=None, search_amount=20, follows_count=1000, like_count=50): 467 | """返回过去 n 天内与搜索查询匹配的推文,最多7天 468 | 469 | Attributes: 470 | query: 搜索条件 471 | start_time: 格式:YYYY-MM-DD HH:mm:ss 例:2023-02-25 13:00:00。开始日期,返回此日期之后的推文 472 | end_time: 格式:YYYY-MM-DD HH:mm:ss 例:2023-02-25 13:00:00。结束日期,返回此日期之前的推文。默认值None,表示当前时间 473 | search_amount: 获取的推文数量, 获取太多没意义,反应也太慢 474 | follows_count: 推文作者的关注者数量 475 | like_count: 推文的喜欢数量 476 | return: 477 | twitters: 推文信息,包含id、text、author_id、author_username、follows_count、llike_count 478 | """ 479 | tweets = [] 480 | start_time_iso, end_time_iso = self.parse_time(start_time, end_time) 481 | # 开始时间必须在过去7天以内 482 | seven_time_iso = (datetime.datetime.utcnow() - datetime.timedelta(days=7)).strftime("%Y-%m-%dT%H:%M:%SZ") 483 | if start_time_iso < seven_time_iso: 484 | print('最多查询过去7天的推文') 485 | return 486 | # 结束时间必须至少比请求时间早 10 秒 487 | current_time = (datetime.datetime.utcnow().replace(microsecond=0) - datetime.timedelta(seconds=10)) # replace(microsecond=0) 去掉毫秒信息 488 | end_time = date_parse(end_time_iso).replace(tzinfo=None) # replace(tzinfo=None)去掉时区信息 489 | if end_time > current_time: 490 | time_diff_seconds = int((end_time - current_time).total_seconds()) 491 | end_time_iso = (date_parse(end_time_iso) - datetime.timedelta(seconds=time_diff_seconds)).strftime('%Y-%m-%dT%H:%M:%SZ') 492 | 493 | # expansions: author_id,referenced_tweets.id,referenced_tweets.id.author_id,entities.mentions.username,attachments.poll_ids,attachments.media_keys,in_reply_to_user_id,geo.place_id 494 | # tweet_fields: attachments,author_id,context_annotations,conversation_id,created_at,entities,geo,id,in_reply_to_user_id,lang,non_public_metrics,organic_metrics,possibly_sensitive,promoted_metrics,public_metrics,referenced_tweets,reply_settings,source,text,withheld 495 | # entities是推文提到的实体,里面可能包含urls(推文提到的url)、mentions(推文提到的用户) 496 | # user_fields: created_at,description,entities,id,location,name,pinned_tweet_id,profile_image_url,protected,public_metrics,url,username,verified,withheld 497 | args = { 498 | 'query': query, 499 | 'max_results': 50, # 一页最大返回数量。10-100,默认10 500 | 'start_time': start_time_iso, 501 | 'end_time': end_time_iso, 502 | 'expansions': ['author_id'], 503 | 'tweet_fields': ['author_id', 'created_at', 'public_metrics', 'entities', 'conversation_id'], 504 | 'user_fields': ['public_metrics', 'entities', 'username'], 505 | 'user_auth': False 506 | } 507 | for page in tweepy.Paginator(self.client.search_recent_tweets, **args): 508 | # print(page) 509 | if len(tweets) >= search_amount: 510 | break 511 | # page.data包括默认的id(twitter的id)、text(twitter内容)和author_id(发推文的作者id)、created_at(发推文的时间)、public_metrics(推文点赞数量、转推数量、回复数量、引用数量等信息) 512 | # page.includes['users']包括默认的id(发推文的作者id)、username(发推文的作者用户名)、name(发推文的作者昵称)和public_metrics(作者的关注者、被关注者、发布的推文数量等信息) 513 | if not page.data: 514 | break 515 | for tweet in page.data: 516 | # print(tweet) 517 | if len(tweets) >= search_amount: 518 | break 519 | tweet_id = tweet.id 520 | # 推文的对话线程id,用于获取回复推文信息。当发布推文以响应推文(称为回复)或回复时,现在每个回复都有一个定义的 conversation_id,它与开始对话的原始推文的推文 ID 相匹配。https://developer.twitter.com/en/docs/twitter-api/conversation-id 521 | tweet_conversation_id = tweet.conversation_id 522 | tweet_text = tweet.text 523 | tweet_author_id = tweet.author_id 524 | # print(tweet_author_id) 525 | # print(tweet.public_metrics) 526 | tweet_like_count = tweet.public_metrics['like_count'] 527 | tweet_mentions_id = [] 528 | if 'mentions' in tweet.entities.keys(): 529 | for mention in tweet.entities['mentions']: 530 | mention_id = mention['id'] 531 | tweet_mentions_id.append(mention_id) 532 | for author in page.includes['users']: 533 | if author.id == tweet_author_id: 534 | # print(author.public_metrics) 535 | tweet_author_username = author.username 536 | tweet_author_followers_count = author.public_metrics['followers_count'] 537 | if tweet_author_followers_count >= follows_count and tweet_like_count >= like_count: 538 | tweets.append({ 539 | 'tweet_id':tweet_id, 540 | 'tweet_text':tweet_text, 541 | 'tweet_like_count':tweet_like_count, 542 | 'tweet_author_id':tweet_author_id, 543 | 'tweet_author_username': tweet_author_username, 544 | 'tweet_author_followers_count':tweet_author_followers_count, 545 | 'tweet_mentions_id': tweet_mentions_id, 546 | 'tweet_conversation_id': tweet_conversation_id}) 547 | # print(tweets) 548 | return tweets 549 | 550 | @try_except_code 551 | def giveaway(self, query, start_time, end_time=None, search_amount=20, follows_count=1000, like_count=50, tag_amount=3, is_use_reply_file=True, is_like=True, is_retweet=True, is_reply=True): 552 | """养号、抽奖 553 | 554 | 根据指定搜索条件,搜索出指定时间内,推文作者满足一定关注量、推文满足一定点赞数的指定数量的推文。从中选择指定数量条推文进行关注、点赞、转推、评论(随机评论话术)操作,每条推文操作间隔指定时间。 555 | 556 | Attributes: 557 | query: 搜索条件 558 | start_time: 格式:YYYY-MM-DD HH:mm:ss 例:2023-02-25 13:00:00。开始日期,返回此日期之后的推文 559 | end_time: 格式:YYYY-MM-DD HH:mm:ss 例:2023-02-25 13:00:00。结束日期,返回此日期之前的推文。默认值None,表示当前时间 560 | search_amount:获取的推文数量,获取太多没意义,反应也太慢 561 | follows_count: 推文作者的关注者数量 562 | like_count: 推文的喜欢数量 563 | tag_amount: 要标记的朋友数量 564 | is_use_reply_file: 是否使用回复文件里的话术 565 | is_like: 是否点赞 566 | is_retweet: 是否转推 567 | is_reply: 是否回复 568 | """ 569 | # 获取验证账号的追随者 570 | account = self.get_account() 571 | account_id = account['account_id'] 572 | all_followers = self.get_user_followers(account_id, num=tag_amount) 573 | if len(all_followers) < tag_amount: 574 | print('账号',account['account_username'],'关注者不够') 575 | return 576 | # 获取推文query, start_time, end_time=None, search_amount=20, follows_count=1000, like_count=50 577 | tweets = self.search_recent_tweets(query=query, start_time=start_time, end_time=end_time, search_amount=search_amount, follows_count=follows_count, like_count=like_count) 578 | # 随机选择一条推文 579 | tweet = random.choice(tweets) 580 | # print(tweet) 581 | # 关注推文作者 582 | self.follow(tweet['tweet_author_id']) 583 | time.sleep(1) 584 | # 关注推文提到的实体 585 | if tweet['tweet_mentions_id'] != []: 586 | for mention_id in tweet['tweet_mentions_id']: 587 | # 作者不要重复关注 588 | if mention_id != tweet['tweet_author_id']: 589 | self.follow(mention_id) 590 | time.sleep(1) 591 | if is_like == True: 592 | # 喜欢 593 | self.like(tweet['tweet_id']) 594 | time.sleep(1) 595 | if is_retweet == True: 596 | # 转推 597 | self.retweet(tweet['tweet_id']) 598 | time.sleep(1) 599 | if is_reply == True: 600 | # 评论并@tag_amount个朋友 601 | follows = '' 602 | if len(all_followers) > 0: 603 | for follower in all_followers: 604 | follows = '@'+follower['user_name'] + ' ' + follows 605 | self.reply(tweet['tweet_id'], tem_reply_text=follows, is_use_reply_file=is_use_reply_file) 606 | print('已三连') 607 | 608 | @try_except_code 609 | def giveaway_from_fix_tweet(self, tweet_id, tag_amount=3, is_use_reply_file=True, is_like=True, is_retweet=True, is_reply=True): 610 | """指定推文抽奖 611 | 612 | Attributes: 613 | tweet_id: 操作的推文id 614 | tag_amount: 要标记的朋友数量 615 | is_use_reply_file: 是否使用回复文件里的话术 616 | is_like: 是否点赞 617 | is_retweet: 是否转推 618 | is_reply: 是否回复 619 | """ 620 | # 获取验证账号的追随者 621 | account = self.get_account() 622 | account_id = account['account_id'] 623 | all_followers = self.get_user_followers(account_id, num=tag_amount) 624 | if len(all_followers) < tag_amount: 625 | print('账号',account['account_username'],'关注者不够') 626 | return 627 | # 获取推文信息 628 | tweet = self.get_tweet(tweet_id) 629 | # 关注推文作者 630 | self.follow(tweet['tweet_author_id']) 631 | time.sleep(1) 632 | # 关注推文提到的实体 633 | if tweet['tweet_mentions_id'] != []: 634 | for mention_id in tweet['tweet_mentions_id']: 635 | # 作者不要重复关注 636 | if mention_id != tweet['tweet_author_id']: 637 | self.follow(mention_id) 638 | time.sleep(1) 639 | if is_like == True: 640 | # 喜欢 641 | self.like(tweet_id) 642 | time.sleep(1) 643 | if is_retweet == True: 644 | # 转推 645 | self.retweet(tweet_id) 646 | time.sleep(1) 647 | if is_reply == True: 648 | # 评论并@tag_amount个朋友 649 | follows = '' 650 | if len(all_followers) > 0: 651 | for follower in all_followers: 652 | follows = '@'+follower['user_name'] + ' ' + follows 653 | self.reply(tweet_id, tem_reply_text=follows, is_use_reply_file=is_use_reply_file) 654 | print('已三连') 655 | 656 | @try_except_code 657 | def set_follow_info(self, query, start_time, end_time=None, follows_count=1000, like_count=50, search_amount=20, to_follow_amount=10, my_twitter_data=None, tem_reply_text='诚信互关,有关必回\n#互关 #互粉 #互fo', is_use_reply_file=True): 658 | """去互关贴下发互关信息 659 | 660 | 根据'互关'搜索条件,搜索出指定时间内,推文作者满足一定关注量、推文满足一定点赞数的指定数量的推文。从中选择一条推文进行转推、评论、关注评论者的操作。 661 | 662 | 自己的小号不互关 663 | 664 | Attributes: 665 | query: 搜索条件 666 | start_time: 格式:YYYY-MM-DD HH:mm:ss 例:2023-02-25 13:00:00。开始日期,返回此日期之后的推文 667 | end_time: 格式:YYYY-MM-DD HH:mm:ss 例:2023-02-25 13:00:00。结束日期,返回此日期之前的推文。默认值None,表示当前时间 668 | follows_count: 推文作者的关注者数量 669 | like_count: 推文的喜欢数量 670 | search_amount:获取的推文数量,获取太多没意义,反应也太慢 671 | to_follow_amount: 要关注的人的数量(一次别关注太多,避免封控),从推文的评论者中获取到的。实际获取到的数量<=这个数值(推文评论者太少的话就是小于这个数值)。 672 | my_twitter_data: 所有的账号信息。用于排除评论者中自己的小号 673 | tem_reply_text: 回复话术(部分) 674 | is_use_reply_file: 是否使用回复文件里的话术 675 | """ 676 | # 通过query随机获取互关推文 677 | tweets = self.search_recent_tweets(query=query, start_time=start_time, end_time=None, search_amount=search_amount, follows_count=follows_count, like_count=like_count) 678 | # print(tweets) 679 | # 随机选择一条推文 680 | tweet = random.choice(tweets) 681 | # print(tweet) 682 | # 关注推文作者 683 | self.follow(tweet['tweet_author_id']) 684 | time.sleep(1) 685 | # 关注推文提到的实体(作者@的人) 686 | if tweet['tweet_mentions_id'] != []: 687 | for mention_id in tweet['tweet_mentions_id']: 688 | # 作者不要重复关注 689 | if mention_id != tweet['tweet_author_id']: 690 | self.follow(mention_id) 691 | time.sleep(1) 692 | # 关注推文评论者 693 | replyers = self.get_tweet_replyers(tweet['tweet_id'], replyer_amount=to_follow_amount, my_twitter_data=my_twitter_data) 694 | # print(replyers) 695 | if len(replyers) != 0: 696 | for replyer in replyers: 697 | self.follow(replyer['replyer_id']) 698 | time.sleep(1) 699 | # 转发此推文 700 | self.retweet(tweet['tweet_id']) 701 | time.sleep(1) 702 | # 回复此推文 703 | self.reply(tweet['tweet_id'], tem_reply_text=tem_reply_text, is_use_reply_file=is_use_reply_file) 704 | print('已关注、已转推、已发布互关信息') 705 | 706 | @try_except_code 707 | def follow_back(self, my_twitter_data=None, once_follow_num=10): 708 | '''回关 709 | Attributes: 710 | my_twitter_data: 所有的账号信息。用于排除评论者中自己的小号 711 | once_follow_num: 关注几个人 712 | ''' 713 | # 获取验证账号 714 | account = self.get_account() 715 | account_id = account['account_id'] 716 | # 获取关注验证账号的人 717 | all_followers = self.get_user_followers(account_id) 718 | # print(all_followers) 719 | # 获取验证账号关注的人 720 | all_followings = self.get_user_followings(account_id) 721 | # print(all_followings) 722 | # 比较关注此账号的人和此账号关注的人。排除重复的人(彼此关注过了),关注此账号的人集合里剩下的人就是需要去关注的。 723 | # 将字典转换为元组,只保留字典中的"id"键 724 | followers_ids = set(tuple(follower["user_name"] for follower in all_followers)) 725 | followings_ids = set(tuple(following["user_name"] for following in all_followings)) 726 | # 执行集合操作 727 | intersection = followers_ids & followings_ids 728 | # 如果小号不互关的话,将自己的twitter账号全部添加进集合 729 | if my_twitter_data is not None: 730 | intersection |= set(my_twitter_data) 731 | # 排除重复的人(彼此关注过了),关注此账号的人集合里剩下的人就是需要去关注的 732 | need_followers = [follower for follower in all_followers if follower["user_name"] not in intersection] 733 | # print(need_followers) 734 | # 回关。设置最大关注数量,如果未关注的人太多,可能会被封控。这个可以慢慢回关 735 | num = len(need_followers) 736 | if num >= once_follow_num: 737 | for i in range(once_follow_num): 738 | need_follower = need_followers[i] 739 | self.follow(need_follower['user_id']) 740 | time.sleep(1) 741 | print('已回关',once_follow_num,'个账号') 742 | elif num > 0 and num < once_follow_num: 743 | for need_follow in need_followers: 744 | self.follow(need_follow['user_id']) 745 | time.sleep(1) 746 | print('已全部回关') 747 | elif num == 0: 748 | print('已全部回关') 749 | 750 | if __name__ == '__main__': 751 | 752 | # 1、组装数据 753 | # 1,1代表第1个账号。2,2代表第二个账号。以此类推... 754 | # 1,20代表第1-20个账号。3,10代表第3-10个账号。以此类推... 755 | # 默认用比特浏览器。如果用ads浏览器需要把s_bitbrowser值改为False 756 | data = my_format_data(start_num=1, end_num=20, is_bitbrowser=True) 757 | # print(data) 758 | 759 | # 所有twitter账号 760 | my_twitter_data = my_twitter_data() 761 | # print(my_twitter_data) 762 | 763 | 764 | 765 | # # 2、程序第一次需要跟用户交互,来获取权限。会自动打开指纹浏览器(指纹app需要先打开),点击授权,只需要交互1次。自动将refresh_token保存备用。 766 | # # 将获取到的refresh_token保存在`twitter_credential_tokens.json`文件中。以后运行程序就不需要再跟用户交互了。程序自动使用refresh_token刷新accress_token来调用twitter api,并将自动更新文件中的refresh_token。 767 | # # 如果后续使用中,refresh_token失效了,需要再次授权交互获取新的refresh_token 768 | # for d in data: 769 | # # 验证 770 | # oauth2 = OAuth2ForTwitterUtil(d['browser_id']) 771 | # oauth2.create_refresh_token(d['twitter_username']) 772 | # exit() 773 | 774 | 775 | 776 | # 3、调用twitter api 处理业务 777 | for d in data: 778 | print('第',d['index_id'],'个账号') 779 | # 实例化TwitterUtil 780 | twitter = TwitterUtil(d['twitter_username'], d['user_agent'], d['proxy']) 781 | 782 | # # 通过用户名获取用户id 783 | # user_id = twitter.get_user_id_from_username('gaohongxiang') 784 | # print(user_id) 785 | 786 | # # 获取用户的关注者、用户关注的人 787 | # twitter.get_user_followers(user_id) 788 | # twitter.get_user_followings(user_id) 789 | 790 | # # 关注别人 791 | # twitter.follow(user_id) 792 | 793 | # # 点赞转发评论 794 | # twitter.like('1555521594424004608') 795 | # twitter.retweet('1555521594424004608') 796 | # 参数tweet_id, tem_reply_text='', is_use_reply_file=True 797 | # twitter.reply('1555521594424004608', 'good') 798 | 799 | # # 发布推文。从推文文件中随机选取1条发布 800 | # twitter.create_tweet() 801 | 802 | # # 指定推文抽奖 803 | # # 参数:tweet_id, tag_amount=3, is_use_reply_file=True, is_like=True, is_retweet=True, is_reply=True 804 | # twitter.giveaway_from_fix_tweet(tweet_id='1628969073488048128', tag_amount=0, is_use_reply_file=True, is_like=True, is_retweet=True, is_reply=True) 805 | 806 | # # 随机获取符合一定条件的推文关注点赞转推评论 807 | # # 参数:query, start_time, end_time=None, search_amount=20, follows_count=1000, like_count=50, tag_amount=3, is_use_reply_file=True, is_like=True, is_retweet=True, is_reply=True 808 | # twitter.giveaway(query='(follow OR like OR rt OR tag OR retweet OR 关注 OR 喜欢 OR 转推) (#nft OR #gamefi) has:hashtags -is:retweet -is:reply -is:quote', start_time=1, end_time=None, search_amount=10, follows_count=100, like_count=50, tag_amount=0, is_use_reply_file=True, is_like=True, is_retweet=True, is_reply=True) 809 | 810 | # # 随机获取互关贴,留言、关注作者及评论者 811 | # # 参数:query, start_time, end_time=None, follows_count=1000, like_count=50, search_amount=20, to_follow_amount=10, my_twitter_data=None, tem_reply_text='诚信互关,有关必回\n#互关 #互粉 #互fo', is_use_reply_file=True 812 | # twitter.set_follow_info(query='(互关 OR 涨粉 OR 互粉) (#互关 OR #互粉 OR #有关必回) has:hashtags -is:retweet -is:reply -is:quote', start_time=1, end_time=None, follows_count=1000, like_count=10, search_amount=10, to_follow_amount=10, my_twitter_data=my_twitter_data, tem_reply_text='诚信互关,有关必回\n#互关 #互粉 #互fo', is_use_reply_file=False) 813 | 814 | # # 获取自己的关注者,没有关注的回关 815 | # # 参数:my_twitter_data=None, once_follow_num=10 816 | # twitter.follow_back(my_twitter_data=my_twitter_data, once_follow_num=10) 817 | # time.sleep(50) 818 | 819 | # 日常养号:发布推文|随机获取符合一定条件的推文关注点赞转推评论|随机获取互关贴,留言,回关 820 | lucky = random.choice([1,2,3]) 821 | if lucky == 1: 822 | twitter.create_tweet() 823 | elif lucky == 2: 824 | # query, start_time, end_time=None, search_amount=20, follows_count=1000, like_count=50, tag_amount=3, is_use_reply_file=True, is_like=True, is_retweet=True, is_reply=True 825 | twitter.giveaway(query='(follow OR like OR rt OR tag OR retweet OR 关注 OR 喜欢 OR 转推) (#nft OR #gamefi) has:hashtags -is:retweet -is:reply -is:quote', start_time=1, end_time=None, search_amount=10, follows_count=100, like_count=50, tag_amount=0, is_use_reply_file=True, is_like=True, is_retweet=True, is_reply=True) 826 | elif lucky == 3: 827 | # 1、获取自己的关注者,关注。2、随机找到互关贴,发互关消息。 828 | # query, start_time, end_time=None, follows_count=1000, like_count=50, search_amount=20, to_follow_amount=10, my_twitter_data=None, tem_reply_text='诚信互关,有关必回\n#互关 #互粉 #互fo', is_use_reply_file=True 829 | twitter.set_follow_info(query='(互关 OR 涨粉 OR 互粉) (#互关 OR #互粉 OR #有关必回) has:hashtags -is:retweet -is:reply -is:quote', start_time=1, end_time=None, follows_count=1000, like_count=10, search_amount=10, to_follow_amount=10, my_twitter_data=my_twitter_data, tem_reply_text='诚信互关,有关必回\n#互关 #互粉 #互fo', is_use_reply_file=False) 830 | # my_twitter_data=None, once_follow_num=10 831 | twitter.follow_back(my_twitter_data=my_twitter_data, once_follow_num=10) 832 | if len(data) > 1: 833 | interval_time = 60 834 | time.sleep(random.randrange(interval_time, interval_time+10)) 835 | 836 | 837 | 838 | 839 | --------------------------------------------------------------------------------