Skip to content

Instantly share code, notes, and snippets.

@junichim

junichim/COPYING

Last active Dec 23, 2020
Embed
What would you like to do?
ブログ『はてなブログのコードハイライトを SyntaxHighlighter から はてなの markdown に変更』で作ったツール

ブログ記事

はてなブログのコードハイライトを SyntaxHighlighter から はてなの markdown に変更

で作ったツールの Gist

ファイルの説明

  • convert_syntax.py : 言語一覧取得および変換ツール
  • fix_syntax.py : 変換ミス修正ツール
  • check_syntax.py : 変換後の表記チェック用ツール

(以下は共通で利用)

  • code_lang.py : 言語対応辞書
  • entry_util.py : エントリーXML用のユーティリティ
  • hatena_entries.py : エントリー一覧提供クラスなど

準備

このツールを試す場合は、全てのファイルを同じディレクトリに保存してください。

次に、 hatena_entries.py を開き、

  • ユーザーID
  • APIキー
  • はてなID
  • ブログID

を指定します。 これで、準備完了です。

使い方

python3 ツールファイル名 -h

でヘルプが見れます。

ライセンス

WTFPL ライセンス

import xml.etree.ElementTree as ET
import re
import hatena_entries as HE
# 機能
#
# はてなブログの markdown によるコードハイライトによるコードの記述チェック
# 指定された正規表現が含まれている記事タイトルをリストアップする
# チェック内容は次のとおり
# htmlエスケープ文字を含む: < , > & が対象
# br タグを含む
# html エスケープ文字検出用
escapedPattern = re.compile('(?:<|>|&)')
# br タグ検出用
# ``` と ``` に囲まれた中に br タグがあるものを検出
# markdown のコードハイライトで言語名がない場合があると正しく
# 対応しないが、チェック用なので、とりあえずOKとする
brPattern = re.compile('```.*?<\s*br\s*/?>.*?```', re.MULTILINE | re.DOTALL)
#brPattern = re.compile('<\s*br\s*/?>', re.MULTILINE | re.DOTALL)
# 対象記事数
num_check = 0
def escapedChecker(text):
m = escapedPattern.findall(text)
return len(m) > 0
def brChecker(text):
m = brPattern.findall(text)
return len(m) > 0
# 記事情報の取得
def listup(entry, checker):
global num_check
content = entry.find('def:content', HE.ns)
title = entry.find('def:title', HE.ns)
resultSet = set()
# content に 指定のパターンがあるか
if content is not None and content.text is not None:
if checker(content.text):
num_check += 1
resultSet.add(title.text)
return resultSet
def printResultSet(resultSet):
for r in sorted(resultSet):
print(r)
# メイン処理
def proc(args):
entries = HE.HatenaEntries()
resultSet = set()
for entry in entries:
if args.br:
resultSet.update(listup(entry, brChecker))
elif args.escaped:
resultSet.update(listup(entry, escapedChecker))
print("\nnum of articles: {}".format(entries.getCurrentArticles()))
print("num of articles for check: {}\n".format(num_check))
if resultSet:
printResultSet(resultSet)
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="check entries for converted markdown about code highlight")
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument("-b", "--br", action="store_true", help="list article titles that has br tag between triple quote")
group.add_argument("-e", "--escaped", action="store_true", help="list article titles that has html escaped character(s)")
args = parser.parse_args()
num_convert = 0
proc(args)
# 言語指定の変換一覧
# key: SyntaxHighlighter
# val: はてなブログ markdown
lang= {
"actionscript3": "actionscript",
"bash": "sh",
"cpp": "cpp",
"dart": "dart",
"html": "html",
"java": "java",
"javascript": "javascript",
"js": "javascript",
"json": "javascript",
"php": "php",
"python": "python",
"scala": "scala",
"sql": "sql",
"text": "", # text には単なるコードブロックを対応させる
"vb": "vb",
"xml": "xml",
"yaml": "yaml",
}
import xml.etree.ElementTree as ET
import re
import hatena_entries as HE
import entry_util as eutil
import code_lang as codeLang
# 機能
#
# SyntaxHighlighter によるコードハイライトを含む記事一覧の取得
# 指定されている言語コード一覧の取得
# SyntaxHighlighter によるコードハイライトを はてなブログの markdown に変換
# SyntaxHighlighter の記述を見つけるための正規表現
#
# <pre> タグと </pre> タグに囲まれた部分を見つける
# 典型的な pre タグは次のようなものを想定
# <pre class="brush: bash" data-unlink="">
# <pre class="brush: bash", data-unlink="">
# <pre class="brush: bash;">
# data-unlink 指定はない場合もある
#
# 以下をグループとして取得する
# \1 : <pre> タグ
# \2 : class 属性を指定する際のクォーテーション
# \3 : 言語名
# \4 : コード本体
# \5 : </pre> タグ
#
# コード本体が複数行に渡るので、複数行でのマッチを行う
pattern = re.compile('(<\s*pre\s+class\s*=\s*(?P<quot>[\"\']?)\s*brush\s*:\s*([\d\w]+)\s*;?\s*(?P=quot)\s*,?\s*(?:data-unlink\s*=\s*(?:\"\"|\'\'))?\s*>)(.+?)(</\s*pre\s*>)', re.MULTILINE | re.DOTALL)
# 変換対象漏れチェック用
#pattern = re.compile('<\s*pre\s+class\s*=\s*[\"\']?\s*brush', re.MULTILINE | re.DOTALL)
# 変換対象記事数
num_convert = 0
def replacer(matchObj):
nl1 = "" if matchObj.group(4)[0] == "\n" else "\n"
nl2 = "" if matchObj.group(4)[-1] == "\n" else "\n"
# 問題再現
#nl1 = ""
#nl2 = ""
return "```" + codeLang.lang[matchObj.group(3)] + nl1 + matchObj.group(4) + nl2 + "```"
def getConvertedContent(content_text):
return re.sub(pattern, replacer, content_text)
# 記事情報の取得
# isLang, true: 言語一覧, false: 記事タイトル一覧
def listup(entry, isLang):
global num_convert
content = entry.find('def:content', HE.ns)
title = entry.find('def:title', HE.ns)
resultSet = set()
# content に syntaxhighlighter の記述 があるか
if content is not None and content.text is not None:
m = pattern.findall(content.text)
if len(m) > 0:
num_convert += 1
if isLang:
for v in m:
resultSet.add(v[2]) # #3 lang
else:
resultSet.add(title.text)
return resultSet
def convert(entry):
global num_convert
content = entry.find('def:content', HE.ns)
# content に syntaxhighlighter の記述 があるか
if content is not None and content.text is not None:
m = pattern.findall(content.text)
if len(m) > 0:
num_convert += 1
# ある場合の処理
# syntaxhighlighter -> markdown に変換
converted_text = getConvertedContent(content.text)
# 更新用の xml 文字列を生成
xml_str = eutil.getConvertedXml(entry, converted_text)
# 記事更新
#print('-------------------------')
#print(xml_str)
#print('-------------------------')
# entry_id を取得
entry_id = eutil.getEntryId(entry)
HE.updateEntry(entry_id, xml_str)
def printResultSet(resultSet):
for r in sorted(resultSet):
print(r)
# メイン処理
def proc(args):
entries = HE.HatenaEntries()
resultSet = set()
for entry in entries:
if args.list:
resultSet.update(listup(entry, False))
elif args.lang:
resultSet.update(listup(entry, True))
elif args.convert:
convert(entry)
print("\nnum of articles: {}".format(entries.getCurrentArticles()))
print("num of articles to convert: {}\n".format(num_convert))
if resultSet:
printResultSet(resultSet)
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="convert syntaxhighlighter to markdown")
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument("-l", "--list", action="store_true", help="list article titles that has syntaxhighliter code")
group.add_argument("-g", "--lang", action="store_true", help="list language name")
group.add_argument("-c", "--convert", action="store_true", help="convert syntaxhighlighter to markdown")
args = parser.parse_args()
num_convert = 0
proc(args)
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. You just DO WHAT THE FUCK YOU WANT TO.
import xml.etree.ElementTree as ET
import hatena_entries as HE
def getEntryId(entry):
for child in entry.findall('def:link', HE.ns):
if child.get('rel') == 'edit':
url = child.get('href')
return url.split('/')[-1]
return None
def getConvertedEntry(current, content_converted):
# 修正後の entry を生成
entry = ET.Element('entry')
entry.set('xmlns', 'http://www.w3.org/2005/Atom')
entry.set('xmlns:app', 'http://www.w3.org/2007/app')
# title
title = ET.Element('title')
title.text = current.find('def:title', HE.ns).text
entry.append(title)
# author
author = ET.Element('author')
name = ET.Element('name')
name.text = current.find('def:author/def:name', HE.ns).text
author.append(name)
entry.append(author)
# content
content = ET.Element('content')
content.set('type', 'text/plain')
content.text = content_converted
entry.append(content)
# updated
updated = ET.Element('updated')
updated.text = current.find('def:updated', HE.ns).text
entry.append(updated)
# category ある場合のみ、複数あり
for elm in current.findall('def:category', HE.ns):
category = ET.Element('category')
category.set('term', elm.get('term'))
entry.append(category)
# app:control
control = ET.Element('app:control')
draft = ET.Element('app:draft')
draft.text = current.find('app:control/app:draft', HE.ns).text
control.append(draft)
entry.append(control)
return entry
def getXmlString(entry):
s = ET.tostring(entry, encoding='utf8', method='xml').decode('utf-8')
s = s.replace("encoding='utf8'", "encoding='utf-8'")
return s
def getConvertedXml(entry, converted_text):
'''
更新用の xml 文字列を生成
'''
new_entry = getConvertedEntry(entry, converted_text)
xml_str = getXmlString(new_entry)
return xml_str
import xml.etree.ElementTree as ET
import re
import hatena_entries as HE
import entry_util as eutil
import code_lang as clang
# 変換済み markdown のコードハイライトの不具合を修正するための正規表現
#
# 解決する不具合
# トリプルクォートがコード部分と改行文字を挟まずにつながっている
#
# 想定する形式
# ```lang
# content
# ```
# lang は指定がない場合もある
#
# 不具合のある状態
# (1) ```langcontent
# (2) ```content
# (3) content```
# ※ ```langcontent``` のような1行の形式は、2回に分けて修正することで対応する
#
# なお、トリクルクォートの開始・終了前後に文字がある場合は
# 想定外とする(必要があれば手作業で修正対応)
pattern_pre = re.compile('^(```)(.+)$', re.MULTILINE)
pattern_post = re.compile('^(.+)(```)$', re.MULTILINE)
# 修正対象記事数
num_fix = 0
def pre_replacer(matchObj):
langs = set([lng for lng in clang.lang.values() if len(lng) > 0])
for lng in langs:
if matchObj.group(2).startswith(lng):
pre = matchObj.group(1) + lng
content = matchObj.group(2)[len(lng):]
if len(content) > 0:
return pre + "\n" + content
else:
return pre
return matchObj.group(1) + "\n" + matchObj.group(2)
def post_replacer(matchObj):
return matchObj.group(1) + "\n" + matchObj.group(2)
def fixPreMarkdown(content_text):
return re.sub(pattern_pre, pre_replacer, content_text)
def fixPostMarkdown(content_text):
return re.sub(pattern_post, post_replacer, content_text)
def listup(entry):
global num_fix
content = entry.find('def:content', HE.ns)
title = entry.find('def:title', HE.ns)
resultSet = set()
# content に対象の不具合があるか
if content is not None and content.text is not None:
mpre = pattern_pre.findall(content.text)
mpost = pattern_post.findall(content.text)
if len(mpre) > 0 or len(mpost) > 0:
#for m in mpre:
# print(m)
#for m in mpost:
# print(m)
num_fix += 1
resultSet.add(title.text)
return resultSet
def fix(entry):
global num_fix
content = entry.find('def:content', HE.ns)
# content に syntaxhighlighter の記述 があるか
if content is not None and content.text is not None:
mpre = pattern_pre.findall(content.text)
mpost = pattern_post.findall(content.text)
if len(mpre) > 0 or len(mpost) > 0:
num_fix += 1
# 不具合修正
# 2段階に修正することで、1行に
fixed_text = content.text
if len(mpre) > 0:
for m in mpre:
print(m)
#print("pre before:")
#print(fixed_text)
fixed_text = fixPreMarkdown(fixed_text)
#print("pre after:")
#print(fixed_text)
if len(mpost) > 0:
#print("post before:")
#print(fixed_text)
fixed_text = fixPostMarkdown(fixed_text)
#print("post after:")
#print(fixed_text)
# 更新用の xml 文字列を生成
xml_str = eutil.getConvertedXml(entry, fixed_text)
# 記事更新
#print('-------------------------')
#print(xml_str)
#print('-------------------------')
# entry_id を取得
entry_id = eutil.getEntryId(entry)
HE.updateEntry(entry_id, xml_str)
def printResultSet(resultSet):
for r in sorted(resultSet):
print(r)
# メイン処理
def proc(args):
entries = HE.HatenaEntries()
resultSet = set()
for entry in entries:
if args.list:
resultSet.update(listup(entry))
elif args.fix:
fix(entry)
print("\nnum of articles: {}".format(entries.getCurrentArticles()))
print("num of articles to fix: {}\n".format(num_fix))
if resultSet:
printResultSet(resultSet)
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="fix invalid converted markdown about code highlight")
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument("-l", "--list", action="store_true", help="list article titles that has invalid markdown code highlight")
group.add_argument("-f", "--fix", action="store_true", help="fix invalid markdown code highlight")
args = parser.parse_args()
num_fix = 0
proc(args)
import os.path
import urllib.request
import urllib.parse
import urllib.error
import base64
import xml.etree.ElementTree as ET
# 認証
USER = "ユーザーID"
PASSWORD = "APIキー"
# ブログの情報
HATENA_ID = 'はてなID'
BLOG_ID = 'ブログID'
BASEURL = "https://blog.hatena.ne.jp/"
ENTRY_PATH = HATENA_ID + "/" + BLOG_ID + "/atom/entry"
# ドキュメント XML の namespace
ns = {'def': 'http://www.w3.org/2005/Atom', 'app': 'http://www.w3.org/2007/app'}
class HatenaEntries():
'''
はてなブログのエントリー(記事)を取得し、イテレータで扱うためのクラス
'''
def __init__(self):
self.__isFirst = True
self.__count = 0
self.__index = None
self.__entries = None
def __iter__(self):
return self
def __next__(self):
# 最初の呼び出し
if self.__isFirst:
self.__isFirst = False
url = self.__getFirstpageUrl()
self.__getCollectionFromHatena(url)
if self.__entries is not None and self.__index == len(self.__entries) - 1:
# 次のコレクションを設定
self.__getCollectionFromHatena(self.__nextUrl)
if self.__entries is None:
raise StopIteration()
self.__index += 1
return self.__entries[self.__index]
def __getCollectionFromHatena(self, url):
if url is None:
self.__entries = None
self.__index = None
self.__nextUrl = None
return
root = self.__getEntryCollection(url)
self.__entries = root.findall('def:entry', ns)
if self.__entries is None:
self.__index = None
else:
self.__index = -1
self.__count += len(self.__entries)
self.__nextUrl = self.__getNextpageUrl(root)
def __getFirstpageUrl(self):
url = os.path.join(BASEURL, ENTRY_PATH)
return url
def __getNextpageUrl(self, root):
for child in root.findall('def:link', ns):
if child.get('rel') == 'next':
return child.get('href')
return None
def __getEntryCollection(self, url):
basic = base64.b64encode('{}:{}'.format(USER,PASSWORD).encode('utf-8'))
headers={"Authorization": "Basic " + basic.decode('utf-8')}
print("request url: {}".format(url))
req = urllib.request.Request(url, headers=headers)
try:
with urllib.request.urlopen(req) as res:
data = res.read()
except urllib.error.HTTPError as err:
print("exception occured to get entry collection");
print(err)
return None
except urllib.error.URLError as err:
print("failed connection to server");
print(err)
return None
# XML に変換
root = ET.fromstring(data)
return root
def getCurrentArticles(self):
'''
現在までに読み込んだエントリー数
'''
return self.__count
def updateEntry(entry_id, entry_xml):
'''
エントリーの更新
'''
url = os.path.join(BASEURL, ENTRY_PATH + '/' + entry_id)
basic = base64.b64encode('{}:{}'.format(USER,PASSWORD).encode('utf-8'))
headers={
"Authorization": "Basic " + basic.decode('utf-8'),
"Content-type": "application/xml",
}
print("request url: {}".format(url))
req = urllib.request.Request(url, headers=headers, data=entry_xml.encode('utf-8'), method='PUT')
try:
with urllib.request.urlopen(req) as res:
data = res.read()
except urllib.error.HTTPError as err:
print("exception occured for update, entry id: {}".format(entry_id));
print(err)
print("continue next entry")
except urllib.error.URLError as err:
print("failed connection to server");
print(err)
print("continue next entry")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment