python实现电影日历推荐今天偶然间看到一个此刻 电影日历-每天一部优秀电影 (cikeee.com)关于电影推荐的网站，感觉做的很不错，很简洁明了，突然希望能够实现邮件给我推送这些推荐的电影相关技术python爬虫：由于网站采用的是静态存储策略，因此采用requests+xpath获取所需字段python邮件发送功能：可以参考我之前写的健康打卡成功后的邮件提醒python实现自动健康打卡 - Xherlockstmp发送html文本最后希望邮件格式像这样的忠犬八公的故事电影详情 高清在线观看 网盘下载 磁力下载链接-此刻电影 (cikeee.com)代码流程相关函数库import requests import base64 import smtplib from lxml import etree from email.mime.text import MIMEText爬虫往日推荐-此刻 电影日历 (cikeee.com)从这顶上获取最近七天的电影链接url = 'https://www.cikeee.com/wangri' resp = requests.get(url).text html = etree.HTML(resp) paths = html.xpath('//a[@class="mov-img-a"]/@href')接着使用for循环遍历请求每个url，获取电影相关信息到此爬虫部分完成html拼接想要返回完整的html代码需要包含head和body，head部分需要拼接的有css，主要有两个css（最开始没有注意到，只发现PC端需要第一个css，后来发现第二个是适配手机版的css）源代码中定位对应的css文件复制下来，这里我进行了缩减和删除不需要的css，代码见最后完整的html代码，包含head的css和body的电影部分，使用{}进行占位，后面字符串format可以进行填充：单个电影框的html代码，同理使用了{}进行占位，便于后面将爬虫信息填入：python读取文件为字符串：使用movie_list接收每个电影的html代码并拼接中间遇到的两个问题图片存在跨域访问失败，直接拼接出来的url不能用在自己的前端不想保存图片到本地，因此采用requests获取图像的二进制流后转为base64字符串，注意要将base64加密后的内容再转为字符串格式，否则字符串无法和字节型数据拼接后面发送邮件发现手机格式很混乱因此将第二个适配版的css也加入了html代码中，相当于两份各自适配终端的css邮件发送完整代码good_movies.pyimport requests import base64 import smtplib from lxml import etree from email.mime.text import MIMEText MAIL_USER = 'xxx@xx.com' MAIL_PWD = '授权码' MAIL_TO = 'xxx@xx.com' def mail(mail_text): # 设置邮件内容 msg = MIMEText(mail_text, _subtype='html', _charset='utf-8') # 设置邮件主题、发送方和接收方 msg['Subject'] = "电影日历推荐" msg['From'] = MAIL_USER msg['To'] = MAIL_TO # 发送邮件 send = smtplib.SMTP_SSL("smtp.qq.com", 465) send.login(MAIL_USER, MAIL_PWD) send.send_message(msg) # 退出邮件 send.quit() def get_movies(): url = 'https://www.cikeee.com/wangri' resp = requests.get(url).text html = etree.HTML(resp) paths = html.xpath('//a[@class="mov-img-a"]/@href') cnt = 0 page = open('page.html', encoding='utf-8').read() css = open('movie.css', encoding='utf-8').read() adaptivte_css = open('movie-adaptive.css', encoding='utf-8').read() movie = open('movie.html', encoding='utf-8').read() movie_list = '' for path in paths: # 获取字段值 movie_html = etree.HTML(requests.get('https://www.cikeee.com'+path).text) movie_title = movie_html.xpath('//*[@id="movie-title"]/text()')[0] rate_box = movie_html.xpath('//*[@id="rate-box"]/@href')[0] score = movie_html.xpath('//*[@id="rate-box"]/span/text()')[0] movie_info = movie_html.xpath('//*[@id="movie-information"]')[0].xpath('string(.)').split() category = movie_info[0] year = movie_info[1] country = movie_info[2] director = movie_info[3] stars = movie_info[4] movie_text = movie_html.xpath('//*[@id="movie-text"]/text()')[0] movie_intro = movie_html.xpath('//*[@id="movie-intro"]/text()')[0] img_url = 'https://www.cikeee.com'+movie_html.xpath('//*[@class="movie-img"]/@src')[0] img = 'data:image/jpeg;base64,'+str(base64.b64encode(requests.get(img_url).content), encoding="utf-8") movie_list += movie.format(movie_title, rate_box, score, category, year, country, director, stars, movie_text, movie_intro, img) # 获取过去连续7天电影推荐 cnt += 1 if cnt == 7: break return page.format(css, adaptivte_css, movie_list) if __name__ == '__main__': mail_text = get_movies() mail(mail_text)page.html 电影日历推荐 {} movie.html {} {} {} {} {} {} {} {} {} movie.csshtml,body{ width:100%; height:100%; margin:0; padding: 0; font-family: -apple-system, 'Helvetica Neue', sans-serif; font-size: 16px; } body{ position: relative; background-color:#1e1e1e; /* 浏览器不支持的时候显示 */ } #bg{ position: fixed; left:0; top:0; width:100%; height:100%; background-color:#1e1e1e; /* 浏览器不支持的时候显示 */ background-image:linear-gradient(45deg,#1a161a 70%,#271f28); z-index:-3; } #title{ color:#f4e5b3; text-align: center; margin-top: 10px; } /* 滚动条样式 */ ::-webkit-scrollbar { width: 10px; } /* 滚动槽 */ ::-webkit-scrollbar-track { -webkit-box-shadow: inset006pxrgba(0, 0, 0, 0.2); border-radius: 5; } /* 滚动条滑块 */ ::-webkit-scrollbar-thumb { border-radius: 15px; background: rgb(51, 50, 51); -webkit-box-shadow: inset006pxrgba(0, 0, 0, 0.1); } ::-webkit-scrollbar-thumb:window-inactive { background: rgba(0, 0, 0, 0.2); } #banner-wrap{ background-color: rgb(10, 8, 10); background:rgba(0, 0, 0, 0.4); width:100%; } #banner-width{ color:white; margin:0 auto; width:90%; max-width:1600px; padding-bottom:80px; } /* movie-banner________________________________________________ */ #movie-banner{ width:100%; border:1px solid #f4e5b3; padding:50px 50px; box-sizing: border-box; display: flex; justify-content: space-between; min-height:260px; } #movie-info-wrap{ margin-right:20px; } .movie-img{ width:220px; border-radius:10px; } #movie-title-box{ color:#f4e5b3; display: flex; align-items: center; } #movie-title{ font-size:36px; margin-right:15px; line-height: 0px; } #rate-box{ font-size:14px; color: #f4e5b3; text-decoration: none; background-color:rgba(255, 251, 197, 0.1); border-radius:100px; display: flex; align-items: center; margin:0; line-height: 0; padding:4px 5px; user-select: none; -ms-user-select: none; transform: translate(0,2px); transition: all .5s; } #rate-box:hover{ background-color:rgba(255, 251, 197, 0.2); } #rate-box span{ font-weight:lighter; } .db-icon{ width:16px; margin-right:5px; } #movie-information{ font-size:16px; color:#f4e5b3; margin-top:16px; } #movie-text{ font-size:22px; margin-top: 45px; transform: translate(-12px,0); } #movie-intro{ font-size:14px; margin-top:10px; max-width:90%; }movie-adaptive.css@media (max-width:640px){ /* movie-banner________________________________________________ */ #banner-width{ width:85%; padding-bottom:40px; } #movie-banner{ padding:20px 20px; min-height:260px; flex-flow:column-reverse wrap; justify-content:center; align-items: center; } #movie-info-wrap{ margin-right:0px; } #movie-title-box{ justify-content: center; align-items: center; } #movie-title{ font-size:25px; margin-right:15px; } .movie-img{ width:150px; border-radius:5px; margin-bottom:20px; } #movie-text{ margin-top: 45px; transform: translate(0,0); } #movie-intro{ margin-top:10px; max-width:100%; } }部署到服务器宝塔面板的计划任务最终效果PC端：手机端：
js逆向爬虫3目标：百度翻译百度翻译-200种语言互译、沟通全世界！ (baidu.com)定位需要的返回数据如上图，查看它的请求参数，api为v2transapi开头搜索上面几个参数，其中最容易定位的是simple_means_flag，只搜索到一个结果在源代码中打开，搜索simple_means_flag得到两个结果，都打上断点刷新调试；发现跳转到了第二个，说明这个才是关键加密函数通过多次刷新发现，除了sign其他参数均为固定值，单步进入b函数，将整个函数复制下来创建了个获取请求参数的函数，返回结果为一个字典变量运行查看结果：41行的r未定义找到源码中对应的位置打断点，查看r值，发现r值为空故在41行前添加如下代码后再次运行var r = null;报错说window变量未定义，光标移动到window变量上，在弹窗中找到gtk的值（d变量的值为gtk）在40行前添加如下代码：var window = {'gtk': '320305.131321201'}如图，获取了全部参数完整代码：baidu.pyimport requests import subprocess from functools import partial subprocess.Popen = partial(subprocess.Popen, encoding="utf-8") import execjs translate_words = input('请输入要翻译的内容：') ctx = execjs.compile(open('baidu.js', encoding='utf-8').read()) params = ctx.call('get_req_data', translate_words) # 获取输入语言类型 detect_lang = requests.post('https://fanyi.baidu.com/langdetect', data={'query': translate_words}).json() if detect_lang['lan'] == 'en': params['from'] = 'en' params['to'] = 'zh' headers = { 'Cookie': 'your cookie', } results = requests.post('https://fanyi.baidu.com/v2transapi?from=zh&to=en', data=params, headers=headers).json() print(results['trans_result']['data'][0]['dst']) baidu.jsfunction n(t, e) { for (var n = 0; n < e.length - 2; n += 3) { var r = e.charAt(n + 2); r = "a" <= r ? r.charCodeAt(0) - 87 : Number(r), r = "+" === e.charAt(n + 1) ? t >>> r : t << r, t = "+" === e.charAt(n) ? t + r & 4294967295 : t ^ r } return t } function b(t) { var o, i = t.match(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g); if (null === i) { var a = t.length; a > 30 && (t = "".concat(t.substr(0, 10)).concat(t.substr(Math.floor(a / 2) - 5, 10)).concat(t.substr(-10, 10))) } else { for (var s = t.split(/[\uD800-\uDBFF][\uDC00-\uDFFF]/), c = 0, u = s.length, l = []; c < u; c++) "" !== s[c] && l.push.apply(l, function(t) { if (Array.isArray(t)) return e(t) }(o = s[c].split("")) || function(t) { if ("undefined" != typeof Symbol && null != t[Symbol.iterator] || null != t["@@iterator"]) return Array.from(t) }(o) || function(t, n) { if (t) { if ("string" == typeof t) return e(t, n); var r = Object.prototype.toString.call(t).slice(8, -1); return "Object" === r && t.constructor && (r = t.constructor.name), "Map" === r || "Set" === r ? Array.from(t) : "Arguments" === r || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r) ? e(t, n) : void 0 } }(o) || function() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.") }()), c !== u - 1 && l.push(i[c]); var p = l.length; p > 30 && (t = l.slice(0, 10).join("") + l.slice(Math.floor(p / 2) - 5, Math.floor(p / 2) + 5).join("") + l.slice(-10).join("")) } var window = {'gtk': '320305.131321201'} var r = null; for (var d = "".concat(String.fromCharCode(103)).concat(String.fromCharCode(116)).concat(String.fromCharCode(107)), h = (null !== r ? r : (r = window[d] || "") || "").split("."), f = Number(h[0]) || 0, m = Number(h[1]) || 0, g = [], y = 0, v = 0; v < t.length; v++) { var _ = t.charCodeAt(v); _ < 128 ? g[y++] = _ : (_ < 2048 ? g[y++] = _ >> 6 | 192 : (55296 == (64512 & _) && v + 1 < t.length && 56320 == (64512 & t.charCodeAt(v + 1)) ? (_ = 65536 + ((1023 & _) << 10) + (1023 & t.charCodeAt(++v)), g[y++] = _ >> 18 | 240, g[y++] = _ >> 12 & 63 | 128) : g[y++] = _ >> 12 | 224, g[y++] = _ >> 6 & 63 | 128), g[y++] = 63 & _ | 128) } for (var b = f, w = "".concat(String.fromCharCode(43)).concat(String.fromCharCode(45)).concat(String.fromCharCode(97)) + "".concat(String.fromCharCode(94)).concat(String.fromCharCode(43)).concat(String.fromCharCode(54)), k = "".concat(String.fromCharCode(43)).concat(String.fromCharCode(45)).concat(String.fromCharCode(51)) + "".concat(String.fromCharCode(94)).concat(String.fromCharCode(43)).concat(String.fromCharCode(98)) + "".concat(String.fromCharCode(43)).concat(String.fromCharCode(45)).concat(String.fromCharCode(102)), x = 0; x < g.length; x++) b = n(b += g[x], w); return b = n(b, k), (b ^= m) < 0 && (b = 2147483648 + (2147483647 & b)), "".concat((b %= 1e6).toString(), ".").concat(b ^ f) } function get_req_data(query){ const w = { from: 'zh', to: 'en', query: query, transtype: '', simple_means_flag: 3, sign: b(query), token: "d21ee97178386951469ceda3f0d7caf7", domain: 'common' } return w; } console.log(get_req_data('今天是周三'))小结：百度翻译接口破解也很简单，和有道翻译相比要简单些，大部分参数都是固定的，只加密了一个；翻译上来看貌似有道更生动些:smile:，百度甚至把must be直译成了必须是
js逆向爬虫1目标：有道翻译首先输入两个翻译，发现提交的参数请求中两个参数不同，一个是带time的，另一个是sign。很明显一个是时间生成器生成的数，另一个是做了加密的签名，很可能包含mysticTime的加密在截包器中搜索关键词sign，发现有很多js包含，不好定位；因此转而去直接搜索mysticTime，定位到app.js查看源文件，格式化后Ctrl+F搜索定位mysticTime，发现只有一个匹配结果，且很容易看出sign后的那个b函数就是签名函数打断点调试，输入一个新的翻译，页面来到sign前，此时t已经在前一行的getTime函数中得到值；只需知道sign函数即可，单步进入函数发现函数返回一个拼接的字符串（r="fanyideskweb"，i="webfanyi"），并经过p函数加密处理，再次单步进入p函数此时这个函数看起来很像Crypto库的哈希函数加密的，光标移到方法名前的c.a，可以看到其下有很多方法，基本验证猜想在网上搜索到了示例代码如下，格式完全一样最后只需确定最开始f函数传入的e值，再次运行发现传入e值不变，仍然为"fsdsogkndfokasodnaso"编写js代码const crypto = require('crypto'); const r = "fanyideskweb", i = "webfanyi"; function p(e) { return crypto.createHash("md5").update(e.toString()).digest("hex") } function b(e, t) { return p(`client=${r}&mysticTime=${e}&product=${i}&key=${t}`) } function f() { const e = 'fsdsogkndfokasodnaso', s = 'client,mysticTime,product', u = 'fanyi.web', l = '1.0.0', d = 'web'; const t = (new Date).getTime(); return { sign: b(t, e), client: r, product: i, appVersion: l, vendor: d, pointParam: s, mysticTime: t, keyfrom: u } } console.log(f())输出如下，格式正确编写python发送翻译请求import requests import execjs ctx = execjs.compile(open('youdao.js', encoding='utf-8').read()) params = ctx.call('f') data = { 'i': '你好', 'from': 'zh-CHS', 'to': 'en', 'domain': '0', 'dictResult': 'true', 'keyid': 'webfanyi' } headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36 Edg/108.0.1462.54', 'Cookie': 'OUTFOX_SEARCH_USER_ID_NCOO=976405377.6815147; OUTFOX_SEARCH_USER_ID=-198948307@211.83.126.235; _ga=GA1.2.1162596953.1667349221; search-popup-show=12-2', 'Referer': 'https://fanyi.youdao.com/' } data.update(params) results = requests.post('https://dict.youdao.com/webtranslate', data=data, headers=headers) print(results.text)发现返回结果被加密，说明前端做了解密处理，需要找到解密函数从上一断点继续运行，很快就发现密文到明文的转换过程，可以看到下图蓝框中o接收了decode后的数据，并赋给a变量，而a变量正是解密后的数据，因此得出结论tn["a"].decodeData是关键解密函数在这个函数上打断点后再次提交翻译输入，进入函数复制该函数下来到js文件中测试查询alloc函数发现属于Buffer下的方法，m函数也是c.a（Crypto）下的方法，直接替换为crypto再次运行js，密文已经被解密运行python时发现始终报错，说是gbk编码问题解决方法：导入execjs前加入如下代码即可import subprocess from functools import partial subprocess.Popen = partial(subprocess.Popen, encoding="utf-8")最终效果图：完整代码：youdao.pyimport requests import subprocess from functools import partial subprocess.Popen = partial(subprocess.Popen, encoding="utf-8") import execjs translate_words = input('请输入要翻译的内容：') ctx = execjs.compile(open('youdao.js', encoding='utf-8').read()) params = ctx.call('f') data = { 'i': translate_words, 'from': 'AUTO', 'to': 'AUTO', 'domain': '0', 'dictResult': 'true', 'keyid': 'webfanyi' } headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36 Edg/108.0.1462.54', 'Cookie': 'OUTFOX_SEARCH_USER_ID_NCOO=976405377.6815147; OUTFOX_SEARCH_USER_ID=-198948307@211.83.126.235; _ga=GA1.2.1162596953.1667349221; search-popup-show=12-2', 'Referer': 'https://fanyi.youdao.com/' } data.update(params) results = requests.post('https://dict.youdao.com/webtranslate', data=data, headers=headers) fanyi = eval(ctx.call('A', results.text)) print(fanyi['translateResult'][0][0]['tgt'])youdao.jsconst crypto = require('crypto'); const r = "fanyideskweb", i = "webfanyi"; function m(e) { return crypto.createHash("md5").update(e).digest() } function p(e) { return crypto.createHash("md5").update(e.toString()).digest("hex") } function b(e, t) { return p(`client=${r}&mysticTime=${e}&product=${i}&key=${t}`) } function f() { const e = 'fsdsogkndfokasodnaso', s = 'client,mysticTime,product', u = 'fanyi.web', l = '1.0.0', d = 'web'; const t = (new Date).getTime(); return { sign: b(t, e), client: r, product: i, appVersion: l, vendor: d, pointParam: s, mysticTime: t, keyfrom: u } } A = (t,o,n)=>{ o = "ydsecret://query/key/B*RGygVywfNBwpmBaZg*WT7SIOUP2T0C9WHMZN39j^DAdaZhAnxvGcCY6VYFwnHl" n = "ydsecret://query/iv/C@lZe2YzHtZ2CYgaXKSVfsb7Y4QWHjITPPZ0nQp87fBeJ!Iv6v^6fvi2WN@bYpJ4" if (!t) return null; const a = Buffer.alloc(16, m(o)) , r = Buffer.alloc(16, m(n)) , i = crypto.createDecipheriv("aes-128-cbc", a, r); let s = i.update(t, "base64", "utf-8"); return s += i.final("utf-8"), s }
python读写csv文件CSV：逗号分隔值（Comma-Separated Values，CSV，有时也称为字符分隔值，因为分隔字符也可以不是逗号），其文件以纯文本形式存储表格数据（数字和文本）CSV（逗号分隔值文件格式）_百度百科 (baidu.com)python使用csv库import csv写入文件写入列表newline=''时不自动换行writer中delimiter为分隔符，默认为逗号with open('demo.csv', 'w', newline='') as wf: writer = csv.writer(wf, delimiter=',') writer.writerow(['x', 'y', 'z']) writer.writerow([1, 2, 3]) wf.flush()文件：x,y,z 1,2,3写入字典with open('info.csv', 'w', newline='') as wf: fieldnames = ['name', 'age'] csv_writer = csv.DictWriter(wf, fieldnames=fieldnames) csv_writer.writeheader() csv_writer.writerow({'name': 'xherlock1', 'age': 1}) csv_writer.writerow({'name': 'xherlock2', 'age': 2})文件：name age xherlock1 1 xherlock2 2读取文件读取文件时候需要注意文件分隔符是否为逗号读取每一行with open('demo.csv') as rf: reader = csv.reader(rf) for row in reader: print(row)输出['x', 'y', 'z'] ['1', '2', '3']读取某一列with open('demo.csv') as rf: reader = csv.reader(rf) column = [row[0] for row in reader] print(column)输出['x', '1']读取字典（带表头）with open('info.csv') as rf: reader = csv.DictReader(rf) for row in reader: print(row['name'] + ' is ' + row['age'] + ' years old')输出xherlock1 is 1 years old xherlock2 is 2 years old
socket实现局域网S-C通信服务器端代码from socket import * sk = socket(AF_INET, SOCK_STREAM) c_Port = 9999 sk.connect(('127.0.0.1', c_Port)) # 绑定地址到套接字 print('(1)show me OS info:') print('(2)show me Disk info:') print('(3)exit:') while True: choice = input('input your choice:') if not choice: continue sk.send(choice.encode()) s_reply = sk.recv(1024).decode() if not s_reply: print('通信结束') break print(s_reply) sk.close()所使用库：socket客户端代码from socket import * from platform import * from psutil import * sk = socket(AF_INET, SOCK_STREAM) s_Port = 9999 sk.bind(('127.0.0.1', s_Port)) # 绑定地址到套接字 sk.listen(5) # 开始监听 print('The server is ready to receive!') while True: conn, addr = sk.accept() # 被动接收客户端连接，返回通信对象和客户端地址 while True: c_request = conn.recv(1024).decode('utf-8') # recv接收的最大数据量 if c_request == '1': print('客户端请求OS info') os_info = '操作系统及版本信息:{0}\n系统版本号:{1}\n系统名称:{2}\n系统位数:{3}\n计算机类型:{4}\n计算机名称:{5}\n处理器类型:{6}\n计算机相关信息:{7}\n'\ .format(platform(), version(), system(), str(architecture()),machine(), node(), processor(), str(uname())) conn.send(os_info.encode()) elif c_request == '2': print('客户端请求Disk info') disk_num = len(disk_partitions()) disk_size = '' GB = 1024*1024*1024 for i in range(disk_num): disk_size += disk_partitions()[i].device.split(':')[0] + ':' + str(round(disk_usage(disk_partitions()[i].device).total / GB)) + 'GB\n' disk_info = '磁盘数目：{0}\n磁盘大小：\n{1}'.format(disk_num, disk_size) conn.send(disk_info.encode()) elif c_request == '3': exit('通信结束') else: print('Error!') conn.close() sk.close()所使用库：socket、platform（获取操作系统信息）、psutil（获取磁盘数目和大小）运行说明环境：python3安装依赖：pip install psutil运行：# 先运行server.py再运行client.py > ./server.py > ./client.py运行截图左边服务器端，右边客户端
python实现自动健康打卡经过试验成功打卡两天，在此记录下数据获取和提交首先打卡本质上就是在一个网址上提交了表单数据给校园服务器，所以在第一天尝试提交数据，查看提交数据格式和网址https://wfw.scu.edu.cn/ncov/wap/default/save，保持登录状态，获取save下的表单数据和header# 数据 save_url = 'https://wfw.scu.edu.cn/ncov/wap/default/save' headers = { # 你的请求头（包含referer、cookie、uagent等重要信息） }接下来对表单分析，发现有一项必须每天变化，就是打卡日期，格式是20220302这样的引入time库，采用如下函数实现格式化日期new_date = time.strftime('%Y%m%d', time.localtime(time.time())) form_data = { # 你的表单信息，日期那一栏值替换为new_date }然后就可以使用requests库进行post dataresponse = requests.post(url=save_url, headers=headers, data=form_data).text print('打卡信息：' + response) # 查看打卡结果信息可以看到正常打卡信息中存在提示“操作成功”{e: 0, m: "操作成功", d: {}}邮箱提醒打卡成功因为想要实现邮件提醒打卡成功，所以导入了python自带的邮件相关库import smtplib from email.mime.text import MIMEText设置相关信息MAIL_USER = '自己的邮箱' MAIL_PWD = '授权码' # 需要开启邮箱的STMP服务 MAIL_TO = '自己的邮箱'函数实现发送邮件 参考：使用Python实现平台自动打卡 - 知乎 (zhihu.com)# 从网上找的代码并改进 def mail(mail_text): # 设置邮件内容 msg = MIMEText(mail_text) # 设置邮件主题、发送方和接收方 msg['Subject'] = "每日健康打卡通知" msg['From'] = MAIL_USER msg['To'] = MAIL_TO # 发送邮件 send = smtplib.SMTP_SSL("smtp.qq.com", 465) send.login(MAIL_USER, MAIL_PWD) send.send_message(msg) # 退出邮件 send.quit() if '操作成功' in response: mail_text = '今日已打卡，哦耶！' mail(mail_text)实现每日执行最开始想要用windows的任务计划程序来实现每天执行脚本，费了老大劲设置，就跟这个作者弄得差不多，结果发现能执行，但是必须的开着机，而且很不稳定（确实，你关机了，笔记本还能运行程序就怪了）然后我就突然想到不是有服务器吗，放那吃灰呢，搁那顶上不就行了嘛，然后还真可以要设置宝塔面板每日计划，参考：(2条消息) 宝塔运行python文件（极简）_luxuncang的博客-CSDN博客_宝塔运行py文件按着这个我就搞定了，设置每天7点打卡（我怕不是每天第一个打卡的，嘿嘿
python实现为图片批量添加水印参考：使用 Python 给图片添加水印，其中一种还是隐形的盲水印呢！ - 知乎 (zhihu.com) python通过对话框实现文件或文件夹路径的选择并获得路径_请选择的博客-CSDN博客_python选择文件夹或文件[python] OSError: cannot write mode RGBA as JPEG - 黄天星 - 博客园 (cnblogs.com)需求：课程报告截图得带信息水印，因为觉得截图时添加文字麻烦，下载图片处理软件占空间，在线处理老付费，索性写个脚本魔改：结合tkinter可以直接调出文件选择框，通过计算输入文字和图片大小，大致确定合适添加水印位置可以修改fnt里的字体：调用C:Program FilesMicrosoft OfficerootvfsFontsprivate下的文件即可可以修改d.text中fill水印颜色：在线调色网址ColorSpace - Color Palettes Generator and Color Gradient Tool (mycolor.space)from PIL import Image, ImageDraw, ImageFont import tkinter as tk from tkinter import filedialog import numpy as np import time def create_watermark(): print('请选择要添加水印的图片：') time.sleep(1) files_path = get_files_path() if files_path is None: return else: for i in files_path: print(i) text = input('请输入要添加的水印文字：') for i in files_path: img = Image.open(i) # 获取图片大小以计算水印大致位置 arr_img = np.asarray(img) x, y = arr_img.shape[1], arr_img.shape[0] # 长和宽 fnt_size = round(x / 2 / len(text)) # 文字大小 if fnt_size < 20: fnt_size = 20 txt_x = x / 2 - fnt_size * len(text) / 2 # 文字开始的x坐标 txt_y = y / 2 - fnt_size / 2 # 文字开始的y坐标 # 开始添加水印 with img.convert("RGBA") as base: # 必须先转换成RGBA才能出现色彩 txt = Image.new("RGBA", base.size, (255, 255, 255, 0)) fnt = ImageFont.truetype("SIMLI.TTF", fnt_size) d = ImageDraw.Draw(txt) d.text((txt_x, txt_y), text, font=fnt, fill=(249, 12, 31)) new = Image.alpha_composite(base, txt) # 将水印和原图片合成 new.save(i) print('添加水印完成！') # 获取图片路径 def get_files_path(): root = tk.Tk() root.withdraw() filepath = filedialog.askopenfilenames() return filepath def run(): create_watermark() if __name__ == '__main__': run() 效果：很可惜没能实现倾斜文字水印，就是常见的pdf里那种，因为如果采用PIL下rotate函数会导致文字偏移很难计算具体位置，索性就这样了，能够基本满足我做课程报告为截图批量添加水印的需求就好！
SCU抢课脚本分析过程初步计算前两个结果相符找tokenvalue，最后在教务处主页找到，初步估计是每次选完刷新一次tokenvalue的值这个貌似是教务处防脚本选课的机制（可能，因为这个功能在我看来没多大用处，具体对web机制很不了解），它是模拟你查找一个老师，你正常选课时肯定不可避免地鼠标移到选课老师上面，它会利用类似hover的功能对这个老师进行一个查找，就是下图的queryTeacherJL，返回的数据也很让我迷惑，所以结合前人代码，我感觉代码加上这个应该没啥问题了（希望不会被请喝茶）代码部分设置各个接口设置伪装请求头要注意去除cookie，因为每回值不一样登陆函数采用循环登录方式，错误的话不断重新来，直到登陆成功此处采用了下载验证码图片的功能，能够模拟出我们教务系统登录的方式对登陆时后端接受的信息分析，其中密码采取了md5加密，在函数中导入此模块，对密码加密进行传输获取响应数据中存在“欢迎您”即 登陆成功！获取课表# 获取选课结果课表 def getClassTable(s): classTable_res = s.get(url=classTable_url, headers=headers) class_list = classTable_res.json()['xkxx'] print('您已选的所有课程如下：') cnt = 0 print("--- {0:25} {1:8} {2}_{3}".format('课程名', '老师名', '课程号', '课序号')) # 使用format时一定不要在:间空格，否则报错，找了老长时间问题了 for i in class_list: for j in i.values(): cnt += 1 print("[{0}] {1:25} {2:8} {3}_{4}".format( str(cnt), j['courseName'], re.split(r"[*]", j['attendClassTeacher'])[0], list(j['id'].values())[0], list(j['id'].values())[1] )) class_dict = {} if j['courseName'] not in selected_class: class_dict['kch'] = list(j['id'].values())[0] class_dict['kxh'] = list(j['id'].values())[1] class_dict['kcm'] = j['courseName'] selected_class.append(class_dict)这里采用列表的数据形式接收课程信息，循环打印，并添加到字典中，为后面 检查是否已选课做准备下面是json数据格式化后的样式，据此找到各个部分的信息[ { "105396020_21": { "attendClassTeacher": "李喜雪* ", "courseCategoryCode": "", "courseCategoryName": "", "courseName": "大学英语（创意阅读）-4", "coursePropertiesCode": "001", "coursePropertiesName": "必修", "courseSelectionTime": "20211224012618", "dgFlag": "81086440,李喜雪（无）", "examTypeCode": "01", "examTypeName": "考试", "flag": "", "fsktms": "", "id": { "coureNumber": "105396020", "coureSequenceNumber": "21", "executiveEducationPlanNumber": "2021-2022-2-1", "studentNumber": "xxxxxxx" // 此处省略 }, "programPlanName": "网络空间安全培养方案", "programPlanNumber": "8160", "restrictedCondition": "", "rlFlag": "81086440,李喜雪（无）", "selectCourseStatusCode": "016", "selectCourseStatusName": "置入", "sfczfskt": "", "studyModeCode": "01", "studyModeName": "正常", "timeAndPlaceList": [], "unit": 2, "xkzy": "", "ywdgFlag": "", "zkxh": "" }, "106777020_01": {}, "107061050_44": {}, "107118000_12": {}, "314003030_03": {}, "314037040_03": {}, "314038030_03": {}, "314039040_01": {}, "314042020_01": {}, "314049020_01": {}, "314056030_03": {}, "999005030_05": {} } ]搜索函数# 搜索相关课程信息 def search(s): search = input('请输入你要搜索的关键字（课程名或者老师名）:') data = { 'searchtj': search, 'xq': '0', 'jc': '0', 'kclbdm': '' } search_json = s.post(url=search_url, data=data, headers=headers).json() result_list = search_json['rwRxkZlList'] # 此时成为字符串，所以后续错误，因为按照列表来读取数据的 result_list = json.loads(result_list) if not result_list: # 判断列表为空 print('无结果') return for item in result_list: print('{0}----{1}----{2}_{3}----课余量：{4}'.format(item['kcm'], item['skjs'], item['kch'], item['kxh'], item['bkskyl'])) 获取选课信息# 获取要选课的各种信息 def getSelectInfo(s, kch, kxh, kcm): data = { 'searchtj': kch, 'xq': '0', 'jc': '0', 'kclbdm': '' } search_json = s.post(url=search_url, data=data, headers=headers).json() result_list = search_json['rwRxkZlList'] # 此时成为字符串，所以后续错误，因为按照列表来读取数据的 result_list = json.loads(result_list) if not result_list: # 判断列表为空 return -2 # 已选同课程号的课 for item in result_list: if item['kcm'] == kcm and item['kch'] == kch and item['kxh'] == kxh: # 判断输入信息无误 if int(item['bkskyl']) > 0: return 1 # 有课余量 else: return -1 # 无课余量 else: return -3 # 输入信息有误根据不同情况返回值不同防喝茶机制# 防教务处请喝茶机制（应该是这样），它是模拟你查找一个老师，你正常选课时肯定不可避免地鼠标移到选课老师上面，它会利用类似hover的功能对这个老师进行一个查找 def queryTeacherJL(s, kch, kxh): query_data = { 'id': '2021-2022-1@' + kch + '@' + kxh } return s.post(url=query_url, data=query_data, headers=headers)选课函数# 选课 def select(s): kch = input('请输入课程号（9位数字）：') kxh = input('请输入课序号（01、02等）：') kcm = input('请输入课程名（完整的）：') new_kcms = kcm + kch + '@' + kxh kcms = '' for i in new_kcms: kcms = kcms + str(ord(i)) + ',' cnt = 0 while True: if not queryTeacherJL(s, kch, kxh): return # 及时退出 cnt += 1 print('第{0}轮选课：'.format(cnt)) select_result = getSelectInfo(s, kch, kxh, kcm) if select_result == 1: token_res = s.get(url=token_url, headers=headers).text tokenvalue = re.compile("([a-fA-F0-9]{32})").findall(token_res)[0] select_data = { 'dealType': '5', 'kcIds': kch + '@' + kxh + '@2021-2022-2-1', # 分析：大的数应该是中文的unicode编码，小的是课程号和课序号编码 # 'kcms': '20154, 31867, 30340, 35821, 35328, 45, 45, 35821, 35328, 23398, 20837, 38376, 40, 49, 48, 53, 50, 57, 57, 48, 50, 48, 64, 48, 49, 41', # 人类的语言--语言学入门(105299020_01) # 数码摄影科学与艺术(205300020_01) # 25968, 30721, 25668, 24433, 31185, 23398, 19982, 33402, 26415, 40, 50, 48, 53, 51, 48, 48, 48, 50, 48, 64, 48, 49, 41, # 经验证确实如此，这些数字是Unicode的10进制编码 'kcms': kcms, 'fajhh': '8160', 'sj': '0_0', 'searchtj': '', 'kclbdm': '', 'inputCode': '', 'tokenValue': tokenvalue } select_res = s.post(url=select_url, data=select_data, headers=headers).json() print(select_res) getClassTable(s) break elif select_result == -1: print('无课余量') elif select_result == -2: print('已选同课程号的课或者课程号错误') return elif select_result == -3: print('输入信息有误') return time.sleep(random.uniform(1.5, 3))主函数def run(s): login(s) print(Fore.LIGHTRED_EX + "*****欢迎来到Xherlock的选课系统*****") print(Fore.LIGHTRED_EX + " (1) 查看课表") print(Fore.LIGHTRED_EX + " (2) 查询课程信息") print(Fore.LIGHTRED_EX + " (3) 选课") print(Fore.LIGHTRED_EX + " (4) 退出") print(Style.RESET_ALL) while True: choice = input('请输入选项：') if choice == '1': getClassTable(s) elif choice == '2': search(s) elif choice == '3': select(s) elif choice == '4': quit() def quit(): print(Fore.LIGHTBLUE_EX + "感谢您使用Xherlock的文件管理系统！") sys.exit(0) if __name__ == '__main__': # ASCII字符签名，好看而已 print(Fore.LIGHTMAGENTA_EX + "___ ___ __ __ _______ .______ __ ______ ______ __ ___") print("{0}\\ \\ / / | | | | | ____|| _ \\ | | / __ \\ / || |/ /".format(Fore.LIGHTMAGENTA_EX)) print("{0} \\ V / | |__| | | |__ | |_) | | | | | | | | ,----\'| \' / ".format(Fore.LIGHTMAGENTA_EX)) print("{0} > < | __ | | __| | / | | | | | | | | | < ".format(Fore.LIGHTMAGENTA_EX)) print("{0} / . \\ | | | | | |____ | |\\ \\----.| `----.| `--\' | | `----.| . \\ ".format(Fore.LIGHTMAGENTA_EX)) print("{0}/__/ \\__\\ |__| |__| |_______|| _| `._____||_______| \\______/ \\______||__|\\__\\".format(Fore.LIGHTMAGENTA_EX)) print(Style.RESET_ALL) session = requests.session() # 保存cookie run(session)
为数据快速添加引号在学习python时，使用一些数据时需要为每个key和value添加引号非常麻烦，所以采用Python爬虫—为表单数据快速添加单引号（2021最有效解决方法）_Tom197的博客-CSDN博客_python爬虫 表单里的方法首先pycharm ctrl+R调出正则表达框需要点击高亮红框里的按钮第一行输入(w+): ?r?(.*S?)第二行输入'$1': '$2',但这里有一点是数据的最后一行也会带上逗号，但不影响前后的效果如图