Qt Apache日志分析,httpLogsView是Apache日志分析的一款专业工具,但很多统计功能收费,且IP归属是英文显示。Qt Apache日志分析仿照httpLogsView实现一款具有类似功能的工具软件,方便站点管理。
Download - http Logs Viewer
https://www.apacheviewer.com/download/
Apache日志可文本查看,分为7个部分:
(1)第一项信息是远程主机的地址,即它表明访问网站的究竟是谁。(可以要求apache查出所有的主机名字,并在日志文件中用主机名字来替代IP地址,但这种做法会极大的影响服务器记录日志的速度,从而降低整个网站的效率,不值得推荐)。
然而,如果确实有必要让apache找出远程主机的名字,可以使用如下指令:
HostNameLookups on
如果HostNameLookups设置成double而不是on,日志记录程序将对它找到的主机名字进行反向查找,验证该主机名字确实指向了原来出现的IP地址。
(2)日志记录的第二项是空白,用一个“-”占位符替代。实际上绝大多数时候这一项都是如此。这个位置用于记录浏览者的标识,这不只是浏览者的登录名字,而是浏览者的email地址或者其他唯一标识符。这个信息由identd返回,或者直接由浏览器返回。(为了避免用户的邮箱被垃圾邮件骚扰,第二项就用“-”取代了)。
(3)日记记录的第三项也是空白。这个位置用于记录浏览者进行身份验证时提供的名字。当然,如果网站的某些内容要求用户进行身份验证,那么这项信息室不会空白的。但是,对于大多数网站来说,日志文件的大多数记录中这一项仍旧是空白的。
(4)日志记录的第四项是请求的时间。这个信息用方括号包围,而且采用“公用日志格式”或者“标准英文格式”。因此,时间信息最后的“-0400”表示服务器所处时区位于UTC之前的4小时。
(5)日志记录的第五项信息或许是整个日志记录中最有用的信息,它告诉我们服务器受到的是一个什么样的请求。该项信息的典型格式是“METHOD RESOURCE PROTOCOL”即“方法 资源 协议”(我们通常进行日志监控的时候,主要也是看这项内容)。例子中METHOD是GET,还有POST、HEAD等其他类型,主要是这三种。
RESOURCE是指浏览者向服务器请求的文档或者URL。在这个例子中,浏览者请求的是“/”,即网站的根或者主页。大多数情况下,“/”指向DocumentRoot目录的index.html文档,但根据服务器配置的不同也可能指向其他文件。
PROTOCOL通常是HTTP,然后再加上版本号。
(6)日志的第六项信息是状态代码。它告诉我们请求是否成功,或者遇到了什么样的错误。大多数时候这项是200,它表示服务器已经成功的响应浏览器的请求,一切正常。(以2开头的状态码表示成功,以3开头的状态码表示由于各种不同的原因用户请求被重定向到了其他位置,以4开头的状态代码表示客户端存在某种错误,以5开头的状态代码表示服务器遇到了某个错误)。
(7)日志记录的第七项表示发送客户端的总字节数。它告诉我们传输是否被打断(即该数值是否和文件的大小相同)。
GitHub - lionsoul2014/ip2region: Ip2region is a offline IP location library with accuracy rate of 99.9% and 0.0x millseconds searching performance. DB file is ONLY a few megabytes with all IP address stored. binding for Java,PHP,C,Python,Nodejs,Golang,C#,lua. Binary,B-tree,Memory searching algorithm
https://github.com/lionsoul2014/ip2region
IP归属地使用ip2region,计划使用https://www.ip138.com/iplookup.asp?ip=19.168.10.10&action=2,但有访问控制和反爬虫,轻量可以使用。
开始使用这个接口https://www.ip138.com/iplookup.asp?ip=19.168.10.10&action=2获取IP归属地,https出现以下错误
QT连接HTTPS,解决HTTPS问题_拽拽就是我的博客-CSDN博客_qt支持https
https://blog.csdn.net/qq_32355021/article/details/124089942
Win32/Win64 OpenSSL Installer for Windows - Shining Light Productions
http://slproweb.com/products/Win32OpenSSL.html
这个链接没有1.0的,从下面的old链接下载源码然后编译
编译最后报cl.exe错误,改调用Python抓取IP归属地。
创建IP归属地线程
mIP138 = new IP138(this);
connect(mIP138, SIGNAL(sendData(QString,QString)), this, SLOT(updateData(QString,QString)));
connect(mIP138, SIGNAL(done()), this, SLOT(getIP138Done()));
connect(mIP138, SIGNAL(done()), mIP138, SLOT(quit()));
timer随线程更新界面归属地
mUpdateTimer = new QTimer(this);
mUpdateTimer->setInterval(20);
connect(mUpdateTimer, SIGNAL(timeout()), this, SLOT(timerout()));
程序启动时数据库获取所有归属地数据,减少界面更新和线程操作,目前五万条数据
void MainWindow::selectAllIP138()
{
if (!mAllIP138Map.isEmpty())
{
return;
}
mAllIP138Map.clear();
QString retStr = mDBHelper.getConnectDB();
if (!retStr.isEmpty())
{
informationMessageBox(QStringLiteral("提示"), QStringLiteral("数据库连接失败:\n%1").arg(retStr), true);
return;
}
QStringList columnLst;
columnLst << "ip_address" << "ip_138";
QString str = QStringLiteral("select columns from t_ip_138;");
QList<QStringList> retLst = mDBHelper.getSqlSelect(str, columnLst);
foreach (QStringList ret, retLst)
{
mAllIP138Map.insert(ret[0], ret[1].replace(" 0", ""));
}
}
数据导入和分析,如果有数据库查不到归属地的IP,转到线程获取
void MainWindow::on_pushButton_data_import_clicked()
{
QString dataImportFile = QFileDialog::getOpenFileName(this, QStringLiteral("数据导入"), QStringLiteral("."), QStringLiteral(""));
if (!dataImportFile.isEmpty())
{
QFile file(dataImportFile);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
{
return;
}
if (mIP138->isRunning())
{
mIP138->quit();
mIP138->wait();
}
if (mUpdateTimer->isActive())
{
mUpdateTimer->stop();
}
QList<LogData> dataLst;
mIPLst.clear();
mDataQueue.clear();
mTableIndexMap.clear();
mIsDone = false;
QTextStream in(&file);
in.setCodec("UTF-8");
while (!in.atEnd())
{
QString str = in.readLine();
if (!str.isEmpty())
{
QStringList strLst = str.split(" ");
QString ip = strLst[0];
QString dateTime = strLst[3].replace("[", "");
QString statusCode = strLst.at(strLst.size() - 2);
QString size = strLst.at(strLst.size() - 1);
QString url = str.split("\"")[1];
{
QStringList tmpLst = dateTime.split(":");
QString time = QStringLiteral("%1:%2:%3").arg(tmpLst[1]).arg(tmpLst[2]).arg(tmpLst[3]);
QStringList tmpLst2 = tmpLst[0].split("/");
QString date = QStringLiteral("%1/%2/%3").arg(tmpLst2[2]).arg(shortMonthToNumber(tmpLst2[1])).arg(tmpLst2[0]);
dateTime = QStringLiteral("%1 %2").arg(date).arg(time);
}
LogData data;
data.IP = ip;
data.DateTime = dateTime;
data.StatusCode = statusCode;
data.Size = size;
data.URL = url;
data.IP138 = mAllIP138Map.value(ip);
dataLst.append(data);
if (!mAllIP138Map.contains(ip) && !mIPLst.contains(ip))
{
mIPLst.append(ip);
}
}
}
file.close();
dataTableWidget(ui->tableWidget, dataLst);
if (mIPLst.isEmpty())
{
informationMessageBox(QStringLiteral("提示"), QStringLiteral("分析完成"), true);
}
else
{
mIP138->setIPLst(mIPLst);
mIP138->start();
}
}
}
界面显示,为了timer更新界面速度快,提前把IP的row存起来,根据状态码设置行的不同颜色
void MainWindow::dataTableWidget(QTableWidget *tableWidget, QList<LogData> dataLst)
{
int rowCount = tableWidget->rowCount();
for (int i = rowCount; i > 0; --i)
{
tableWidget->removeRow(0);
}
QList<int> indexLst;
foreach (LogData data, dataLst)
{
int rowCount = tableWidget->rowCount();
tableWidget->insertRow(rowCount);
QString ip = data.IP;
QString country = data.IP138;
QTableWidgetItem *itemIp = new QTableWidgetItem(ip);
QTableWidgetItem *itemCountry = new QTableWidgetItem(country);
QTableWidgetItem *itemDate = new QTableWidgetItem(data.DateTime);
QTableWidgetItem *itemStatus = new QTableWidgetItem(data.StatusCode);
QTableWidgetItem *itemSize = new QTableWidgetItem(data.Size);
QTableWidgetItem *itemUrl = new QTableWidgetItem(data.URL);
itemCountry->setToolTip(country);
itemUrl->setToolTip(data.URL);
if (country.isEmpty())
{
if (mTableIndexMap.contains(ip))
{
indexLst = mTableIndexMap.value(ip);
}
else
{
indexLst.clear();
}
indexLst.append(rowCount);
mTableIndexMap.insert(ip, indexLst);
}
tableWidget->setItem(rowCount, 0, itemIp);
tableWidget->setItem(rowCount, 1, itemCountry);
tableWidget->setItem(rowCount, 2, itemDate);
tableWidget->setItem(rowCount, 3, itemStatus);
tableWidget->setItem(rowCount, 4, itemSize);
tableWidget->setItem(rowCount, 5, itemUrl);
for (int i = 0; i < tableWidget->columnCount(); ++i)
{
QString status = data.StatusCode;
if (status.startsWith("2"))
{
tableWidget->item(rowCount, i)->setTextColor(QColor("blue"));
}
else if (status.startsWith("3"))
{
tableWidget->item(rowCount, i)->setTextColor(QColor("green"));
}
else if (status.startsWith("4"))
{
tableWidget->item(rowCount, i)->setTextColor(QColor("red"));
}
else if (status.startsWith("5"))
{
tableWidget->item(rowCount, i)->setTextColor(QColor("yellow"));
}
}
}
}
线程接收槽,归属地数据使用队列管理,每20条批量insert数据库
void MainWindow::updateData(QString ip, QString ip138)
{
QString str = QStringLiteral("%1-%2").arg(ip).arg(ip138);
mDataQueue.enqueue(str);
mSQLLst.append(str);
if (mSQLLst.size() >= 20)
{
get20SQL();
}
if (!mUpdateTimer->isActive())
{
mUpdateTimer->start();
}
}
void MainWindow::get20SQL()
{
QStringList tmpLst;
for (int i = 0; i < 20; ++i)
{
tmpLst.append(mSQLLst.at(i));
}
for (int i = 0; i < 20; ++i)
{
mSQLLst.removeFirst();
}
insertSQL(tmpLst);
}
void MainWindow::insertSQL(QStringList sqlLst)
{
QString retStr = mDBHelper.getConnectDB();
if (retStr.isEmpty())
{
QString sqlValue;
foreach (QString sql, sqlLst)
{
sqlValue += QStringLiteral("('%1', '%2'), ").arg(sql.split("-")[0]).arg(sql.split("-")[1]);
}
sqlValue.remove(sqlValue.lastIndexOf(", "), 2);
QString str = mDBHelper.getSqlExcute(QStringLiteral("insert ignore into `t_ip_138` values%1;").arg(sqlValue));
if (!str.isEmpty())
{
informationMessageBox(QStringLiteral("提示"), QStringLiteral("数据库Insert失败:\n%1").arg(str), true);
}
}
}
timer更新界面归属地
void MainWindow::timerout()
{
if (!mDataQueue.isEmpty())
{
QString str = mDataQueue.dequeue();
QString ip = str.split("-")[0];
QString ip138 = str.split("-")[1];
QList<int> indexLst = mTableIndexMap.value(ip);
foreach (int index, indexLst)
{
QTableWidgetItem *item = new QTableWidgetItem(ip138);
ui->tableWidget->setItem(index, 1, item);
ui->tableWidget->item(index, 1)->setTextColor(ui->tableWidget->item(index, 0)->textColor());
ui->tableWidget->item(index, 1)->setToolTip(ip138);
}
ui->tableWidget->update();
ui->tableWidget->viewport()->update();
qApp->processEvents();
}
if (mIsDone && mDataQueue.isEmpty())
{
insertSQL(mSQLLst);
mSQLLst.clear();
mUpdateTimer->stop();
ui->tableWidget->update();
ui->tableWidget->viewport()->update();
qApp->processEvents();
informationMessageBox(QStringLiteral("提示"), QStringLiteral("分析完成"), true);
}
}
线程run函数 ,两种接口,一种ip138网站,一种ip2region
void IP138::run()
{
if (initPython())
{
mIsInit = true;
}
QString ret;
bool flag = (mChoose == 0);
foreach (QString ip, mIPLst)
{
if (flag)
{
ret = getIP2Region(ip);
ret = QStringLiteral("%1 %2 %3 %4").arg(ret.split("|")[0]).arg(ret.split("|")[2]).arg(ret.split("|")[3]).arg(ret.split("|")[4]);
ret.replace(" 0", "");
}
else
{
ret = getIP138(ip);
}
if (!ret.isEmpty())
{
emit sendData(ip, ret);
}
if (!flag)
{
this->msleep(mMsec + 1000);
}
}
emit done();
}
QString IP138::getIP138(QString ip)
{
if (!mIsInit)
{
return "";
}
PyObject *pFun = PyObject_GetAttrString(mPythonModule, "get_ip_138");
PyObject *args = Py_BuildValue("(s)", ip.toStdString().c_str());
PyObject *pRet = PyObject_CallObject(pFun, args);
if (pRet == nullptr)
{
return "";
}
return getPythonListByIndex(pRet, 0).trimmed().replace(" ", " ");
}
QString IP138::getIP2Region(QString ip)
{
if (!mIsInit)
{
return "";
}
PyObject *pFun = PyObject_GetAttrString(mPythonModule, "get_ip_region");
PyObject *args = Py_BuildValue("(s)", ip.toStdString().c_str());
PyObject *pRet = PyObject_CallObject(pFun, args);
if (pRet == nullptr)
{
return "";
}
return getPythonListByIndex(pRet, 0).trimmed().replace(" ", " ");
}
Python归属地获取
# coding: utf-8
from urllib import request
import re
import ssl
ssl._create_default_https_context = ssl._create_unverified_context
import socket
socket.setdefaulttimeout(20.0)
from ip2Region import Ip2Region
def get_ip_138(ip):
index_url = "https://www.ip138.com/iplookup.asp?ip=%s&action=2" %(ip)
headers = {'Host': 'www.ip138.com', 'Connection': 'keep-alive', 'Pragma': 'no-cache', 'Cache-Control': 'no-cache', 'DNT': 1, 'Upgrade-Insecure-Requests': 1, 'User-Agent': 'Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', 'Sec-Fetch-Site': 'none', 'Sec-Fetch-Mode': 'navigate', 'Accept-Encoding': 'gzip, deflate, br', 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', 'Cookie': 'ASPSESSIONIDASDRCTQS=LJOHHGEDLEBEOGADDGALNNJO'}
req = request.Request(url=index_url, headers=headers)
start = False
try:
with request.urlopen(req) as f:
data = f.read().decode("gb2312", "ignore").split("\r\n")
for line in data:
if line.find("navigator.userAgent") != -1:
start = True
if start:
if line.find("<div class=\"wrapper\">") != -1:
break
m = re.findall(r"\"ASN归属地\":\"(.+?)\",", line)
if m:
return m
except Exception as e:
print(e)
def get_ip_region(ip):
searcher = Ip2Region("ip2region.db")
#data = searcher.binarySearch(ip)
#data = searcher.memorySearch(ip)
data = searcher.btreeSearch(ip)
region = data["region"].decode('utf-8')
searcher.close()
return [region]