爬取备份“忧郁的弟弟”站点Galgame

我的Galgame资源发布站 - 忧郁的弟弟 是由忧郁的弟弟提供的汉化Galgame下载站点,关于该站点介绍请访问:关于若干注意事项(新人必读) | 我的Galgame资源发布站

资源备份档分享请见:Mygalgame全站资源备份
他人抓取项目请见: Mygalgame backup

弟弟站点html结构十分规范,而且爬取特别容易。问题在于该站的资源都是用百度云进行存储,而百度云的转存与下载较为麻烦。这里我们采取抓取和转存分别进行的方法,构造备份站点。步骤如下:

  1. 对弟弟站所有页面进行抓取下载并存储。
  2. 进行百度云批量转存,使用BaiduPCS-GO进行下载操作。
  3. 使用rclone转存到GDrive以及OneDrive。
  4. 使用OneIndex进行展示~

关于“忧郁的弟弟”站点备份,Github已有类似项目,具体可参见:Beats0/www.mygalgame.com

站点爬取

目前弟弟站的百度云链接需要使用post的方式二次获取,故在获取到文章链接后,构造post表单进行获取。方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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
A516 オトメ*ドメイン 体験版 参见他人备份补齐
A574 千恋*万花 手动补齐

百度云转存下载

使用Selenium半自动登陆并转存。代码如下,前半部分登陆手动完成,后面分享转存交由程序进行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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))

TIM截图20180806131047.jpg

注意:

  • 估算每个Gal的体积为2G,故1T百度云盘约能转存500左右游戏。
  • 请使用开发者工具block掉一些请求,以防止页面长时间等待加快速度。例如:

TIM截图20180806113155.jpg

而下载使用 BaiduPCS-GO。使用前面获取的BUDSS进行登陆,并使用screen挂在后台进行下载即可。(然而使用国外服务器下载还是很慢23333)

简介清洗

