备份 CSDN 博客(上)

背景

因为 CSDN 的博客没有批量导出功能,所以我就琢磨写个脚本可以一键备份博客,最好是 markdown 格式。

搜了一波,极少有能拿来就用的,那就自己探索吧。

思路解析

思路很简单:

  1. 得到每篇文章的链接(URL)
  2. 根据 URL 下载每篇文章,转换成 markdown 格式

囿于篇幅,这篇文章先解决第 1 个问题。

其实我不太懂 python 爬虫和前端,算是现学现卖,说得不对的地方,请您指正。

如何获得每篇文章的 URL

在这里插入图片描述

如图所示,我的博客总共有 7 页,第 2 页的文章列表的地址是:https://blog.csdn.net/longintchar/article/list/2

第 3 页的文章列表的地址是:

https://blog.csdn.net/longintchar/article/list/3

其中的规律很明显,就是在 https://blog.csdn.net/longintchar/article/list/ 后面加上页码

在这里插入图片描述

当你把鼠标悬停在标题上,就能看到左下角会显示这篇文章的 URL(我用的是谷歌浏览器)。所以,从文章列表页面就可以解析出本页面每一篇文章的 URL

以第 1 页举例,我们要做的事情有 2 个

  1. 把这页内容爬下来(从网站获取源代码)
  2. 从里面提取每篇文章的 URL

urllib.request

urllib 是 Python 內建的 HTTP 库,使用 urllib 可以只需要很简单的步骤就能高效采集数据。

urllib 包含以下4个子模块

  1. urllib.request
  2. urllib.error
  3. urllib.parser
  4. urllib.robotparser

其中 urllib.request 子模块是最常用的,用来从网站获取源代码

一个简单的示例:

# 引入 urllib.request
import urllib.request  
# 打开 URL
response =  urllib.request.urlopen('http://www.zhihu.com')  
# 读取内容
html = response.read()     
# 解码
html = html.decode('utf-8')            
print(html)

第 4 行:urlopen() 方法返回的是一个 http.client.HTTPResponse 对象(<class ‘http.client.HTTPResponse’>),需要通过 read() 方法做进一步的处理

第 6 行:调用 read() 方法,返回的数据类型为 bytes 类型

第 8 行:bytes 类型经过 decode() 解码转换成 string 类型

照猫画虎,对于我们要下载的页面,代码是

import  urllib.request

url = 'https://blog.csdn.net/longintchar/article/list/1'
res = urllib.request.urlopen(url) 
html = res.read().decode('utf-8')
print(html)

第 6 行打印结果,结果如下图(截取了一小部分)

在这里插入图片描述

注意第 900 行,这里面就有我们要的文章 URL,如何把这些 URL 提取出来呢?

HTML 的元素构成

爬取网页信息,可以理解成从 HTML 代码中抽取我们需要的信息。HTML 由一系列的“元素”组成,这是从网上搜来的一张图:

在这里插入图片描述

我们要做的,可以分成两部分

  • 精确定位元素
  • 从元素中提取信息

比如下面这个元素

<p><a href='www.wenzi.com'>hello</a></p>

一般要提取“hello”部分,或者链接 www.wenzi.com 部分

如何精确定位到某个元素呢?可以利用标签名(比如上面的 “p”)和标签属性来识别。

例如:

<title>标题</title>
<body>
    <ul class='list1'>
        <li>列表1第1项</li>
        <li>列表1第2项</li>
    </ul>
    <p class='first'>文字1</p>
    <p class='second'>文字2</p>
    <ul class='list2'>
        <li>列表2第1项</li>
        <li>列表2第2项</li>
    </ul>
</body>
  • 如果要提取“标题”,只需要使用标签名 title 来识别,因为只出现过一次 title 标签
  • 如果要提取“文字1”,不能只使用p标签,因为“文字2”也对应了p标签,所以要用p标签且class属性值是'first'来识别
  • 如果“文字1”和“文字2”都要,就可以通过获取所有p标签提取内容
  • 如果想提取列表1中的两项,就不能靠获取所有li标签,因为列表2中也有li标签。此时需要先识别其父节点,即先定位到<ul class='list1'>这个标签上(通过ul标签和class属性值是list1定位)。在这个标签里,所有li都是我们想要的

