1.4 爬虫修炼之道——从网页中提取结构化数据并保存(以爬取糗百文本板块所有糗事为例)

欢迎大家关注我的专题:爬虫修炼之道

上篇 爬虫修炼之道——编写一个爬取多页面的网络爬虫主要讲解了如何使用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学习可参考以下链接:

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派

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,817评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,329评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,354评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,498评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,600评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,829评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,979评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,722评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,189评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,519评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,654评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,329评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,940评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,762评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,993评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,382评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,543评论 2 349

推荐阅读更多精彩内容

  • # Python 资源大全中文版 我想很多程序员应该记得 GitHub 上有一个 Awesome - XXX 系列...
    aimaile阅读 26,454评论 6 428
  • 一、十月综述 【阅读书籍2本】【主题阅读薪酬总额管理】【工作文明达标、年度预算、修编制度等】;【参加线下课程《商业...
    葉行绫素阅读 328评论 0 1
  • 找了好久才找到可以码字的一个地方。我姓刘,我心为你留。我说多年后我要带着你给我买的手机、衣服还有床头的肉包子我们通...
    陌生人的姑娘阅读 406评论 0 0
  • 骤然疏冷的空气,显示在手机屏幕上是14摄氏度。只穿着薄线衫的我被前一日的阳光欺骗,坐在教室里,手脚冰凉。 和北方的...
    毛毛锦阅读 450评论 0 3
  • 原来“简书”就是简单书写的意思。我也想在这里留下自己的点点滴滴,自己的爱恨情仇…… 坚持一年看一年后自己能成为什么...
    fall_in_love阅读 376评论 3 4