因为OneIndex能展示READMD.md,所以将原有html格式简介清洗为markdown格式的文件,示例格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
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('备注:<span style="color: #ff0000;">(.+?)</span>',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上面的文件夹完全转发。

漫谈PT架构(5): 构造一个Simple-Private-Tracker(Scrape部分)

在这节及之后的PT架构书写过程中,我将使用ThinkPHP 5作为MVC框架,rchouinard/bencode 作为Bencode编码库,实现一个演示性质的Private Tracker。在此,我将默认你已经对前面的内容有所理解,并对PHP以及composer有了解运用。

该项目代码见: https://github.com/Rhilip/Simple-Private-Tracker ,仅供学习无法运行~

请注意,本文所列方法,仅表示本人的一种实现。实际只要符合BEP 0003以及BEP 0027的都是可行的实现~
请注意,本处所列代码并不一定是最新的,仅代表思考逻辑过程,具体代码请看repo

相关commit:c2c37e668a3f63722b6d4d736e957c8cda76b2a8

基础准备

首先,我们需要准备好PHP环境(建议为7.x)以及数据库,因为学习,所以缓存暂时使用文件缓存。并使用composer安装 ThinkPHP5以及bencode ,其命令分别如下:

1
2
3
composer create-project topthink/think=5.0.* tp5  --prefer-dist
cd pt
composer require rych/bencode

准备相关数据表,分别用来存储 Torrent(种子信息)、User(用户信息)、Peer(做种人信息)、Snatch(做种完成情况),此处为了方便(偷懒)直接使用NP的建表语句(-> 见 NexusPHP/_db/dbstructure.sql 相关)就行(实际很多字段不需要)。顺带也方面后续兼容~

而文件夹方面,依次添加以下文件:

1
2
3
4
├─application
│ ├─tracker
│ │ ├─controllers
│ │ │ └─Index.php

并在路由(route\route.php)中注册两个控制器

1
2
Route::get('tracker/scrape','tracker/Index/scrape');
Route::get('tracker/announce','tracker/Index/announce');

并在设置中开启你的debug模式以及应用trace,准备postman或其他作为调试工具。

方法准备

我们先要为TrackerController 准备一些公用方法,分别用于构造响应信息(包括正常的以及错误)、禁用浏览器访问。修改app\Http\Controllers\TrackerController.php为以下信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
<?php

namespace app\tracker\controller;

use think\Controller;
use think\Db;
use think\facade\Cache;
use think\Request;
use think\Response;
use think\Validate;

use Rych\Bencode\Bencode;

class Index extends Controller
{

private $errormsg = [
// Error message about requests params
// Error message about Bittorrent Client
// Error message about User Account
// Error message about Torrent
// Error message about Server
];

private $announce_param = []; // Announce Param HERE~

public function announce(Request $request)
{
}

public function scrape(Request $request)
{
}

private function block_browser()
{
$judge = false;
$request = Request();
if (preg_match("/Mozilla|Opera|Links|Lynx/", $request->header("user_agent"))) {
$judge = true;
}
if (!$request->isSSl()) {
if (
(null !== $request->header("Cookie", null)) ||
(null !== $request->header("Accept-Language", null)) ||
(null !== $request->header("Accept-Charset", null))
) {
$judge = true;
}
}
return $judge;
}

private function sendErrorMsg($code = 999, $msg = null)
{
if ($code && !$msg) {
$msg = $this->makeErrorMsg($code);
}

return $this->bencResp([
"failure reason" => $msg,
]);
}

private function makeErrorMsg($code)
{
return "$code - " . $this->errormsg[$code];
}

private function bencResp($obj)
{
$rep_benc = Bencode::encode($obj);
return response($rep_benc)
->header("Content-Type", "text/plain; charset=utf-8")
->header("Pragma", "no-cache");
}
}

我们将在$errormsg中定义错误信息,并在announce以及scrape这两个公开方法中定义具体逻辑。而所有的响应应该使用bencResp构造。

构建Scrape

相比较为复杂的Announce逻辑,我们先来处理较为简单的Scrape逻辑:

  1. 从请求头中获取所有info_hash信息,
  2. 从数据库中匹配出来对应的做种内容,
  3. 构造返回或错误信息。

下面我们开始写Scrape的具体逻辑。首先我们先禁用掉 非GET请求 以及 浏览器及非BT客户端请求。代码如下,但实际上,因为已经设置的路由关系,我们其实已经禁止了非GET请求,这里需要不需要都无所谓了

1
2
3
4
// 1. Block NON-GET requests, (though it should be block in Router)
if (!$request->isGet()) return $this->sendErrorMsg(100);
// 2. Block Browser, Crawler or Cheater
if ($this->block_browser()) return $this->sendErrorMsg(101);

然后我们从请求头中获取info_hash信息,并检查其是否存在(这里附加对各个info_hash的字节数进行检查也行)。当其不存在时,返回错误信息。

注意,根据BEP0048规定 ,info_hash在url中是以info_hash=xxxxx&info_hash=yyyyy的形式存在的,故本人之前的写法是错误的(之前写法见Archive.org的备份,只能匹配info_hash[]=xxxx的情况)

应为:

1
2
preg_match_all('/info_hash=([^&]*)/i', urldecode(Request::server('query_string')), $info_hash_match);
$info_hash = $info_hash_match[1];

针对info_hash未找到的情况进行处理。

1
2
3
4
if (count($info_hash) < 1) {
return $this->sendErrorMsg(null,
str_replace(':attribute', 'info_hash', $this->makeErrorMsg(102)));
}

使用查询构造器生成SQL语句并查询,并对查询结果进行检查;当数据库中未检查到该种子时,返回错误信息。

1
2
3
4
5
6
7
$res = Db::table("torrents")
->field(['info_hash', 'times_completed', 'seeders', 'leechers'])
->where('info_hash', "IN", $info_hash)
->select();
if (count($res) < 1) {
return $this->sendErrorMsg(131);
}

如果没有任何问题,我们需要把原来数据库中查询的结果(如下)进行转换

1
2
3
4
5
6
7
8
array(1) {
[0] => array(4) {
["info_hash"] => string(20) "aaaaaaaaaaaaaaaaaaaaa"
["times_completed"] => int(0)
["seeders"] => int(0)
["leechers"] => int(0)
}
}

方法如下,并最后使用bencResp($obj)的方式进行编码并发送

1
2
3
4
5
6
7
8
9
10
11
12
$rep = [
'files' => array_map(function ($item) {
return array (
$item["info_hash"] => [
"complete" => $item["seeders"],
"downloaded" => $item["times_completed"],
"incomplete" => $item["leechers"],
]
);
},$res)
];
return $this->bencResp($rep);

scrape_resp.jpg

漫谈PT构架(4):框架结构及相关分析

在实质性的进入Tracker内部逻辑之前,让我先水一篇分析性的文章~

框架结构

  • NexusPHP

像NP这种较老的框架并没有一个很明显的架构,各项信息杂糅在一起。但从导航栏相关中,大体可以分为以下几个子模块。

以下分类方式仅代表本人意见。

nexusphp_framework.jpg

当然,目前各站在这个基础上均增加了一些新的系统,比如(万恶的)勋章系统、发布预告系统、转种系统(含自引用与他站引用)、任务系统、考核系统;对原有系统也有些许扩展,例如Bangumi榜单、AniDB榜单等的添加。

  • meanTorrent

meanTorrent的可以在其modules下明确的了解其架构。撰稿时,架构如下:

meantorrent_framwork.jpg

Tracker请求生命周期

从整个请求流程来看,用户的Bittorrent客户端使用HTTP GET的形式向Tracker服务器发起请求,Tracker服务器根据内部逻辑向数据库及缓存请求请求相关信息,并最终回复给用户。具体流程可见下方简图:

lifetime.jpg

当然上面的肯定过于简单了,没有对Tracker的内部逻辑进行分析,而就Tracker的内部逻辑来看,以NP这个面向流程的框架为例子,简图如下:

tracker_life.jpg

而从整个Tracker网络(见下方图片示意)来看,用户在请求后,Tracker并不参与文件交换,同样,也不能真实的获取用户实际交换信息情况。(于是,我又可以就这个方向水一篇SP了)

main-qimg-ff65100c115e14ddf6c606b3799e0ae7.png

图片来自 Why is BitTorrent not downloading? - Quora

Tracker分析

在这一手记前,首先先列一下几个相关文件,以方便快速查阅:


首先从最简单的信息查询来看,BT客户端会在还未正式向 /announce 发送请求之前,会向 /scrape 先发送请求(一般在种子 添加或者校验的时候),请求头仅添加种子的info_hash信息,示例如下:

1
2
3
4
5
GET http://nexusphp.localhost/scrape.php?passkey=110099109e337b7e335aa368200d7907c&info_hash=%e4%40%ad%c6%27%db%02%40%f7%c8%ed%9e%7b%aab%a3%e1gFW HTTP/1.1
Host: nexusphp.localhost
User-Agent: uTorrent/2210(25302)
Accept-Encoding: gzip
Connection: Close

关于scrape的相关定义及返回信息在之前列出来的BEP 0048中,根据定义,这次抓取(scrape) 会向用户返回一个字典描述对应种子的完成、未完成以及下载中的情况。本处直接引述定义:

The response to a successful request is a bencoded dictionary containing one key-value pair: the key files with the value being a dictionary of the 20-byte string representation of an infohash paired with a dictionary of swarm metadata.

根据NP的实现(见 NexusPHP/scrape.php ),会从param字段中搜索并匹配出来info_hash信息,如果param中没有能匹配出info_hash,则会直接返回站内所有种子的做种信息。(如下图)

TIM截图20180730165317.jpg

以上问题修复patch见 :lock: Not allow empty info_hash when request scrape page by Rhilip · Pull Request #11 · zcqian/tjupt,截止目前,多数站点未对该问题进行修复。

而如果存在info_hash的话,则会对每一个提供的hash值进行数据库查找,如果查询结果不存在,则返回Torrent not registered with this tracker.的错误,而如果存在,则以一个benc编码的字典返回结果。其json格式如下:

1
2
3
4
5
6
{
"files": {
"xxxxxxxxxxxxxxxxxxxx": {"complete": 11, "downloaded": 13772, "incomplete": 19},
"yyyyyyyyyyyyyyyyyyyy": {"complete": 21, "downloaded": 206, "incomplete": 20}
}
}

此外,一个正常的客户端(例如Transmission 2.94)会定期向Tracker发送(announce)以下信息,并期望从tracker中获取回复以获取其他peer信息(这里仅指Private Tracker,其他的还有DHT网络、用户交换等方式)。

1
2
3
4
5
GET http://nexusphp.localhost/announce.php?passkey=db534098baaaa68bd725aaaae3051518&info_hash=aaaaaaaaaaaaaaaaaaaa&peer_id=-TR2940-lhqkh1jtwjqp&port=21736&uploaded=0&downloaded=0&left=739573325&corrupt=0&key=DDDD2A2B&event=started&numwant=200&compact=1&no_peer_id=1 HTTP/1.1
Host: nexusphp.localhost
User-Agent: Transmission/2.94
Accept-Encoding: gzip
Connection: Close

而tracker则会根据GET字段中的compact以及no_peer_id两个字段分别返回不同构造的结果,并设置相应头为

1
2
3
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Pragma: no-cache

compact=0&no_peer_id=0时,其返回的json格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"interval": 50,
"min interval": 20,
"complete": 6871,
"incomplete": 3,
"peer": [
{
"ip": "xxx.xxxx.xxx.xxx",
"peer_id": "-TR2940-lhqkh1j31jqp",
"port": 5698,
},
....
]
}

