Pythonを使って、noteエクスポートファイル(xml)から、本文のhtmlを取り出し、別形式のXMLファイルに出力する
noteのエクスポートファイル(XML)を、自前サイトの形式に変換して保管しようとしている。
タイトルと投稿日はXML部分から取り出せる。しかし、本文はXML内にHTML形式(XHTMLでない。閉じてないタグあり。<BR />ではなく<BR>)で格納されている。XSLでサクッと変換するのは難しそうなのと、Pythonの標準?ライブラリでどこまで出来るか、勉強も兼ねてプログラミングしてみた。
エクスポートファイルを見て気づいたこと
まずは、入力となるエクスポートファイルを見てみたが、自分の雑さを知ることになる。
改行を使って、見た目だけの段落分け(や見出し付け)をしていている
中身のない段落や見出しを放置している。
こんな感じ↓
★改行でタイトルと本文を分けている
<p>
追記 2024/12/19
<br>
新しいルーターに繋いだHDDは、PCからは見えるが、TVから見えない。DLNAをサポートしていないことが判明。メディアサーバーとしては使えない。どうせTV番組の録画だし、初期化して別のことに使おうか。
</p>
★無意味な空の段落
<p>
<br>
</p>
これからは、もうちょっと丁寧に作成しよう。
HTMLからXMLへの変換
変換前:入力ファイル(noteエクスポートファイル)抜粋
1つのファイルの中に複数の記事を格納している。<item>~</item>でひとつの記事を囲っていて、これが複数並んでいる。
:
<item>
<title><![CDATA[Inkscape 1.4]]></title>
<link>https://note.com/bright_xxxxx/n/nfxxxxx</link>
:
<description>
</description>
<content:encoded>
<![CDATA[
<figure name="...">
<img src="/assets/xxx.png">
<figcaption></figcaption>
</figure>
<p name="..." id="b66bccb9...">ドロー系お絵かきソフトInkscape。<br>ぼーっとしている間に、バージョンが上がっていた(2024/10)ので、インストールしてみた。</p>
<p name="..." id="2363929a...">旧バージョン(Inkscape 1.02)を使うときは、解像度の異なるディスプレイをつなぐと、マウスカーソルの位置がずれて、使い物にならないので、サブ画面の電源を切っていた。今回インストールしたバージョンではサブ画面の電源を入れても問題なく操作できる(もっと前のバージョンで解決されていたのかもしれないけれど)。</p>
<p name="..." id="1e3cba06...">作画画面が広く使えるようになった。感謝。</p>
]]>
</content:encoded>
:
<wp:post_date>
<![CDATA[2024-12-27 11:57:07]]>
</wp:post_date>
:
</item>
:
変換後:出力ファイル(上記部分を変換したもの)
ひとつの記事(<item>~</item>)をひとつのファイルに出力する。なので、複数のファイルを出力することになる。ファイル名は、「固定文字+投稿日+投稿時分.xml」とした(1分間に2つ以上投稿しない前提)。
<?xml version='1.0' encoding='utf-8'?>
<?xml-stylesheet href="web.xsl" type="text/xsl"?>
<doc n="Inkscape 1.4">
<p>2024-12-27 11:57:07</p>
<figure href="assets/xxx.png" n="" />
<p>ぼーっとしている間に、バージョンが上がっていた(2024/10)ので、インストールしてみた。</p>
<p>旧バージョン(Inkscape 1.02)を使うときは、解像度の異なるディスプレイをつなぐと、マウスカーソルの位置がずれて、使い物にならないので、サブ画面の電源を切っていた。今回インストールしたバージョンではサブ画面の電源を入れても問題なく操作できる(もっと前のバージョンで解決されていたのかもしれないけれど)。</p>
<p>作画画面が広く使えるようになった。感謝。</p>
</doc>
ブラウザ表示例(上記XMLファイルを表示)
ヘッダの写真は 割愛。
やったこと
入力XML本文のHTML(content)からXMLを生成する
生成するXMLに、入力XMLのタイトル(title)と投稿日(post_date)を追加する
生成するXMLに、スタイルシートの定義行を追加する
変換したHTMLタグ:h1,h2,ol,ul,li,p,img,pre,code,figure,figcaption
読み捨てたHTMLタグ:br
記事毎にXMLファイルを分けて出力する。出力ファイルのタイトルを一覧するXMLファイル(index.xml)を出力する
※自分用ツールなので、自分が使っていないタグは対応できない。
ソースコード
バグあるかも。いろいろ雑。使いながら困ったら直す。かも。
import sys
## XML
import xml.etree.ElementTree as ET
## HTML
from html.parser import HTMLParser
## mode
test = True #False #True
## XML name space
xmlns = {"excerpt":"http://wordpress.org/export/1.2/excerpt/",
"content":"http://purl.org/rss/1.0/modules/content/",
"wfw" :"http://wellformedweb.org/CommentAPI/",
"dc" :"http://purl.org/dc/elements/1.1/",
"wp" :"http://wordpress.org/export/1.2/"}
## メイン関数
def main(input_file,output_dir):
print ("** main start")
print ("** input :",input_file)
print ("** output:",output_dir)
file_name_list = []
#parse start
tree = ET.parse(input_file)
root = tree.getroot()
for item in root.findall('channel/item'):
dprint ("")
#タイトルを抽出
title = item.find('title').text
dprint ("** title :",title)
#XMLエレメントを作成
doc = ET.Element('doc')
doc.set('n',title)
#投稿日を抽出
post_date = item.find("wp:post_date", xmlns).text
dprint ("** post date :",post_date)
p_post_date = ET.SubElement(doc,'p')
p_post_date.text = post_date
#本文を抽出
content = item.find("content:encoded", xmlns).text
dprint ("** content(html) :")
#本文(html)をXMLに変換
print ("** ",post_date," ",title)
htmlparser = NoteHTMLParser()
htmlparser.setxml(doc)
htmlparser.feed(content)
doc_result = htmlparser.getxml()
#stylesheet定義だけのエレメントを作成
top = ET.Element(None)
style = ET.ProcessingInstruction('xml-stylesheet','href="web.xsl" type="text/xsl"')
top.append(style)
#変換済ドキュメントを追加
top.append(doc_result)
#XMLファイル出力
doctree = ET.ElementTree(top)
output_file_name ="doc_"+post_date[0:4]+post_date[5:7]+post_date[8:10]+"-"+post_date[11:13]+post_date[14:16]+".xml"
doctree.write(output_dir + "/" + output_file_name, encoding='utf-8', xml_declaration=True)
file_name_list.append(output_file_name)
#索引ファイル(index.xml)作成
print ("** make index")
index_xml = ET.Element(None)
index_style = ET.ProcessingInstruction('xml-stylesheet','href="web.xsl" type="text/xsl"')
index_xml.append(style)
index_doc = ET.SubElement(index_xml,'doc')
index_doc.set("n","no+e バックアップ")
index_doc.set("index","no")
index_ul = ET.SubElement(index_doc,'ul')
for file_name in file_name_list:
index_li = ET.SubElement(index_ul,'li')
doclink = ET.SubElement(index_li,'link')
doclink.set("href", file_name)
doctree = ET.ElementTree(index_xml)
output_file_name ="index.xml"
doctree.write(output_dir + "/" + output_file_name, encoding='utf-8', xml_declaration=True)
print ("** main end")
###################
## HTML parser
class NoteHTMLParser(HTMLParser):
subelement = [0]*10 # 出力XML生成用
tag_nest = [] # 入力HTMLの階層解析用
def setxml(self,ixml):
#XMLのルート設定
subelement_depth = 0
self.subelement[subelement_depth] = ixml
self.tag_nest = ["h0"]
def getxml(self):
#生成後XMLの応答
return self.subelement[0]
def handle_starttag(self, tag, attrs): #ex. <p class="x">
if (tag == "h1" or tag == "h2" or tag == "h3"):
if (self.tag_nest[-1] >= tag):
start_tag = self.tag_nest.pop()
if (len(self.tag_nest) > 0 and self.tag_nest[-1] >= tag):
start_tag = self.tag_nest.pop()
nest = self.tag_nest.append(tag)
subelement_depth = len(self.tag_nest) - 1
dprint (self.tag_nest," start tag:",tag)
if (tag == "br"):
start_tag = self.tag_nest.pop()
elif (tag == "img"):
for attr in attrs:
(key,value) = attr
dprint (" attr: ",key,"=",value)
if (key == "src"):
self.subelement[subelement_depth-1].set("href", value[1:]) # 先頭の"/"を外して、相対パスに変更
start_tag = self.tag_nest.pop()
elif (tag == "p"):
self.subelement[subelement_depth] = ET.SubElement(self.subelement[subelement_depth-1],'p')
elif (tag == "h1"):
self.subelement[subelement_depth] = ET.SubElement(self.subelement[subelement_depth-1],'se')
elif (tag == "h2"):
self.subelement[subelement_depth] = ET.SubElement(self.subelement[subelement_depth-1],'se')
elif (tag == "h3"):
self.subelement[subelement_depth] = ET.SubElement(self.subelement[subelement_depth-1],'se')
elif (tag == "ul"):
self.subelement[subelement_depth] = ET.SubElement(self.subelement[subelement_depth-1],'ul')
elif (tag == "li"):
self.subelement[subelement_depth] = ET.SubElement(self.subelement[subelement_depth-1],'li')
elif (tag == "ol"):
self.subelement[subelement_depth] = ET.SubElement(self.subelement[subelement_depth-1],'ol')
elif (tag == "pre"): #割り切って図として扱う
self.subelement[subelement_depth] = ET.SubElement(self.subelement[subelement_depth-1],'figure')
self.subelement[subelement_depth].set("n", "")
elif (tag == "code"):
self.subelement[subelement_depth] = ET.SubElement(self.subelement[subelement_depth-1],'pre')
elif (tag == "figure"):
self.subelement[subelement_depth] = ET.SubElement(self.subelement[subelement_depth-1],'figure')
elif (tag == "figcaption"):
pass
else:
dprint (" skip: ",tag)
def handle_endtag(self, tag): #ex. </p>
dprint(self.tag_nest," end tag :", tag)
if (tag == "ol" or tag == "ul" or tag == "li" or tag == "p" or tag == "figure" or tag == "figcaption" or tag == "pre" or tag == "code"):
start_tag = self.tag_nest.pop()
def handle_data(self, data):
dprint (self.tag_nest," data:",data)
subelement_depth = len(self.tag_nest) -1
current_tag = self.tag_nest[subelement_depth]
if (current_tag == "p"):
prev_data = self.subelement[subelement_depth].text
prev_text =""
if (prev_data is not None):
prev_text = str(prev_data)
self.subelement[subelement_depth].text = prev_text + data
if (current_tag == "pre"):
self.subelement[subelement_depth].text = data
if (current_tag == "code"):
self.subelement[subelement_depth].text = data
if (current_tag == "h1"):
self.subelement[subelement_depth].set('n',data)
if (current_tag == "h2"):
self.subelement[subelement_depth].set('n',data)
if (current_tag == "h3"):
self.subelement[subelement_depth].set('n',data)
if (current_tag == "figcaption"):
self.subelement[subelement_depth-1].set('n',data)
def dprint(*args):
if (test == True):
for arg in args:
print (arg, end=" ")
print ("")
###################
if __name__ == "__main__":
print ("***** note からエキスポートしたデータから、必要なデータを取り出す *****")
print (" python note2pendoc.py 入力XMLファイル XML出力フォルダ")
print ("")
#コマンドラインパラメーター取り込み
args = sys.argv
input_file = args[1]
output_dir = args[2]
#メイン関数
main(input_file, output_dir)
追記 2025/2/2
生成したXMLは改行が無く見にくい。これを改善するには、xml.etree.ElementTree.indent を使う。
変更前
doctree = ET.ElementTree(index_xml)
output_file_name ="index.xml"
doctree.write(output_dir + "/" + output_file_name, encoding='utf-8', xml_declaration=True)
変更後
インデントにTABをひとつ挿入。Pythonのドキュメントにはスペースに関する記述しかないので、space='\t'が動作するのは仕様なのか、たまたまなのか分からない。
doctree = ET.ElementTree(index_xml)
ET.indent(doctree, space='\t', level=-0)
output_file_name ="index.xml"
doctree.write(output_dir + "/" + output_file_name, encoding='utf-8', xml_declaration=True)