使用Vultr的Snapshots API完成自动备份(Python版) Dec 08,2018 in Python,VPS,Knowledge read (191) 除了免费赠送可挂载的50G Block Storage之外,Snapshots可能也是吸引我使用[Vultr](https://www.vultr.com/?ref=7603581)的原因。虽然我一直没有从快照中还原过2333(但是好歹有个心理安慰是不是) > 注意,默认情况下最多创建11个Snapshots!!!! 可能是因为本人搜索姿势不对吧,使用“vultr snapshot script”在Google中只搜索到 [Automated Snapshots](https://gist.github.com/mcknco/cbf337e00479a20b2df11e7601d60207) 这一个使用PHP写的脚本。但是为了系统安全,我在`php.ini`中禁用了“shell_exec”等方法。同时,该PHP脚本只能在Vultr主机上运行且只对当前机器进行备份。 所以自己模仿着这个脚本写了一下Python3的版本,全部脚本如下,你也可以到Gist上查阅:[Rhilip/**vultr-snapshot**](https://gist.github.com/Rhilip/8c7b8579ac115f84a5ede70f83ad69d2) ```python import re import time import datetime import requests # -------------用户设置 开始-------------- # # 填写Vultr的API KEY API_KEY = "" # 请填写服务器主IP地址或者SUBID,(二选一即可) MAIN_IP = "1.2.3.4" SUBID = None BACKUP_TAG_PREFIX = "auto_backup" # 备份头 MAX_NUM_OF_BACKUPS = 3 # 最大备份数 # -------------用户设置 结束-------------- # # Get base info api_endpoint = "https://api.vultr.com/v1/" day = time.strftime("%Y-%m-%d", time.localtime()) # simple wrapper to access vultr api def vultr(method = "GET",action = "" , data = None): return requests.request(method,"{}{}".format(api_endpoint,action),headers = {"API-Key" : API_KEY}, data=data) server_list = vultr("GET","server/list").json() # Find subid if not set. if SUBID == None: for server_subid,server_info in server_list.items(): if server_info.get("main_ip", None) == MAIN_IP: SUBID = server_subid break if SUBID == None: raise Exception("Fail to find subid for IP: {}".format(MAIN_IP)) snapshot_list_raw = vultr("GET","snapshot/list").json() # Resort the raw snapshot list dict to list obj snapshot_list = [v for k,v in snapshot_list_raw.items()] # Get auto-backup snapshot list backup_snapshot_list = list(filter(lambda x:re.search("{}-{}".format(BACKUP_TAG_PREFIX,SUBID),x["description"]),snapshot_list)) # Remove old auto-backup-snapshot if len(backup_snapshot_list) >= MAX_NUM_OF_BACKUPS: to_remove_snapshot_list = sorted(backup_snapshot_list, key = lambda k:datetime.datetime.strptime(k["date_created"],"%Y-%m-%d %H:%M:%S") )[:-MAX_NUM_OF_BACKUPS] for s in to_remove_snapshot_list: vultr("POST","snapshot/destroy",{"SNAPSHOTID": s["SNAPSHOTID"]}) # create New auto-backup-snapshot vultr("POST","snapshot/create",{"SUBID": SUBID,"description": "{}-{}-{}".format(BACKUP_TAG_PREFIX,SUBID,day)}) ``` ## 完整食用方法如下 Continue reading
使用 CF-Firewall 与 Nginx 联动限制访问频率以及自动ban IP Nov 13,2018 in Python,VPS read (290) 因为movieinfogen源站的原因,导致我原来公开的PT-Help工具的moveinfo/gen遭到了大量的使用,此外因为原先设计理念上的问题(ps. 不是因为我懒~~(事实就是,前面都是借口)~~),没有对相关请求做相关的身份验证。 导致部分人使用脚本批量请求该接口生成对应简介信息,以至于经常被豆瓣封请求,使得正常用户不可用。 **(滥用公开服务可耻!!!!为什么不自建!!!源代码都是公开的!!!** > [请不要恶意使用PT-Gen【pt吧】_百度贴吧](https://tieba.baidu.com/p/5938778095)  -------- 那么就来做些限制吧。(想不到我一个写爬虫开始学编程的人如今也要开始做反爬了23333 ## 使用Nginx对User-Agent限制 这是最开始想到的办法,因为很早之前我就使用了 [mitchellkrogza/nginx-ultimate-bad-bot-blocker](https://github.com/mitchellkrogza/nginx-ultimate-bad-bot-blocker) 的相关规则以及自动更新模块,来对一些Bad Bot基于UA头进行限制。 关于这一系列规则的安装还请见其项目的README就行,挺简单的,唯一有些不便的就是它所有的配置都是围绕使用包管理器安装的Nginx来进行的,每次使用都需要夹带相关参数来覆盖默认配置。 所以记得用`-h`多看对应说明,没确认前千万不要用`-x`执行。 因为当初的安装记录找不到了,这里就贴一下crontab升级时候使用的吧,希望对同样使用lnmp.org提供的lnmp一件包的朋友有帮助。 ```crontab 00 22 * * * sudo /usr/local/sbin/update-ngxblocker -c /usr/local/nginx/conf/ -b /usr/local/nginx/conf/bot.d/ -n Y ``` **注意:**安装后,可能会通不过Nginx的检验。。记得注释掉没过的就ok。。 该规则自带空白模块,允许用户在`bot.d`目录下的`blacklist-user-agents.conf`中添加基于UA的屏蔽,因为原滥用者都是使用脚本调用curl来进行的,所以直接屏蔽UA头为curl的。故在文件中添加 ```nginx "~*\bcurl\b" 3; ``` 然而好景不长,毕竟UA头都是可以伪造的,简单的一个`IE`就直接破解了2333  ## Nginx自带的 rate_limit 通过对访问频率来限制,相关的教程可见官方的文档: - [NGINX Rate Limiting](https://www.nginx.com/blog/rate-limiting-nginx/) - [Module ngx_http_limit_req_module](http://nginx.org/en/docs/http/ngx_http_limit_req_module.html) 考虑到使用的方便,毕竟这个`/tools`下不止挂了本人的movieinfogen服务,所以简单设置如下: ```nginx limit_req_zone $binary_remote_addr zone=pthelp:10m rate=5r/m; server { server_name api.rhilip.info ; location ~* ^/tool(/.*)?$ { limit_req zone=pthelp burst=3 nodelay; # .... } } ``` 相对较为宽松,仅限制了每分钟5个请求,而且还补充了burst和nodelay项。对超过限制的直接返回503就行~ 这个破起来多简单,我当初自己爬豆瓣的时候都知道`time.sleep()`,滥用者会想不到吗? 503的话就休息一段时间。或者可以都不用这么这样,反正Nginx最后一定会放行的,疯狂请求不就ok了吗?  > ps. 这个IP的同学在我**一天**的Nginx记录中疯狂刷出了近2000条记录,其中返回503的就有近1700条。。。。 ## 使用 CF-Firewall 基于IP自动限制 根据对之前的访问频率的判断,我们可以知道,正常用户一天的请求量不会超过50次。~~(不,是我瞎讲的,不是基于统计的结果)~~ 大体只有爬虫会疯狂的进行请求,所以我们不如根据总访问次数来对爬虫IP进行限制。 但是由于网站在Cloudflare的CDN之后,所以直接在系统层面使用iptables以及配套日志分析脚本对单一IP进行限制是不可能的,如果在Nginx上使用deny则每次修改都要重新reload较为麻烦,所以不如直接使用CF-Firewall进行限制较为方便。 > 与之有关的项目 [SukkaW/cloudflare-block-bad-bot-ruleset](https://github.com/SukkaW/cloudflare-block-bad-bot-ruleset) 手动添加可以直接在网页上使用Firewall面板,并设置规则为Block即可。  通过日志可以看到确实被有效的屏蔽了~~~ 通过试验,以CF-Firewall屏蔽会返回404状态码。  ---------- 但是毕竟我们不可能时时刻刻都查看日志吧,所以需要用脚本自动分析日志并通过CF-API来自动禁用对应IP。 CF的Global API Key在profile面板获取。 示例脚本如下 ```python import re import CloudFlare from collections import Counter # 基本信息 开始 CF_API_KEY = '' CF_API_EMAIL = '' DOMAIN = '' LOG_FILE = '' MAX_RATE = 50 # 基本信息 结束 # 后面无需更改 cf = CloudFlare.CloudFlare(email = CF_API_EMAIL, token = CF_API_KEY) zone_info = cf.zones.get(params = { 'name': DOMAIN }) zone_id = zone_info[0]['id'] # 获取当前被封禁IP列表防止重复插入 ip_ban_list_raw = cf.zones.firewall.access_rules.rules.get(zone_id, params = { "configuration.target": "ip", "mode": "block" }) ip_ban_list = list(map(lambda x: x["configuration"]["value"], ip_ban_list_raw)) # 获取当前访问日志信息 with open(LOG_FILE, 'r') as f: log_now = f.readlines() # 获取访问ip地址 movieinfo_gen_log = list(filter(lambda x: re.search("movieinfo/gen", x), log_now)) ip_log = list(filter(lambda x: x not in ip_ban_list, map(lambda x: re.search("^(.+?) - - \[", x).group(1), movieinfo_gen_log))) # 使用Counter来计数, 并筛选 ip_count = Counter(ip_log).most_common() ban_ip_list = list(filter(lambda x: x[1] > MAX_RATE, ip_count)) # 自动禁用 for ip, count in ban_ip_list: cf.zones.firewall.access_rules.rules.post(zone_id, data = { "mode": "block", "configuration": { "target": "ip", "value": ip }, "notes": "Pt-Gen max-rate reached." }) ``` 然后放到crontab中每隔几分钟运行一次不久ok了吗? ## 后续注意点 1. Nginx使用`set_real_ip_from`模块从Cloudflare中获取用户真实IP地址,并记录到access_log中。相关方法将之前Blog: [Cloudflare 下 Nginx 获取用户真实 IP 地址 - R 酱小窝](https://blog.rhilip.info/archives/256/) 2. **上述示例脚本真的仅作示意**,临时现写的。~~所以为什么宁愿多写一个脚本也不修改原有架构呀~~
爬取备份“忧郁的弟弟”站点Galgame Aug 07,2018 in Python,Game read (1750) [我的Galgame资源发布站 - 忧郁的弟弟](https://www.mygalgame.com/) 是由忧郁的弟弟提供的汉化Galgame下载站点,关于该站点介绍请访问:[关于若干注意事项(新人必读) | 我的Galgame资源发布站](https://www.mygalgame.com/guanyuruoganzhuyishixiangxinrenbidu.html) > 资源备份档分享请见:https://archive.rhilip.info/mygalgame/ > 他人抓取项目请见: [Mygalgame backup](https://beats0.github.io/www.mygalgame.com/) 弟弟站点html结构十分规范,而且爬取特别容易。问题在于该站的资源都是用百度云进行存储,而百度云的转存与下载较为麻烦。这里我们采取抓取和转存分别进行的方法,构造备份站点。步骤如下: 1. 对弟弟站所有页面进行抓取下载并存储。 2. 进行百度云批量转存,使用BaiduPCS-GO进行下载操作。 3. 使用rclone转存到GDrive以及OneDrive。 4. 使用OneIndex进行展示~ > 关于“忧郁的弟弟”站点备份,Github已有类似项目,具体可参见:[Beats0/www.mygalgame.com](https://github.com/Beats0/www.mygalgame.com) ## 站点爬取 目前弟弟站的百度云链接需要使用post的方式二次获取,故在获取到文章链接后,构造post表单进行获取。方法如下: ```python import re import time import sqlite3 import requests from bs4 import BeautifulSoup s = requests.Session() s.cookies.update({"switchtheme":"mygalgame2"}) s.headers.update({"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36"}) db = sqlite3.connect(r"f:/mygalgame.db") cur = db.cursor() cur.execute("CREATE TABLE mygalgame ("+ "raw_link VARCHAR (255),"+ "name VARCHAR (100),"+ "baidu_link VARCHAR (255),"+ "secret_key VARCHAR (4) UNIQUE PRIMARY KEY,"+ "descr TEXT"+ ");" ) for p in range(1,84): page = s.get("https://www.mygalgame.com/page/{}/".format(p)) page = BeautifulSoup(page.text,"lxml") a_list = page.select("div#article-list > div > section > div.title-article > h1 > a") article_list = list(map(lambda x:x["href"], a_list)) print("Craw Page {}, and find {} links".format(p,len(article_list))) for a in article_list: page_1 = s.get(a) if re.search("A\d{3}",page_1.text): secret_key = re.search("A\d{3}",page_1.text).group(0) page_2 = s.post(a,data={"e_secret_key":secret_key}) page_bs = BeautifulSoup(page_2.text,"lxml") name_ = page_bs.select_one("div.title-article a").get_text() baidu_link = re.search("go\.php\?url=(https?://pan.baidu.com/s/[^']+)'",page_2.text).group(1) descr = str(page_bs.select_one("div.centent-article")) descr = descr[:descr.find("")] cur.execute("INSERT INTO `mygalgame` (`raw_link`, `name`, `baidu_link`, `secret_key`, `descr`) VALUES (?,?,?,?,?)",(a,name_,baidu_link,secret_key,descr)) print("{}: {} OK~".format(secret_key,a)) db.commit() time.sleep(5) print("Page {} Over~".format(p)) time.sleep(5) ``` 经过上述爬取,仍发现几项缺失,具体列在下表: | 缺失项 | 可能的名称 | 备注 | | ------ | ----------------------------------------------------------- | ------------------------------------------------------------ | | A345 | ***资源吃饭 | 没找到~ | | A457 | 下级生2 | 与下级生1(A456)同一页面 | | A458 | 恋×シンアイ彼女 体験版 | 正式版见[A584](https://www.mygalgame.com/xiangyaochuandageinideailian.html) | | A516 | オトメ*ドメイン 体験版 | 参见他人备份补齐 | | A574 | [千恋*万花](https://www.mygalgame.com/qianlianwanhua.html) | 手动补齐 | ## 百度云转存下载 使用Selenium半自动登陆并转存。代码如下,前半部分登陆手动完成,后面分享转存交由程序进行。 ```python from selenium import webdriver from selenium.webdriver.common.keys import Keys browser = webdriver.Chrome(r"d:\chromedriver.exe") browser.get("https://pan.baidu.com") # 用户登陆(手动) uk_list = cur.execute("SELECT baidu_link,secret_key from mygalgame order by secret_key asc").fetchall() for url,key in uk_list: try: browser.get(url) # 打开分享链接 browser.implicitly_wait(3) # 等待3秒 browser.find_element_by_tag_name("input").send_keys(key,Keys.ENTER) # 输入分享密码并回车 browser.implicitly_wait(5) browser.find_element_by_xpath('//*[@id="shareqr"]/div[2]/div[2]/div/ul[1]/li[1]/div/span[1]').click() # 全选分享文件 browser.find_element_by_xpath('//*[@id="bd-main"]/div/div[1]/div/div[2]/div/div/div[2]/a[1]/span/span').click() # 点击转存按钮 browser.implicitly_wait(2) browser.find_element_by_xpath('//*[@id="fileTreeDialog"]/div[3]').click() # 勾选上次保存位置 browser.implicitly_wait(2) browser.find_element_by_xpath('//*[@id="fileTreeDialog"]/div[4]/a[2]/span').click() # 点击确认按钮 browser.implicitly_wait(2) print("Transfer {}#{} Success~".format(url,key)) except Exception: print("Transfer {}#{} Fail!!!!".format(url,key)) ```  注意: - 估算每个Gal的体积为2G,故1T百度云盘约能转存500左右游戏。 - 请使用开发者工具block掉一些请求,以防止页面长时间等待加快速度。例如:  而下载使用 [BaiduPCS-GO](https://github.com/iikira/BaiduPCS-Go)。使用前面获取的BUDSS进行登陆,并使用screen挂在后台进行下载即可。(然而使用国外服务器下载还是很慢23333) ## 简介清洗 因为OneIndex能展示`READMD.md`,所以将原有html格式简介清洗为markdown格式的文件,示例格式如下: ```python import subprocess import sqlite3 import re from markdownify import markdownify as md from bs4 import BeautifulSoup process = subprocess.Popen(['rclone','lsd','GDrive:/galgame'], stdout=subprocess.PIPE) output, _ =process.communicate() output = str(output,"utf-8") outlist = output.splitlines() db = sqlite3.connect(r"f:/mygalgame.db") cur = db.cursor() temple = """ # {name} > 来源于: [{source_link}]({source_link}) ## 游戏截图 {img_list} ## 游戏简介 {introduce} ## 汉化STAFF {staff} ## 文件信息 备注: {beizhu} {note} 标签: {tag} """ for i in outlist: oid = re.search("A\d{3}",i).group(0) print("开始处理{}".format(i)) sourcename = re.search("A\d{3}.*?$",i).group(0) fetch_ = cur.execute("SELECT * FROM mygalgame where secret_key = ? ",[oid]) link,name,bdlink,key,descr,_ = fetch_.fetchone() de_bs = BeautifulSoup(descr,"lxml") img_list = de_bs.find_all("img") img_md = "\n".join(map(lambda x:"".format(x["src"]),img_list)) introduce = staff = beizhu = tag = "" main_part = de_bs.find_all("div",class_="alert-success") if len(main_part) == 1: i_tag = main_part[0] introduce = md(str(i_tag)) elif len(main_part) > 1: i_tag,s_tag = main_part[:2] staff = md(str(s_tag)) introduce = md(str(i_tag)) beizhu_search = re.search('备注:(.+?)',descr) if beizhu_search: beizhu = beizhu_search.group(1) tag_list_raw = de_bs.find_all("a",rel="tag") if len(tag_list_raw) > 0: tag_list = map(lambda x:x.get_text(strip=True),tag_list_raw) tag = ", ".join(tag_list) note = md(str(de_bs.find("div",class_="alert-info"))).replace("任何解压相关问题请看网站顶部解压必读,不看而发问直接小黑屋","").strip() md_data = temple.format(name=name,source_link=link,img_list=img_md,introduce=introduce.replace("\n","\n\n"), staff=staff.replace("\n","\n\n"),note=note.replace("\n","\n\n"),beizhu=beizhu,tag=tag) print("生成的Markdown格式如下",md_data) with open(r"F:\Temp\README.md","w",encoding="utf-8") as f: f.write(md_data) print("写入临时文件完成") process = subprocess.Popen(['rclone','copy',r"F:\Temp\README.md",'GDrive:/galgame/{}'.format(sourcename)], stdout=subprocess.PIPE) process.communicate() print("上传 README.md 完成") dstname = "{}@{}".format(key,name) process = subprocess.Popen(['rclone','moveto','GDrive:/galgame/{}'.format(sourcename),'GDrive:/mygalgame/{}'.format(dstname)], stdout=subprocess.PIPE) process.communicate() print("文件夹 重命名+移动 完成") ``` ## OneIndex建站 随意找一个微软5T的盘,绑定OneIndex进行展示,使用rclone将Google Drive上面的文件夹完全转发。
蒲公英(NPUBits)站点 Banner分析 Jul 15,2018 in Python,PT read (719) 无所事事的时候看到这站内的这个帖子,正好最近在~~做~~(学习)数据分析的工作,同时我对主帖子中的一些问题表示关注,所以顺带水一片博文进行分析。  首先对从那些地方能获取到数据要进行分析:用户在论坛发帖提交Banner会留下记录,管理员使用Banner更换系统进行自动更换时会在“普通”日志中留下记录,已经展示过的Banner有集中展示页面。 > 所有数据基于**站内公开数据**,数据最后更新~~(爬取)~~于`2018/07/11 17:00`,未统计早期(2015年1月至11月)Banner信息 > **本文仅限NPUBits内部论坛以及本人博客( https://blog.rhilip.info )发布,禁止转载。** Continue reading
某站5.20开放注册活动结果分析 Jul 11,2018 in Python,PT read (470) 很荣幸在本次活动中参与了某站点的最终审核过程。下面根据整次活动的过程进行梳理。 该活动以“将特定图片上传到微信朋友圈,发完后截图朋友圈,并上传截图”的形式展开,用户通过上传的朋友圈截图信息,就可以获得一定数量以上的奖励。 活动对上传的截图文件有以下要求: - 请不要通过仅自己可见等设限方式上传到朋友圈 - 需要截图朋友圈带有自己发布内容的区域,不可以截图个人相册等其他区域 - 请完整截图,不要裁剪 - 如果同时加入了自己的文字推荐内容(如向大家介绍下本站),将有更高的几率获得更多奖励 同样,也列出了一些无法通过审核的原因: - 图片没有标题栏或者标题没有写“朋友圈”字样,“我的相册”、“详情”均不能通过审核 - 上传编辑也不能通过审核 - 没有正确的图片,不能通过审核 - 请尽量包含朋友圈右侧个人头像区域,无法辨别是否本人所发的不能通过审核 ## 结果一览 - **文件数量** 在本次活动中,总共收集到了 `10434` 张上传照片,总大小 `4.87 GB( 5,239,552,373 字节)`,平均单个文件的大小为 `490.392 KiB`。图片大小最大为`6.57 MB`,最小仅为`1 KB`(当然是无效图片233)。 - **上传格式** 用户上传的截图文件类型主要为jpg以及png格式(居然还有一张是gif格式的Orz)。其占比如下:  - **用户截图/提交时间** 因为后台服务器并不对图片日期标签进行更改,所以通过读取图片文件的修改时间即可获取用户进行截图/上传的时间(这里有个假设就是用户截图后没多久就上传)。 在这些图片中,最早的一张的日期为`2018/5/18 17:42`,最后一张上传图片的日期戳为`2018/5/21 0:18`。通过对图片文件的时间进行统计并作图,可见,在20号15点52前提交人数均较少,而随着开放注册活动的持续进行,从20号19点30分到23点17分,出现用户参与活动的高潮,平均每次计数间隔(1分钟)均有超过30次提交。最高出现在`20:23`,在这一分钟内出现了共54次提交。 (这个日活还是挺恐怖的23333  - **图片分辨率** 如果假设用户未对图片做任何处理的话(实际上确实存在用户上传裁剪的或者其他情况存在),图片的分辨率应该是由手机自身分辨率确定的。通过使用Python的PIL库对文件夹内所有图片的分辨率进行统计,得到以下结果:有将近一半的上传照片的分辨率为`1080 x 1920` (5727张),即其他绝大部分的图片分辨率为`16:9`。很有意思的数据偏移点出现在 `240 x 240` 分辨率上,经过查询对应图片,发现该部分用户将网页提供的活动图片又原模原样的上传上来了23333 (掩耳盗铃是会被发现的23333  通过统计,最常见(数量超过50)的图片分辨率为下表 | imgW | imgH | count | | :-----: | :-----: | :-----: | | 1080 | 1920 | 5727 | | 750 | 1334 | 1587 | | 1080 | 2160 | 573 | | 640 | 1136 | 367 | | 1242 | 2208 | 335 | | 1440 | 2560 | 225 | | 720 | 1280 | 204 | | ~~240~~ | ~~240~~ | ~~105~~ | | 1080 | 2220 | 76 | | 1125 | 2436 | 72 | | 1080 | 2339 | 69 | | 540 | 960 | 59 | | 1080 | 2280 | 57 | - 审查结果 通过图片审查以及分类,将最终图片结果分为以下各类。**(注:本处统计结果不代表最终活动评奖结果。** | 类别 | 说明 | | :------: | ------------------------------------------------------------ | | 合格图片 | 使用手机截图功能对朋友圈进行截图,并可能加入文字推荐或纯图片上传的图片。 | | 违规图片 | 违反活动规则的图片,如:微信非朋友圈(编辑页面、我的相册等)截图、微信朋友圈无法进行身份识别的图片、上传到其他平台的截图、原始图片二次上传、无关图片 |  Continue reading