其中, peer_id 视用户Requests字段这一项可以不要,而如果用户申请使用compact的形式返回,那么peer字段则会返回一个string而不是dict。

如果发生错误,则会返回一个错误字段(string),JSON格式内容如下:

1
2
3
{
"failure reason": "Error msg",
}

Bencode编码

在上面的例子中,我都是以json格式的形式来展示tracker返回的Response信息,但实际上,返回的信息是以Bencode的形式进行编码的。关于Bencode的介绍,可以访问Bencode - Wikipedia。这里我们只需要知道一下编码规则就行:

data types raw encoded
int -42 i-42e
string ‘spam’ 4:spam
list [‘XYZ’, 4321] l3:XYZi4321ee
dict {‘XYZ’: 4321}’ d3:XYZi4321ee

这些甚至不需要你自己一个一个拼合,现在有超多的库帮你进行拼合,你只需要构造好相应的字典,直接调用即可,例如:

Online 5o(Start-2-XS-SATA)测试

走过路过不要错过,Online的5o传家宝开售了

首先需要明确滴是,这款机子馁并不是特价机,初始10刀安装费。(且Online前三个月不能使用Paypal付款,需要外币信用卡)

1.购买

网址:https://console.online.net/en/order/server

TIM截图20180726095630.jpg

目前多数状态下没货~

至于Online的注册、付款以及开机不是本文重点,请另行搜索~

2.测试

  • 整体测试

    仅从整体看,IO受限较大。据群里其他人开机测试结果来看,基本也是40-60左右,仅有少部分能开到鸡血100。

