教女朋友学python系列--手把手教你用Python3进行网络爬虫

手把手教你用Python3进行网络爬虫


2018/6/11 星期一 整理

运行的环境:

  1. win10 x64

  2. 安装了anaconda3,基于Python3环境运行

  3. 使用Pycharm编程

1. 前期工作

2. 主要目的

作为一个从事大数据小白,既然口口声声的说自己从事大数据,那么如果说自己不懂得怎么去收集数据,实在是有点说不过去。之前一直有过进行爬虫的相关经验,但是一直的没有对自己的知识进行一些整理,每次都是按照教程一步一步的设置,然后爬取数据。总的过程很是繁琐,也走了不少弯路。废话不多说:laughing:。主要也是帮助我的「女朋友」完成论文所需要的文本素材。

直奔主题:本次主要是想收集小说网站,「武侠小说网http://www.wuxia.net.cn/author.html,中的关于武侠的所有文章。

网站的首页如下:

网站首页

我们使用chrome浏览器来分析其中的网络页面,“选中感兴趣的链接”→“右键检查”,弹出感兴趣的部分数据。

操作如图

使用谷歌浏览器,我们可以分析出我们需要爬取的主要两个步骤。

  1. 爬取「主页」中的所有作者链接,如第一张图片所示;

  2. 爬取作者页面下「所有文章」的文章链接;

  3. 爬取文章页面下的「所有文章章节」链接。

明确了思路之后,我们就依次实现这上面的实例。

3. 如何进行单个网页的爬取

我们明白了上面的思路,但我们该怎么一步一步的去实现,这个目的呢,就先就是如何利用Python进行单个网页的连接与解析。

如何使用request模块来获得网页的请求?这里有一个很重要的概念就是,在同一网站中,尽可能的使用同一个「session」。这样的目的可以很大节省我们“请求”→“服务器”之间请求的时间。具体代码操作如下。

session = requests.Session()
session.get(startUrl)  # 设置回话

查看request模块中,session.get方法的解释

解释图片

就是,我们使用get()方法之后,会获得一个对象,也会设定了一个会话session。这个session,可以继续用于我们在同一个网站内的访问。如果后面继续解析url的时候,没有使用同一个session (直接就是requests.get(url)),就会类似于认为每次重新打开浏览器,然后再输入链接,获取Response对象。而使用同一个session,就会认为是同一人在网站内直接内部跳转,这样可以加速解析url的速度。这也是每次我开始爬虫的时候,都会考虑设置一个「Session」对象。

1. 解析URL链接

url = "http://www.wuxia.net.cn/author.html"
res = session.get(url)  # 通过session来获得Response对象

如果你打印res的类型的话,就会发现是 <class 'requests.models.Response'>。返回Response对象,res就是描述整个url网页链接内容的描述结构。打印res.html属性,就会发现,和你直接在浏览器中查看源码所看到的内容是一样的。

2. 抛出链接解析的异常

当我们在链接大量的网页连接的时候,总是可能在执行的时候出现很多异常的情况,比如,链接超时,网页不存在等。导致网页异常,但是我们程序在运行的时候,通常都不会报告这种异常。我通常会在代码中加入 res.raise_for_status(),用来手动抛出异常。如果解析出现问题的话。

3. 正确的编码格式

不同网站的内容往往经过不同的编码,就算同一个网站,不同网页之间也存在使用不同编码的情况。如果不能够很好的处理网页编码的内容,那么很容易就得不到我们想要的结果。在解析的Response对象中,往往也会告诉我们该网站使用了何种编码格式。具体实现代码如下:

html = res.text
# 需要重新定义下编码格式,不然会出现乱码,无法正确匹配数据
html = html.encode(encoding=res.encoding, errors='ignore').decode(decodeType, errors='ignore')

上面中,res.encoding是Response对象的res的属性,decodeType是自己指定的类型,我通常指定为“GBK”。

4. 使用合适的解析器来解析html文档

