[Github] 깃헙 프로필 꾸미기 - 깃헙 블로그 최신글 추가

깃헙 프로필에 블로그 최신글을 넣으려고 한다.

tistory, velog 등 외부 플랫폼 자료는 많은데 비해, 깃헙 블로그 자료는 못 찾아 만들어 보기로 했다.

결과는 깃헙 프로필 확인

작업 방법

글 목록만 만들어서 github action에서 README.md를 고치도록 만들면 된다.

github action : github CI/CD 기능으로 workflow를 자동화 하는 기능. 코드 빌드/테스트/배포가 가능하다.

작업순서

  1. 블로그 최신글의 제목과 url을 파싱한다.
  2. 파싱한 데이터를 README.md에 추가한다.
  3. 1-2를 실행하는 코드를 github action에 붙여준다.

상세 작업

1. 블로그 최신글 파싱

환경

환경 버전
python 3.8
requests 2.25.1
bs4 4.6.0
pytz 2020.5
  • 모두 최신 버전으로 사용해도 상관없다. 최소한 위의 버전이라면 오류가 나지 않더라.

1-1. html 받아오기

내 블로그는 메인 화면에 최신글 5개가 표출되기 때문에 메인 화면을 가져오면 된다.

import requests
from bs4 import BeautifulSoup

blog_url = "https://cherrue.github.io"
my_headers = {'Content-Type': 'application/json; charset=utf-8'}
my_timeout = 5
res = requests.get(blog_url, headers=my_headers, timeout=my_timeout)
soup = BeautifulSoup(res.text, "html.parser")

soup.prettify()

실행 결과

<!DOCTYPE doctype html>

<!--
  Minimal Mistakes Jekyll Theme 4.21.0 by Michael Rose
  Copyright 2013-2020 Michael Rose - mademistakes.com | @mmistakes
  Free for personal and commercial use under the MIT license
  https://github.com/mmistakes/minimal-mistakes/blob/master/LICENSE
-->
<html class="no-js" lang="ko">
<head>
<meta charset="utf-8"/>
<!-- begin _includes/seo.html --><title>체르에의 개발 블로그</title>
(이하 생략)

1-2. 원하는 데이터 가져와서 markdown으로 만들기

html_tag

크롬 개발자도구에서 동그라미 친 버튼을 누르고 원하는 내용을 클릭하면, 네모박스 처럼 확장되어 보인다.

우리가 가져올 것은 article 하위의 a 태그들을 가져오면 된다는 것을 알 수 있다.

import datetime

articles = soup.select('article')
articles_data = [(article.select_one('a').get_text(), article.select_one('a').get("href"))
                     for article in articles]
write_text = ""
for post in articles_data:
    write_text += f"- [{post[0]}]({blog_url}{post[1]}) <br>\n"
write_text += "Updated at " + datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")

print(write_text)

beautifulsoup4 의 select 함수에 css 선택자를 넣어주면 일치하는 태그들을 list로 가져온다. select_one은 첫번째만 가져온다.

실행 결과

- [[강의요약] MS Learn - Azure Database 및 분석 서비스 살펴보기\n](https://cherrue.github.io/azure/azure_fundamentals/Azure-Database) <br>\n- [[강의요약] MS Learn - Azure Storage 서비스 살펴보기\n](https://cherrue.github.io/azure/azure_fundamentals/Azure-Storage) (이하생략)

1-3. README.md에 데이터 작성

markdown 관련 파이썬 모듈도 있지만, 굳이 쓸 필요없다. python 내장 파일 입출력으로 충분하다.

README.md를 잠깐 보자.

### 📝 Recent Blog Posts  
<!-- BLOG-POST-LIST:START -->  
<!-- BLOG-POST-LIST:END -->  
<br/>  

이 START와 END 주석 사이에 데이터를 작성하면 되겠다.

with open(file_url, mode="r+", encoding="utf-8") as f:
    data = f.readlines()
    f.seek(0)

    erase_flag = False
    for line in data:
        if "<!-- BLOG-POST-LIST:START -->" in line:
            erase_flag = True
            f.write(line)
            f.write(write_text)
        elif "<!-- BLOG-POST-LIST:END -->" in line:
            erase_flag = False
        if not erase_flag:
            f.write(line)
    f.truncate()

한 줄 한 줄 가져와서 START 주석을 만나면 만들어둔 markdown을 삽입하고

END를 만날 때 까지의 line을 무시한다.


2. Github action 추가

github_action

github action > Publish Python Package 를 선택하여 yaml파일을 만들자.

on에 발생하는 트리거를, jobs에 원하는 작업을 나열해주면 된다. yaml은 띄어쓰기에 민감하니 주의

name: Update README.md

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]
  schedule:
    - cron: "0 0 */1 * *"