BeautifulSoup

BeautifulSoup 是一个 HTML/XML 的解析器,用来解析和提取 HTML/XML 数据,利用它不用编写正则表达式也能方便地抓取网页信息。

这里展示一下使用 BeautifulSoup 实现上述提取的代码,以对这个库的提取思路有一个大致的了解。

a = '''<title>标题</title>
<body>
    <ul class='list1'>
        <li>列表1第1项</li>
        <li>列表1第2项</li>
    </ul>
    <p class='first'>文字1</p>
    <p class='second'>文字2</p>
    <ul class='list2'>
        <li>列表2第1项</li>
        <li>列表2第2项</li>
    </ul>
</body>'''

from bs4 import BeautifulSoup
soup = BeautifulSoup(a, "html.parser")
# 1. 如果要提取“标题”,只需要使用`title`标签名来识别,因为只出现过一次`title`标签
# 提取元素的内容:使用.text
print(soup.title.text) 


# 2. 提取“文字1”
# 注意,find方法,只能找到第一个
print(soup.find('p', attrs={'class':'first'}).text)

# 3. 提取“文字1”和“文字2”
print(soup.find_all('p')) #  再分别从中提取文字,这里略

# 4. 提取列表1中的两项
print(soup.find('ul', attrs={'class':'list1'}).find_all('li')) 

运行结果是:

标题
文字1
[<p class="first">文字1</p>, <p class="second">文字2</p>]
[<li>列表1第1项</li>, <li>列表1第2项</li>]

第 16 行,第二个参数指明解析器。BeautifulSoup 提供了三个解析器,它们各自的优缺点如下

  • html.parser :内置不依赖扩展,容错能力强,速度适中
  • lxml:速度最快,容错能力强,但是依赖 C 扩展
  • html5hib:速度最慢,容错能力最强,依赖扩展

第 30 行,当需要根据属性来筛选的时候,可以用 attrs={属性名:值}指定属性的键值对。

根据标签和属性识别

我们再看几个例子,请仔细看注释和输出结果。

a = '''
<p id='p1'>段落1</p>
<p id='p2'>段落2</p>
<p class='p3'>段落3</p>
<p class='p3' id='pp'>段落4</p>
'''

from bs4 import BeautifulSoup
soup = BeautifulSoup(a, "html.parser")

# 第一种,直接将属性名作为参数名,但是有些属性不行,比如像"a-b"这样的属性
print(soup.find_all('p', id = 'p1') )# 一般情况
print(soup.find_all('p', class_='p3') )# class是保留字比较特殊,需要后面加一个_

# 最通用的方法
print(soup.find_all('p', attrs={'class':'p3'}) )# 包含这个属性就算,而不是仅有这个属性
print(soup.find_all('p', attrs={'class':'p3','id':'pp'}) )# 使用多个属性匹配
print(soup.find_all('p', attrs={'class':'p3','id':False}) )# 指定不能有某个属性
print(soup.find_all('p', attrs={'id':['p1','p2']}) )# 属性值是p1或p2
print(soup.find_all('p', attrs={'class':True})) # 含有class属性即可
# 正则表达式匹配
import re
print(soup.find_all('p', attrs={'id':re.compile('^p')})) # 使用正则表达式,id以p开头
[<p id="p1">段落1</p>]
[<p class="p3">段落3</p>, <p class="p3" id="pp">段落4</p>]

[<p class="p3">段落3</p>, <p class="p3" id="pp">段落4</p>] //16[<p class="p3" id="pp">段落4</p>]   //17[<p class="p3">段落3</p>]           //18[<p id="p1">段落1</p>, <p id="p2">段落2</p>] //19[<p class="p3">段落3</p>, <p class="p3" id="pp">段落4</p>] //20[<p id="p1">段落1</p>, <p id="p2">段落2</p>, <p class="p3" id="pp">段落4</p>]

需要说明的是:

  • find 方法:只能找到第一个符合要求的标签
  • find_all 方法:找到所有符合要求的标签,返回一个 list,如果只找到一个也是返回 list,可以用[0]提取
根据标签和内容识别

继续举例子

