背景
在上一篇文章 备份 CSDN 博客(上)中,已经解决了第一个问题——获取所有博文的 URL,这篇博文就讨论如何根据 URL 把文章下载下来,并转换成 markdown 格式。
fileinput 模块
python 中的 fileinput 模块可以对一个或多个文件中的内容进行迭代、遍历等操作。该模块的 input() 函数有点类似操作文件的 readlines() 方法,区别在于:前者是一个迭代对象,即每次只生成一行,需要用 for 循环迭代;后者是一次性读取所有行。
用 fileinput 对文件进行循环遍历,格式化输出,查找、替换等操作,非常方便。
典型用法为:
import fileinput
for line in fileinput.input():
process(line)
此程序会迭代 sys.argv[1:]
中列出的所有文件内的行,如果列表为空则会使用 sys.stdin
。
详细的说明可以在官网查询: https://docs.python.org/zh-cn/3/library/fileinput.html
这里举个例子。
假设我已经得到了每篇文章的 URL,保存在 url.txt 中
cat url.txt
https://blog.csdn.net/u013490896/article/details/113796436
https://blog.csdn.net/u013490896/article/details/113075606
https://blog.csdn.net/u013490896/article/details/113074860
https://blog.csdn.net/u013490896/article/details/113062803
我想保留每一行最后的编号。
import fileinput
for line in fileinput.input():
a = line.replace('\n', '').split('/')
print(a[-1]) # 获取文章的 ID
运行:$ python3 id.py url.txt
$ python3 id.py url.txt
113796436
113075606
113074860
113062803
代码第 5 行:replace(’\n’, ‘’) 表示要去掉原来文件中每一行后面的换行;.split(’/’) 表示用 “/” 分割字符串
第 6 行,a[-1] 表示取数组的最后一个元素,因为被分割后的结果是一个数组,比如第一行被分割成
['https:', '', 'blog.csdn.net', 'u013490896', 'article', 'details', '113796436']
再举个例子,利用 fileinput 对多文件操作,并原地修改内容。
我们把 url.txt 复制 2 个,一个是 1.txt,一个是 2.txt
cat 1.txt
https://blog.csdn.net/u013490896/article/details/113796436
https://blog.csdn.net/u013490896/article/details/113075606
https://blog.csdn.net/u013490896/article/details/113074860
https://blog.csdn.net/u013490896/article/details/113062803
$ cat 2.txt
https://blog.csdn.net/u013490896/article/details/113796436
https://blog.csdn.net/u013490896/article/details/113075606
https://blog.csdn.net/u013490896/article/details/113074860
https://blog.csdn.net/u013490896/article/details/113062803
把代码修改一下,保存成 id2.py
import fileinput
def process(line):
return line.replace('\n', '').split('/')[-1]
for line in fileinput.input(['1.txt','2.txt'], inplace=1):
print(process(line))
运行结果是
$ python3 id2.py
$ cat 1.txt
113796436
113075606
113074860
113062803
$ cat 2.txt
113796436
113075606
113074860
113062803
代码第 6 行,inplace=1
表示要原地修改内容,官方的解释是“将标准输出定向到输入文件”
如果给出了 backup 形参 (通常形式为 backup='.<some extension>'
),它将指定备份文件的扩展名,并且备份文件会被保留;默认情况下扩展名为 '.bak'
其实不指定 inplace = 1,利用命令行的重定向,也是可以起到备份的作用。
好了,fileinput 就介绍到这里,总之利用 fileinput 可以很容易地遍历文件中的每一行。
安装 clean-mark
在博文 网页转 markdown 的工具 中,我介绍了一个工具,可以非常方便地把网页转换成 markdown 格式。
所以留给我们的工作很简单,几行代码就搞定
以下代码保存为 get_blogs1.py
import fileinput
import os
for line in fileinput.input():
# 去掉末尾的换行
url = line.replace('\n', '')
# 下载文章
os.system("clean-mark "+ url)
第 8 行需要解释一下,system 函数可以将字符串转化成命令在系统上运行;其原理是每一条 system 函数执行时,都会创建一个子进程来执行命令行,子进程的执行结果无法影响主进程。
需要提醒的是:
import os
os.system('cd /usr/local')
os.mkdir('aaa.txt)
上述程序运行后会发现 .txt 文件并没有创建在 /usr/local 文件夹下,而是在当前的目录下,这是为什么呢?就是因为上面说的,‘cd /usr/local’ 是一个子进程,它的结果无法影响第 4 行创建文件。
为了保证 system 执行多条命令可以成功,多条命令需要在同一个子进程中运行:
import os
os.system('cd /usr/local && mkdir aaa.txt')
# 或者
os.system('cd /usr/local ; mkdir aaa.txt')
再回到 get_blogs1.py,我们试运行一下
$ cat url.txt
https://blog.csdn.net/u013490896/article/details/113796436
https://blog.csdn.net/u013490896/article/details/113075606
https://blog.csdn.net/u013490896/article/details/113074860
https://blog.csdn.net/u013490896/article/details/113062803
$ python3 get_blogs1.py url.txt
=> Processing URL ...
> 113796436.md
=> URL converted!
=> Processing URL ...
> 113075606.md
=> URL converted!
=> Processing URL ...
> 113074860.md
=> URL converted!
=> Processing URL ...
> 113062803.md
=> URL converted!
可以看到,已经下载了,且以文章的 ID 命名。
打开一篇文章看看
基本上符合要求,但是图片并没有下载下来
点击图片,看到的是从 CSDN 官网上得到的图片,图片并不在本地。为了把图片下载到本地,我们需要添加功能。对了,下载的 .md 文件是以文章 ID 命名的,我希望以文章标题命名。
所以,接下来的任务可以细化为:
迭代 url.txt 的每一行,对于每一行:
- 通过 URL 下载文章,假设下载后得到文件 111.md
- 打开 111.md,解析出标题,假设标题是 abc
- 创建文件夹 abc
- 把下载的 111.md 移动到文件夹 abc 里
- 在 abc 文件夹里面创建文件夹 img
- 打开 111.md,解析出所有图片的 URL
- 下载所有图片到文件夹 img
功能明确了,我们看代码
import fileinput
import os
import linecache
for line in fileinput.input():
# 去掉末尾的换行
url = line.replace('\n', '')
# 下载文章
os.system("clean-mark "+ url)
a = url.split('/')
#print(a[-1]) # 获取文章的 ID
file_path = a[-1] + ".md" # 获取文件名
title = linecache.getline(file_path, 3).\
replace(' ', '').replace('title:', '').replace('\n', '')
#print(title) # 获取文章标题
os.makedirs(title, exist_ok = True) # 以文章标题为目录名称,创建目录
os.system("mv " + file_path + " ./" + title) # 移动下载的 .md 文件到目录
os.chdir(title) # 切换目录
# 下载图片, TODO
os.chdir('../') # 切换目录
第 14 行,使用了 linecache 模块
linecache 模块
该模块允许从任何文件里得到任何的行,并且使用缓存进行优化,常见的情况是从单个文件读取多行。
linecache.getlines(filename)
从名为 filename 的文件中得到全部内容,输出为列表格式,以文件每行为列表中的一个元素
linecache.getline(filename,lineno)
从名为 filename 的文件中得到第 lineno 行(换行符将包含在找到的行里)。我们用的就是这个函数。
下载后的 .md,都有一个文件头,第 3 行 “title:xxxxx” 就是标题
代码第 14 行:
title = linecache.getline(file_path, 3).\ replace(' ', '').replace('title:', '').replace('\n', '')
首先,读取文件的第三行,得到的是 title:xxxxx
;去掉空格,去掉前面的’title:’,再去掉末尾的换行,最后就得到了标题。
代码第 17 行:
os.makedirs(title, exist_ok = True)
创建目录,exist_ok = True 表示只有在目录不存在时创建目录,目录已存在时不会抛出异常。
到了这里,就差图片下载了。
下载图片
下载图片的函数是
def get_img_url(file_path):
pattern = "![](https://"
fd = open(file_path, 'r')
lines = fd.readlines()
os.makedirs('img', exist_ok = True) # 创建 img 目录
for line in lines:
if pattern in line:
a = line.split("?")
url_img = a[0].replace('![](','') # 去除前面的字符 “![](”
#print(url_img)
name = url_img.split('/')[-1]
filename = '{}{}{}'.format('img', os.sep, name)
urlretrieve(url_img, filename) # 下载图片
参数是 .md 文件的路径
第 3 行:open() 函数用于打开一个文件,创建一个 file 对象,“r” 表示以只读方式打开文件。
第 4 行:readlines() 方法用于读取所有行(直到结束符 EOF)并返回列表,该列表可以由 Python 的 for… in … 结构进行处理。
第 7 行:in 是成员运算符,用来测试给定值是否为序列中的成员。通过研究下载的 .md 文件,我发现图片链接是以"![](https://"
开头的
举例:
![](https://img-blog.csdn.net/20180414233743206?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTM0OTA4OTY=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70)
8-9 行,得到的是 https://img-blog.csdn.net/20180414233743206
11 行,得到的是 20180414233743206
12 行:字符串格式化,os.sep 根据你所处的平台,自动采用相应的路径分隔符号,因为我在 Linux 上实验,所以得到的是 img/20180414233743206
13 行,urlretrieve() 方法将远程数据下载到本地
urlretrieve(url, filename=None, reporthook=None, data=None)
参数 filename 指定了保存的路径。
完整代码
import fileinput
import os
import linecache
from urllib.request import urlretrieve
def get_img_url(file_path):
pattern = "![](https://"
fd = open(file_path, 'r')
lines = fd.readlines()
os.makedirs('img', exist_ok = True) # 创建 img 目录
for line in lines:
if pattern in line:
a = line.split("?")
url_img = a[0].replace('![](','') # 去除前面的字符 “![](”
#print(url_img)
name = url_img.split('/')[-1]
filename = '{}{}{}'.format('img', os.sep, name)
urlretrieve(url_img, filename) # 下载图片
for line in fileinput.input():
# 去掉末尾的换行
url = line.replace('\n', '')
# 下载文章
os.system("clean-mark "+ url)
a = url.split('/')
#print(a[-1]) # 获取文章的 ID
file_path = a[-1] + ".md" # 获取文件名
title = linecache.getline(file_path, 3).\
replace(' ', '').replace('title:', '').replace('\n', '')
#print(title) # 获取文章标题
os.makedirs(title, exist_ok = True) # 以文章标题为目录名称,创建目录
os.system("mv " + file_path + " ./" + title) # 移动下载的 .md 文件到目录
os.chdir(title) # 切换目录
# 下载图片
get_img_url(file_path)
os.chdir('../') # 切换目录
总结
容错性差。比如:
- clean-mark 这个工具不完美,转换效果有时候特别不好,比如目录混乱,代码片乱码等;有时候这个工具还报错,比如
- 图片下载的时候,很多时候会报错,因为有的图片链接不按照牌理出牌
如何解决呢?
- 寻找其他的 HTML 转 Markdown 的工具
- 图片链接似乎有 2 种格式,要分情况处理
其他备份博客的思路
利用浏览器,提取博客的主体。
div class=“blog-content-box”
把这个元素拷贝出来
拷贝后保存为 .html 文件,然后用浏览器打开,这时候页面会清爽很多。下面是效果图。
这样也可以达到备份的效果,但是图片还是不在本地,需要提取链接并下载。
我尝试用代码实现上述过程
import urllib.request
from bs4 import BeautifulSoup
url = 'https://blog.csdn.net/longintchar/article/details/43193289'
def get_content(url):
res = urllib.request.urlopen(url)
html = res.read().decode('utf-8')
soup = BeautifulSoup(html,'html.parser')
divs = soup.find_all('div', attrs={'class':'blog-content-box'})
print(divs[0])
get_content(url)
但是输出的东西(第 11 行)和前面保存下来的不一样。
为什么不一样,如何才能一样?这里留个坑。
其实还有简单粗暴的备份办法,就是利用浏览器的 “另存为”
这样可以把整个网页(包括图片等)保存到本地。
肯定不能手工操作,我有 200 多篇博文,这样操作太累了。自动化的方法还在探索验证中…
【End】