欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2022-06-06 09:49:07
...

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


2018/6/11 星期一 整理

运行的环境:

  1. win10 x64

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

  3. 使用Pycharm编程

1. 前期工作

2. 主要目的

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

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

网站的首页如下:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

就是,我们使用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工具获取,具体的操作如下。

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

选中,“检查”→“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. 解析每个作者的文章链接

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

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

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

整体之间的代码如下:

    # 第二次爬取所有的文章链接
    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 &

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

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