a = '''
<p id='p1'>段落1</p>
<p class='p3'>段落2</p>
<p class='p3'>文章</p>
<p></p>
'''
from bs4 import BeautifulSoup
soup = BeautifulSoup(a, "html.parser")

print(soup.find_all('p', text='文章'))
print(soup.find_all('p', text=['段落1','段落2']))

# 正则表达式
import re
print(soup.find_all('p', text=re.compile('段落')))

# 传入函数
def nothing(c):
    return c not in ['段落1','段落2','文章']
print(soup.find_all('p',text=nothing))

def something(c):
    return c in ['段落1','段落2','文章']
print(soup.find_all('p',text=something))


def nothing(c):  
    return c is None
print(soup.find_all('p',text=nothing))

运行结果

[<p class="p3">文章</p>]
[<p id="p1">段落1</p>, <p class="p3">段落2</p>]
[<p id="p1">段落1</p>, <p class="p3">段落2</p>] // 第15行
[<p></p>]  // 第20行
[<p id="p1">段落1</p>, <p class="p3">段落2</p>, <p class="p3">文章</p>] // 第24行
[<p></p>] // 第29行

注意,代码第 17 行和后面,举例如何使用函数来过滤

20:把 nothing 这个函数作用在 text 上面,如果返回 True,则符合条件,其他类似。

find_all() 函数和 BeautifulSoup 的详细说明,可以看官方文档,地址是

https://beautifulsoup.readthedocs.io/zh_CN/v4.4.0/

在这里插入图片描述

其他操作

包括嵌套标签的提取、获取内容、获取属性值

a = '''
<body>
    <h><a href='www.biaoti.com'>标题</a></h>
    <p>段落1</p>
    <p>段落2</p>
</body>
'''

from bs4 import BeautifulSoup
soup = BeautifulSoup(a, 'html.parser')

# 提取内容

for p in soup.find_all('p'):
    print(p.text)
	
	
print(soup.h.text) # 多层嵌套也可以直接返回,即提取内层标签 a 的内容
print(soup.h.a.text) # 也可以这样
print(soup.a.text) # 也可以这样


print(soup.body.text) # 里面有多个内容时 

# 提取属性值,像字典一样提取
print(soup.h.a['href'])
print(soup.a['href']) # 也可以这样
print(soup.h.a.get('href')) # 也可以这样

运行结果

段落1
段落2 
标题  // 18-20 
标题
标题

标题  // 23
段落1
段落2

www.biaoti.com  // 26-28
www.biaoti.com
www.biaoti.com

代码

终于铺垫完了,可以讲代码了。

文件名:get_id.py

此模块的功能是提取我所有文章的 URL

import  urllib.request
from bs4 import BeautifulSoup


def getid(x):
	url = 'https://blog.csdn.net/longintchar/article/list/' + str(x)
	res = urllib.request.urlopen(url) 
	html = res.read().decode('utf-8')
	soup = BeautifulSoup(html,'html.parser')
	divs = soup.find_all('div', attrs={'class':'article-item-box csdn-tracking-statistics'})
	for div in divs:
		print(div.h4.a['href'])
		
for i in range(1, 8):
	getid(i)
	

因为我的博客有 7 页,所以第 14 行 range 的参数是(1,8)

6:最开头已经分析了,我的博客列表地址是 https://blog.csdn.net/longintchar/article/list/1https://blog.csdn.net/longintchar/article/list/7

7-8:前文已经讲了,利用 urllib.request 子模块从网站获取源代码

9:利用 BeautifulSoup 解析 HTML 源码

在这里插入图片描述

通过分析 HTML 的代码,我发现我需要的链接在某些 div 标签中,准确地说,是 div.h4.ahref 属性对应的值。这个 div 标签有特点,特点是其 class 属性的值是 “article-item-box csdn-tracking-statistics”,靠这个属性值就可以排除其他 div 标签。

所以,就有这几行代码

	divs = soup.find_all('div', attrs={'class':'article-item-box csdn-tracking-statistics'})
	for div in divs:
		print(div.h4.a['href'])

运行 get_id.py,就可以输出所有博文的链接。

如:

在这里插入图片描述

第一个问题已经搞定,下篇博文我们看看如何下载每篇文章,转换成 markdown 格式。


参考资料

【1】BeautifulSoup全面总结

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值