- 原文地址:Detecting Bots in Apache & Nginx Logs
- 原文作者:Mark Litwintschik
- 譯文出自:掘金翻譯計劃
- 譯者:luoyaqifei
- 校對者:forezp,1992chenlu
在 Apache 和 Nginx 日誌裡檢測爬蟲機器人
現在阻止基於 JavaScript 追蹤的瀏覽器外掛享有九位數的使用者量,從這一事實可以看出,web 流量日誌可以成為一個很好的、能夠感知有多少人在訪問你的網站的地方。但是任何監測過 web 流量日誌一段時間的人都知道,有成群結隊的爬蟲機器人在爬網站。然而,在 web 伺服器日誌裡分辨出機器人和人為產生的流量是一個難題。
在這篇博文中,我將帶你們重現那些我在建立一個基於 IPv4 所屬和瀏覽器字串(browser string)的機器人檢測指令碼時用過的步驟。
本文中用到的程式碼在這個 程式碼片段 裡。
IP 地址所屬資料庫
首先,我會安裝 Python 和一些依賴包。接下來的指令會在一個新的 Ubuntu 14.04.3 LTS 安裝過程中執行。
$ sudo apt-get update
$ sudo apt-get install
python-dev
python-pip
python-virtualenv複製程式碼
接下來我要建立一個 Python 虛擬環境,並且啟用它。通過 pip 安裝庫時,容易遇到許可權問題,這樣可以緩解這種問題。
$ virtualenv findbots
$ source findbots/bin/activate複製程式碼
MaxMind 提供了一個免費的資料庫,資料庫裡有 IPv4 地址對應的國家和城市註冊資訊。和這些資料集一起,他們還發布了一個基於 Python 的庫,叫 “geoip2”,這個庫可以將他們的資料集對映到記憶體對映的檔案裡,並且用基於 C 的 Python 擴充套件來執行非常快的查詢。
下面的命令會安裝它們的包,下載、解壓它們在城市那一層的資料集。
$ pip install geoip2
$ curl -O http://geolite.maxmind.com/download/geoip/database/GeoLite2-City.mmdb.gz
$ gunzip GeoLite2-City.mmdb.gz複製程式碼
我看過一些 web 流量日誌,並且抓取出來一些恰好請求了「robots.txt」的流量。從那個列表裡,我重點檢查了經常出現的 IP 地址中的一些,發現不少 IP 其實是屬於主機和雲服務提供商的。我想知道是不是有可能攢出來一個列表,無論完不完整,包括了這些提供商所有的 IPv4 地址。
Google 有一個基於 DNS 的機制,用於收集它們用於提供雲的 IP 地址列表。這個最初的呼叫將給你一系列可以查詢的主機。
$ dig -t txt _cloud-netblocks.googleusercontent.com | grep spf複製程式碼
_cloud-netblocks.googleusercontent.com. 5 IN TXT "v=spf1 include:_cloud-netblocks1.googleusercontent.com include:_cloud-netblocks2.googleusercontent.com include:_cloud-netblocks3.googleusercontent.com include:_cloud-netblocks4.googleusercontent.com include:_cloud-netblocks5.googleusercontent.com ?all"複製程式碼
以上闡明瞭 _cloud-netblocks[1-5].googleusercontent.com 將包含 SPF 記錄,這些記錄裡包括他們實用的 IPv4 和 IPv6 CIDR 地址。像如下這樣查詢所有的五個地址,應當會給你一個最新的列表。
$ dig -t txt _cloud-netblocks1.googleusercontent.com | grep spf複製程式碼
_cloud-netblocks1.googleusercontent.com. 5 IN TXT "v=spf1 ip4:8.34.208.0/20 ip4:8.35.192.0/21 ip4:8.35.200.0/23 ip4:108.59.80.0/20 ip4:108.170.192.0/20 ip4:108.170.208.0/21 ip4:108.170.216.0/22 ip4:108.170.220.0/23 ip4:108.170.222.0/24 ?all"複製程式碼
去年三月,基於 Hadoop 的 MapReduce 任務,我嘗試著抓取了整個 IPv4 地址空間的 WHOIS 細節,並且釋出了一篇 部落格文章。這個任務在過早結束之前,跑了接近兩個小時,留給了我一份雖然不完整,但是大小可觀的資料集,裡面有 235,532 個 WHOIS 記錄。這個資料集已經存在一年之久了,除了有點過時,應該還是有價值的。
$ ls -l複製程式碼
-rw-rw-r-- 1 mark mark 5946203 Mar 31 2016 part-00001
-rw-rw-r-- 1 mark mark 5887326 Mar 31 2016 part-00002
...
-rw-rw-r-- 1 mark mark 6187219 Mar 31 2016 part-00154
-rw-rw-r-- 1 mark mark 5961162 Mar 31 2016 part-00155複製程式碼
當我重點檢查那些爬到「robots.txt」的爬蟲機器人的 IP 所屬時,除了 Google,這六家公司也出現了很多次:Amazon、百度、Digital Ocean、Hetzner、Linode 和 New Dream Network。我跑了以下的命令,嘗試去取出它們的 IPv4 WHOIS 記錄。
$ grep -i `amazon` part-00* > amzn
$ grep -i `baidu` part-00* > baidu
$ grep -i `digital ocean` part-00* > digital_ocean
$ grep -i `hetzner` part-00* > hetzner
$ grep -i `linode` part-00* > linode
$ grep -i `new dream network` part-00* > dream複製程式碼
我需要從以上六個檔案中,解析二次編碼的 JSON 字串,這些字串包含了檔名和頻率次數資訊。我使用了 iPython 程式碼來獲得不同的 CIDR 塊,程式碼如下:
import json
def parse_cidrs(filename):
lines = open(filename, `r+b`).read().split(`
`)
recs = []
for line in lines:
try:
recs.append(
json.loads(
json.loads(`:`.join(line.split(` `)[0].split(`:`)[1:]))))
except ValueError:
continue
return set([str(rec.get(`network`, {}).get(`cidr`, None))
for rec in recs])
for _name in [`amzn`, `baidu`, `digital_ocean`,
`hetzner`, `linode`, `dream`]:
print _name, parse_cidrs(_name)複製程式碼
下面是一份清理完畢的 WHOIS 記錄例項,我已經去掉了聯絡資訊。
{
"asn": "38365",
"asn_cidr": "182.61.0.0/18",
"asn_country_code": "CN",
"asn_date": "2010-02-25",
"asn_registry": "apnic",
"entities": [
"IRT-CNNIC-CN",
"SD753-AP"
],
"network": {
"cidr": "182.61.0.0/16",
"country": "CN",
"end_address": "182.61.255.255",
"events": [
{
"action": "last changed",
"actor": null,
"timestamp": "2014-09-28T05:44:22Z"
}
],
"handle": "182.61.0.0 - 182.61.255.255",
"ip_version": "v4",
"links": [
"http://rdap.apnic.net/ip/182.0.0.0/8",
"http://rdap.apnic.net/ip/182.61.0.0/16"
],
"name": "Baidu",
"parent_handle": "182.0.0.0 - 182.255.255.255",
"raw": null,
"remarks": [
{
"description": "Beijing Baidu Netcom Science and Technology Co., Ltd...",
"links": null,
"title": "description"
}
],
"start_address": "182.61.0.0",
"status": null,
"type": "ALLOCATED PORTABLE"
},
"query": "182.61.48.129",
"raw": null
}複製程式碼
這份七個公司的列表不是一個關於爬蟲機器人來源的全面的列表。我發現,除了一個從世界各地連線的分散式爬蟲戰隊,很多爬蟲流量來源於一些在烏克蘭、中國的住宅 IP,源頭很難分辨。說實話,如果我想要一個全面的爬蟲機器人實用的 IP 列表,我只需要看看 HTTP 頭的順序,檢查下 TCP/IP 的行為,搜尋 偽造 IP 註冊(請看 28 頁),列表就出來了,並且這就像貓和老鼠的遊戲一樣。
安裝庫
對於這個專案而言,我會實用一些寫得很好的庫。Apache Log Parser 可以解析 Apache 和 Nginx 生成的流量日誌。這個庫支援從日誌檔案中解析超過 30 種不同型別的資訊,並且我發現,它相當彈性、可靠。Python User Agents 可以解析使用者代理的字串,並執行一些代理使用的基本分類操作。Colorama 協助建立有高亮的 ANSI 輸出。Netaddr 是一種成熟的、維護得很好的網路地址操作庫。
$ pip install -e git+https://github.com/rory/apache-log-parser.git#egg=apache-log-parser
-e git+https://github.com/selwin/python-user-agents.git#egg=python-user-agents
colorama
netaddr複製程式碼
爬蟲機器人監控指令碼
接下來的部分是跑 monitor.py 的內容。這段指令碼從 stdin(標準輸入) 管道中接收 web 流量日誌。這說明你可以通過 ssh 在遠端伺服器上看日誌,在本地跑這段指令碼。
我先從 Python 標準庫裡匯入兩個庫,並通過 pip 安裝了五個外部庫。
import sys
from urlparse import urlparse
import apache_log_parser
from colorama import Back, Style
import geoip2.database
from netaddr import IPNetwork, IPAddress
from user_agents import parse複製程式碼
接下來我設定好 MaxMind 的 geoip2 庫,以使用「GeoLite2-City.mmdb」城市級別的庫。
我還設定了 apache_log_parser,來處理儲存的 web 日誌格式。你的日誌格式可能不一樣,所以可能需要花點時間比較下你的 web 伺服器的流量日誌配置與這個庫的 格式文件。
最後,我有一個我發現的屬於那七家公司的 CIDR 塊的字典。在這個列表裡,從本質上來說,百度不是一家主機或者雲提供商,但是跑著很多無法通過它們的使用者代理所識別的爬蟲機器人。
reader = geoip2.database.Reader(`GeoLite2-City.mmdb`)
_format = "%h %l %u %t "%r" %>s %b "%{Referer}i" "%{User-Agent}i""
line_parser = apache_log_parser.make_parser(_format)
CIDRS = {
`Amazon`: [`107.20.0.0/14`, `122.248.192.0/19`, `122.248.224.0/19`,
`172.96.96.0/20`, `174.129.0.0/16`, `175.41.128.0/19`,
`175.41.160.0/19`, `175.41.192.0/19`, `175.41.224.0/19`,
`176.32.120.0/22`, `176.32.72.0/21`, `176.34.0.0/16`,
`176.34.144.0/21`, `176.34.224.0/21`, `184.169.128.0/17`,
`184.72.0.0/15`, `185.48.120.0/26`, `207.171.160.0/19`,
`213.71.132.192/28`, `216.182.224.0/20`, `23.20.0.0/14`,
`46.137.0.0/17`, `46.137.128.0/18`, `46.51.128.0/18`,
`46.51.192.0/20`, `50.112.0.0/16`, `50.16.0.0/14`, `52.0.0.0/11`,
`52.192.0.0/11`, `52.192.0.0/15`, `52.196.0.0/14`,
`52.208.0.0/13`, `52.220.0.0/15`, `52.28.0.0/16`, `52.32.0.0/11`,
`52.48.0.0/14`, `52.64.0.0/12`, `52.67.0.0/16`, `52.68.0.0/15`,
`52.79.0.0/16`, `52.80.0.0/14`, `52.84.0.0/14`, `52.88.0.0/13`,
`54.144.0.0/12`, `54.160.0.0/12`, `54.176.0.0/12`,
`54.184.0.0/14`, `54.188.0.0/14`, `54.192.0.0/16`,
`54.193.0.0/16`, `54.194.0.0/15`, `54.196.0.0/15`,
`54.198.0.0/16`, `54.199.0.0/16`, `54.200.0.0/14`,
`54.204.0.0/15`, `54.206.0.0/16`, `54.207.0.0/16`,
`54.208.0.0/15`, `54.210.0.0/15`, `54.212.0.0/15`,
`54.214.0.0/16`, `54.215.0.0/16`, `54.216.0.0/15`,
`54.218.0.0/16`, `54.219.0.0/16`, `54.220.0.0/16`,
`54.221.0.0/16`, `54.224.0.0/12`, `54.228.0.0/15`,
`54.230.0.0/15`, `54.232.0.0/16`, `54.234.0.0/15`,
`54.236.0.0/15`, `54.238.0.0/16`, `54.239.0.0/17`,
`54.240.0.0/12`, `54.242.0.0/15`, `54.244.0.0/16`,
`54.245.0.0/16`, `54.247.0.0/16`, `54.248.0.0/15`,
`54.250.0.0/16`, `54.251.0.0/16`, `54.252.0.0/16`,
`54.253.0.0/16`, `54.254.0.0/16`, `54.255.0.0/16`,
`54.64.0.0/13`, `54.72.0.0/13`, `54.80.0.0/12`, `54.72.0.0/15`,
`54.79.0.0/16`, `54.88.0.0/16`, `54.93.0.0/16`, `54.94.0.0/16`,
`63.173.96.0/24`, `72.21.192.0/19`, `75.101.128.0/17`,
`79.125.64.0/18`, `96.127.0.0/17`],
`Baidu`: [`180.76.0.0/16`, `119.63.192.0/21`, `106.12.0.0/15`,
`182.61.0.0/16`],
`DO`: [`104.131.0.0/16`, `104.236.0.0/16`, `107.170.0.0/16`,
`128.199.0.0/16`, `138.197.0.0/16`, `138.68.0.0/16`,
`139.59.0.0/16`, `146.185.128.0/21`, `159.203.0.0/16`,
`162.243.0.0/16`, `178.62.0.0/17`, `178.62.128.0/17`,
`188.166.0.0/16`, `188.166.0.0/17`, `188.226.128.0/18`,
`188.226.192.0/18`, `45.55.0.0/16`, `46.101.0.0/17`,
`46.101.128.0/17`, `82.196.8.0/21`, `95.85.0.0/21`, `95.85.32.0/21`],
`Dream`: [`173.236.128.0/17`, `205.196.208.0/20`, `208.113.128.0/17`,
`208.97.128.0/18`, `67.205.0.0/18`],
`Google`: [`104.154.0.0/15`, `104.196.0.0/14`, `107.167.160.0/19`,
`107.178.192.0/18`, `108.170.192.0/20`, `108.170.208.0/21`,
`108.170.216.0/22`, `108.170.220.0/23`, `108.170.222.0/24`,
`108.59.80.0/20`, `130.211.128.0/17`, `130.211.16.0/20`,
`130.211.32.0/19`, `130.211.4.0/22`, `130.211.64.0/18`,
`130.211.8.0/21`, `146.148.16.0/20`, `146.148.2.0/23`,
`146.148.32.0/19`, `146.148.4.0/22`, `146.148.64.0/18`,
`146.148.8.0/21`, `162.216.148.0/22`, `162.222.176.0/21`,
`173.255.112.0/20`, `192.158.28.0/22`, `199.192.112.0/22`,
`199.223.232.0/22`, `199.223.236.0/23`, `208.68.108.0/23`,
`23.236.48.0/20`, `23.251.128.0/19`, `35.184.0.0/14`,
`35.188.0.0/15`, `35.190.0.0/17`, `35.190.128.0/18`,
`35.190.192.0/19`, `35.190.224.0/20`, `8.34.208.0/20`,
`8.35.192.0/21`, `8.35.200.0/23`,],
`Hetzner`: [`129.232.128.0/17`, `129.232.156.128/28`, `136.243.0.0/16`,
`138.201.0.0/16`, `144.76.0.0/16`, `148.251.0.0/16`,
`176.9.12.192/28`, `176.9.168.0/29`, `176.9.24.0/27`,
`176.9.72.128/27`, `178.63.0.0/16`, `178.63.120.64/27`,
`178.63.156.0/28`, `178.63.216.0/29`, `178.63.216.128/29`,
`178.63.48.0/26`, `188.40.0.0/16`, `188.40.108.64/26`,
`188.40.132.128/26`, `188.40.144.0/24`, `188.40.48.0/26`,
`188.40.48.128/26`, `188.40.72.0/26`, `196.40.108.64/29`,
`213.133.96.0/20`, `213.239.192.0/18`, `41.203.0.128/27`,
`41.72.144.192/29`, `46.4.0.128/28`, `46.4.192.192/29`,
`46.4.84.128/27`, `46.4.84.64/27`, `5.9.144.0/27`,
`5.9.192.128/27`, `5.9.240.192/27`, `5.9.252.64/28`,
`78.46.0.0/15`, `78.46.24.192/29`, `78.46.64.0/19`,
`85.10.192.0/20`, `85.10.228.128/29`, `88.198.0.0/16`,
`88.198.0.0/20`],
`Linode`: [`104.200.16.0/20`, `109.237.24.0/22`, `139.162.0.0/16`,
`172.104.0.0/15`, `173.255.192.0/18`, `178.79.128.0/21`,
`198.58.96.0/19`, `23.92.16.0/20`, `45.33.0.0/17`,
`45.56.64.0/18`, `45.79.0.0/16`, `50.116.0.0/18`,
`80.85.84.0/23`, `96.126.96.0/19`],
}複製程式碼
我建立了一個工具函式,可以傳入一個 IPv4 地址和一個 CIDR 塊列表,它會告訴我這個 IP 地址是不是屬於給定的這些 CIDR 塊中的任何一個。
def in_block(ip, block):
_ip = IPAddress(ip)
return any([True
for cidr in block
if _ip in IPNetwork(cidr)])複製程式碼
下面這個函式接收請求( req )和瀏覽器代理( agent )的物件,並嘗試用這兩個物件來判斷流量源頭/瀏覽器代理是否來自爬蟲機器人。這個瀏覽器代理物件是使用 Python 使用者代理庫構造的,並且有一些測試用於判斷,使用者代理字串是否屬於某個已知的爬蟲機器人。我已經用一些我從庫的分類系統中看到的 token 來擴充套件這些測試。同時我在 CIDR 塊迭代,來判斷遠端主機的 IPv4 地址是否在裡面。
def bot_test(req, agent):
ua_tokens = [`daum/`, # Daum Communications Corp.
`gigablastopensource`,
`go-http-client`,
`http://`,
`httpclient`,
`https://`,
`libwww-perl`,
`phantomjs`,
`proxy`,
`python`,
`sitesucker`,
`wada.vn`,
`webindex`,
`wget`]
is_bot = agent.is_bot or
any([True
for cidr in CIDRS.values()
if in_block(req[`remote_host`], cidr)]) or
any([True
for token in ua_tokens
if token in agent.ua_string.lower()])
return is_bot複製程式碼
下面是指令碼的主要部分。web 流量日誌從標準輸入裡一行行地讀入。內容的每一行都被解析成一個帶 token 版本的請求、使用者代理和被請求的 URI。這些物件讓與這些資料打交道變得更容易,不需要去麻煩地在空中解析它們。
我嘗試著用 MaxMind 的庫查詢與這些 IPv4 相關的城市和國家。如果有任何型別的查詢失敗,結果會簡單地設定為 None。
在爬蟲機器人測試後,我準備輸出。如果請求看起來是從爬蟲機器人處傳送的,它會被標成紅色背景,高亮在輸出上。
if __name__ == `__main__`:
while True:
try:
line = sys.stdin.readline()
except KeyboardInterrupt:
break
if not line:
break
req = line_parser(line)
agent = parse(req[`request_header_user_agent`])
uri = urlparse(req[`request_url`])
try:
response = reader.city(req[`remote_host`])
country, city = response.country.iso_code, response.city.name
except:
country, city = None, None
is_bot = bot_test(req, agent)
agent_str = `, `.join([item
for item in agent.browser[0:3] +
agent.device[0:3] +
agent.os[0:3]
if item is not None and
type(item) is not tuple and
len(item.strip()) and
item != `Other`])
ip_owner_str = ` `.join([network + ` IP`
for network, cidr in CIDRS.iteritems()
if in_block(req[`remote_host`], cidr)])
print Back.RED + `b` if is_bot else `h`,
country,
city,
uri.path,
agent_str,
ip_owner_str,
Style.RESET_ALL複製程式碼
爬蟲機器人檢測實戰
接下來是一個例子,在把這些內容放到監測指令碼時,我是用下面這種方式連線輸出 web 流量日誌的最後一百行的。
$ ssh server
`tail -n100 -f access.log`
| python monitor.py複製程式碼
有可能來源於爬蟲機器人的請求將使用紅色背景和「b」字首高亮。不存在爬蟲機器人的流量將被打上「h」的字首,代表 human(人)。下面是從指令碼出來的樣例輸出,不過沒有 ANSI 背景色。
...
b US Indianapolis /robots.txt Python Requests 2.2 Linux 3.2.0
h DE Hamburg /tensorflow-vizdoom-bots.html Firefox 45.0 Windows 7
h DE Hamburg /theme/css/style.css Firefox 45.0 Windows 7
h DE Hamburg /theme/css/syntax.css Firefox 45.0 Windows 7
h DE Hamburg /theme/images/mark.jpg Firefox 45.0 Windows 7
b US Indianapolis /feeds/all.atom.xml rogerbot 1.0 Spider Spider Desktop
b US Mountain View /billion-nyc-taxi-kdb.html Google IP
h CH Zurich /billion-nyc-taxi-rides-s3-vs-hdfs.html Chrome 56.0.2924 Windows 7
h IE Dublin /tensorflow-vizdoom-bots.html Chrome 56.0.2924 Mac OS X 10.12.0
h IE Dublin /theme/css/style.css Chrome 56.0.2924 Mac OS X 10.12.0
h IE Dublin /theme/css/syntax.css Chrome 56.0.2924 Mac OS X 10.12.0
h IE Dublin /theme/images/mark.jpg Chrome 56.0.2924 Mac OS X 10.12.0
b SG Singapore /./theme/images/mark.jpg Slack-ImgProxy Spider Spider Desktop Amazon IP複製程式碼