jobs:
  deploy:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v2
    - name: Set up Python
      uses: actions/setup-python@v2
      with:
        python-version: '3.8'
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install build
        pip install bs4
        pip install requests
        pip install pytz
    - name: Update README.md
      run: |
        python main.py
    - name: Commit README.md
      run: |
        git pull
        git add .
        git diff
        git config --local user.email "action@gihtub.com"
        git config --local user.name "GitHub Action"
        git commit -m "[Auto Update] Update recent blog posts to README.md"
        git push

결과

push/pull request가 발생하거나 crontab에 설정한 시간이 되면 action이 자동으로 실행된다.

실행 로그 화면

github_action_log

실패하면 아래와 같은 메일도 보내준다. (블로그 최신글 목록이 안 바뀌어 commit 할 사항이 없어도 메일이 온다.)

fail_mail

최종 소스코드는 https://github.com/Cherrue/Cherrue 를 참고하세요.

main.py
import requests
from bs4 import BeautifulSoup
import datetime
import pytz

timeout = 5
blog_url = "https://cherrue.github.io"
dest_file_url = "README.md"
KST = pytz.timezone('Asia/Seoul')


def getTitleAndLinkFromResponse(res):
    soup = BeautifulSoup(res.text, "html.parser")
    articles = soup.select('article')
    articles_data = [(article.select_one('a').get_text(), article.select_one('a').get("href"))
                     for article in articles]
    return articles_data


def getPostsTop5(_url: str, _timeout):
    MAX_RETRY = 5
    my_headers = {'Content-Type': 'application/json; charset=utf-8'}
    retries = 0
    while True:
        res = requests.get(_url, headers=my_headers, timeout=_timeout)
        retries = retries + 1

        # like do while
        if res is not None and res.status_code == 200:
            break

        # prevent endless loop
        if retries > MAX_RETRY:
            return [("posts parse failed", "about:blank/")]
    return getTitleAndLinkFromResponse(res)


def getMarkdownTextFromPosts(_posts: list):
    result = ""
    for post in _posts:
        result += f"- [{post[0]}]({blog_url}{post[1]}) <br>\n"
    result += "Updated at " + \
        datetime.datetime.now(KST).strftime(
            "%Y-%m-%d %H:%M:%S") + " (+09:00)<br>\n"
    return result


def writeNewPostListToFile(file_url: str, write_text: str):
    with open(file_url, mode="r+", encoding="utf-8") as f:
        data = f.readlines()
        f.seek(0)

        erase_flag = False
        for line in data:
            if "<!-- BLOG-POST-LIST:START -->" in line:
                erase_flag = True
                f.write(line)
                f.write(write_text)
                continue  # don't erase START line
            if "<!-- BLOG-POST-LIST:END -->" in line:
                erase_flag = False
            if not erase_flag:
                f.write(line)
        f.truncate()


posts = getPostsTop5(blog_url, timeout)
markdown_text = getMarkdownTextFromPosts(posts)
writeNewPostListToFile(dest_file_url, markdown_text)

.github/workflows/update-recent-posts.yml
# This workflow will upload a Python Package using Twine when a release is created
# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries

# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.

name: Update README.md

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]
  schedule:
    - cron: "0 0 */1 * *"
#  release:
#    types: [published]

jobs:
  deploy:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v2
    - name: Set up Python
      uses: actions/setup-python@v2
      with:
        python-version: '3.8'
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install build
        pip install bs4
        pip install requests
        pip install pytz
    - name: Update README.md
      run: |
        python main.py
    - name: Commit README.md
      run: |
        git pull
        git add .
        git diff
        git config --local user.email "action@gihtub.com"
        git config --local user.name "GitHub Action"
        git commit -m "[Auto Update] Update recent blog posts to README.md"
        git push
#     - name: Build package
#       run: python -m build
#     - name: Publish package
#       uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
#       with:
#         user: __token__
#         password: $

댓글남기기