前言

由于推的团里不断有人毕业,保存她们在毕业前的博客成了一件非常重要的事。

All in one

最初的想法是将所有的博客一股脑地塞到wiki中,wiki用的是mkdocs和material主题,在GitHub上翻了半天,找到一款叫做mkdocs-blogging-plugin的插件,实现了在mkdocs中插入基本的博客内容,顺着这个插件的要求写了第一版爬虫,也对插件做了一些魔改。

修改插件默认模板

先来看一眼markdown头上需要写的信息,以随便找的一个页面为例

1
2
3
4
5
6
7
8
title: 22/7 新メンバーの月城咲舞です! 
template: comment.html
author: 月城咲舞
description: 改めまして、新メンバーの月城咲舞です。 今、コンサート開始15分前です... 緊張しています。ものすごくものすごく 顔がこわばりそうで怖いのですが、今までやってきたことを思い出しながら、 一生懸命頑張りま...
avatar: https://cdn.jsdelivr.net/gh/zzzhxxx/227WiKi@master/docs/assets/photo/avatar/emma.jpg
date: "2022-03-10"
tags:
- 月城咲舞

首先是必不可少的title、description、date、tag、author这些都没什么好说的。

可以注意到,出现了avatar这原插件中不存在的字段,而这一字段的作用是为了提供作者的头像。

这也就是魔改最主要的点,为卡片列表页面增加头像。

根据官方文档给出的教程,先在mkdocs用于自定义的overrides文件夹下创建custom-blog.html

