275 lines
8.5 KiB
PHP
Executable File
275 lines
8.5 KiB
PHP
Executable File
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Services\Cron;
|
|
|
|
use Illuminate\Http\Client\ConnectionException;
|
|
use Illuminate\Http\Client\Pool;
|
|
use Illuminate\Http\Client\RequestException;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Symfony\Component\DomCrawler\Crawler;
|
|
use App\Models\ShadowRocketIds;
|
|
use Illuminate\Support\Carbon;
|
|
use App\Services\TomTool\Telegram\Slave as TeleSlave;
|
|
|
|
final class ShadowRocketCrawl
|
|
{
|
|
private const ARRAY_TARGET_URLS = [
|
|
'https://proxygo.org/freeappleid/',
|
|
'https://proxygo.org/1-freeappleid/',
|
|
];
|
|
|
|
/**
|
|
* 执行多页面并发账号爬取任务并合并去重
|
|
*
|
|
* @return array
|
|
*/
|
|
public static function run()
|
|
{
|
|
echo "/n/n ShadowRocketCrawl::run";
|
|
try {
|
|
$aResponses = Http::pool(fn (Pool $oPool) => array_map(
|
|
fn (string $sUrl) => $oPool->as($sUrl)
|
|
->withHeaders([
|
|
'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
'Accept-Language' => 'zh-CN,zh;q=0.9,en;q=0.8',
|
|
])
|
|
->withOptions([
|
|
'curl' => [
|
|
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
|
|
],
|
|
])
|
|
->connectTimeout(10)
|
|
->timeout(30)
|
|
->retry(3, 1000)
|
|
->get($sUrl),
|
|
self::ARRAY_TARGET_URLS
|
|
));
|
|
} catch (\Throwable $oException) {
|
|
TeleSlave::warn("ShadowRocketCrawl:并发请求执行异常:" . $oException->getMessage());
|
|
return [];
|
|
}
|
|
|
|
$aAllAccountList = [];
|
|
|
|
foreach (self::ARRAY_TARGET_URLS as $sUrl) {
|
|
$oResponse = $aResponses[$sUrl] ?? null;
|
|
|
|
if ($oResponse instanceof \Throwable) {
|
|
TeleSlave::warn("ShadowRocketCrawl:请求网络异常:" . $sUrl . ",错误: " . $oResponse->getMessage());
|
|
continue;
|
|
}
|
|
|
|
if (!$oResponse || $oResponse->failed()) {
|
|
$iStatusCode = $oResponse ? $oResponse->status() : 0;
|
|
TeleSlave::warn("ShadowRocketCrawl:请求页面失败:" . $sUrl . ",状态码: " . $iStatusCode);
|
|
continue;
|
|
}
|
|
|
|
$sPageContent = $oResponse->body();
|
|
if (empty(trim($sPageContent))) {
|
|
TeleSlave::warn("ShadowRocketCrawl:请求页面内容为空:" . $sUrl);
|
|
continue;
|
|
}
|
|
|
|
$aParsedAccounts = self::parseHtmlToAccounts($sPageContent);
|
|
$aAllAccountList = array_merge($aAllAccountList, $aParsedAccounts);
|
|
}
|
|
|
|
$arr = self::deduplicateAccounts($aAllAccountList);
|
|
$json = json_encode($arr);
|
|
|
|
$sTodayDate = date('Y-m-d');
|
|
|
|
$oShadowRocketRecord = ShadowRocketIds::whereDate('created_at', $sTodayDate)->first()
|
|
?? new ShadowRocketIds();
|
|
|
|
if (empty($json)) {
|
|
$json = "[]";
|
|
}
|
|
$oShadowRocketRecord->arr = $json;
|
|
$oShadowRocketRecord->save();
|
|
|
|
echo "\n ok";
|
|
}
|
|
|
|
/**
|
|
* 按邮箱对爬取的账号进行去重
|
|
*
|
|
* @param array $aAccountList
|
|
* @return array
|
|
*/
|
|
private static function deduplicateAccounts(array $aAccountList): array
|
|
{
|
|
$aUniqueAccounts = [];
|
|
foreach ($aAccountList as $aAccount) {
|
|
$sEmail = $aAccount['sEmail'] ?? '';
|
|
if (!empty($sEmail) && !isset($aUniqueAccounts[$sEmail])) {
|
|
$aUniqueAccounts[$sEmail] = $aAccount;
|
|
}
|
|
}
|
|
|
|
return array_values($aUniqueAccounts);
|
|
} // 好像因为这里成为0
|
|
|
|
/**
|
|
* 解析 HTML 并提取账号列表(包含地区与检测时间)
|
|
*
|
|
* @param string $sHtmlContent
|
|
* @return array
|
|
*/
|
|
private static function parseHtmlToAccounts(string $sHtmlContent): array
|
|
{
|
|
$aAccountList = [];
|
|
$oCrawler = new Crawler($sHtmlContent);
|
|
|
|
$oCrawler->filter('.posts-item')->each(function (Crawler $oNode) use (&$aAccountList) {
|
|
$sEmail = self::extractEmailFromNode($oNode);
|
|
$sPassword = self::extractPasswordFromNode($oNode);
|
|
$sRegion = self::extractRegionFromNode($oNode);
|
|
$sCheckTime = self::extractCheckTimeFromNode($oNode);
|
|
|
|
if (!empty($sEmail) && !empty($sPassword)) {
|
|
$aAccountList[] = [
|
|
'sEmail' => $sEmail,
|
|
'sPassword' => $sPassword,
|
|
'sRegion' => $sRegion,
|
|
'sCheckTime' => $sCheckTime,
|
|
];
|
|
}
|
|
});
|
|
|
|
return $aAccountList;
|
|
}
|
|
|
|
/**
|
|
* 从单个卡片节点中精准提取完整 Email
|
|
*
|
|
* @param Crawler $oNode
|
|
* @return string
|
|
*/
|
|
private static function extractEmailFromNode(Crawler $oNode): string
|
|
{
|
|
$oFullLinkNode = $oNode->filter('.item-tags a.external');
|
|
if ($oFullLinkNode->count() > 0) {
|
|
$sHref = $oFullLinkNode->attr('href') ?? '';
|
|
if (str_contains($sHref, '#')) {
|
|
$aParts = explode('#', $sHref);
|
|
$sDecoded = self::decodeCloudflareEmail($aParts[1] ?? '');
|
|
if (self::isValidEmail($sDecoded)) {
|
|
return $sDecoded;
|
|
}
|
|
}
|
|
}
|
|
|
|
$oTagEmailNode = $oNode->filter('.item-tags [data-cfemail]');
|
|
if ($oTagEmailNode->count() > 0) {
|
|
$sHex = $oTagEmailNode->attr('data-cfemail') ?? '';
|
|
$sDecoded = self::decodeCloudflareEmail($sHex);
|
|
if (self::isValidEmail($sDecoded)) {
|
|
return $sDecoded;
|
|
}
|
|
}
|
|
|
|
$oAllCfNodes = $oNode->filter('[data-cfemail]');
|
|
foreach ($oAllCfNodes as $oDomElement) {
|
|
$sHex = $oDomElement->getAttribute('data-cfemail');
|
|
$sDecoded = self::decodeCloudflareEmail($sHex);
|
|
if (self::isValidEmail($sDecoded)) {
|
|
return $sDecoded;
|
|
}
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* 从单个卡片节点中提取密码
|
|
*
|
|
* @param Crawler $oNode
|
|
* @return string
|
|
*/
|
|
private static function extractPasswordFromNode(Crawler $oNode): string
|
|
{
|
|
$oBtnNodes = $oNode->filter('button[copy]');
|
|
foreach ($oBtnNodes as $oDomElement) {
|
|
$sCopyVal = trim($oDomElement->getAttribute('copy'));
|
|
if (!empty($sCopyVal)) {
|
|
return $sCopyVal;
|
|
}
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* 从单个卡片节点中提取账号地区(如:美区、日本、中国大陆)
|
|
*
|
|
* @param Crawler $oNode
|
|
* @return string
|
|
*/
|
|
private static function extractRegionFromNode(Crawler $oNode): string
|
|
{
|
|
$oSpans = $oNode->filter('h5 span');
|
|
if ($oSpans->count() >= 2) {
|
|
$sRawText = trim($oSpans->eq(1)->text(''));
|
|
return trim(str_replace(['【', '】'], '', $sRawText));
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* 从单个卡片节点中提取检测时间
|
|
*
|
|
* @param Crawler $oNode
|
|
* @return string
|
|
*/
|
|
private static function extractCheckTimeFromNode(Crawler $oNode): string
|
|
{
|
|
$oTimeNode = $oNode->filter('.text-muted');
|
|
if ($oTimeNode->count() > 0) {
|
|
$sRawText = trim($oTimeNode->text(''));
|
|
return trim(str_replace('检测时间:', '', $sRawText));
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* 解密 Cloudflare XOR 加密邮箱
|
|
*
|
|
* @param string $sEncodedHex
|
|
* @return string
|
|
*/
|
|
private static function decodeCloudflareEmail(string $sEncodedHex): string
|
|
{
|
|
if (empty($sEncodedHex) || strlen($sEncodedHex) < 4 || !ctype_xdigit($sEncodedHex)) {
|
|
return '';
|
|
}
|
|
|
|
$iKey = hexdec(substr($sEncodedHex, 0, 2));
|
|
$sDecodedEmail = '';
|
|
$iLength = strlen($sEncodedHex);
|
|
|
|
for ($iIndex = 2; $iIndex < $iLength; $iIndex += 2) {
|
|
$sDecodedEmail .= chr(hexdec(substr($sEncodedHex, $iIndex, 2)) ^ $iKey);
|
|
}
|
|
|
|
return trim($sDecodedEmail);
|
|
}
|
|
|
|
/**
|
|
* 校验邮箱合法性
|
|
*
|
|
* @param string $sEmail
|
|
* @return bool
|
|
*/
|
|
private static function isValidEmail(string $sEmail): bool
|
|
{
|
|
return !empty($sEmail) && filter_var($sEmail, FILTER_VALIDATE_EMAIL) !== false;
|
|
}
|
|
}
|