bench.jpg

  • 国内测速

speed_china.jpg

  • Speedtest测速
1
root@sd-91960:~# wget down.46.tn/tool/speedtest.py && python speedtest.py --share

7500672394.png

  • Smarttool硬盘信息

盘是HGST的2.5机械-> HGST官网信息 <-,那么IO在60还是能理解的。开机时间1.3W小时,也是中流水平。。

smartctl.jpg

3.评价

如果说sys的5o arm机受限于CPU太弱了,那么online的就是受限于硬盘io,在较高强度的PT环境下直接能把io给卡死了。io限速大法好

此外硬盘空间较小,刷起来较为吃力,需要经常性更换种子,清理空间。

当然5o这种价格也就不要想着什么都齐全,性价比还行。推荐对网速不是要求很高的购买吧~

毕竟,有钱dalao们都上Andy了

漫谈PT构架(3):meanTorrent简介及搭建

在简单的介绍完NP的历史以及搭建,让我们先跳出NP框架,了解下其他的一些前面讲过的框架及其搭建过程。

meanTorrent是目前来看,国人新框架中功能最为齐全、开发程度较高的一个。(其他我了解的都基本在开发过程中滴说)

从官方的介绍来看:

meanTorrent - MEAN.JS BitTorrent Private Tracker - Full-Stack JavaScript Using MongoDB, Express, AngularJS, and Node.js, A BitTorrent Private Tracker CMS with Multilingual, and IRC announce support, CloudFlare support.

相比已经停止开发维护的NP框架,一个有正经团队维护的项目从好的看,出现问题能统一修复解决,但从另一方面,有点剥离站点个性化的需求——从目前项目列出来的几个站点来看,站点雷同程度有过之而无不及反正国人擅长建站,小站死了又是一批,能活成大站就是可以收割的时候2333)

同样考虑到更新频繁这一点,本处的搭建手记也仅做示例,具体还是请参见项目主页README的介绍。

2018.08.27 请注意meanTorrent自 v1.8 停止开源维护。且该版本存在严重的信息暴露漏洞,不建议使用该框架建站
2019.07.24 MINE站已经长时间522,而本月的18号另外一个用meanTorrent建站的中型站点PTDream+也宣布重新回到NPHP框架,实际也意味着meanTorrent框架的失败了。

依赖分析

meanTorrent其所用的依赖十分基础:Git(版本管理)、Node.js(运行程序)、MongoDB(数据库) 、Bower (浏览器包管理器)。

2018-07-25_152451.jpg

开机及环境准备

按照本人惯例,开一台 Vultr (←这里有个很生硬的afflink) 的$5虚拟机进行搭建测试,系统为Ubuntu 16.04,地点随意。

同样,对基础依赖不大,所以全部程序直接包管理安装即可,喜欢折腾的可以通过源代码编译安装,但这里只是测试(踩坑),所以一切就简。

1
2
3
4
5
6
sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 9DA31620334BD75D9DCB49F368818C72E52529D4
echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu xenial/mongodb-org/4.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-4.0.list
curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash -
sudo apt-get update && sudo apt-get upgrade
sudo apt-get install -y git nodejs mongodb-org
sudo service mongod start

然后安装npm的包管理器bower (话说现在不应该都用Webpack或者Yarn了嘛?)

1
npm install -g bower

安装meanTorrent

建议使用非root账户进行安装!请使用adduser以及visudo创建新账户并赋予新账号超级用户权限。

首先从Github上clone一份源代码来

1
git clone https://github.com/taobataoma/meanTorrent.git

然后使用npm安装依赖(过程中一堆WARNING都可以不用管233)

1
2
cd meanTorrent
npm install

但似乎还是有问题,报错如下。在安装node-gyp、iconv以及sharp的时候报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
rhilip@vultr:~/meanTorrent$ npm install

> [email protected] install /root/meanTorrent/node_modules/iconv
> node-gyp rebuild

Traceback (most recent call last):
File "/usr/lib/node_modules/npm/node_modules/node-gyp/gyp/gyp_main.py", line 13, in <module>
import gyp
File "/usr/lib/node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/__init__.py", line 8, in <module>
import gyp.input
File "/usr/lib/node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/input.py", line 5, in <module>
from compiler.ast import Const
ImportError: No module named compiler.ast
gyp ERR! configure error
gyp ERR! stack Error: `gyp` failed with exit code: 1
gyp ERR! stack at ChildProcess.onCpExit (/usr/lib/node_modules/npm/node_modules/node-gyp/lib/configure.js:336:16)
gyp ERR! stack at emitTwo (events.js:126:13)
gyp ERR! stack at ChildProcess.emit (events.js:214:7)
gyp ERR! stack at Process.ChildProcess._handle.onexit (internal/child_process.js:198:12)
gyp ERR! System Linux 4.4.0-127-generic
gyp ERR! command "/usr/bin/node" "/usr/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js" "rebuild"
gyp ERR! cwd /root/meanTorrent/node_modules/iconv
gyp ERR! node -v v8.11.3
gyp ERR! node-gyp -v v3.6.2
gyp ERR! not ok