根据官方给出的魔改example,先写一个基本的模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
{# Import the original template, required. #}
{% extends "blog.html" %}

{# A macro to render a blog entry, required. #}
{% macro render_blog(title, description, time, url, page) -%}
<div class="blog-post">
<h3 class="blog-post-title">
<a class="link" href="{{ url }}">{{ title }}</a>
</h3>
<div class="blog-post-extra">
<b>{{ page.meta["author"] + " · " if "author" in page.meta else "" }}</b>
<span>{{ time }}</span>
</div>
{% if show_tags and "tags" in page.meta %}
{% call render_tags(page.meta["tags"], index_url) %}
{% endcall %}
{% endif %}
<p class="blog-post-description">
{{ description }}
</p>
<hr />
</div>
{{ caller() }}
{%- endmacro %}

{# Custom css, optional. #}
{% block style %}
{{ super() }}
<style>
.md-typeset .blog-post-description {
margin-bottom: 0;
}
</style>
{% endblock %}

这一段代码展示了在页面中展示author、date、tags等字段,我们的目的是在名字前展示头像,所以删去<b>{{ page.meta["author"] + " · " if "author" in page.meta else "" }}</b>下读取author的那一段,把他改成

1
<img loading="lazy" src={{ page.meta["avatar"] }} style="width:40px; height:40px; border-radius:50%;">

这样就能够成功读取自定义字段avatar,并在页面中显示圆形头像,但光靠头像可能认不出作者,所以还需要把作者的姓名加上

1
2
3
4
5
6
 <span>
<span class="twemoji">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M313.6 304c-28.7 0-42.5 16-89.6 16-47.1 0-60.8-16-89.6-16C60.2 304 0 364.2 0 438.4V464c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48v-25.6c0-74.2-60.2-134.4-134.4-134.4zM400 464H48v-25.6c0-47.6 38.8-86.4 86.4-86.4 14.6 0 38.3 16 89.6 16 51.7 0 74.9-16 89.6-16 47.6 0 86.4 38.8 86.4 86.4V464zM224 288c79.5 0 144-64.5 144-144S303.5 0 224 0 80 64.5 80 144s64.5 144 144 144zm0-240c52.9 0 96 43.1 96 96s-43.1 96-96 96-96-43.1-96-96 43.1-96 96-96z"></path></svg>
</span>
<b>{{ " "+page.meta["author"] + " · " if "author" in page.meta else "" }}</b>
</span>

其实就是把原先的复制过来,为了美观,加了一个twemoji的图标

只有姓名有图标也很怪,所以给日期也加一个

1
2
3
4
<span>
<span class="twemoji"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill-rule="evenodd" d="M6.75 0a.75.75 0 0 1 .75.75V3h9V.75a.75.75 0 0 1 1.5 0V3h2.75c.966 0 1.75.784 1.75 1.75v16a1.75 1.75 0 0 1-1.75 1.75H3.25a1.75 1.75 0 0 1-1.75-1.75v-16C1.5 3.784 2.284 3 3.25 3H6V.75A.75.75 0 0 1 6.75 0zm-3.5 4.5a.25.25 0 0 0-.25.25V8h18V4.75a.25.25 0 0 0-.25-.25H3.25zM21 9.5H3v11.25c0 .138.112.25.25.25h17.5a.25.25 0 0 0 .25-.25V9.5z"></path></svg></span>
{{ page.meta["date"] + " · " if "date" in page.meta else "" }}
</span>

这样就在date前加上了图标,顺便加上了中间用·分割不同信息栏目

效果:

mkdocs部分到此结束

爬虫

之前对爬虫的理解仅限于高中信息书上教的,所以接下来的代码会又臭又长,谨慎观看

首先是常规的头

1
2
3
4
5
6
7
8
import requests
import io
import sys
import os
import time
sys.stdout = io.TextIOWrapper(sys.stdout.buffer,encoding='utf8')
from bs4 import BeautifulSoup
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.67 Safari/537.36 Edg/87.0.664.47'}

获取文章简介

在首页爬取所有文章的作者、简介以及后面需要爬取文章的链接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
def get_info():
global title
global authors
global day
global toLink
title=[]
day=[]
toLink=[]
authors=[]
url=url_base+str(page)
data=requests.get(url,headers=headers)
bs=BeautifulSoup(data.text,"html.parser")
blog_title=bs.find_all('div',class_="blog-list__title")
blog_descripiton=bs.find_all('div',class_="blog-list__txt")
blog_link=bs.find_all('div',class_="blog-list__more")
for i in blog_descripiton:
desc=i.find('p',class_="txt").text
desc=desc.replace("\n","")
desc=desc.replace("\ufeff","")
des.insert(0,desc)
for i in blog_title:
dates=i.find('p',class_="date").text
day.insert(0,dates.replace(".","-"))
name=i.find('p',class_="title").text
title.insert(0,name)
author=i.find('p',class_="name").text
authors.insert(0,author)
for i in blog_link:
l=i.find('a').get('href')
toLink.insert(0,l)

最基本的爬虫操作,找到不同div存的不同东西就将他爬下来并存进数组里,因为一次操作同时存入三个不同数组,可以实现信息的一一对应。

同时也要对源信息做一些处理,把一些换行标记符删掉,改一下日期的类型。

确定是否有东西能爬

因为没博客的界面会显示記事がありません。,所以写一个确定状态的函数

1
2
3
4
5
6
7
8
9
10
def checkstatus():
global page
url=url_base+str(page)
data=requests.get(url,headers=headers)
bs=BeautifulSoup(data.text,"html.parser")
if_no_content=bs.find_all('p')
if str(if_no_content[0]) == '<p style="padding:0 0 60px;">記事がありません。</p>' :
return False
else:
return True

根据这个函数返回的状态,写一个最简单暴力的获取所有页数的函数

1
2
3
4
5
6
def get_pages():
global page
while(checkstatus()):
page+=1
page-=1
return page

爬取文章

文章页面所有的文字存在blog_contents这个div下,所以只需要搞到这一段的所有文字内容就能搞到博客的文字。

1
2
blog_contents=bs.find('div',class_="blog_detail__main")
con=blog_contents.text

但是文章里除了文字还有图像,所以需要把图保存下来

1
2
3
4
5
6
7
8
9
10
11
12
13
a=blog_contents.find_all('img')
for j in range(len(a)):
try:
if 'src' in a[j].attrs:
iml=url_base2 +a[j].get('src')
imagelink.insert(0,iml)
c+=1
the_image = requests.get(iml)
final_return+=base+dir_name+"_"+str(c)+".jpg)"+"\n\n"
with open(os.getcwd()+"/images/"+dir_name+"_"+str(c)+".jpg", "wb+") as f:
f.write(the_image.content)
except:
pass

写入文件

先要根据作者的姓名和发布日期生成一下文件名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
def changefilename(au):
if au == '西條和':
return "nagomi"
if au == '涼花萌':
return "moe"
if au == '天城サリー':
return "sally"
if au == '河瀬詩':
return "uta"
if au == '白沢かなえ':
return "kanae"
if au == '宮瀬玲奈':
return "reina"
if au == '相川奈央':
return "nao"
if au == '麻丘真央':
return "mao"
if au == '椎名桜月':
return "satsuki"
if au == '雨夜音':
return "oto"
if au == '清井美那':
return "mina"
if au == '四条月':
return "luna"
if au == '月城咲舞':
return "emma"
if au == '望月りの':
return "rino"
for i in range(len(title)):
author_filename.append(changefilename(authors[i]))

author_filename中存了所有文件的文件名,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
for z in range(page,-1,-1):
print('Now page:',page)
get_info()
with open("current_page.txt","w",encoding='utf-8') as cp:
cp.write(str(page))
for i in range(len(title)):
cpn=last_get_filename
author_filename.append(changefilename(authors[i]))
last_get_filename= author_filename[i]+"-"+day[i]
real_name=''
if(not(cpn == '') and cpn == last_get_filename):
real_name=last_get_filename + '-' + str(count) + ".md"
same_date=True
count+=1
if(cpn == '' or not(cpn == last_get_filename)):
real_name=last_get_filename + ".md"
count=2
same_date=False
with open(real_name,"w",encoding='utf-8') as f:
f.write("---\ntitle: "+title[i]+"\n"+"template: comment.html\n"+"author: "+authors[i]+"\n"+"description: "+des[i]+"\n"+"avatar: https://cdn.jsdelivr.net/gh/zzzhxxx/227WiKi@master/docs/assets/photo/avatar/"+author_filename[i]+".jpg"+"\n"+"date: "+'"'+day[i]+'"'+"\n"+"tags:\n - "+authors[i]+"\n---\n\n"+getcontents(toLink[i]))
with open("history.txt","w",encoding='utf-8') as save:
save.write(last_get_filename)
author_filename=[]
page-=1

一个嵌套循环,从最后一页开始爬

完整代码在GitHub

缺点

这样爬有很多缺点

  1. 不容易找bug
  2. 会出现一些玄学漏图漏文的情况
  3. 一天一个人发了很多文章就会漏

重构

主要想要重构的原因是mkdocs生成页面的速度太慢了,博客的文件有几千个,觉得分离出来放到hexo里做会更好,同时之前由于爬的太暴力原文的排版全都丢了,所以重新爬一遍。

设计

因为之前一下子爬所有人的写的又臭又长,所以这次一个人写一个代码,而且基本结构都差不多,所以改起来也没什么难度。

分析

大部分成员的文章都是用一个大<div>标签括起来的,以阿和这一篇为例

1
2
3
4
5
6
7
8
9
<div class="blog_detail__main">
<br><br>緊張している。<br><br><br><br><br><br><br><br>言霊という言葉もある通り、<br><br><br><br><br>そう口に出すから余計緊張が増すのではないか<br><br><br><br><br>それならば緊張という単語をなるべく避けてみたらいいではないか<br><br><br><br><br><br>なんだかんだあり試行錯誤の末、<br><br><br><br><br><br><br>気まずい。という言葉に変換されました<br><br><br><br><br><br><br>しかしそれ以降気まずいが口癖のようになってしまい<br><br><br><br><br><br><br>あらゆることが気まずくなり、<br><br><br><br><br><br><br>もはやここに存在していることまでもが気まずくなってきたこの頃です。<br><br><br><br><br><br>ブログを書くのも気まずいね<br><br><br><br><br><br>どうもこんにちは<br><br><br><br><br><br>たいへん出遅れましたどころか、<br><br><br><br><br><br>明日次のライブが始まっちゃうからはよ更新しとかな〜という圧迫感みえみえですが。<br><br><br><br><br><br>ナナニジスプリングパレードという名の春ツアーが終わりました〜<br><br><br><br><br>今回は旗なんか持ちゃったり、<br>花々したお洋服も着させてもらったりなんかして。<br><br><br><br><br><br>ちゃっかり春をぱれぱれしちゃいました、!<br><br><br>えへへ〜<br><br><br><br><br>衣装がね、これがまたかわよかったのです○<br><br><br><br>ふぁい<br><br><br><br><img src="/files/30/diary/n227/blog/S__70705195.jpg" alt="" width="326" height="250"><br><br><br><br>このね、真ん中のお花で作っていただいてるのが私が本番着ていた服のデザインになります。<br><br><br><br><br><br><br>りのちゃんがいなかったので去年みたいに私単体の写真は存在しておりませんが本当にこの通りなのでその通りです。はい。<br><br><br><br>さらっと公開しちゃいましたがツアー最終日にて3年ぶりにお花が解禁になりましたꕤ*<br><br><br><br><img src="/files/30/diary/n227/blog/S__70705197.jpg" alt="" width="248" height="331"><br><br><br><br><br><img src="/files/30/diary/n227/blog/S__70705198.jpg" alt="" width="244" height="325"><br><br><br><br>おにぎりだったりミッフィーだったり、色や素材も、<br><br>私を想ってくれたんだなというのが感じられてひとつひとつうれしく見させてもらいました<br><br><br><br><br>ありやとです。<br><br><br><br><br><br>ふぃ〜<br><br><br><br><br>やっとツアーのこと書けたっ<br><br><br><br><br><br>てなてなそんなわけで、<br><br><br><br>無事りのちゃんも戻ってきてくれたことですし明日のライブいっちょはりきってがんばりますか、!<br><br><br><br><br><br><br>まっぴんくになるんだろうなぁ<br><br><br><br><br>きっと綺麗だね<br><br><br><br>おしまい。
<!--twitter-->
<div class="btnTweet">
<iframe id="twitter-widget-0" scrolling="no" frameborder="0" allowtransparency="true" allowfullscreen="true" class="twitter-share-button twitter-share-button-rendered twitter-tweet-button" style="position: static; visibility: visible; width: 89px; height: 20px;" title="Twitter Tweet Button" src="https://platform.twitter.com/widgets/tweet_button.2b2d73daf636805223fb11d48f3e94f7.ja.html#dnt=false&amp;id=twitter-widget-0&amp;lang=ja&amp;original_referer=https%3A%2F%2Fblog.nanabunnonijyuuni.com%2Fs%2Fn227%2Fdiary%2Fdetail%2F2254%3Fima%3D5846%26cd%3Dblog&amp;size=m&amp;text=%E3%80%9022%2F7%20%E3%82%AA%E3%83%95%E3%82%A3%E3%82%B7%E3%83%A3%E3%83%AB%E3%83%96%E3%83%AD%E3%82%B0%E3%80%91%F0%93%8D%AF%C2%B7%C2%B0%EF%BC%88%E8%A5%BF%E6%A2%9D%E5%92%8C%EF%BC%89&amp;time=1685793521022&amp;type=share&amp;url=https%3A%2F%2Fblog.nanabunnonijyuuni.com%2Fs%2Fn227%2Fdiary%2Fdetail%2F2254%3Fcd%3Dblog%26site%3Dn227%26id%3D2254" data-url="https://blog.nanabunnonijyuuni.com/s/n227/diary/detail/2254?cd=blog&amp;site=n227&amp;id=2254"></iframe>
<script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0];if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src="//platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);}}(document,"script","twitter-wjs");</script>
</div>
<!--//twitter-->
</div>

这样就很好根据原文排版了

爬取

首先去除底部twitter的分享部件

1
2
3
tweet =bs.find('div', class_="btnTweet")
if tweet:
bs.find('div', class_="btnTweet").decompose()

剩下的就是纯净的文章加图像标签,为了保持原文的格式,先要把文章里的所有图像链接都存起来,然后再把文章中的<img>替换成markdown的!()[],具体做法就是将blog_contents变量转换为str类型,再用replace函数进行替换,最后再将转换过的文本送到BeautifulSoup中,利用.text返回纯文本格式

1
2
3
4
5
6
7
8
9
10
11
12
blog_contents=bs.find('div',class_="blog_detail__main")
img =blog_contents.find_all('img')
if img:
for i in img:
l='https://blog.nanabunnonijyuuni.com'+i["src"]
get_img(l)
blog_contents=str(blog_contents).replace("</img>"," ")
blog_contents=blog_contents.replace('<img src="' + i["src"] + '">',"![]("+get_link(l)+')')
tr=BeautifulSoup(blog_contents,"html.parser")
return tr.text
else:
return blog_contents.text

这样做就能保证排版的准确,同时流程也简单了许多,代码量也少了很多,但这些全都依靠高中信息课的知识,你不能说他有多优秀。

同样的,完整代码在GitHub。

小结

目前的爬取方法只是最死最简单的方法,但不管怎样,他只要能实现我把博客全都爬下来的目的他就完成了他的目的。

第一次写和重构之间隔的时间很长的原因是因为玲奈的毕业日期的一天一天的临近,我必须得做点什么来留存她的22/7时间的记忆,我很遗憾是因为这个理由重启了这个项目。

最后的最后,

宮瀬玲奈さん,

卒業おめでとうございます