舆情检测爬虫总结

最近和 bak6ry 同学写了一个舆情检测爬虫,还是耗了了不少精力。其中遇到了一些问题,再此记录一下。

搜索方案

这个爬虫的要求是:

  1. 可以指定网站地址、域名、URL,可以支持多个
  2. 可以指定爬虫深度,比如3级链接或5级链接
  3. 可以指定关键字
  4. 搜索结果可以根据关键字查询、生成文档或excel、需要保存命中页面的URL,及相关关键字部分的一些文字摘要

接下来搜索策略。
既然有深度要求,那最简单的实现方法就是用递归。用DFS算法,递归到指定的深度后就回溯。同时再每次搜索时,还要做当前页面的 url 收集和关键内容的爬取,并且将关键内容输出。
很简单的写出了 demo :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import html_download # 网页下载器,用于下载源码,考虑js
import html_parser # 用于解析源码,关键字处理
import message_output # 输出数据

def crawl(urls, keywords, depth):
for url in urls:
if depth == 0: # 深度达到则回溯
html_source = html_download.download(url)
urls, key_message = html_parser.parse(html_source, keywords)
message_output.output(key_message)
crawl(urls, keywords, depth - 1)
else:
return

def main(root_url, keywords, depth):
crawl(root_url, keywords, depth)

if __name__ == "__main__":
main(root_url, keywords, depth) # url(域名 网站地址)、关键字、深度,由用户提供;注意点:url要处理成可访问的
pass

网页下载器

这应该是所有模块中最简单的一个,只要得到当前 url 下的源码就完事了。因为要采集 js ,所以使用无头浏览器来做。
此外还要注意网页无法访问的情况,出现这种情况可能是:

  1. 链接构造错误:毕竟网上的链接千奇百怪,难免会有没想到的构造情况。
  2. 404 无法访问。

所以这里加上一个状态码的判断来回避这些情况。

1
2
3
4
r = requests.get(url)
if r.status_code != 200:
print "ERROR URL : 'url'"
return None

网页解析器

网页解析器的作用是将下载下来的源码进行解析,从中提取其中的链接和关键字筛选的内容。

链接提取

一般网页的链接都在 href 属性中,而 href 又在 a 标签中 ,所以在一开始我想的是,先通过 BeautifulSoup 选择器将所有带有 herf 属性的 a 标签筛选出来,再来构造链接。
但之后在某个新闻网测试时出现了匪夷所思的一幕:

原本在网页上内容居然没有了,所以爬虫在爬取此页面时内容就抓不到。
通过Bp的抓包分析,发现了加载新闻的 html 页面时,还加载了其他链接。其中的文章内容链接就在 iframe 标签下的 src 属性中。

所以还要再抓 src 属性下的链接,再使用黑名单过滤,最后再构造链接。写出 getNewUrls 的demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def get_new_urls(html_source):
cur_urls = set()
hUrls = html_source.findAll("a", attrs={"href": True})
sUrls = html_source.findAll(attrs={"src": True})

for url in hUrls:
url = url_clean(url["href"]) # 处理成可访问的链接
if url is None:
continue
elif re.compile("黑名单过滤规则").findall(url):
continue
else:
old_urls.add(url)
cur_urls.add(url)

return cur_urls

关键字段抓取

我觉得这个部分的搜索策略是一个难点,在有多个关键字的情况下,如何来筛选文本。
如果只有一个关键字的话,搜索关键字前后 n 个字符的文本也不算太难。但如果是多个的话,考虑到文本的重叠和文本的完整性,写起来就比较麻烦了。
用 BeautifulSoup 选择器显然是不够的,所以这里使用了正则表达式来匹配出带有关键字的文本。
就像前人所说:“一个问题如果要用正则表达式,那么它就会变成两个问题。”,在经过一番 Google 后,写出了用来匹配的正则表达式。

(?=.*?aa)(?=.*?bb)

接下来写出生成匹配规则的函数:

1
2
3
4
5
6
7
def match_rule(keywords):
rule = '^'
for keyword in keywords:
rule += "(?=.*?%s)" % (keyword)
rule += '.+$'

return rule