很明显的,还是出现了依赖缺失的情况,根据项目自身的介绍、 node-gyp 以及 lovell/sharp#1087 的相关提醒,安装libicu-dev、GCC编译库(主要是c++以及make这两个会被node-gyp调用)以及python-dev。然后重新安装依赖。

1
2
sudo apt-get install libicu-dev build-essential python-dev python-pip
npm install

使用 npm start 运行。过程中如果出现以下报错,说明bower以及相关浏览器端依赖没有正确安装(特别是在root账户下,这里直接使用新用户解决)。按照作者的建议是使用bower install --allow-root && bower prune --allow-root手动装一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
rhilip@vultr:~/meanTorrent$ npm start

> [email protected] start /root/meanTorrent
> gulp

[08:54:34] Using gulpfile ~/meanTorrent/gulpfile.js
[08:54:34] Starting 'default'...
[08:54:34] Starting 'env:dev'...
[08:54:34] Finished 'env:dev' after 208 μs
[08:54:34] Starting 'copyLocalEnvConfig'...
[08:54:34] Starting 'makeUploadsDir'...
[08:54:34] Finished 'makeUploadsDir' after 835 μs
[08:54:34] Finished 'copyLocalEnvConfig' after 14 ms
[08:54:34] Starting 'lint'...
[08:54:34] Starting 'less'...
Potentially unhandled rejection [2] '../../../../public/lib/bootstrap/less/mixins/text-emphasis.less' wasn't found. Tried - /root/meanTorrent/public/lib/bootstrap/less/mixins/text-emphasis.less,../../../../public/lib/bootstrap/less/mixins/text-emphasis.less in file /root/meanTorrent/modules/core/client/less/mt-var.less line no. 4

如果一切没有问题,那么在运行npm start后会提示一下信息:

TIM截图20180725190508.jpg

Nginx反代以及开机自启

通过以上环节,我们已经把所有的软件都基本配置齐全了,并启用了meanTorrent的开发环境。但是默认监听在localhost而不是0.0.0.0上,同样这个程序会因为ssh的断开而终止。所以我们需要安装Nginx以配置反代,同时使用systemctl或者其他守护工具(如作者使用多个bashscript脚本以及forever进行管理,如果你要使用forever的话,请先全局安装npm i forever -g)使其能开机运行并崩溃重启。

安装Nginx也直接用包管理器吧,lnmp1.5也提供单Nginx安装方式,反正怎么方便怎么来就ok~

1
2
sudo apt-get install -y nginx-extras
vi /etc/nginx/sites-available/default

然后将默认的信息全部替换为以下常见反代配置(无SSL)

如果是在CloudFlare后,建议参照本人之前教程进行设置。(Cloudflare 下 Nginx 获取用户真实 IP 地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
server {
listen 80 default_server;
listen [::]:80 default_server;

server_name _;

location / {
proxy_pass http://localhost:3000/;
proxy_redirect default;
proxy_set_header Accept-Encoding "";
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

location ~ /\.ht {
deny all;
}
}

然后重新载入Nginx配置信息并重启npm,你就可以直接使用 ip 或者域名 来进行访问了。

1
2
sudo systemctl reload nginx
npm start

TIM截图20180725201334.jpg

先不进行注册,退出我们来先写个service文件来使用Systemd进行守护管理。sudo vi /etc/systemd/system/meantorrent.service。并填入以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[Unit]
Description=meanTorrent
Documentation=https://github.com/taobataoma/meanTorrent/wiki
After=network-online.target

[Service]
Type=forking
User=rhilip
Group=rhilip
UMask=007
Environment=NODE_ENV=production
WorkingDirectory=/home/rhilip/meanTorrent
ExecStart=/usr/bin/node server.js
Restart=always
RestartSec=10
StandardOutput=syslog
StandardError=syslog
SyslogIdentifier=meanTorrent

[Install]
WantedBy=multi-user.target

注意修改User、Group、WorkinDirectory为你自己的信息。

然后使用以下命令重载systemd配置以及实现开机启动等管理

1
2
3
4
5
6
sudo systemctl daemon-reload # 重载systemd配置
sudo systemctl start meantorrent # 启动
sudo systemctl stop meantorrent # 停止
sudo systemctl restart meantorrent # 重启
sudo systemctl enable meantorrent # 开机自动启动 (只需执行一次)
sudo systemctl disable meantorrent # 取消开机启动

系统配置

个人建议这部分参考作者介绍 Getting Started With meanTorrent,对config/env/torrent.js 下文件进行配置修改。关于站点个性化设置,本处不再累述~

配置好邮件系统和Tracker Announce部分后,重启meanTorrent,然后注册账号即可。如果你只是和我一样测试,不对邮件系统进行配置,默认情况下注册后因为无法发送邮件导致账号状态为inactive 无法使用,需要进入mongo修改账户信息(其实我是默认config下进行账号注册的2333)。方法如下:

1
2
3
4
5
rhilip@vultr:~/meanTorrent$ mongo
> use mean-v2
switched to db mean-v2
> db.users.update({'username':'admin1234567'},{$set:{'status':'normal'}})
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })

