אחד הדברים שנשאלים הרבה ע"י בעלי אתרים זה כיצד אני יכול לראות סטטיסטיקות על האתר שלי.
אז כמובן שיש פתרונות בשוק, יש מספר תוכנות ליצירת סטטיסטיקות על אתרים כולל גראפים והכל, אבל הן עולות כסף, והרבה כסף, ולא כל מתכנת יכול להרשות לעצמו את הפתרון ב 1000 או 2000 דולר, כאשר כל מה שהוא רוצה לדעת זה כמה אנשים נכנסו לדף שלו.
גם אני רציתי את זה כך, בסך הכל רציתי לדעת לאיזה דפים נכנסו אצלי בסרוור, ומאיזה IP נכנסו, וגם מאיפה הגיעו אל השרת שלי.
כדי לבצע את זה כתבתי לעצמי תוכנית שמשתמשת ב SQL SERVER, DTS, VB, ASP, XML ו JS שבסיום התהליך אני מקבל את המידע הדרוש לי אודות הביקורים שנעשו אצלי באתר.
במאמר הזה אסביר כיצד בונים תוכנית כזאת.
מידע מקדים אודות LOGFILES.
כל פעולה שמתבצעת בשרת ה IIS נרשמת בקובץ לוג יומי, קובץ הלוג נמצא בדרך כלל בספריית ה WINDOWS, או WINNT או כל שם אחר לספריה שמכילה את מערכת ההפעלה, תחת ספריית המשנה 32SYSTEM בספרית LOGFILES.
תחת ספריה זו נמצאים כמה ספריות משנה שקרויות בשמות מוזרים שבדרך כלל נקראות W3SVC1 ואם יש על הסרוור יותר מ SITE אחד הם ממוספרות בהתאם. הספרייה W3AVC1 מכילה בדרך כלל את הלוגים של ה SITE שמוגדר ב IIS כ default web site.
את מיקום קבצי הלוג אפשר לשנות ע"י שימוש באופציית קבצי הלוג ב IIS.
כדי להגיע לאופציה הזאת יש לפתוח את ה INTERNET SERVICE MANAGER מתוך הקונטרול פאנל לבחור בdefault web site וללחוץ על Prosperities.
בחלון שנפתח תחת הלשונית WEB SITE יש חלק שמטפל בקבצי הלוג, ברירת המחדל שלו היא אפשור קבצי לוג.
לחיצה על Prosperities של קבצי הלוג מביאה אותנו למסך נוסף בעל 2 לשוניות בלשונית הראשונה אנחנו יכולים לקבוע את תדירות בניית קבצי הלוג ואת המיקום בו ישמרו. בלשונית השניה אפשר לקבוע איזה פרמטרים ישמרו בתוך קבצי הלוג, יש כמה ברירות מחדל קבועות, אני כשלעצמי משתמש בברירות המחדל אך מוסיף עליהם את ה REFFRER כדי לדעת גם מהיכן הגיע אלי המשתמש.
קבצי הלוג בנויים כשורות טקסט כאשר ההפרדה ביניהם מבוצעת ע"י רווח, הנה דוגמא לשורה בקובץ הלוג 2001-08-30 00:47:32 212.199.49.59 - 192.117.188.19 80 GET /iao/position.htm - 200 Mozilla/4.0+(compatible;+MSIE+5.5;+Windows+NT+5.0) http://www.asp.org.il/forum/ALL.asp?Fnumber=1
השורה אומרת כי ב 30 לאוגוסט 2001 בשעה 00:47 נכנס מאי פי 212.199.49.56 משתמש שאיינו זקוק לזיהוי לסרוור באי פי 192.117.188.19 בפורט 80 במתודת GET ביקש בספריית IAO את הקובץ POSITION.HTM . הוא הצליח לקבל את הקובץ, כאשר הוא פנה לשרת עם אקספלורר 5.5, והוא הגיע אל השרת שלי מהכתובת http://www.asp.org.il/forum/ALL.asp?Fnumber=1.
בניית התוכנית.
החלטתי לבסס את התוכנית על העברת נתוני קבצי הלוג לתוך בסיס נתונים ב 2000 SQL SERVER , וכאשר יש לי נתונים בבסיס נתונים, בעצם אני יכול לעשות איתם כל מה שאפשר לעשות עם נתונים בבסיס נתונים.
בפעם הראשונה ביצעתי יבוא ידני של קובץ הלוג אל תוך בסיס הנתונים בעזרת ה DTS WIZARD.
ה DTS (Data Transfer System) הנו כלי שמגיע יחד עם SQL SERVER ואפשר לבצע איתו יבוא ויצוא נתונים מסוגים שונים של פורמטים, SQL לאקסס, אקסס לאקסל, קבצי טקסט ל SQL SERVRT, ובעצם כל סוג של נתונים שנתמך ע"י ADO.
בסוף תהליך היבוא דרך ה DTD WIZARD יש אפשרות לשמור את קובץ הפעולה כקובץ BAS של VB, שמרתי.
התוכנית הזאת בעצם לוקחת מתוך ספריית ה SYSTEM32 את קובץ הלוג ex010825.log, מייצרת טבלה חדשה על בבסיס הנתונים LOGFILE בשם ex010825 ומעבירה לתוך 16 השדות שיצרה את הנתונים מתוך הקובץ.
בזמן יצירת הקובץ הידני הקפדתי על 2 דברים חשובים.
האחד להתחיל את יבוא הנתונים רק מהשורה הרביעית שבקובץ שכן שלושת השורות הראשונות בקובץ הלוג הן תיאור של הקובץ ואין טעם להכניס גם אותן לתוך בסיס הנתונים. והשני לציין כי ההפרדה בין השדות תהיה ע"י רווח.
בכל מקרה למי שרוצה לבנות תוכנית דומה, הצעתי היא שישמור לעצמו קובץ BAS שמתאים לצרכים שלו בפעם הראשונה, ואת הקובץ יוסיף לתוכנית.
לאחר שהיה בידי את המבנה של כיצד מתבצע ה DTS, כתבתי תוכנית קטנה ב VB הכוללת פורום אחד ומודול אחד.
הכנסתי את 2 משתני ה PUBLIC לתוך המודול
Public goPackageOld As New DTS.Package
Public goPackage As DTS.Package2
יצרתי REFFERENCE ל Microsoft DTSPackage Object Library
לטופס שיצרתי הוספתי שדה טקסט, 2 כפתורים ואת אובייקט ה CommonDialog . (להוספת ה CommonDialog, יש לבחור את הקומפוננט Microsoft Command Dialog מתוך ספריית הקומפוננטות)
לאחד הכפתורים קראתי "רשימת קבצים" והוספתי לו את הקוד הבא
Private Sub cmdFiles_Click()
CommonDialog1.ShowOpen
Text1.Text = CommonDialog1.FileName
End Sub
שפירושו, בזמן לחיצה על הכפתור, תפתח את חלון רשימת הקבצים במחשב, ואחרי שיבחר קובץ תעביר את שמו והמסלול אליו אל שדה הטקסט Text1.
יצרתי 2 משתנים גלובליים
Dim File2DTS As String, File2DTS_Name As String
לכפתור השני קראתי "בצע העברת קבצים" וחיברתי אליו את הקוד הבא.
File2DTS = Text1.Text
PLACE = Len(Right(File2DTS, InStr(StrReverse(File2DTS), "") - 1))
File2DTS_Name = Left(Right(File2DTS, InStr(StrReverse(File2DTS), "") - 1), PLACE – 4)
הקוד הזה מעביר למשתנה file2DTS את שם הקובץ והמסלול שלו, ולמשתנה File2DTS_Nameאת החיתוך של שם הקובץ עצמו ללא המסלול וללא התוספת ".log"
העתקתי את הקוד מתוך הקובץ שנשמר ע"י ה DTS אל אזור הקוד של הפורום, מלבד 2 המשתנים הגלובאלים, אותם כאמור שמרתי במודול, והחלפתי את כל המקומות בהם מופיע מסלול הקובץ למשתנה File2DTS ואת כל המקומות בהם מופיעה שם הטבלה שתווצר בקוד קשיח למשתנה File2DTS_Name. כמובן שהייתי יכול להחליף גם את שם השרת ושם בסיס הנתונים למשתנים, אך לא התעסקתי עם זה היות ואני שומר את כל הטבלאות של קבצי הלוג באותו שרת ובאותו בסיס נתונים.
קימפלתי את התוכנית, יצרתי קיצור דרך אליה מתוך שולחן העבודה שלי, ועכשיו יש לי דרך על בסיס יומי להעביר את קבצי הלוג לתוך בסיס הנתונים שלי.
כמובן: למי שצריך את ניתוח הנתונים הללו על בסיס קבוע, ואין לו חשק כל יום לשבת מול המחשב ולהעביר את הנתונים מקובץ הלוג אל בסיס הנתונים, את אותה תוכנית אפשר לכתוב כ ACTIVEX EXE, ולהריץ אותה בעזרת משימה של ה SQLSERVER כל יום בלילה על קובץ הלוג של היום הקודם
ניתוח הסטטיסטיקות:
לאחר שכל נתוני יום מסוים היו בידי, כל מה שנותר לי זה לשאול את עצמי, מה אני רוצה לדעת בכלל.
החלטתי שמה שמעניין אותי זה:
מספר הנכנסים לשרת שלי, מאיפה הגיעו, ומה רצו לראות.
כל דף שנכנסו אליו, מאיזה אי פי נכנסו, אם זה דף שזקוק להרשאות כניסה, מי המשתמש שנכנס, ומאיפה הגיעו לדף הזה
כל אי פי שהגיע לסרוור שלי, איזה דפים הוא ביקר בסרוור.1
התפלגות כניסות לשרת לפי שעות היממה.
פעולה ראשונה: רשימת הטבלאות בתוך בסיס הנתונים LOGFILES.
כזכור כל יום מתווסף לבסיס הנתונים בתור טבלה ששמה הוא כשם קוץ הלוג מלבד הסיומת, לכן בשלב ראשון בניתי לעצמי תיבה נגללת המכילה את כל שמות הטבלאות שיש לי בבסיס הנתונים LOGFILE
<%
set conn= createobject("ADODB.connection")
conn.open "Provider = SQLOLEDB; Data Source=MyServer; Initial Catalog=logfiles;User Id=; Password="
SQL = "SELECT DISTINCT name,name FROM sysobjects WHERE (name NOT LIKE Sys%) AND "
_& "(xtype = U) AND (status = 0) order by name desc"
set rs= createobject("ADODB.recordset")
set rsNews = conn.Execute(SQL)
strMiddleDelimiter="<"
strEndDelimiter = "
בקוד הזה אני פותח רשימה של שמות הטבלאות בבסיס הנתונים, את הרשימה אני מייצר ב GETSTRING שאליו אני מכניס פרמטרים של טבלה נגללת ומייצר את הטבלה בצורה הכי מהירה שקיימת.
עכשיו יש לי טבלה עם כל שמות קבצי הלוג שנמצאים בבסיס הנתונים, כאשר הכי מאוחר הוא הראשון ברשימה. (בדרך כלל אנחנו רוצים לראות את הנתונים על הקובץ הטרי ביותר)
מתחת לטבלה הנגללת הוספתי מספר כפתורים
הגדרתי אזור שבו יוכנסו הנתונים שיובאו בעזרת XMLHTTP שקראתי לו
כתבתי 4 קבצי ASP
Statistic.asp:
<%
set conn= createobject("ADODB.connection")
conn.open "Provider=SQLOLEDB;Data Source=MyServer;Initial Catalog=logfiles;User Id=sa;Password="
set rs= createobject("ADODB.recordset")
%>
דו"ח ראשון: כמה משתמשים נכנסו אלי לשרת Col003 הנו השדה שמכיל את ה אי פי של הנכנסים השאילתה הבאה תחזיר לי את מספר הנכנסים ע"י ספירה של כל אי פי פעם אחת.
SQL = "SELECT COUNT(DISTINCT Col003) AS Visitors FROM " & Request.QueryString("table")
set rs = conn.Execute(SQL)
Response.Write " " & rs(0) & " Users visited the site"
Response.Write "
"
שאילתה שניה: כמה פניות לדפי HTML או דפי ASP נעשו ע"י כל משתמש (בקבצי הלוג מופיעים גם פניות לאובייקטים אחרים, כמו קבצי תמונות, קבצי JS ו CSS, קבצי פלאש, קבצי מוזיקה, ובעצם כל אובייקט שנמצא בדף) מה שאותי עניין כאמור, זה רק קבצי ASP ו HTM. Col008 הנו השדה שמכיל את האובייקטים בשרת שאליהם פנו המשתמשים. השאילתה הבאה תיתן לי טבלה המכילה מספרי IP ומספר הדפים שכל IP פנה אליהם.
SQL = "SELECT DISTINCT Col003, COUNT(Col003) AS No_of_Visiters FROM "
SQL = SQL & Request.QueryString("table") & " WHERE "
SQL = SQL & "(Col008 LIKE %.asp%) OR (Col008 LIKE %.htm%)"
SQL = SQL & " GROUP BY Col003 ORDER BY COUNT(Col003) DESC"
set rs = conn.Execute(SQL)
Response.Write "
I.P.
Visits"
while not rs.EOF
Response.Write "
" & rs(0) & "
" & rs(1)
rs.MoveNext
wend
Response.Write "
"
שאילתה שלישית: כמה משתמשים פנו לכל דף בין אם הוא HTM או ASP.
<%
SQL = "SELECT DISTINCT Col008, COUNT(Col008) AS Visits FROM " & Request.QueryString("table")
SQL = SQL & " GROUP BY Col008 HAVING (Col008 LIKE %.asp%) OR (Col008 LIKE %.htm%)"
SQL = SQL & " ORDER BY COUNT(Col008) DESC"
set rs = conn.Execute(SQL)
Response.Write "
Page
Visits"
while not rs.EOF
Response.Write "
" & rs(0) & "
" & rs(1(
rs.MoveNext
wend
Response.Write "
"
rs.close
set rs=nothing
conn.Close
set conn=nothing
%>
בלחיצה על כפתור "דוח" כניסות" קראתי לפונקצית JS ששלחה בקשה בעזרת הקומפוננט XMLHTTP להרצת קובץ STATISIC.ASP לסרוור, והחזירה את הטבלאות שבניתי בפרופרטי responseText של האובייקט XMLHTTP. את התשובה שחזרה לי מהסרוור שתלתי ב DIV לו קראתי MainPageDiv
function PostOrder (thisUrl,table){
url=thisUrl + "=" + table
var xmlhttp = new ActiveXObject ("Microsoft.XMLHTTP")
xmlhttp.Open("POST",url,false);
xmlhttp.Send();
MainPageDiv.innerHTML = xmlhttp.responseText ;
{
עכשיו אחרי שידעתי כמה מבקרים נכנסו לי לסרוור, לכמה דפים נכנס כל אחד, וכמה דפים נצפו ע"י כל מבקר, יצרתי לעצמי שתי תוכניות נוספות IP2PAGES ו PAGES2IP
באחת רשימה של כל הדפים באתר שלי שהיו אליהן כניסות ומתחת לכל אחד במבנה עץ את רשימת כל ה אי פי שנכנסו לכל דף ואת האתר ממנו הגיעו אל הדף שלי, במידה ולחצו על לינק מסוים בדף מסוים.
IP2PAGES:
הפעם השתמשתי בשיטה שונה כדי להעביר את הנתונים אל הלקוח, השיטה עצמה היא אותה שיטה, שימוש ב XMLHTTP, אך בצד של השרת במקום לבנות טבלאות כמו בקובץ הקודם ולהעביר אותם לקליינט ע"י שימוש ב responseText, יצרתי קבצי XML ע"י ADO.STRAM, את ה XML שלחתי לתוך ה XMLDOM של הקליינט, ומשם ל DIV אליו אני מכניס את הדטא.
כדי ליצור את קובץ ה XML עצמו בצורת עץ, כלומר, תחת כל דף בשרת תהיה רשימה של כל האי פי שנכנסו אליו, השתמשתי ב PROVIDER DATASHAPE שבא עם ADO. (יותר על ה PROVIDER הזה אפשר לקרוא במאמר הקודם שלי בניית עץ בעזרת Datashape ADO.Stream and XML )
<%
dim rs , cn, SQL
dim m_searchobjStream
Response.CharSet = "ISO-8859-8-i"
set m_searchobjStream = Server.CreateObject("Adodb.Stream")
set cn = Server.Createobject("Adodb.connection")
set rs = Server.Createobject("adodb.recordset")
cn.Provider = "MSDataShape"
cn.Open "Driver={SQL Server};Server=MyServer;Provider=MSDataShape;Uid=;Pwd=;Database=logfiles"
%>
ברמה הראשונה של ה SHAPE לקחתי את כל הרשומות הנמצאות בשדה Col008 אשר הסיומת שלהן ASP או HTM.
ברמה השניה לקחתי את השדות המכילים את שם הקובץ Col008 את שם המשתמש Col004 במידה והדף אליו ניסו לגשת הוא דף המאובטח בסיסמא, את ה IP של המשתמש שנמצא ב Col003 ואת ה URL ממנו הוא הגיע לדף שנמצא ב Col012.
את הקשר (RELATE) בין הרמה הראשונה לרמה השניה יצרתי בעזרת השדה Col003
<%
SQL = "SHAPE {SELECT DISTINCT Col008 FROM " & request("table")
SQL = SQL & " Where Col008 LIKE %.asp% or Col008 LIKE %.htm% order by Col008} "
SQL = SQL & " APPEND ({SELECT distinct Col003,Col004,Col012,Col008 FROM "
SQL = SQL & Request("table") & "} AS visitedPages "
SQL = SQL & " RELATE Col008 to Col008("
rs.Open SQL, cn
if (not RS.eof) and (Err.number = 0) then
rs.save m_searchobjStream,1
if Err.number <> 0 then
errHandler
end if
else
call errHandler
end if
rs.Close
set rs=nothing
Response.CharSet = "ISO-8859-8-i"
Response.ContentType = "text/xml"
Response.Write "" & m_searchobjStream.ReadText()
set rs=nothing
set m_searchobjStream = nothing
Response.flush
sub errHandler
Response.Clear
Response.ContentType = "text/xml"
Response.Write err.description
Response.End
end sub
%>
קובץ ה XML שנוצר נראה כך:
כאשר אני לחוץ על הכפתור "דפים לאי פי" אני מריץ את פונקצית ה JS PostPage2IP
function PostPage2IP(thisUrl,table({
החלק הראשון שולח בקשה לסרוור לבצע את דף ה Page2IP.ASP
var url=thisUrl + "=" + table
var xmlhttp = new ActiveXObject ("Microsoft.XMLHTTP");
xmlhttp.Open("POST",url,false(;
xmlhttp.Send();
הדף שחוזר כ XML נטען לתוך ה XMLDOM על הקליינט
var i,xmlNode,inserttext
xmlDoc = new ActiveXObject("Microsoft.XMLDOM");
xmlDoc.async = false;
xmlDoc.validateOnParse = true;
xmlDoc.load(xmlhttp.responseXML(;
var siteNode = xmlDoc.selectNodes("//z:row")
MainPageDiv.innerHTML = siteNode.length
if (siteNode == null({
MainPageDiv.innerHTML = Temarery Problem with...., Pleae try latter;
return false;
}
ה node הראשי של ה XML הוא ה Data Type שנקרא //z:row
הפונקציה עוברת על כל ה //z:row ואת הערך שנמצא באטרביוט 0 של node (שזה שם הדף אליו המשתמש נכנס) מכניסים לתוך משתנה מחרוזת.
var inserttext="";
for (i = 0; i < siteNode.length; i++) { // for every site
inserttext = inserttext += "" + siteNode.item(i).attributes[0].nodeValue + " "
בכל node ראשי הפונקציה עוברת על ה nodeים המשניים ומכניסה את הערכים שבאטריביוטים המכילים את IP, שם המשתמש והלינק ממנו הוא הגיע, אל המחרוזת הטקסטואלית.
for (j=0;j< siteNode.item(i).childNodes.length;j++){
inserttext+= " "
inserttext+= siteNode.item(i).childNodes.item(j).attributes[0].nodeValue
inserttext+= " " + siteNode.item(i).childNodes.item(j).attributes[1].nodeValue
inserttext+= " "
inserttext+= siteNode.item(i).childNodes.item(j).attributes[2].nodeValue + " "
}
}
המחרוזת הטקסטואלית מועברת אל תוך ה MainPageDiv
MainPageDiv.innerHTML = inserText ;
{
התוצאה שתתקבל:
לדף DEFAULT.ASP נכנסו 2 מבקרים, האחד הגיע ישיר והשני הגיע דרך מנוע החיפוש של ZOOLOO. (כאשר את השדה של ה REFFRER כפי שראיתם יצרתי בתור A HREF לשימוש מהיר להגעה לדף ממנו הגיעו אלי לסרוור)
התוצאה:
הדף האחר IP2Pages.asp בנוי באותה צורה, רק ששם הרמה הראשונה של ה SHAPE הנה ה IP ואילו הרמה השניה מכילה את שם הדף ואת שם הURL ממנו הגיע המשתמש.
SQL = "SHAPE {SELECT DISTINCT Col003 FROM " & request("table")
SQL = SQL & " where Col003 <> 192.117.188.22 AND Col008 not like %default.ida% } "
SQL = SQL & "APPEND ({SELECT distinct Col003, Col008, col012 FROM " & request("table")
SQL = SQL & " where Col008 LIKE %.asp% or Col008 LIKE %.htm% order by Col008}"
SQL = SQL & " AS visitedPages RELATE Col003 to Col003)"
לגבי המשפט הנ"ל: התוספת ברמה הראשונה not like %default.ida%, באה למנוע הכנסה לסטטיסטיקות שלי, של מספרי ה IP של השרתים הנגועים ב CODE RED ומחפשים על השרת שלי את הקובץ כדי להדביק גם אותיdefault.ida.
התפלגות הכניסות:
אני מעוניין לדעת כמה משתמשים ביקרו בשרת שלי בין 3 ל 4 בבוקר או בין 3 ל 4 אחה"צ. את התוצאה הזאת ארצה לראות בגרף.
כדי לבצע זאת בניתי STORED PROCEDURE שמאחד את הכניסות לאתר לפי שעות היממה. השגרה השמורה sp_Hours_Report מייצרת RECORD SET של 24 עמודות שבכל עמודה יש את מספר הפניות לדפים באתר בכל שעה מתוך 24 שעות.
<%
Class clsString
Private m_intLength
Private m_intCounter
Private m_arrString()
Private Sub Class_Initialize()
Dim an array and set position counter
m_intCounter = 1
m_intLength = 100
Redim m_arrString(m_intLength)
End Sub
Public Sub Reset
Erase current array and recreate
Erase m_arrString
Call Class_Initialize()
End Sub
Public Property Get Value
Use Join function to create final string
Value = Join(m_arrString,"")
End Property
Public Sub Add(byval strValue)
Dim intArrLen
Add value to string array
intArrLen = Ubound(m_arrString)
If m_intCounter > intArrLen Then Redim Preserve m_arrString(intArrLen + m_intLength)
m_arrString(m_intCounter) = strValue
Incriment position counter
m_intCounter = m_intCounter + 1
End Sub
End Class
Dim oText
Set oText = New clsString
set conn= createobject("ADODB.connection")
conn.open "Provider=SQLOLEDB; Data Source=myServer; Initial Catalog=logfiles;User Id=; Password="
set rs= createobject("ADODB.recordset")
SQL = "EXECUTE sp_Hours_report " & Request.QueryString("table")
set rs = conn.Execute(SQL)
oText.Add "