背景
因为 CSDN 的博客没有批量导出功能,所以我就琢磨写个脚本可以一键备份博客,最好是 markdown 格式。
搜了一波,极少有能拿来就用的,那就自己探索吧。
思路解析
思路很简单:
- 得到每篇文章的链接(URL)
- 根据 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 个
- 把这页内容爬下来(从网站获取源代码)
- 从里面提取每篇文章的 URL
urllib.request
urllib 是 Python 內建的 HTTP 库,使用 urllib 可以只需要很简单的步骤就能高效采集数据。
urllib 包含以下4个子模块
- urllib.request
- urllib.error
- urllib.parser
- 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/1
到 https://blog.csdn.net/longintchar/article/list/7
7-8:前文已经讲了,利用 urllib.request 子模块从网站获取源代码
9:利用 BeautifulSoup 解析 HTML 源码
通过分析 HTML 的代码,我发现我需要的链接在某些 div 标签中,准确地说,是 div.h4.a
的 href
属性对应的值。这个 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 格式。