Готовим статьи для Хабра: скрипт для подтягивания адресов картинок с habrastorage

в 14:47, , рубрики: habrastorage, python, контент-маркетинг, хабр, хабраконвертер

Готовим статьи для Хабра: скрипт для подтягивания адресов картинок с habrastorage - 1
Программист пишет интересную статью. Холст, масло, ruDALL-E.

Что самое сложное в написании статьи для Хабра? Конечно же сесть и начать писать! А потом вовремя остановиться. Ну а на третьем месте — во всяком случае для меня — стоит загрузка уже готовой статьи на Хабр. Про новый редактор я тактично промолчу, а старый в принципе весьма неплох: статью в markdown можно скопировать в него почти без изменений. Но вот с добавлением картинок есть пара нюансов.

Во-первых, форматирование: markdown не поддерживает ширину-высоту-выравнивание картинок, поэтому если вам захочется красоты, то все теги придется переписать в html. А во-вторых, когда вы зальете картинки на Habrastorage (или в любое другое облако), адреса локальных картинок по всему тексту придется вручную перебивать на ссылки в облаке. Как-то вечером я дописывал статью с ~50 картинками, ужаснулся количеству предстоящей работы, и решил написать простенький скрипт для автоматизации всего этого.

Итак, юзкейз: мы пишем статью в markdown в любимом оффлайновом редакторе и расставляем ссылки на картинки, лежащие где-то рядом на жестком диске.

Готовим статьи для Хабра: скрипт для подтягивания адресов картинок с habrastorage - 2

После этого мы вручную загружаем пачку картинок на Habrastorage, он генерирует ссылки на них. Можно ли вытащить ссылки прямо со страницы Habrastorage? Наверное да, но так как с фронтендом я знаком на уровне

Готовим статьи для Хабра: скрипт для подтягивания адресов картинок с habrastorage - 3

то придется пойти более простым путем. Благо Habrastorage позволяет одним махом скопировать URL всех картинок, которые можно положить в файл (назовем его cloud.txt). Они лежат там в каком-то производном порядке; чтобы понять, где скрывается какая картинка, нужно сравнить их с локальными копиями. Алгоритм простой:

  • вытаскиваем все теги картинок из текста статьи;
  • находим в каждом теге адрес, загружаем по нему картинку;
  • по очереди сравниваем ее с содержимым ссылок в cloud.txt;
  • совпадение? не думаю меняем локальный адрес картинки на ссылку в облаке.

В эту же логику хорошо ложится преобразование тегов из markdown в html и обратно. Если мы смогли распарсить тег и вытащить из него адрес картинки и описание, то

![изысканный жираф](imagesimg1.png)

легко превратить в

<img src="imagesimg1.png" alt="изысканный жираф">

и наоборот. Что ж, приступим.

Ищем теги

Теги в тексте статьи проше всего найти регулярками:

def find_tags(text):
        md_tags = re.findall('![.*](.+)',text)
        html_tags = re.findall('<img.*>',text)
        return md_tags + html_tags

Для работы с тегами создадим класс, который будет отвечать за парсинг тега, его преобразование, а также за загрузку картинок:

class ImageTag():
    def __init__(self,tag):
        self.tag = tag
        self.type = ''
        self.link = None
        self.alt_text = ''
        self._parse_tag()

Адрес картинки, alt-text и остальные аргументы (в случае html) тоже распарсим регулярками.

    def _parse_tag(self):
        if re.match('![.*](.+)',self.tag):
            # this is a markdown tag
            self.type = 'md'
            ... # here goes the parsing

        elif re.match('<img.*>',self.tag):
            # HTML tag
            self.type = 'html'
            ... # here goes the parsing

        else:
            print(f'Tag "{self.tag}" is not recognized')

чуть подробнее

        if re.match('![.*](.+)',self.tag):
            # markdown tag
            self.type = 'md'
            # find the link
            prefix = re.match('![.*](',self.tag)
            postfix = re.search('s*"[^"]*"s*)',self.tag)
            if postfix:
                self.link = self.tag[prefix.end():postfix.start()].strip()
            else:
                self.link = self.tag[prefix.end():-1].strip()
            # find alt text
            self.alt_text = re.match('[^]]*',self.tag[2:]).group(0)

        elif re.match('<img.*>',self.tag):
            # HTML tag
            self.type = 'html'
            # find the link
            s = re.search('srcs*=s*"[^"]*"',self.tag)
            if not s:
                print(f'Cannot find "src" in "{self.tag}"')
                self.type = ''
            prefix = re.match('srcs*=s*"',s.group(0))
            self.link = s.group(0)[prefix.end():-1].strip()
            # find alt text
            s = re.search('alts*=s*"[^"]*"',self.tag)
            if s:
                prefix = re.match('alts*=s*"',s.group(0))
                self.alt_text = s.group(0)[prefix.end():-1].strip()
            # find optional parameters
            s = re.search('widths*=s*"[^"]*"',self.tag)
            if s:
                self.width_tag = s.group(0)
            s = re.search('heights*=s*"[^"]*"',self.tag)
            if s:
                self.height_tag = s.group(0)
            s = re.search('aligns*=s*"[^"]*"',self.tag)
            if s:
                self.align_tag = s.group(0)

Почему не BeautifulSoup? Во-первых, он не работает с Markdown. Во-вторых, он возвращает значение аргумента, которое нас не особо интересует: если мы захотим изменить, скажем, ширину картинки, мы можем найти весь тег width="600" и заменить его на width="400"; какая именно там была ширина, нам безразлично.