那么,就完事了~

success.jpg

漫谈PT构架(2):搭建NexusPHP

为了去了解站点构架,一个简单的搭建过程也是需要了解的。

NP搭建的最重要注意点是,使用的PHP版本最高不应该超过5.6,并安装memcache软件及PHP扩展。对数据库版本要求不是很严格,我使用MySQL 5.7测试可行。

本人写的十分简略(因为真心没有什么好讲的),如果有必要,还请参照他人的详细搭建过程。

Linux下搭建

LNMP环境及Memcache

网上的教程真心啰嗦,lnmp一个一个的写过去,我个人还是喜欢用一键包来配置。lnmp1.5的自动值守命令为

1
2
screen -S lnmp
wget http://soft.vpser.net/lnmp/lnmp1.5.tar.gz -cO lnmp1.5.tar.gz && tar zxf lnmp1.5.tar.gz && cd lnmp1.5 && LNMP_Auto="y" DBSelect="2" DB_Root_Password="lnmp.org" InstallInnodb="y" PHPSelect="4" SelectMalloc="1" ./install.sh lnmp

复制粘贴,然后接杯奶茶等编译完成,我们基础的lnmp环境就搭建好了。(注意:这样安装后主PHP版本为5.5,如果希望主版本用7.x的请自己使用 LNMP一键安装包无人值守命令生成器 生成值守命令或者交互式安装,然后./install.sh mphp 添加多PHP支持)

然后安装memcache,在lnmp1.5文件夹中进入lnmp解压后的目录,执行:./addons.sh install memcached。选择php-memcache即会安装软件及PHP扩展。

NP源码及数据库

NP源码个人建议从SourceForge中获取,而不是从Github仓库。(之前Blog也说过,Github上的建表语句有问题)故,依次进行:

  1. NexusPHP - Browse Files at SourceForge.net 下载最新的zip包并解压到对应网站根目录即可。

  2. 使用phpmyadmin或者其他CLI软件恢复/_db/dbstructure.sql 文件记录。

  3. 修改config/allconfig.php 文件的以下内容使其对应:

    1
    2
    3
    4
    5
    6
    7
    ‘SITENAME’ => ‘站点名称’ 
    ‘baseURL’ => ‘网站URL’
    ‘announce_url’ => ‘localhost/announce.php’(announce的url地址)
    ‘mysql_host’ => ‘MySQL主机’
    ‘mysql_user’ => ‘数据库用户名’
    ‘mysql_pass’ => ‘数据库密码’
    ‘mysql_db’ => ‘数据库名’
  4. 设置目录权限777,因为NP的站点配置是通过操作config目录下文件的修改完成的。

    1
    2
    sudo chmod 777 /dir/to/your/nexusphp
    sudo chmod 777 /dir/to/your/nexusphp/config
  5. 自己访问网站然后注册一个用户名,接着进入数据库管理(phpMyAdmin),在users表里面找到你注册的用户,编辑它的class属性为16

Windows下搭建

Windows下搭建我个人推荐使用Laragon作为基础环境,因为相比其他WNMP、WAMP、XAMPP,环境管理更加方便,内置软件更为齐全。例如我选择的就包含了几乎全套我想要使用的工具2333

官网下载地址:https://laragon.org/download/

laragon.jpg

然而需要注意的是,默认Laragon提供的是PHP 7.x,我们需要额外到PHP官网上下载PHP 5.5版本的Portable以及Memcache扩展。下载位置分别如下:

完成基础环境的搭建后,Win下关于NP自身的文件以及数据库均与Linux下类似,在此不累述。

搭建中一些可能的问题

  • 直接提示HTTP ERROR 500 :多数情况下是使用了PHP 7.x或者其他高于5.6的版本,建议使用PHP 5.3-5.5之间的版本进行搭建。
  • Warning: Memcache::connect() [memcache.connect]: Can’t connect to localhost: 由于连接方在一段时间后没有正确答复或连接的主机没有反应,连接尝试失败。 : 修改classes\class_cache.php中的localhost为127.0.0.1
  • 点击登陆后提示Error: Errno:0 SQL:; : 使用Github上源码(这个源码真的是只能远观不能亵玩233)搭建,换用SourceForge的源。
  • 其他待补充~

漫谈PT构架(1):NexusPHP简介

