見出し画像

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のブラウザ表示例

やったこと

  • 入力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)


いいなと思ったら応援しよう!