因?yàn)楣ぷ鞯脑?,近期筆者開(kāi)始持續(xù)關(guān)注一些安全咨詢(xún)網(wǎng)站,一來(lái)是多了解業(yè)界安全咨詢(xún)提升自身安全知識(shí),二來(lái)也是需要從各類(lèi)安全網(wǎng)站上收集漏洞情報(bào)。
作為安全情報(bào)領(lǐng)域的新手,面對(duì)大量的安全咨詢(xún),多少還是會(huì)感覺(jué)無(wú)從下手力不從心。周末閑來(lái)無(wú)事,突發(fā)奇想,如果搞個(gè)爬蟲(chóng),先把網(wǎng)絡(luò)安全類(lèi)文章爬下來(lái),然后用機(jī)器學(xué)習(xí)先對(duì)文章進(jìn)行分析,自動(dòng)提取文章主成分關(guān)鍵詞,然后再根據(jù)實(shí)際需求有選擇的閱讀相關(guān)文章,豈不是可以節(jié)省很多時(shí)間。
如果能提取文章的關(guān)鍵詞,還可以根據(jù)近期文章的關(guān)鍵詞匯總了解總體的安全態(tài)勢(shì)和輿情,感覺(jué)挺靠譜。
整體思路
如前文所述,思路其實(shí)很簡(jiǎn)單:
- 用Scrapy先去安全咨詢(xún)網(wǎng)站上爬取文章的標(biāo)題和內(nèi)容
- 對(duì)文章的內(nèi)容進(jìn)行切詞
- 使用TF-IDF算法提取關(guān)鍵詞
- 將關(guān)鍵詞保存到數(shù)據(jù)庫(kù)
- 最后可以用可視化將近期出現(xiàn)的比較頻繁的關(guān)鍵詞做個(gè)展示
看起來(lái)也不會(huì)很難,文末有代碼的鏈接。
Scrapy爬蟲(chóng)
Scrapy是非常常用的python爬蟲(chóng)框架,基于scrapy寫(xiě)爬蟲(chóng)可以節(jié)省大量的代碼和時(shí)間,原理這里就不贅述了,感興趣的同學(xué)自行科普Scrapy教程,這里只貼一張圖。