考虑到网页中的文本大多出现在 a 标签和 p 标签中,所以最后就偷懒只循环 p 和 a 标签中的文本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def get_key_message(html_sourc, keywords):
key_message = []
rule = self.match_rule(keywords)
pLabel = html_sourc.findAll("p")
aLabel = html_sourc.findAll("a")

Labels= aLabel + pLabel

serch = re.compile(rule, re.I|re.M)
for p in Labels:
wenben = p.get_text().encode("utf8")
if wenben is None:
continue
Msg = serch.findall(wenben)
if Msg and Msg[0] not in key_message:
key_message.extend(Msg)

return key_message

传入参数

因为最后的成品是要在命令行中运行的,所以需要手动来传参。
这里用到的模块是 argparse ,这是一个用于命令行参数解析的模块,简单强大。使用方法就不细述了,因为很简单。下面简单写下模块的样式代码:

1
2
3
4
5
6
parser = argparse.ArgumentParser()
parser.add_argument("-s", "--string", nargs='+', help="...", required=True)
args = parser.parse_args()
answer = args.string

print answer

但是这简单的地方却出现了一个麻烦的编码问题。这是一段测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# _*_ coding: utf-8 _*_
import argparse

parser = argparse.ArgumentParser()
parser.add_argument("-s", "--string", nargs='+', help="...", required=True)
args = parser.parse_args()
answer = args.string

keywords = ['暗红色的' ,'浅紫色的']

print answer
print keywords
for i in range(0,2):
print answer[i], keywords[i]

设置命令行带参数运行

运行结果:

问题出现了,命令行给的参数在打印时出现了乱码,同时和代码中的字符串相比,编码方式似乎也不同。
再对比在命令行中的运行结果:

刚好相反,代码中的字符串出现了乱码。猜测编译器的文本编码和命令行的文本编码不同。
在 python 解释器打开时,此时的解释器就是一个文本编辑器,而读取代码的第一行内容 # __ coding: utf-8 __ 这一行就是来设定python解释器这个软件的编码使用的编码格式这个编码。
但在 Windows 终端中,文字的编码默认是 gbk2312 所以就出现了乱码。
所以在把终端的字符串传入时,应该修改编码为 utf8 。

1
2
for i in range(0, len(keywords)):
keywords[i] = keywords[i].decode('gb2312').encode('utf8')

输出内容

这一块的策略是,先将 url 、关键字、文本,存入数据库,最后再从数据库导出到 excel 中。
因为这块的代码是 bak6ry 同学写的,详细的细节不清楚,在这也就不提了。不过我在做最后的代码优化时,发现在数据库的操作中有一些要注意的地方。
在 python 中数据库的操作主要使用 窗口/游标 这种方式。在一开始的代码,在进行数据库的创建、插入、最后的导出等操作时,都是先连接数据库再创建游标,如此循环对资源的消耗特别大,而且容易忘了关闭游标对象,从而造成数据泄露。
在优化时,我们在主函数中创建 窗口/游标 对象,传参时传递的就一直是同一个 窗口/游标 对象。在减少代码量的同时,关闭对象也变得更方便。
下面是主函数的完整代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def main(self, dbuser, dbpassword, root_url, keywords, depth, curDepth):
path = os.getcwd()
path = path + '\\phantomjs-2.1.1-windows\\bin\\phantomjs.exe'
diver = webdriver.PhantomJS(executable_path=path)
conn = pymysql.connect(host='localhost', user='%s' % (dbuser), password='%s' % (dbpassword), db='mysql', charset = 'utf8')
cur = conn.cursor()
try:
self.output.create(conn, cur)
self.crawl(diver, root_url, keywords, depth, curDepth, conn, cur)
self.excelput.intoexcel(conn, cur)
finally:
diver.close()
cur.close()
conn.close()

结语

距上次写博客已有一个月之久了,当时的计划是学内网渗透,但计划赶不上变化。这一个多月中……
带着半赌气的性质,学了一会儿前端和 vue.js 的东西,虽然最后这些东西可能都不会再看了,但让我又重新审视了自己,感觉也不算太差。
然后还有这个爬虫写了很久,改了也不少,新加的需求还在处理。
去西安耍了一转,回来还有落下的一大堆课程要补。
今天听了盛锅给大一讲课,突然感觉一处知识的沉淀是多么重要。
“切莫急功近利,小心前功尽弃。”,希望来月还有新愿。