对于[上面](#3. 正确的编码格式) 得到的html,就是我们通常接触到的网页标准格式,有四种不同的解析器来解析html,它们在解析速度和方法上对后面即将介绍的查找方法,有一定程度的影响。就是不同解析器解释出来的对象,使用同样的,“选择器”,“过滤器”可能会得出不一样的结论。如果有兴趣查看4种不同解析器的影响,可以BeautifulSoup 官方文档 安装解析器。我们选用通用性、解析速度都较优的“lxml” 。

bsObj = bs4.BeautifulSoup(html, 'lxml')

这样才真正的获得了一个BeautifulSoup对象。该对象,详细的解释了整个html的结构。

5. 如果获得自己感兴趣的那部分内容

上面4得到的bsObj对象,是完整的描述了整个网页内容,但是我们通常只需要获取其中我们感兴趣的一部分。这就要开始详细介绍BeautifulSoup中的「爬虫利器」,“选择器”select()方法。通过CSS的内容来选中自己需要的信息。

具体的可以参考官网教程

用select()方法寻找元素,用法介绍 soup.select("选择器内容"),“选择器的内容”及可匹配的含义表示如下

  • div: 匹配所有名为 <div> 的元素

  • #author:匹配所有id属性为author的元素

  • .notice:匹配使用CSS中class属性名为notice的元素

  • div span 所有在<div>元素内的 <span> 元素

  • div > span :所有在<div>元素之内的<span>元素,中间没有其它的元素

  • input[name]:所有名为<input>,并有一个name属性,其值无所谓的元素

  • input[type="button":所有名为<input>,并有一个type属性,其值为button的元素

上面只是列举一些常用的CSS选择器的模式,其他的可以参考别的资料。返回的是一个tag对象列表

6. 小结

通过上面的分析,我们可以将它们包装组合到一起,让我们下次,只需要输入「url」和「解析规则」,就可以只返回我们感兴趣的内容。具体代码如下:

def parseFullUrl(url, rule, session=None, decodeType='gbk'):
    """
    根据url和规则解析返回的数据
    :param url:
    :param rule:
    :param session:
    :param decodeType:
    :return:
    """
    try:
        res = session.get(url)
        res.raise_for_status()
    except Exception as e:
        logging.error("connection error: <{}> {}".format(url, e))
        return None

    html = res.text
    # 需要重新定义下编码格式,不然会出现乱码,无法正确匹配数据
    html = html.encode(encoding=res.encoding, errors='ignore').decode(decodeType, errors='ignore')
    bsObj = bs4.BeautifulSoup(html, 'lxml')
    return bsObj.select(rule)

4. 获取武侠主页的所有作者链接

通过上面的分析,我们就应该清晰的知道,需要通过「主页链接」来获得所有的作者链接信息。我们需要两个重要的信息,一个就是主页链接,这个很容易获取,就是“http://www.wuxia.net.cn/author.html”;另一个就是「解析规则」。解析规则可以借助Chrome中的F12工具获取,具体的操作如下。

复制解析规则

选中,“检查”→“Copy”→“Copy Selector”,这个是时候可以看到,复制出来的内容为

#main > table > tbody > tr:nth-child(2) > td.tb > p:nth-child(1) > a

语法需要参考CSS教程,这里不多说。但是如果你直接将这个复制到 bsObj.select(“”)中,你猜你会看到啥?

session = requests.Session()
session.get(startUrl)  # 设置回话
url = "http://www.wuxia.net.cn/author.html"
rule = "#main > table > tbody > tr:nth-child(2) > td.tb > p:nth-child(1) > a"
tags = parseFullUrl(url=url, rule=rule,session=session)
print(tags)

得到的结果,往往会是None。我个人也并不是很理解,很多情况我自己也是不断的在尝试,我个人认为可能是由于不同的解析器规则原因,因为解析的标准和实际CSS选择的标准不一样。如果你有更好的方法,欢迎留言告知。

所以这时候我通常会选择性的删除部分内容。将「解析规则」简化。比如,使用 rule = "tr > td.tb a"解析规则,就可以将所有的网页a标签解析出来。

1. 解释下 BeautifulSoup中的<a> 标签

<a>标签在网页爬取中,太常见啦,因为你通常都是从一个链接中获得一个链接再扩散到其它链接。如何获取 <a>标签中所需要的内容?具体的一个 <a>标签内容如下:

<a href="/author/baiyu.html" title="《偷拳》">白羽</a>

我们通常需要获取 href中的链接,“/author/baiyu.html”,还有 tag里面的内容,“白羽”。其中 hreftitle<a>标签的属性,而“白羽” <a>标签的值。获取方式分别为:

tag.get("href") # 获取href属性值,如果不存在返回None
tag.text # 获取标签内表示的内容

不管是 <a>标签还是其它的html标签,获取属性和值得方式是一样的。

明白了这个,就可以批量的获取所有「作者页面」的所有「作者主页」链接。

2. 分析作者页面部分的源码

通过首页的部分源码,我们分析出,主要包含了两种我们感兴趣的内容,「authorUrl」和「作者名」,经过分析,主要有如两种内容格式:

<a href="/author/baiyu.html" title="《偷拳》">白羽</a>
或者
<a href="/author/bufeiyan.html" title="《武林客栈》《修罗道》《剑侠情缘》《九阙梦华》《华音流韶》"><strong>步非烟</strong></a>

一种是作者在tag下的text内容中,一种是还包含了strong标签来修饰,对于这种我们需要分别处理。对于上面获取的链接,通过如下的代码,把所有的的「authorUrl」和「作者名」保存下来。

    lists = list()  # 用来存放所有的「作者信息」
    for tag in tags:
        dicts = {}
        dicts["authorUrl"]=tag.get("href")
        if tag.strong:
            dicts["author"]=tag.strong.text
        else:
            dicts["author"] = tag.text
        lists.append(dicts)

通常对于这类明显的包含了格式话的数据,后期可以考虑包装成一个类,不然每次序列化和反序列化还需要写出名称,很容易出错。

3. 保存整个list内容

序列化可以通过很多的方式,这个都可以参考很多序列化的内容,比如pickle,ppprint模块等,也可以参考廖雪峰的官网。但是我这里比较喜欢这届序列化为文本形式的json字符串形式,因为阅读性和可修改性非常的好。主要就是因为我自己并不是需要非常的注重性能。

这里我主要写下两个函数,分别用于方便的「读取」和「保存」我们得到的列表变量数据。

  1. 保存列表的变量
def dumpVariableToJson(variable, fileName, extName='.json'):
    if not variable:
        return
    datetimeStr = datetime.now().strftime("%Y%m%d_%H_%M_%S")
    fileName = fileName + datetimeStr + extName
    with open(fileName, 'w', encoding='utf-8') as f:
        for var in variable:
            f.write(str(var) + "\n")
  1. 读取列表中的变量值
def loadVariableFromJson(fileName, encoding='utf-8'):
    result = []
    if not os.path.exists(fileName) or not os.path.isfile(fileName):
        logging.info("文件路径 <%s> 不存在或者不是文件名", fileName)
        return result
    with open(fileName, 'r', encoding=encoding) as f:
        for line in f:
            try:
                tmpJson = eval(line)
                result.append(tmpJson)
            except Exception:
                continue
    return result

我们在上面的程序中运行代码,将会在相对路径下生成如下文件名的文件。authors20180612_23_50_05.json

    # 保存整个列表变量
    dumpVariableToJson(lists,"authors")

我这样费劲心机的序列化的主要原因就在于,方便后续的过程中能够直接从文件中读取数据变量。而不是每次都爬取一遍地址数据,同时可以在文本编辑器中直接的进行修改。因为「程序不是万能的」,也是偶尔可以直接通过文本修改。

加载文件,使用方法如下:

# 读取文件到内存中
lists = loadVariableFromJson("authors20180612_23_50_05.json")

4. 解决网页爬取过程中的相对url路径问题

很多时候我记得网上有一个叫urlparse的模块,专门处理这部分的逻辑,但是我这里就没有,直接使用自己写下的一个函数,简单粗暴的进行合并。

def mergeUrl(baseUrl, *kwargs):
    """
    组装出URL
    :param baseUrl: 基础的Url,起始网站
    :param kwargs: 各类相对路径网址
    :return: 最终的绝对路径
    """
    items = baseUrl.split("/")
    url = baseUrl.replace("/" + items[-1], "")
    for subUrl in kwargs:
        url += subUrl
    return url

5. 小结

下面是基于上面分析的内容,写出的第一次爬取所有的作者链接的代码。

    session = requests.Session()
    startUrl = "http://www.wuxia.net.cn/author.html"
    session.get(startUrl)  # 设置回话
    # # 第一次爬取文章链接
    rule = "tr > td.tb a"
    tags = parseFullUrl(url=startUrl, rule=rule, session=session, decodeType="UTF-8")
    lists = list()  # 用来存放所有的「作者信息」
    for tag in tags:
        dicts = {}
        dicts["authorUrl"]=tag.get("href")
        if tag.strong:
            dicts["author"]=tag.strong.text
        else:
            dicts["author"] = tag.text
        lists.append(dicts)
    # 保存整个列表变量
    dumpVariableToJson(lists,"authors")

5. 解析每个作者的文章链接

解析的主页内容。这里读取第二个作者的连接进行分析。

解析的主页内容

这里我们可以借鉴上面的思路,将获取的链接一样的存入文件。

整体之间的代码如下:

    # 第二次爬取所有的文章链接
    titlesList = [] # 用来存储所有的文章链接
    rule = "ul.co3 li a"
    for authorDict in lists:
        authorUrl = authorDict.get("authorUrl")
        url = mergeUrl(startUrl,authorUrl)
        tags = parseFullUrl(url=url, rule=rule, session=session, decodeType="UTF-8")
        for tag in tags:
            titleDicts = {}
            titleDicts["authorUrl"] = url
            titleDicts["author"] = authorDict.get("author")
            titleDicts["titleUrl"]=tag.get("href")
            if tag.strong:
                titleDicts["titleName"]=tag.strong.text
            else:
                titleDicts["titleName"] = tag.text
            titlesList.append(titleDicts)

    # 保存所有「文章」链接
    dumpVariableToJson(titlesList, "titles")

得到的json结果如下:

{'authorUrl': 'http://www.wuxia.net.cn/author/baiyu.html', 'author': '白羽', 'titleUrl': '/book/touquan.html', 'titleName': '偷拳'}
{'authorUrl': 'http://www.wuxia.net.cn/author/bufeiyan.html', 'author': '步非烟', 'titleUrl': '/book/wulinkezhan.html', 'titleName': '武林客栈'}
{'authorUrl': 'http://www.wuxia.net.cn/author/bufeiyan.html', 'author': '步非烟', 'titleUrl': '/book/xiuluodao.html', 'titleName': '修罗道'}

6. 获取所有文章的章节链接

我们同样的选取所有内容。网站链接。分析网页的源码如下

<dl>
    <dt>日曜卷·蛊神劫</dt>
    <dd><a href="/book/wulinkezhan/1.html">第一章 剑门谁牵碧玉骢</a></dd>
    <dd><a href="/book/wulinkezhan/2.html">第二章 身上衣衫寂寞红</a></dd>
    <dd><a href="/book/wulinkezhan/3.html">第三章 振刀去国意气雄</a></dd>
    <dd><a href="/book/wulinkezhan/4.html">第四章 置酒向君语从容</a></dd>
    <dd><a href="/book/wulinkezhan/5.html">第五章 当时凄然一笑中</a></dd>
    <dd><a href="/book/wulinkezhan/6.html">第六章 此日蹙兮五阵从</a></dd>
    <dd><a href="/book/wulinkezhan/7.html">第七章 定许相思世世同</a></dd>
    <dd><a href="/book/wulinkezhan/8.html">第八章 可怜心事画图空</a></dd>
    <dd><a href="/book/wulinkezhan/9.html">第九章 身化秘魔驭毒龙</a></dd>
    <dd><a href="/book/wulinkezhan/10.html">第十章 长怅秋山望飞鸿</a></dd>
    <div class="clear"></div>
</dl>

根据文章的链接发现,文章的「文章名」在标签 dl dt下,「章节名」在标签 dl dd a标签中。

7. 获得所有的章节链接

    titleList = loadVariableFromJson("titles20180613_00_52_46.json")
    # 第三次爬取所有的短文章节链接
    rule = "div.book  dl"
    chapterList = []  # 存放所有的章节链接
    for titleDict in titleList:
        titleUrl = titleDict.get("titleUrl")
        authorUrl = titleDict.get("authorUrl")
        author = titleDict.get("author")
        titleName = titleDict.get("titleName")
        url = mergeUrl(startUrl,titleUrl)
        tags = parseFullUrl(url=url, rule=rule, session=session, decodeType="UTF-8")
        for tag in tags:
            chapterName = tag.dt.text if tag.dt else None
            for ddVal in tag.select("dd a"):
                chapterDicts = {}
                chapterNameNum = ddVal.text
                chapterNameUrl = ddVal.get("href")

                # 增加
                chapterDicts["author"] = author
                chapterDicts["titleName"] = titleName
                chapterDicts["titleUrl"] = titleUrl
                chapterDicts["chapterName"] = chapterName
                chapterDicts["chapterNameNum"] = chapterNameNum
                chapterDicts["chapterNameUrl"] = chapterNameUrl
                chapterList.append(chapterDicts)

    # # 保存所有「文章章节」链接
    dumpVariableToJson(chapterList, "chapters")

8. 解析每个文章链接中的内容

现在解析内容和前面的核心思想相差无几,就不过多的介绍啦,直接上代码。

    chapterList = loadVariableFromJson("chapters20180613_01_38_46.json")
    for chapterDicts in chapterList:
        author = chapterDicts["author"]
        titleName = chapterDicts["titleName"]
        chapterNameNum = chapterDicts["chapterNameNum"].replace(" ", "")
        chapterNameUrl = chapterDicts["chapterNameUrl"]
        url = mergeUrl(startUrl,chapterNameUrl)
        context = getSinglePageTitileAndContext1(url,session)
        parentPath = "./titles/" + nowDataStr + "/" + author
        writeToFile(authorName=author,titleName=titleName + "_" + chapterNameNum,context=context,parentFilePath=parentPath)

9. 其它的一些相关介绍

常用的一些操作

  • 打开浏览器”
import webbrowser   
#webbrowser.open(strUrl)
  • 使用程序模拟打开一个网页链接的操作

注意事项

  • 当编码格式出错是,可以使用 res.encoding = 'gbk' 来修改读入的编码格式
resp = requests.get(URL, params=params)

resp.encoding = "gb2312"

html = resp.text

# html = html.encode(encoding="gbk", errors="ignore").decode("gbk", errors="ignore")

html = html.encode(encoding="utf-8", errors="ignore").decode("utf-8", errors="ignore")

print(html)

过滤器

实际的爬虫过程中,常用的函数有findAll方法,该方法非常的好用,重点介绍。

先介绍一下过滤器的类,这些过滤器贯穿整个搜索的API.过滤器可以被用在tag的name中,节点的属性中,字符串中或他们的混合中.

  • 字符串:例如,查找文档中所有的<b>标签:soup.find_all('b')

  • 正则表达式:通过正则表达式的 match() 来匹配内容。下面例子中找出所有以b开头的标签,这表示<body><b>标签都应该被找到:

import re
for tag in soup.find_all(re.compile("^b")):
    print(tag.name)
  • 列表:会返回列表中任一元素匹配的内容。找到文档中所有<a>标签和<b>标签:soup.find_all(["a", "b"])

  • True:True 可以匹配任何值,下面代码查找到所有的tag,但是不会返回字符串节点

for tag in soup.find_all(True):
    print(tag.name)
  • 方法: 方法只接受一个元素参数 ,如果这个方法返回 True 表示当前元素匹配并且被找到,如果不是则反回 False。下面方法校验了当前元素,如果包含 class 属性却不包含 id 属性,那么将返回 True。
def has_class_but_no_id(tag):
    return tag.has_attr('class') and not tag.has_attr('id')
  • 使用lambda表达式:也是方法的定义一种,唯一的限制条件必须把函数的标签作为参数且返回结果是布尔类型,例如,返回有两个属性的标签lambda tag: len(tag.attrs)==2

2. find_all()

非常好用的 find_all( name , attrs , recursive , text , **kwargs )。find_all() 方法搜索当前tag的所有tag子节点,并判断是否符合过滤器的条件.

Google API

爬虫过程中编码问题

如果清楚的直到是那种编码格式,那就直接使用,如果不清楚,可以使用 from bs4 import UnicodeDammit来自动检测

html = res.text # 获取得到的文本
html = html.encode(encoding=res.encoding, errors="ignore") # 转换为bytes
dammit = bs4.UnicodeDammit(html) # 检测文本
print(dammit.unicode_markup) # 显示unicode编码的格式

后台运行Python3程序

nohup python3 -u scaptyTitle2.py > log.out 2>&1 &

如果觉得文章不错,欢迎加入我们一起学习大数据

这里写图片描述

猜你喜欢

转载自blog.csdn.net/u013019701/article/details/80673381