(起一个大坑,看看什么是否能填完。目前没想好写关于PT构架的哪些东西,但先开个小头吧~

NexusPHP简介

根据其代码中的介绍(aboutnexus.php 页面)来看:

NexusPHP由来自浙江大学的Nexus团队发起并开发完成。主要是提供一个完整的、有序的、重视用户信誉和知识的资源分享社区的解决方案,是一个PT(Private Tracker,即私有的Tracker服务器,是BT下载的一种)开源软件。

aboutnexus.jpg

(当然也有人和我说,这是某浙大生毕业设计作品Orz

虽然一遍一遍的在群里鄙视着NexusPHP的垃圾代码,但不得不承认就目前来看国内PT圈依然在大量使用NP(这里应该不用做详细数据统计了吧2333)。

而其他一些尝试,诸如:

  • Gazelle:国外著名PT架构,然而部署安装略麻烦,且纯英文界面。国内曾有人尝试翻译并建站,然而该主比我还不靠谱,以致于在17年度被人提及是开站狂魔2333。此外就目前看TVPlay这个基于Gazelle的国内剧集站点现状比一滩死水还不如。
  • YoRHa:根本没有人知道的国内构架,主要原因是因为基于ThinkPHP的构架并没有完成23333,作者曾在PT吧发过主贴 【20171009】开源一个国产PT项目…【pt吧】_百度贴吧 ,而且目前demo还存活(并开放debug模式),但已经有一段时间没有进行更新了Orz。
  • meanTorrent:同样是一个国内开源框架,基于Node.js,同样部署安装麻烦(后面会具体讲~)。目前主站被人戏称(实际是官方简称)地雷站 ,但就我目前感觉,迈的步子有些大——一方面,过于绚丽的前端对于PT是否需要值得思榷;另一方面,主站目前只接受1080P的一些规定是否在站点初期有利于发展。此外,该站积分策略、上传方式中的一些不同与NP的新形势新方法,对于已经在NP惯性中的使用者来说是不是能很快适应。这些都要经过时间的考验,毕竟今年6月(2018年6月)也才正式上线。
  • TBsource:基于PHP,是NP的前身,应该也就只有CFFBits以及TTG这种远古站点在使用了吧~
  • XBNBT: 一个基于C++的构架,国内就只有紫荆PT在使用。
  • 其他一些在Discuz基础上修改的tracker:这块多为自己写的,而且未能找到公开repo,在此仅作站点列举:六维空间、北交知行、天雪PT。

所以,面对众多的 NP改 站点来说,其实这个古老的(甚至连官网都挂了的)架构依然在支撑着国内PT网站的运行。

缺陷

但是从现代的角度来看NP,我们其实能发现很多问题。

有些属于已经被发现漏洞,多为跨站请求类以及SQL注入类。截止目前(2018/7/19),在 国家信息安全漏洞库 中以 NexusPHP 为关键词搜索可以找出近26条记录 。

CNNVD.jpg

而更多被诟病的其实是其代码架构过于老旧也不宜于维护。下面列几点我印象比较深刻的:

  • 使用这个mysql已经被抛弃的库(PHP 5.5.0起废弃,PHP 7.0.0起移除),而不是mysqli库或PDO库。而PHP 7至少从评测上看,执行效率比PHP 5优秀很多~
  • 代码使用print等方式输出网页。项目受限于时代背景,没有使用MVC等先进思想。
  • 数据库设置不合理,大量查询以及写入语句落在 users 以及 torrents 等表上,造成数据表过热,故当站点人数过多或者种子数过多时,对站点所在服务器配置要求极其高(譬如NPU就因搜索问题换用外挂搜索引擎);而另一方面,诸如faq、rules这些应该可以直接静态输出的却使用数据库进行存储(估计是为了多语言支持)
  • torrentsrss.php页面对passkey是否存在没有进行检查,导致非站内用户能通过构造相关链接直接读取站内所有种子信息。(部分站点已经修复这个漏洞)

目前分支情况

鉴于NP在很早之前就已经停止维护(Github上停留在7年前,即2011年),目前很多使用NP作为基础架构的站点都对其进行了一定的改良。我仅从用户体验(比如前端页面的更改)将站点分成以下几类:

  • 基本没怎么动: 不得不说,很多站点对原始NP模板并没有进行修改(这是句瞎话,比如BYR看起来和原版似乎一模一样,但是后端的修改还是挺多的;又如二娘,修改了种子的BUFF系统。)。但其实不管该站Sysop到底出力没出力,都可以将对应站点视为一个模板。
  • 葡萄:该前端模板将个人信息做成置顶栏,而不是和基本模板一样放在导航栏之下。种子搜索增加“随意看看”。

putao_group.jpg

  • 蝴蝶:以蝴蝶和珞樱为代表,对搜索词的词长存在限制,拥有用户自定义Javascript以及自定义css。
  • 蚂蚁:虽然蚂蚁因为一些原因已经停止运营,但是模板并没有消失2333。目前仍有很多新站使用蚂蚁的模板进行建站,如高清街以及美盒子(都是小站)。我个人不是很喜欢蚂蚁的模板,它将一个种子的所有信息使用两个tr加一堆rowspan进行合并展示,在我看来其实增加后期维护的困难程度,尤其是在没有模板引擎的NP构架之下。

mayi_tamp.jpg

截图为他人提供的美盒子截图。

  • 蒲公英:蒲公英是魔改NP最为成功的一个站点。在它之前或者之后都有人尝试将Bootstrap与NexusPHP进行结合,虽不能说他们都失败了,但至少NPU成为了一个典型喵喵喵。。其他一些更改,比如rp系统、自动转种系统等都是该站点特点。
  • HDA:良心站点HDArea添加了种子预告系统,后被集市使用。(毕竟这两个站点早期开发是同一个人)

嘛,我影响稍微深刻的就这些站点,以及他们使用的模板。其他还有一些站点,此处就不一一列举了。

而从公开仓库来看,目前站点公开并有在更新代码的有以下:

也确实是寥寥无几~

蒲公英(NPUBits)站点 Banner分析

无所事事的时候看到这站内的这个帖子,正好最近在(学习)数据分析的工作,同时我对主帖子中的一些问题表示关注,所以顺带水一片博文进行分析。

TIM截图20180711200228.jpg

首先对从那些地方能获取到数据要进行分析:用户在论坛发帖提交Banner会留下记录,管理员使用Banner更换系统进行自动更换时会在“普通”日志中留下记录,已经展示过的Banner有集中展示页面。

所有数据基于站内公开数据,数据最后更新(爬取)2018/07/11 17:00,未统计早期(2015年1月至11月)Banner信息

本文仅限NPUBits内部论坛以及本人博客( https://blog.rhilip.info )发布,禁止转载。

某站5.20开放注册活动结果分析

很荣幸在本次活动中参与了某站点的最终审核过程。下面根据整次活动的过程进行梳理。

该活动以“将特定图片上传到微信朋友圈,发完后截图朋友圈,并上传截图”的形式展开,用户通过上传的朋友圈截图信息,就可以获得一定数量以上的奖励。

活动对上传的截图文件有以下要求:

  • 请不要通过仅自己可见等设限方式上传到朋友圈
  • 需要截图朋友圈带有自己发布内容的区域,不可以截图个人相册等其他区域
  • 请完整截图,不要裁剪
  • 如果同时加入了自己的文字推荐内容(如向大家介绍下本站),将有更高的几率获得更多奖励

同样,也列出了一些无法通过审核的原因:

  • 图片没有标题栏或者标题没有写“朋友圈”字样,“我的相册”、“详情”均不能通过审核
  • 上传编辑也不能通过审核
  • 没有正确的图片,不能通过审核
  • 请尽量包含朋友圈右侧个人头像区域,无法辨别是否本人所发的不能通过审核

结果一览

  • 文件数量

在本次活动中,总共收集到了 10434 张上传照片,总大小 4.87 GB( 5,239,552,373 字节),平均单个文件的大小为 490.392 KiB。图片大小最大为6.57 MB,最小仅为1 KB(当然是无效图片233)。

  • 上传格式

用户上传的截图文件类型主要为jpg以及png格式(居然还有一张是gif格式的Orz)。其占比如下:

count_type.png

  • 用户截图/提交时间

因为后台服务器并不对图片日期标签进行更改,所以通过读取图片文件的修改时间即可获取用户进行截图/上传的时间(这里有个假设就是用户截图后没多久就上传)。

在这些图片中,最早的一张的日期为2018/5/18 17:42,最后一张上传图片的日期戳为2018/5/21 0:18。通过对图片文件的时间进行统计并作图,可见,在20号15点52前提交人数均较少,而随着开放注册活动的持续进行,从20号19点30分到23点17分,出现用户参与活动的高潮,平均每次计数间隔(1分钟)均有超过30次提交。最高出现在20:23,在这一分钟内出现了共54次提交。

(这个日活还是挺恐怖的23333

upload_time.png

  • 图片分辨率

    如果假设用户未对图片做任何处理的话(实际上确实存在用户上传裁剪的或者其他情况存在),图片的分辨率应该是由手机自身分辨率确定的。通过使用Python的PIL库对文件夹内所有图片的分辨率进行统计,得到以下结果:有将近一半的上传照片的分辨率为1080 x 1920 (5727张),即其他绝大部分的图片分辨率为16:9。很有意思的数据偏移点出现在 240 x 240 分辨率上,经过查询对应图片,发现该部分用户将网页提供的活动图片又原模原样的上传上来了23333 (掩耳盗铃是会被发现的23333

imgW_H.png

通过统计,最常见(数量超过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
  • 审查结果

通过图片审查以及分类,将最终图片结果分为以下各类。(注:本处统计结果不代表最终活动评奖结果。

类别 说明
合格图片 使用手机截图功能对朋友圈进行截图,并可能加入文字推荐或纯图片上传的图片。
违规图片 违反活动规则的图片,如:微信非朋友圈(编辑页面、我的相册等)截图、微信朋友圈无法进行身份识别的图片、上传到其他平台的截图、原始图片二次上传、无关图片

score.png

发种姬修复之 &quot;请填写必填项目上传失败!&quot;

这个其实是一个一致很困扰的问题,虽然以前大体可以定位是因为python的requests库原因,导致file的filename属性在上传的时候不能正确的被编码,故服务器接收失败。但是之前这个并不是很影响发种姬发布(实际被影响的种子只有几个)。故一直没有去解决。

但最近在配置新的发种姬的时候,却发现所有种子、所有站点都出现了同样的错误。那么就说明了这个问题需要解决了23333

error_log.jpg

额,如果你看不下去中间过程,请直接翻阅到最底下就行233333