Преобразуем теги

С преобразованием в markdown все просто: если тег уже был в markdown, ничего делать не надо; если он был в html, достаточно взять адрес картинки и alt-text и создать новый тег:

    def to_markdown(self):
        if self.type == 'md':
            return self.tag

        elif self.type == 'html':
            return '![' + self.alt_text + '](' + self.link + ')'

Преобразование из markdown в html аналогично, нужно только не забыть добавить дополнительный аргумент (ширину, высоту, выравнивание), если его задал пользователь:

    def to_html(self,width=None,height=None,align=None):
        if self.type == 'md':
            new_tag = '<img src="' + self.link + '"'
            if self.alt_text:
                new_tag += f' alt="{self.alt_text}"'
            if width:
                new_tag += f' width="{width}"'
            if height:
                new_tag += f' height="{height}"'
            if align:
                new_tag += f' align="{align}"'
            new_tag += ">"
            return new_tag
        ...

Если же мы преобразовываем html в html, то нужно проверить, не был ли аргумент уже установлен, и в противном случае заменить его. Генерировать заново весь тег не будем, в нем может быть много другой информации:

        ...
        elif self.type == 'html':
            new_tag = self.tag
            if width:
                if self.width_tag:
                    new_tag = new_tag.replace(self.width_tag,f'width="{width}"')
                else:
                    new_tag = new_tag[:-1] + f' width="{width}"' + ">"
            if height:
                if self.height_tag:
                    new_tag = new_tag.replace(self.height_tag,f'height="{height}"')
                else:
                    new_tag = new_tag[:-1] + f' height="{height}"' + ">"
            if align:
                if self.align_tag:
                    new_tag = new_tag.replace(self.align_tag,f'align="{align}"')
                else:
                    new_tag = new_tag[:-1] + f' align="{align}"' + ">"
            return new_tag

С преобразованием на этом все. Осталось завернуть все это в функцию main(), которую будет вызывать пользователь:

def main(file_in,file_out,format=None,
        width=None,height=None,align=None):

    with open(file_in,'r',encoding="UTF-8") as f:
        text = f.read()
    tags = find_tags(text)
    text_tags = [ImageTag(tag,path=dir_in) for tag in tags]

    if format:
        if format == 'md':
            for tag in text_tags:
                new_tag = tag.to_markdown()
                text = text.replace(tag.tag, new_tag)

        elif format == 'html':
            for tag in text_tags:
                new_tag = tag.to_html(width=width,height=height,align=align)
                text = text.replace(tag.tag, new_tag)

    with open(file_out,'w',encoding="UTF-8") as f:
        f.write(text)

Меняем адреса картинок

Habrastorage отдает нам список ссылок на картинки в виде списка тегов markdown, html, или просто URL-адресов. Добавим в класс ImageTag() поддержку bare URL и подумаем, как лучше сравнивать картинки. На самом деле оптимизировать тут почти нечего: наибольшее время тратится на загрузку картинок с сервера, места в памяти они занимают немного, а одна картинка может встречаться в тексте много раз. Поэтому выберем самый простой путь: будем по очереди идти по картинкам из текста и искать для каждой первое совпадение на сервере:

def main(file_in,file_out,format=None,rename=None,
        width=None,height=None,align=None):
    ...

    if rename:
        # достаем ссылки на облачные картинки из файла
        with open(rename,'r') as f:
            content = f.read()
        hsto_urls = find_tags(content,bare_urls=True)
        hsto_tags = [ImageTag(url) for url in hsto_urls]

        # цикл по локальным картинкам и ссылкам в облако
        for tag in text_tags:
            for hsto_tag in hsto_tags:
                if hsto_tag == tag:
                    text = text.replace(tag.link,hsto_tag.link)
                    matched = True
                    break

Как сравивать объекты класса ImageTag()? Разумеется через сравнение картинок! Их мы будем подгружать по ссылке из тега по первому запросу:

    def __eq__(self, other: 'ImageTag') -> bool:
        return self.img == other.img

    @property
    def img(self):
        if self._img:
            return self._img
        if self._img_failed:
            return None
        self._load_img()
        return self.img

Здесь _img содержит собственно картинку, а флаг _img_failed устанавливается, если ее не удалось загрузить по указанному адресу. Так как адрес может быть и локальным адресом файла, и URL, то мы будем проверять оба (пожалуй, это не самое красивое решение):

    def _load_img(self):
        try:
            if os.path.isfile(os.path.join(self.path,self.link)):
                # загружаем как файл
                self._img = Image.open(os.path.join(self.path,self.link))
            else:
                # загружаем по внешей ссылке
                response = requests.get(self.link)
                image_bytes = io.BytesIO(response.content)
                self._img = Image.open(image_bytes)
        except:
            self._img_failed = True
            print(f'Cannot access image "{self.link}"')

Вот в принципе и все. Осталось прикрутить argparse, чтобы вызывать скрипт как модуль, передавая аргументы через командную строку.

Все вместе

Ключ -f или --format запускает преобразование в markdown или html:

python -m hsto-rename input.md output.md -f md

При преобразовании в html можно добавить аргументы --width, --height и --align:

python -m hsto-rename input.md output.md -f html --align=center

Готовим статьи для Хабра: скрипт для подтягивания адресов картинок с habrastorage - 4
Кликабельно

Замена локальных адресов на облачные запускается ключом -r, --rename, который в качестве аргумента принимает имя файла с ссылками на облако:

python -m hsto-rename input.md output.md -r cloud.txt

Автор: Альберт

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js