├── README.md ├── core ├── __init__.py ├── api │ ├── __init__.py │ └── api.py ├── config.py ├── filter │ ├── __init__.py │ ├── blacklist.txt │ ├── whitelist.txt │ └── xy_filter.py ├── frida │ ├── __init__.py │ └── xianyu.py ├── js │ └── rpc.js ├── pusher.py ├── service │ ├── __init__.py │ └── service.py └── task │ ├── __init__.py │ ├── task_manage.py │ └── xy_task.py ├── main.py ├── requirements.txt └── test ├── __init__.py └── test_sign.py /README.md: -------------------------------------------------------------------------------- 1 | # xianyu -------------------------------------------------------------------------------- /core/__init__.py: -------------------------------------------------------------------------------- 1 | import aioredis 2 | 3 | redis = aioredis.from_url("redis://localhost") 4 | -------------------------------------------------------------------------------- /core/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jxytest/xianyu/100387e1463a15fa8664ba9ea4c7e9ddf33f56ab/core/api/__init__.py -------------------------------------------------------------------------------- /core/api/api.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import json 3 | import os 4 | import threading 5 | import time 6 | from urllib.parse import quote 7 | from loguru import logger 8 | from core.frida.xianyu import XianYu 9 | import aiohttp 10 | 11 | 12 | # 生成随机字符串 13 | def random_str(random_length=8): 14 | """ 15 | 生成一个指定长度的随机字符串,其中 16 | string.digits=0123456789 17 | string.ascii_letters=abcdefghigklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ 18 | """ 19 | import random 20 | import string 21 | str_list = [random.choice(string.digits + string.ascii_letters) for i in range(random_length)] 22 | return ''.join(str_list) 23 | 24 | 25 | class Api: 26 | def __init__(self, use_proxy=False): 27 | self.search_url = "https://g-acs.m.goofish.com/gw/mtop.taobao.idlemtopsearch.search/1.0/" 28 | js_path = os.path.join(os.path.dirname(__file__), "../js/rpc.js") 29 | self.xian_yu = XianYu(js_path) 30 | self.headers = { 31 | # 登录后获取。没有这个字段,则搜出的结果始终是7小时前的 32 | 'x-sid': '2d1981fda8edbf5cb4410f7e37b860d0', 33 | 'x-uid': '2143549739', 34 | 35 | 'x-nettype': 'WIFI', 36 | 'x-pv': '6.3', 37 | 'x-nq': 'WIFI', 38 | 'first_open': '0', 39 | 'x-features': '27', 40 | 'x-app-conf-v': '0', 41 | 'content-type': 'application/x-www-form-urlencoded;charset=UTF-8', 42 | 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', 43 | 'x-bx-version': '6.5.88', 44 | 'x-extdata': 'openappkey=DEFAULT_AUTH', 45 | 'x-ttid': '1561625392549@fleamarket_android_7.8.40', 46 | 'x-app-ver': '7.8.40', 47 | 'x-location': '0%2C0', 48 | 'x-utdid': quote(random_str(24)), 49 | 'x-appkey': '21407387', 50 | 'x-devid': quote(random_str(44)), 51 | 'user-agent': 'MTOPSDK/3.1.1.7 (Android;12;Xiaomi;Redmi K30 Pro Zoom Edition)', 52 | # 'Accept-Encoding': 'gzip', 53 | 'Connection': 'Keep-Alive', 54 | } 55 | self.proxy = None 56 | if use_proxy: 57 | # 更新一次代理 58 | asyncio.run(self.get_proxy()) 59 | # 每隔5分钟更新一次代理 60 | threading.Timer(60 * 5, lambda: asyncio.run(self.get_proxy())).start() 61 | 62 | def update_headers(self, data: str): 63 | t = str(int(time.time())) 64 | sign = self.xian_yu.get_sign(data, self.headers, t) 65 | self.headers.update({ 66 | "x-t": t, 67 | "x-mini-wua": sign.get('x-mini-wua'), 68 | "x-sgext": sign.get('x-sgext'), 69 | "x-sign": sign.get('x-sign'), 70 | "x-umt": sign.get('x-umt'), 71 | "umid": sign.get('x-umt'), 72 | 'x-c-traceid': f'X/{random_str(22)}{t}0230112176', 73 | }) 74 | 75 | async def search(self, keyword): 76 | data = { 77 | 'activeSearch': False, 78 | 'bizFrom': 'home', 79 | 'clientModifiedCpvNavigatorJson': json.dumps({'tabList': [], 'fromClient': False}).replace(' ', ''), 80 | 'disableHierarchicalSort': 0, 81 | 'forceUseInputKeyword': False, 82 | 'forceUseTppRepair': False, 83 | 'fromCombo': 'Sort', 84 | 'fromFilter': True, 85 | 'fromKits': False, 86 | 'fromLeaf': False, 87 | 'fromShade': False, 88 | 'fromSuggest': False, 89 | 'keyword': keyword, 90 | 'pageNumber': 1, 91 | 'propValueStr': json.dumps({'searchFilter': 'publishDays:3'}).replace(' ', ''), 92 | 'resultListLastIndex': 0, 93 | 'rowsPerPage': 10, 94 | # 'searchReqFromActivatePagePart': 'searchButton', 95 | 'searchReqFromActivatePagePart': 'historyItem', 96 | 'searchReqFromPage': 'xyHome', 97 | 'searchTabType': 'SEARCH_TAB_MAIN', 98 | 'shadeBucketNum': -1, 99 | 'sortField': 'create', 100 | 'sortValue': 'desc', 101 | # 'suggestBucketNum': 37, 102 | 'suggestBucketNum': 33 103 | } 104 | data = json.dumps(data, ensure_ascii=False) 105 | # 去除空格、换行 106 | data = data.replace(" ", "").replace("\n", "") 107 | self.update_headers(data) 108 | data = { 109 | 'data': data 110 | } 111 | # logger.info(f"请求参数: {data}") 112 | # logger.info(f"请求头: {self.headers}") 113 | # async with aiohttp.ClientSession( 114 | # connector=TCPConnector(ssl=False), 115 | # connector_owner=False, 116 | # ) as session: 117 | # async with session.post(self.search_url, headers=self.headers, data=data, proxy=self.proxy) as resp: 118 | # resp = await resp.json() 119 | # logger.info(f"search请求完成") 120 | # # logger.info(f"search请求结果: {resp}") 121 | # result = self.parser_search_result(resp) 122 | # logger.info(f"search解析结果: {result}") 123 | # return result 124 | 125 | async with aiohttp.request('POST', self.search_url, headers=self.headers, data=data, proxy=self.proxy) as resp: 126 | resp = await resp.json() 127 | logger.info(f"search请求完成") 128 | logger.info(f"search请求结果: {resp}") 129 | result = self.parser_search_result(resp) 130 | logger.info(f"search解析结果: {result}") 131 | return result 132 | # 改用httpx 133 | # async with httpx.AsyncClient(proxies={'http:': self.proxy, 'https:': self.proxy}, verify=False) as client: 134 | # resp = await client.post(self.search_url, headers=self.headers, data=data) 135 | # resp = resp.json() 136 | # logger.info(f"search请求完成") 137 | # # logger.info(f"search请求结果: {resp}") 138 | # result = self.parser_search_result(resp) 139 | # logger.info(f"search解析结果: {result}") 140 | # return result 141 | 142 | async def get_proxy(self): 143 | """https://www.hailiangip.com/""" 144 | api = "http://ecs.hailiangip.com:8422/api/getIpEncrypt?dataType=1&encryptParam=SlDyzgfgDW12vuaMHmQkMz9pKEmWH7kDAoD1ZC4KkxrlVhShpdEjb9vG2YRiwpyE7%2FmtHBf0UytBN%2FbvoFFQxR34HqF6jcH2DIa9lAfHZKAtZ1ij%2BTipB%2BPa4OIC6Fak0EBMPBGst8aumQxGQxXUym0riZNcRTbKMjSvYRYqLmjYFsJvJYxeLU9YDql4IJq6KHmQBjYm32MK13MpScW7XF7%2FeDXlL0x6IKTgy4kKtwD10%2FrggxuKwg%2Fa3uSVATqr" 145 | # 更新一次代理 146 | async with aiohttp.request("GET", api) as resp: 147 | resp = await resp.text() 148 | logger.info(f"获取代理结果: {resp}") 149 | self.proxy = f"http://{resp}" 150 | logger.info(f"更新代理: {self.proxy}") 151 | 152 | @staticmethod 153 | def parser_search_result(response): 154 | try: 155 | if not response.get('data'): 156 | logger.error(f"search请求结果: {response}") 157 | return [] 158 | res = response.get("data").get("resultList") 159 | result = [] 160 | for item in res: 161 | try: 162 | main = item.get("data").get("item").get("main") 163 | click_param = main.get("clickParam") 164 | ex_content = main.get("exContent") 165 | target_url = main.get("targetUrl") 166 | publish_time = click_param.get('args').get("publishTime") 167 | # 时间戳转换 168 | publish_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(int(publish_time) / 1000)) 169 | title = ex_content.get("title") 170 | item_id = ex_content.get("itemId") 171 | pic_url = ex_content.get("picUrl") 172 | user_nick = ex_content.get('detailParams').get("userNick") 173 | price = ex_content.get('detailParams').get("soldPrice") 174 | # 组装数据 175 | data = { 176 | "itemId": item_id, 177 | "userNick": user_nick, 178 | "price": price, 179 | "title": title, 180 | "pic_url": pic_url, 181 | "publish_time": publish_time, 182 | } 183 | result.append(data) 184 | except Exception as e: 185 | logger.error(f"解析商品数据失败: {e}") 186 | return result 187 | except Exception as e: 188 | logger.exception(e) 189 | logger.error(f"解析search结果失败: {e}") 190 | return [] 191 | -------------------------------------------------------------------------------- /core/config.py: -------------------------------------------------------------------------------- 1 | # 应用配置 2 | corp_id = 'ww1919559e3f42ef22' # 你的企业id 3 | corp_secret = '5LifkbXtlZUb6p2MWD_LQLos0gfXauhBg7IimAb3yyQ' # 你的应用凭证密钥 4 | agent_id = '1000007' # 你的应用id 5 | 6 | # 回调配置 7 | sToken = "FPuJpsbgFSL7Uh7p0VK6" 8 | sEncodingAESKey = "jj8WgCyNtfVGHDXXt53EgkBXqjEc9PPRhd1T8h2Jnja" 9 | 10 | # 钉钉推送 11 | ding_webhook = "https://oapi.dingtalk.com/robot/send?access_toke" \ 12 | "=cf5385fc80c9d74e620068954dfb10438725a24fac727bb2213e2f4550f42077 " 13 | 14 | # 企业微信推送开关 15 | wx_push_open = True 16 | # 钉钉推送开关 17 | ding_push_open = True 18 | -------------------------------------------------------------------------------- /core/filter/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jxytest/xianyu/100387e1463a15fa8664ba9ea4c7e9ddf33f56ab/core/filter/__init__.py -------------------------------------------------------------------------------- /core/filter/blacklist.txt: -------------------------------------------------------------------------------- 1 | 回收 2 | 高价 -------------------------------------------------------------------------------- /core/filter/whitelist.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jxytest/xianyu/100387e1463a15fa8664ba9ea4c7e9ddf33f56ab/core/filter/whitelist.txt -------------------------------------------------------------------------------- /core/filter/xy_filter.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | class FilterXY: 5 | def __init__(self, file_name): 6 | self.file_path = os.path.join(os.path.dirname(__file__), file_name) 7 | os.makedirs(os.path.dirname(self.file_path), exist_ok=True) 8 | self.filter_keywords = [] 9 | self.read_filter() 10 | 11 | def read_filter(self): 12 | if os.path.exists(self.file_path): 13 | with open(self.file_path, 'r', encoding='utf-8') as f: 14 | self.filter_keywords = f.read().splitlines() 15 | else: 16 | self.write_filter() 17 | 18 | def write_filter(self): 19 | with open(self.file_path, 'w', encoding='utf-8') as f: 20 | self.filter_keywords = list(set(self.filter_keywords)) 21 | f.write('\n'.join(self.filter_keywords)) 22 | 23 | def filter(self, data): 24 | for keyword in self.filter_keywords: 25 | if keyword in data: 26 | return True 27 | return False 28 | 29 | def filter_list(self): 30 | return self.filter_keywords 31 | 32 | def del_filter(self, keyword): 33 | self.filter_keywords.remove(keyword) 34 | self.write_filter() 35 | 36 | def add_filter(self, keyword): 37 | self.filter_keywords.append(keyword) 38 | self.write_filter() 39 | 40 | def clear_filter(self): 41 | self.filter_keywords.clear() 42 | self.write_filter() 43 | 44 | def update_filter(self, keywords): 45 | self.filter_keywords = keywords 46 | self.write_filter() 47 | 48 | def filter_count(self): 49 | return len(self.filter_keywords) 50 | -------------------------------------------------------------------------------- /core/frida/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jxytest/xianyu/100387e1463a15fa8664ba9ea4c7e9ddf33f56ab/core/frida/__init__.py -------------------------------------------------------------------------------- /core/frida/xianyu.py: -------------------------------------------------------------------------------- 1 | import json 2 | from urllib.parse import quote_plus 3 | 4 | from loguru import logger 5 | import frida 6 | 7 | 8 | # 生成随机字符串 9 | def random_str(random_length=8): 10 | """ 11 | 生成一个指定长度的随机字符串,其中 12 | string.digits=0123456789 13 | string.ascii_letters=abcdefghigklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ 14 | """ 15 | import random 16 | import string 17 | str_list = [random.choice(string.digits + string.ascii_letters) for i in range(random_length)] 18 | return ''.join(str_list) 19 | 20 | 21 | class XianYu: 22 | def __init__(self, file_path): 23 | self.sign = None 24 | self.app_name = "闲鱼" 25 | self.file_path = file_path 26 | self.hook_code = self.read_js() 27 | self.process = frida.get_remote_device().attach(self.app_name) 28 | self.script = self.process.create_script(self.hook_code) 29 | self.script.on("message", self.on_message) 30 | self.script.load() 31 | 32 | def read_js(self): 33 | """ 34 | 读取js文件 35 | :return: 36 | """ 37 | with open(self.file_path, encoding='utf-8') as f: 38 | hook_code = f.read() 39 | return hook_code 40 | 41 | def get_sign(self, data: str, headers: dict, t): 42 | """ 43 | 获取sign 44 | :param headers: 请求头 45 | :param data: 请求参数 46 | :param t: 时间戳 47 | :return: 48 | """ 49 | sign_params = { 50 | 'deviceId': headers['x-devid'], 51 | 'appKey': headers['x-appkey'], 52 | 'x-features': headers['x-features'], 53 | 'api': 'mtop.taobao.idlemtopsearch.search', 54 | 'v': '1.0', 55 | 'utdid': headers['x-utdid'], 56 | 'sid': headers.get('x-sid'), 57 | 'ttid': headers['x-ttid'], 58 | 'extdata': headers['x-extdata'], 59 | 'uid': headers.get('x-uid'), 60 | 'data': data, 61 | 'lat': '0', 62 | 'lng': '0', 63 | 't': t 64 | } 65 | self.script.exports.getSign(json.dumps(sign_params)) 66 | return self.sign 67 | 68 | def on_message(self, message, data): 69 | """ 70 | 获取sign 71 | :param message: 72 | :param data: 73 | :return: 74 | """ 75 | sign = message.get("payload").get("sign") 76 | self.sign = dict([x.split('=', 1) for x in sign[1:-1].split(", ")]) 77 | for k, v in self.sign.items(): 78 | self.sign[k] = quote_plus(v) 79 | logger.info(self.sign) 80 | 81 | -------------------------------------------------------------------------------- /core/js/rpc.js: -------------------------------------------------------------------------------- 1 | const PAGE_ID = 'pageId'; 2 | const PAGE_NAME = 'pageName'; 3 | // 定义 HashMap 对象和 String 对象 4 | const hashMap = Java.use('java.util.HashMap'); 5 | const string = Java.use('java.lang.String'); 6 | 7 | 8 | function hashPut(hashMap, key, value) { 9 | if (value === null) { 10 | return; 11 | } 12 | hashMap.put(string.$new(key), string.$new(value)); 13 | } 14 | 15 | // 创建 h1 和 h2 两个 HashMap 对象 16 | let h1 = hashMap.$new(); 17 | let h2 = hashMap.$new(); 18 | hashPut(h2, PAGE_ID, ""); 19 | hashPut(h2, PAGE_NAME, ""); 20 | 21 | let s2 = string.$new(); 22 | let s3 = string.$new('r_106'); 23 | 24 | 25 | 26 | rpc.exports = { 27 | getsign: function(sign_params) { 28 | Java.perform(function() { 29 | console.log("get_sign"); 30 | // 解析sign_params 31 | let headers_obj = JSON.parse(sign_params); 32 | // 遍历json对象 33 | for (let key in headers_obj) { 34 | console.log(key + " : " + headers_obj[key]); 35 | hashPut(h1, key, headers_obj[key]); 36 | } 37 | let s1 = string.$new(headers_obj['appKey']); 38 | // 调用 com.taobao.wireless.security.sdk.SecurityGuardManagerImpl.getStaticDataSign 方法 39 | Java.choose("mtopsdk.security.InnerSignImpl", { 40 | onMatch: function (instance) { 41 | console.log("Found instance: " + instance); 42 | var result = instance.getUnifiedSign(h1, h2, s1, s2, false, s3); 43 | console.log(result); 44 | send({ "sign": result.toString()}); 45 | // 必须返回stop,否则会遍历所有的实例 46 | return "stop"; 47 | }, 48 | onComplete: function () { 49 | console.log("Done"); 50 | }, 51 | // 使用 onMatchOnce: true 选项 52 | onMatchOnce: true 53 | }); 54 | }); 55 | }, 56 | }; 57 | // hook mtopsdk.mtop.global.SwitchConfig,返回false,禁用spdy协议,可以进行抓包 58 | Java.perform(function() { 59 | var SwitchConfig = Java.use('mtopsdk.mtop.global.SwitchConfig'); 60 | SwitchConfig.A.overload().implementation = function () { 61 | return false; 62 | } 63 | }); 64 | 65 | 66 | -------------------------------------------------------------------------------- /core/pusher.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import aiohttp 3 | from corpwechatbot import AppMsgSender 4 | 5 | from core.config import corp_id, corp_secret, agent_id, ding_webhook, wx_push_open, ding_push_open 6 | 7 | try: 8 | app = AppMsgSender(corpid=corp_id, # 你的企业id 9 | corpsecret=corp_secret, # 你的应用凭证密钥 10 | agentid=agent_id, # 你的应用id 11 | log_level=10) 12 | except Exception as e: 13 | print(e) 14 | app = None 15 | 16 | 17 | async def ding_push(message): 18 | try: 19 | if not ding_push_open: 20 | print("ding_push_open is False") 21 | return 22 | text = { 23 | "msgtype": "markdown", 24 | "markdown": { 25 | "title": "result", 26 | "text": message 27 | }, 28 | "at": { 29 | "atUserIds": [ 30 | 31 | ], 32 | "isAtAll": False 33 | } 34 | } 35 | headers = { 36 | "Content-Type": "application/json" 37 | } 38 | async with aiohttp.request("POST", url=ding_webhook, headers=headers, json=text) as resp: 39 | print(resp.status) 40 | print(await resp.text()) 41 | except Exception as e: 42 | print(e) 43 | print("ding_push error") 44 | finally: 45 | return 1 46 | 47 | 48 | async def wx_push(message): 49 | try: 50 | if not wx_push_open or not app: 51 | print("wx_push_open is False") 52 | return 53 | app.send_text(message, touser=['lmx']) 54 | except Exception as e: 55 | print(e) 56 | print("wx_push error") 57 | finally: 58 | return 1 59 | 60 | 61 | if __name__ == '__main__': 62 | asyncio.run(ding_push( 63 | "{'itemId': '629239751120', 'userNick': 'caoniuniu', 'price': '1049.99', 'title': '10台戴尔图形工作站R7610,一块E5-2609CPU,支 持双CPU,8G内存,无硬盘,单电源,机器全部正常工作,剩下最后10台,本地自取!'")) 64 | asyncio.run(wx_push("{'itemId':")) -------------------------------------------------------------------------------- /core/service/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jxytest/xianyu/100387e1463a15fa8664ba9ea4c7e9ddf33f56ab/core/service/__init__.py -------------------------------------------------------------------------------- /core/service/service.py: -------------------------------------------------------------------------------- 1 | import json 2 | import time 3 | from typing import List 4 | 5 | from fastapi import APIRouter, Query 6 | from pydantic import BaseModel 7 | 8 | from .. import redis 9 | from core.filter.xy_filter import FilterXY 10 | from core.task.task_manage import XianYuTask 11 | 12 | router = APIRouter() 13 | 14 | # 白名单过滤器 15 | whitelist = FilterXY('whitelist.txt') 16 | # 黑名单过滤器 17 | blacklist = FilterXY('blacklist.txt') 18 | xian_yu_task = XianYuTask() 19 | 20 | 21 | class TaskInfo(BaseModel): 22 | keywords: List[str] 23 | seconds: int 24 | type: int = Query(default=0, description='0:添加任务 1:删除任务 2:暂停任务 3:恢复任务', ge=0, le=3) 25 | 26 | 27 | @router.get("/getGoodsList") 28 | async def getGoodsList(q: str = Query(default=None, min_length=2, max_length=20), 29 | page: int = Query(default=1, ge=1, le=100), 30 | page_size: int = Query(default=10, ge=1, le=100)): 31 | # 从redis中获取所有数据,并过滤title中包含filter的数据 32 | data = await redis.keys() 33 | print(data) 34 | res = [] 35 | index = 1 36 | for item in data: 37 | try: 38 | result = await redis.get(item.decode()) 39 | result = json.loads(result.decode('unicode-escape')) 40 | if blacklist.filter(result): 41 | continue 42 | # if not whitelist.filter(result): 43 | # continue 44 | if q and q not in result.get("title"): 45 | continue 46 | result["index"] = index 47 | index += 1 48 | res.append(result) 49 | except Exception as e: 50 | print(e) 51 | await redis.delete(item.decode()) 52 | continue 53 | # 根据价格排序 54 | # res = sorted(res, key=lambda x: float(x.get("price"))) 55 | # 根据时间排序 56 | res = sorted(res, key=lambda x: time.mktime(time.strptime(x.get("publish_time"), "%Y-%m-%d %H:%M:%S")), 57 | reverse=True) 58 | # 分页返回数据 59 | start = (page - 1) * page_size 60 | end = page * page_size 61 | 62 | return {"code": 200, "msg": "获取成功", "data": res[start:end]} 63 | 64 | 65 | @router.post("/tasks") 66 | async def tasks(task_info: TaskInfo): 67 | if task_info.type == 0: 68 | # 添加任务 69 | xian_yu_task.add_task(task_info.keywords, task_info.seconds) 70 | elif task_info.type == 1: 71 | # 删除任务 72 | xian_yu_task.del_task(task_info.keywords, task_info.seconds) 73 | elif task_info.type == 2: 74 | # 暂停任务 75 | xian_yu_task.pause() 76 | elif task_info.type == 3: 77 | # 恢复任务 78 | xian_yu_task.resume() 79 | return {"code": 200, "msg": "操作成功"} 80 | 81 | 82 | @router.post("/setFilter") 83 | async def setFilter(type_filter: int = Query(default=0, description='0:添加白名单 1:添加黑名单 2:从白名单删除 3:从黑名单删除 4:查看白名单列表 ' 84 | '5:查看黑名单列表', ge=0, le=4), 85 | keywords: List[str] = Query(default=None, description='关键字列表')): 86 | if type_filter == 0: 87 | # 添加白名单 88 | for keyword in keywords: 89 | whitelist.add_filter(keyword) 90 | elif type_filter == 1: 91 | # 添加黑名单 92 | for keyword in keywords: 93 | blacklist.add_filter(keyword) 94 | elif type_filter == 2: 95 | # 从白名单删除 96 | for keyword in keywords: 97 | whitelist.del_filter(keyword) 98 | elif type_filter == 3: 99 | # 从黑名单删除 100 | for keyword in keywords: 101 | blacklist.del_filter(keyword) 102 | elif type_filter == 4: 103 | # 查看白名单列表 104 | return {"code": 200, "msg": "获取成功", "data": whitelist.filter_list()} 105 | elif type_filter == 5: 106 | # 查看黑名单列表 107 | return {"code": 200, "msg": "获取成功", "data": blacklist.filter_list()} 108 | return {"code": 200, "msg": "操作成功"} 109 | -------------------------------------------------------------------------------- /core/task/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jxytest/xianyu/100387e1463a15fa8664ba9ea4c7e9ddf33f56ab/core/task/__init__.py -------------------------------------------------------------------------------- /core/task/task_manage.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from apscheduler.schedulers.background import BackgroundScheduler 4 | 5 | from .xy_task import start_task 6 | 7 | 8 | class TaskManage: 9 | def __init__(self): 10 | self.scheduler = BackgroundScheduler() 11 | self.start() 12 | self.job_id = None 13 | 14 | def add_interval_task(self, func, seconds: int, args=None): 15 | self.job_id = self.scheduler.add_job(func, 'interval', seconds=seconds, args=args, replace_existing=True) 16 | 17 | def update_interval_task(self, func, seconds: int, args=None): 18 | # 重新添加 19 | if self.job_id: 20 | self.add_interval_task(func, seconds, args) 21 | else: 22 | raise Exception("任务不存在") 23 | 24 | def del_interval_task(self): 25 | self.scheduler.remove_job(self.job_id) 26 | self.job_id = None 27 | 28 | def task_list(self): 29 | return self.scheduler.get_jobs() 30 | 31 | def start(self): 32 | self.scheduler.start() 33 | 34 | def shutdown(self): 35 | self.scheduler.shutdown() 36 | 37 | def pause(self): 38 | self.scheduler.pause() 39 | 40 | def resume(self): 41 | self.scheduler.resume() 42 | 43 | 44 | class XianYuTask(TaskManage): 45 | def __init__(self): 46 | super().__init__() 47 | self.look = threading.Lock() 48 | self.keywords = [] 49 | 50 | def add_task(self, keywords, seconds: int): 51 | with self.look: 52 | self.keywords.extend(keywords) 53 | self.add_interval_task(start_task, seconds, args=[self.keywords]) 54 | 55 | def update_task(self, keywords, seconds: int): 56 | with self.look: 57 | self.keywords = keywords 58 | self.update_interval_task(start_task, seconds, args=[self.keywords]) 59 | 60 | def del_task(self, keywords, seconds: int): 61 | with self.look: 62 | for keyword in keywords: 63 | if keyword in self.keywords: 64 | self.keywords.remove(keyword) 65 | self.update_interval_task(start_task, seconds, args=[self.keywords]) 66 | 67 | def clear_task(self): 68 | with self.look: 69 | self.keywords.clear() 70 | self.update_interval_task(start_task, 1000, args=[self.keywords]) 71 | 72 | def task_count(self): 73 | return len(self.keywords) 74 | 75 | def task_list(self): 76 | return self.keywords 77 | 78 | -------------------------------------------------------------------------------- /core/task/xy_task.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import json 3 | import time 4 | 5 | from loguru import logger 6 | 7 | from .. import redis 8 | from ..api.api import Api 9 | from ..pusher import ding_push 10 | from ..pusher import wx_push 11 | 12 | api = Api() 13 | 14 | 15 | loop = asyncio.new_event_loop() 16 | asyncio.set_event_loop(loop) 17 | # loop.run_forever() 18 | 19 | 20 | async def xy_task(keywords): 21 | try: 22 | for keyword in keywords: 23 | # 获取数据 24 | print("开始获取数据", keyword) 25 | data = await api.search(keyword) 26 | # 存入redis 27 | for item in data: 28 | if not await redis.exists(item.get("itemId")) and 4000 > float(item.get("price")) > 700 \ 29 | and "回收" not in item.get("title") and "求购" not in item.get("title") \ 30 | and time.mktime(time.strptime(item.get("publish_time"), "%Y-%m-%d %H:%M:%S")) > \ 31 | time.time() - 3600 * 24 * 3: 32 | # {"itemId":"695010780965","userNick":"薄利多销小店","price":"3888","publish_time":"2022-12-19 33 | # 23:47:03","title":"苹果12promax,外版无锁128功能全好面容秒解,成色如 图新原装机。屏幕上角小老化如图不明显,朋友自用机闲置低价出。", 34 | # "pic_url":"http://img.alicdn.com/bao/uploaded/i2/O1CN01uNcGsu2CWLpwt92zT_!!0-fleamarket.jpg 35 | # "} 解析成md格式 36 | message = f"### {item.get('title')}\n" \ 37 | f"> 价格:{item.get('price')}\n" \ 38 | f"> 发布时间:{item.get('publish_time')}\n" \ 39 | f"> 商品链接:https://h5.m.goofish.com/item?id={item.get('itemId')}&\n" \ 40 | f"> ![商品图片]({item.get('pic_url')})\n" 41 | 42 | await ding_push(message) 43 | await wx_push(message) 44 | await redis.set(item.get("itemId"), json.dumps(item)) 45 | except Exception as e: 46 | logger.exception(e) 47 | finally: 48 | pass 49 | 50 | 51 | def start_task(keyword): 52 | logger.info("==============================") 53 | logger.info(f"开始任务:{keyword}") 54 | 55 | xt = loop.create_task(xy_task(keyword)) 56 | loop.run_until_complete(asyncio.wait([xt])) 57 | # loop.close() 58 | # asyncio.run(xy_task(keyword)) 59 | logger.info(f"任务结束:{keyword}") 60 | logger.info("-------------------------------") 61 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from core.service.service import router 2 | from fastapi import FastAPI 3 | import uvicorn 4 | 5 | app = FastAPI() 6 | app.include_router(router) 7 | 8 | 9 | if __name__ == '__main__': 10 | uvicorn.run(app, host="0.0.0.0", port=5005) -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | loguru>=0.6.0 2 | aiohttp>=3.6.2 3 | frida-tools==12.0.4 4 | frida==16.0.8 5 | fastapi>=0.88.0 6 | uvicorn>=0.20.0 7 | apscheduler>=3.9.1 8 | pydantic>=1.10.2 9 | corpwechatbot>=0.0.3 10 | aioredis>=2.0.0 -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jxytest/xianyu/100387e1463a15fa8664ba9ea4c7e9ddf33f56ab/test/__init__.py -------------------------------------------------------------------------------- /test/test_sign.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | import time 4 | from urllib.parse import quote 5 | from core.api.api import Api 6 | from core.frida.xianyu import XianYu 7 | 8 | xian_yu = XianYu(os.path.join(os.path.dirname(__file__), "../core/js/rpc.js")) 9 | 10 | 11 | def update_headers(headers, data: str): 12 | t = str(int(time.time())) 13 | sign = xian_yu.get_sign(data, headers, t) 14 | headers.update({ 15 | "x-t": t, 16 | "x-mini-wua": quote(sign.get('x-mini-wua')), 17 | "x-sgext": quote(sign.get('x-sgext')), 18 | "x-sign": quote(sign.get('x-sign')), 19 | "x-mut": quote(sign.get('x-umt')), 20 | }) 21 | return headers 22 | 23 | 24 | # 生成随机字符串 25 | def random_str(random_length=8): 26 | """ 27 | 生成一个指定长度的随机字符串,其中 28 | string.digits=0123456789 29 | string.ascii_letters=abcdefghigklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ 30 | """ 31 | import random 32 | import string 33 | str_list = [random.choice(string.digits + string.ascii_letters) for i in range(random_length)] 34 | return ''.join(str_list) 35 | 36 | 37 | def test_api(): 38 | api = Api() 39 | asyncio.run(api.search("dell r730")) 40 | --------------------------------------------------------------------------------