安裝Scrapy
筆者基于python3.6來(lái)安裝Scrapy,所以前提是你的機(jī)器已經(jīng)安裝好python3的環(huán)境。scrapy安裝辦法非常簡(jiǎn)單,使用pip可以一鍵安裝
pip3 install scrapy
裝好以后,不熟悉scrapy的同學(xué)可以先看看官方示例程序熟悉一下,在cmd里執(zhí)行下面的命令生成示例程序
scrapy startproject tutorial
即可在當(dāng)前目錄自動(dòng)創(chuàng)建一個(gè)完整的示例教程,這里我們可以看到整個(gè)爬蟲(chóng)的目錄結(jié)構(gòu)如下:
tutorial/
scrapy.cfg # deploy configuration file
tutorial/ # project's Python module, you'll import your code from here
__init__.py
items.py # project items definition file
pipelines.py # project pipelines file
settings.py # project settings file
spiders/ # a directory where you'll later put your spiders
__init__.py
分析網(wǎng)頁(yè)
本例以“E安全”網(wǎng)站為例,他們提供的安全咨詢(xún)質(zhì)量還是不錯(cuò)的,每天都有更新。大致看一眼網(wǎng)站的結(jié)構(gòu),會(huì)發(fā)現(xiàn)這個(gè)站點(diǎn)導(dǎo)航欄上有十多個(gè)安全咨詢(xún)分類(lèi),點(diǎn)進(jìn)去發(fā)現(xiàn)每個(gè)分類(lèi)的url大致為https://www.easyaq.com/type/*.shtml,而每個(gè)分類(lèi)下面又有相關(guān)的文章和鏈接若干。到這里思路就很清楚了,先遍歷這幾個(gè)文章分類(lèi),然后動(dòng)態(tài)獲取每個(gè)分類(lèi)下的文章鏈接,之后挨個(gè)訪問(wèn)文章鏈接并把內(nèi)容保存下來(lái),下面分析一下主要的代碼。
爬取網(wǎng)頁(yè)
爬蟲(chóng)主體代碼如下,使用scrapy的框架開(kāi)發(fā)的爬蟲(chóng)實(shí)際的代碼是非常精簡(jiǎn)的
import scrapy
from scrapy import Request, Selector
from sec_news_scrapy.items import SecNewsItem
class SecNewsSpider(scrapy.Spider):
name = "security"
allowed_domains = ["easyaq.com"]
start_urls = []
for i in range(2, 17):
req_url = 'https://www.easyaq.com/type/%s.shtml' % i
start_urls.append(req_url)
def parse(self, response):
topics = []
for sel in response.xpath('//*[@id="infocat"]/div[@class="listnews bt"]/div[@class="listdeteal"]/h3/a'):
topic = {'title': sel.xpath('text()').extract(), 'link': sel.xpath('@href').extract()}
topics.append(topic)
for topic in topics:
yield Request(url=topic['link'][0], meta={'topic': topic}, dont_filter=False, callback=self.parse_page)
def parse_page(self, response):
topic = response.meta['topic']
selector = Selector(response)
item = SecNewsItem()
item['title'] = selector.xpath("http://div[@class='article_tittle']/div[@class='inner']/h1/text()").extract()
item['content'] = "".join(selector.xpath('//div[@class="content-text"]/p/text()').extract())
item['uri'] = topic['link'][0]
print('Finish scan title:' + item['title'][0])
yield item
我們把網(wǎng)站上所有分類(lèi)的url枚舉出來(lái)放在start_url里面,parse是框架執(zhí)行爬蟲(chóng)任務(wù)的入口,框架會(huì)自動(dòng)訪問(wèn)前面start_url設(shè)置的頁(yè)面,返回一個(gè)response對(duì)象,從這個(gè)對(duì)象中可以通過(guò)xpath提取有用的信息。
這里我們要從每一個(gè)類(lèi)型頁(yè)面的html中分析出文章的標(biāo)題和訪問(wèn)uri,谷歌的chrome提供了很好的xpath生成工具,可以快速提取目標(biāo)的xpath,在瀏覽器中按F12可以看到網(wǎng)頁(yè)的html源碼,找到需要提取的內(nèi)容,右鍵可以提取xpath。

獲取到文章內(nèi)容的uri還沒(méi)有完,我們還需要進(jìn)一步訪問(wèn)該uri,并且把文章的內(nèi)容記錄下來(lái)供下一步分析,這里的parse_page函數(shù)就是用來(lái)做內(nèi)容抽取的,方法同上,借助chrome的xpath分析工具很快就能提取到文章內(nèi)容。
內(nèi)容提取到以后,這里將內(nèi)容存到Item中,Item是Scrapy框架的另一個(gè)組成部分,類(lèi)似于字典類(lèi)型,主要是用來(lái)定義傳遞數(shù)據(jù)的格式,而傳遞是為了下一步數(shù)據(jù)持久化。
數(shù)據(jù)持久化
Item.py
class SecNewsItem(scrapy.Item):
title = scrapy.Field()
content = scrapy.Field()
uri = scrapy.Field()
pass
pipeline.py
import jieba
import jieba.analyse
import pymysql
import re
def dbHandle():
conn = pymysql.connect(
host="localhost",
user="root",
passwd="1234",
charset="utf8",
db='secnews',
port=3306)
return conn
def is_figure(str):
value = re.compile(r'^\d+$')
if value.match(str):
return True
else:
return False
def save_key_word(item):
words = jieba.analyse.extract_tags(item['content'], topK=50, withWeight=True)
conn = dbHandle()
cursor = conn.cursor()
sql = "insert ignore into t_security_news_words(title, `key`, val) values (%s,%s,%s)"
try:
for word in words:
if is_figure(word[0]):
continue
cursor.execute(sql, (item['title'][0], word[0], int(word[1] * 1000)))
cursor.connection.commit()
except BaseException as e:
print("存儲(chǔ)錯(cuò)誤", e, "<<<<<<原因在這里")
conn.rollback()
def save_article(item):
conn = dbHandle()
cursor = conn.cursor()
sql = "insert ignore into t_security_news_article(title, content, uri) values (%s,%s,%s)"
try:
cursor.execute(sql, (item['title'][0], item['content'], item['uri']))
cursor.connection.commit()
except BaseException as e:
print("存儲(chǔ)錯(cuò)誤", e, "<<<<<<原因在這里")
conn.rollback()
class TutorialPipeline(object):
def process_item(self, item, spider):
save_key_word(item)
save_article(item)
return item
settings.py
ITEM_PIPELINES = {
'sec_news_scrapy.pipelines.TutorialPipeline': 300,
}
爬蟲(chóng)主程序中收集到的Item會(huì)傳入到這里,這里有兩個(gè)步驟save_key_word和save_article,后者將文章的標(biāo)題、內(nèi)容、uri存入到MySQL表里;這里著重介紹前者save_key_word函數(shù)。
我們的目標(biāo)是自動(dòng)分析文章里面跟主題相關(guān)的關(guān)鍵字,并且分析出每個(gè)詞的權(quán)重,具體來(lái)說(shuō)包含以下步驟:
- 切詞:中文切詞工具有很多,這里我選擇用jieba實(shí)現(xiàn)
- 提取關(guān)鍵字:jieba里面已經(jīng)實(shí)現(xiàn)好了TF/IDF的算法,我們利用該算法從每篇文章里選擇top50的詞匯,并且?guī)蠙?quán)重。用這種方式提取關(guān)鍵字還可以直接把常見(jiàn)的提用詞過(guò)濾掉,當(dāng)然jieba也支持自定義停用詞
words = jieba.analyse.extract_tags(item['content'], topK=50, withWeight=True)

