欢迎大家关注我的专题:爬虫修炼之道
上篇 爬虫修炼之道——编写一个爬取多页面的网络爬虫主要讲解了如何使用python编写一个可以下载多页面的爬虫,如何将相对URL转为绝对URL,如何限速,如何设置代理。本篇将讲解如何从下载下来的html文件中提取结构化数据。涉及到的模块有:
- re - python中和正则表达式相关的库
- urlparse 此模块定义了一个标准接口,用于组合URL字符串,并将“相对URL”转换为给定“基本URL”的绝对URL。
- urllib2 - 此模块对urllib模块进行了增加,增加了将url封装为Request的打开方式。
- lxml - lxml是用于在Python语言中处理XML和HTML的最具功能和易于使用的库。
- pandas 是一个数据分析库,可以对csv、excel等格式文件进行方便的读写。
明确目标
我们这次想要做的就是提取出糗百的文本板块的和糗事相关的数据,并结构化它们:作者、糗事的链接、作者的链接、作者的性别、糗事的内容、糗事的链接、好笑数、评论数。在这儿我们称为item,对应的python代码为:
item = ['author', 'author_href', 'author_sex', 'context', 'context_url', 'vote', 'comment']
解析HTML
python中解析HTML文件常用的库有三个:re(正则表达式库)、lxml 和 Beautiful Soup。三个库的特点如下:
名称 | 性能 | 使用难度 | 安装难度 |
---|---|---|---|
re | 快 | 困难 | 简单(内置) |
Beautiful Soup | 慢 | 简单 | 简单(纯python) |
Lxml | 快 | 简单 | 相对困难 |
这儿我们选用Lxml这个库
选择器
我们使用xpath来提取数据。xpath学习可参考以下链接:
- http://www.w3school.com.cn/xpath/index.asp
- http://zvon.org/xxl/XPathTutorial/General_chi/examples.html
- https://msdn.microsoft.com/zh-cn/library/ms256115(v=vs.80).aspx
HTML结构
我们观察糗百的文本模块,发现一页有20条糗事,我们可以先拿到包含20条糗事的模块的html代码,然后再从中解析出每条糗事的html代码,每条糗事的html代码类似于下面的结构:
<div class="article block untagged mb15" id="qiushi_tag_118589872">
<div class="author clearfix">
<a href="/users/31146403/" target="_blank" rel="nofollow">
![飞o鸟](http://upload-images.jianshu.io/upload_images/4758863-d37f49f1d7e27422.JPEG?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
</a>
<a href="/users/31146403/" target="_blank" title="飞o鸟">
<h2>飞o鸟</h2>
</a>
<div class="articleGender manIcon">28</div>
</div>
<a href="/article/118589872" target="_blank" class="contentHerf">
<div class="content">
<span>今天美女同事对我说:“有件事我前几天就想问你了,你明天有空吗?”我心里一惊,莫非这是迟来的约会吗?于是赶快回答:“有空!”女同事感激道:“太好了,替我代一天班吧,我要出去约会!”额...</span>
</div>
</a>
<div class="stats">
<span class="stats-vote"><i class="number">1177</i> 好笑</span>
<span class="stats-comments">
<span class="dash"> · </span>
<a href="/article/118589872" data-share="/article/118589872" id="c-118589872" class="qiushi_comments" target="_blank">
<i class="number">4</i> 评论
</a>
</span>
</div>
<div id="qiushi_counts_118589872" class="stats-buttons bar clearfix">
<ul class="clearfix">
<li id="vote-up-118589872" class="up">
<a href="javascript:voting(118589872,1)" class="voting" data-article="118589872" id="up-118589872" rel="nofollow">
<i></i>
<span class="number hidden">1192</span>
</a>
</li>
<li id="vote-dn-118589872" class="down">
<a href="javascript:voting(118589872,-1)" class="voting" data-article="118589872" id="dn-118589872" rel="nofollow">
<i></i>
<span class="number hidden">-15</span>
</a>
</li>
<li class="comments">
<a href="/article/118589872" id="c-118589872" class="qiushi_comments" target="_blank">
<i></i>
</a>
</li>
</ul>
</div>
<div class="single-share">
<a class="share-wechat" data-type="wechat" title="分享到微信" rel="nofollow">微信</a>
<a class="share-qq" data-type="qq" title="分享到QQ" rel="nofollow">QQ</a>
<a class="share-qzone" data-type="qzone" title="分享到QQ空间" rel="nofollow">QQ空间</a>
<a class="share-weibo" data-type="weibo" title="分享到微博" rel="nofollow">微博</a>
</div>
<div class="single-clear"></div>
</div>
提取内容并保存
我们使用xpath来提取数据,代码如下:
# coding=utf-8
import re
import urlparse
import lxml.html
import pandas as pd
from link_crawler import download
def parse_item(html, url, item):
"""
从html字符串中提取出结构化数据,然后返回rows
:param html: 需要提取的html字符串
:param url: 该html所对应的url
:param item: 需要提取的字段
:return:
"""
# 存放当前页面的所有糗事模块的内容,子元素类型也为列表,内容为author,author_href,author_sex,context,vote,comment
rows = []
tree = lxml.html.fromstring(html)
# 提取了一个包含当前页面的所有糗事模块列表,长度为20,对应当前页面的20条糗事模块
frame = tree.xpath('//*[@id="content-left"]/div[@class="article block untagged mb15"]')
sex_regex = re.compile('\s+(.*)Icon')
for i in xrange(len(frame)):
row = []
# 提取作者名字、作者链接、作者性别
author_module = frame[i].xpath('./div[@class="author clearfix"]')[0]
try:
author = author_module.xpath('./a[2]/@title')[0]
author_href = urlparse.urljoin(url, author_module.xpath('./a[2]/@href')[0])
author_sex = sex_regex.findall(author_module.xpath('./div/@class')[0])[0]
except:
author = author_module.xpath('.//h2/text()')[0]
author_href = ''
author_sex = ''
row.append(author)
row.append(author_href)
row.append(author_sex)
# 糗事内容和糗事链接
context = unicode(frame[i].xpath('string(./a[1])')).strip()
context_url = urlparse.urljoin(url, unicode(frame[i].xpath('string(./a[1]/@href)')))
row.append(context)
row.append(context_url)
# 好笑数和评论数
vote = frame[i].xpath('string(./div[@class="stats"]/span[@class="stats-vote"]/i)')
comment = frame[i].xpath('string(./div[@class="stats"]/span[@class="stats-comments"]//i)')
row.append(vote)
row.append(comment)
print dict(zip(item, row))
rows.append(row)
return rows
def save_csv(file_name, rows, columns):
"""
将内容存为csv文件
:param file_name: 写入的文件名
:param rows: 写入的内容
:param columns: 写入的字段
:return:
"""
data = pd.DataFrame(rows, columns=columns)
data.to_csv(file_name, index=False, encoding='utf_8_sig')
if __name__ == "__main__":
url = 'http://www.qiushibaike.com/text/'
headers = {'User-Agent': 'crawl'}
html = download(url, headers)
file_name = 'qsbk_text.csv'
item = ['author', 'author_href', 'author_sex', 'context', 'context_url', 'vote', 'comment']
data = parse_item(html, url, item)
save_csv(file_name, data, item)
其中 download 方法是上篇中的方法。运行如下:
Downloading url: http://www.qiushibaike.com/text/
{'comment': '40', 'author_sex': 'women', 'context_url': u'http://www.qiushibaike.com/article/118585314', 'author': u'\u54c7\u567b\uff5epeach', 'author_href': 'http://www.qiushibaike.com/users/30423399/', 'context': u'\u6700\u8fd1\u5728\u601d\u8003\u529e\u4e2a\u5065\u8eab\u623f\u7684\u4f1a\u5458\uff0c\u95fa\u871c\u77e5\u9053\u4e86\u8ddf\u6211\u8bf4:\u201c\u529e\u90a3\u4e2a\u5e72\u561b\uff1f\u8df3\u5e7f\u573a\u821e\u591a\u597d\uff0c\u8fd8\u4e0d\u65f6\u6709\u5927\u5988\u7ed9\u4f60\u4ecb\u7ecd\u5bf9\u8c61\u3002\u201d', 'vote': '2010'}
{'comment': '28', 'author_sex': 'man', 'context_url': u'http://www.qiushibaike.com/article/118584535', 'author': u'\u4e13\u4e1a^O^\u4e30\u80f8', 'author_href': 'http://www.qiushibaike.com/users/8324042/', 'context': u'\u521a\u521a\u9047\u5230\u7684\uff0c\u6211\u4eec\u5c0f\u533a\u4e00\u56db\u5c81\u5c0f\u5b69\u7279\u6dd8\u6c14\uff0c\u4eca\u5929\u88ab\u4ed6\u7238\u7238\u5988\u5988\u7237\u7237\u5976\u5976\u6bcf\u4eba\u6253\u4e86\u4e00\u987f\uff0c\u4e00\u4e2a\u4eba\u5728\u95e8\u5916\u561f\u56d4\uff1a\u2018\u8fd9\u5bb6\u4eba\u6ca1\u4e00\u4e2a\u597d\u4e1c\u897f\uff0c\u90fd\u6253\u6211\u2019\u2018\u5f53\u65f6\u6211\u5c31\u7b11\u55b7\u4e86\uff0c\uff0c\uff0c\uff0c', 'vote': '2005'}
....
然后使用excel打开 qsbk_text.csv
文件,可以看到类似于这样的内容:
设置回调函数
上篇我们讲解了如何下载多个页面(URL),在这里我们可以将上篇内容和这篇内容结合起来,每当下载一个URL后,就调用我们的parse_item方法。最后将所有页面的item
想要完成以上功能,需要修改上篇中的 link_crawler 方法。修改后的 <a id="link_crawler" href="#link_crawler">link_crawler</a> 如下:
def link_crawler(seed_url, link_regex, delay=2, user_agent='crawl', headers=None, proxy=None, num_retries=2, time_out=3, item=None, callback=None):
"""
下载一个URL,提取出给定的item,然后根据link_regex规则跟进链接并提取出来跟进链接中的item
:param seed_url: 种子URL
:param link_regex: 从种子URL页面中提取跟进链接所用的正则表达式
:param delay: 爬取同一域下的URL暂停时间
:param user_agent: 用户代理
:param headers: 头
:param proxy: 代理
:param num_retries: 下载一个页面失败后的重试次数
:param time_out: 下载一个URL的超时时间
:param item: 需要提取的item
:param callback: 回调函数,用于提取item
:return: 如果callback和item不为None,则返回一个item列表,否则返回空列表
"""
item_list = []
crawl_queue = [seed_url] # 需要下载的URL列表
throttle = Throttle(delay) # 限速器
headers = headers or {}
if user_agent:
headers['User-agent'] = user_agent
while crawl_queue:
url = crawl_queue.pop() # 将种子URL弹出
html = download(url, headers, proxy, num_retries, time_out) # 下载当前URL页面
if callback:
items = callback(html, url, item)
item_list.extend(items)
throttle.wait(url) # 根据该url所对应的域来决定是否需要暂停delay秒
links = get_links(html, link_regex) # 得到当前URL页面的跟进链接
print 'links: %s' % links
for link in links:
link = urlparse.urljoin(seed_url, link)
crawl_queue.append(link)
return item_list
测试运行
代码github地址:https://github.com/Oner-wv/spider_note/tree/master/chapter02
现在来运行整个代码
link_regex = re.compile('<li>.*<a\s+href="(.*?)".*<span class="next">', re.S)
item = ['author', 'author_href', 'author_sex', 'context', 'context_url', 'vote', 'comment']
data = link_crawler('http://www.qiushibaike.com/text/', link_regex, delay=1, user_agent='crawl', num_retries=2, time_out=3, item=item, callback=parse_item)
file_name = "qsbk_text_full.csv"
save_csv(file_name, data, item)
运行后打开qsbk_text_full.csv文件。一共有701行数据,除去头文件,共700行,35 * 20 = 700
,35为页数,20为每页的糗事条数,说明已经将糗事百科的文本板块下的数据全部爬取下来了。
下篇我们将讲解如何爬取图片 爬虫修炼之道——从网页中下载所需图片(以下载糗百热图为例)
更多内容请关注公众号:AI派