Ea5ter's Bolg

舆情检测爬虫总结

字数统计: 2k阅读时长: 7 min
2018/10/17 Share

最近和 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 的东西,虽然最后这些东西可能都不会再看了,但让我又重新审视了自己,感觉也不算太差。
然后还有这个爬虫写了很久,改了也不少,新加的需求还在处理。
去西安耍了一转,回来还有落下的一大堆课程要补。
今天听了盛锅给大一讲课,突然感觉一处知识的沉淀是多么重要。
“切莫急功近利,小心前功尽弃。”,希望来月还有新愿。

CATALOG
  1. 1. 搜索方案
  2. 2. 网页下载器
  3. 3. 网页解析器
    1. 3.1. 链接提取
    2. 3.2. 关键字段抓取
  4. 4. 传入参数
  5. 5. 输出内容
  6. 6. 结语