-
數(shù)據(jù)存儲(chǔ):提取到需要的信息,下一步需要把信息保存到MySQL,在python3下面可以用pymysql來(lái)操作MySQL
文章列表
關(guān)鍵字列表
關(guān)鍵詞可視化-詞云
通過(guò)上面的程序,我們已經(jīng)可以把網(wǎng)站上的安全咨詢(xún)文章全部爬取到數(shù)據(jù)庫(kù),并且從每篇文章里面提取50個(gè)關(guān)鍵字。接下來(lái)我們希望把這些關(guān)鍵詞用可視化的方式展示出來(lái),出現(xiàn)頻度高的關(guān)鍵詞做高亮顯示,所以很自然的想到用詞云展示。
這里我們用eChart提供的echarts-wordcloud組件來(lái)做。做法非常簡(jiǎn)單,從MySQL的關(guān)鍵詞表里統(tǒng)計(jì)數(shù)據(jù),生成k-v字串用正則直接替換到html頁(yè)面,當(dāng)然這里更優(yōu)雅的做法應(yīng)該是用ajax從DB里取數(shù)據(jù),我這里就先取個(gè)巧了。
def get_key_word_from_db():
words = {}
conn = dbHandle()
try:
with conn.cursor() as cursor:
cursor.execute(
"select `key`, sum(val) as s from t_security_news_words group by `key` order by s desc limit 300")
for res in cursor.fetchall():
words[res[0]] = int(res[1])
return words
except BaseException as e:
print("存儲(chǔ)錯(cuò)誤", e, "<<<<<<原因在這里")
conn.rollback()
return {}
finally:
conn.close()
查看動(dòng)態(tài)效果點(diǎn)這里,詞云將詞匯按照出現(xiàn)的頻度或者權(quán)重與字體大小做關(guān)聯(lián),頻度越高字體越大,從中我們可以大致感知到當(dāng)前業(yè)界一些安全趨勢(shì),當(dāng)然這也僅僅是一個(gè)例子。

調(diào)試技巧
python有很多IDE可選,筆者選擇用pycharm,在調(diào)試scrapy程序的時(shí)候,需要用到scrapy的引擎啟動(dòng),所以用默認(rèn)的pycharm沒(méi)法調(diào)試,需要做一些設(shè)置,如下圖所示
run -> Edit Configurations
script填寫(xiě)scrapy安裝目錄里面的cmdline.py的位置;Script parameters是執(zhí)行scrapy時(shí)用的參數(shù),security是我們這個(gè)爬蟲(chóng)的名字;Working directory寫(xiě)爬蟲(chóng)的根目錄。

配置好以后就可以直接用pycharm來(lái)啟動(dòng)debug了,run -> debug 'xxx'
完整的代碼示例,包含echart的部分,請(qǐng)見(jiàn)github

