├── requirements.txt ├── vercel.json ├── Pipfile ├── .gitignore ├── api └── index.py ├── trending.py └── README.md /requirements.txt: -------------------------------------------------------------------------------- 1 | lxml==4.8.0 2 | Flask==2.2.2 3 | asgiref 4 | flask[async] 5 | requests==2.28.2 -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "rewrites": [{ "source": "/(.*)", "destination": "/api/index" }] 3 | } 4 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | flask = "*" 8 | 9 | [requires] 10 | python_version = "3.9" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | venv/ 2 | .venv 3 | __pycache__/* 4 | .idea/* 5 | 6 | .vercel 7 | *.log 8 | *.pyc 9 | 10 | # Environments 11 | .env 12 | .venv 13 | env/ 14 | venv/ 15 | ENV/ 16 | env.bak/ 17 | venv.bak/ 18 | 19 | .vercel 20 | 21 | .vercel 22 | -------------------------------------------------------------------------------- /api/index.py: -------------------------------------------------------------------------------- 1 | from flask import Flask,jsonify,request 2 | 3 | from trending import NO_RESULT, REPOSITORY, get_all_language, get_trending 4 | 5 | app = Flask(__name__) 6 | 7 | @app.route('/') 8 | def home(): 9 | return 'Hello github trending!' 10 | 11 | @app.route('/lang') 12 | async def lang(): 13 | langs = await get_all_language() 14 | size = len(langs) 15 | if size > 0: 16 | return jsonify({ 17 | 'msg': 'suc', 18 | 'count': size, 19 | 'items': [x.replace('\n', '').strip() for x in langs], 20 | }), 201 21 | else: 22 | return jsonify(NO_RESULT), 404 23 | 24 | @app.route('/repo') 25 | async def repo(): 26 | return await trending(request.args, REPOSITORY) 27 | 28 | 29 | async def trending(args, start_url: str): 30 | lang = args.get("lang", None) 31 | since = args.get("since", None) 32 | url = start_url 33 | if lang is not None: 34 | lang = lang.replace('-shuo', '%23') 35 | url += lang 36 | params = None 37 | if since is not None: 38 | params = {'since': since} 39 | result = await get_trending(url=url, params=params) 40 | if result['count'] > 0: 41 | return jsonify(result), 201 42 | else: 43 | return jsonify(result), 404 44 | -------------------------------------------------------------------------------- /trending.py: -------------------------------------------------------------------------------- 1 | from lxml import etree 2 | import requests 3 | 4 | GITHUB_URL = 'https://github.com/' 5 | REPOSITORY = GITHUB_URL + 'trending/' 6 | USER_AGENT = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 ' \ 7 | 'Safari/537.36 ' 8 | HEADER = {'User-Agent': USER_AGENT} 9 | TIMEOUT = 9 10 | NO_RESULT = { 11 | 'count': 0, 12 | 'msg': 'Unavailable', 13 | 'items': [], 14 | } 15 | 16 | async def get_trending(url: str, params: dict = None) -> dict: 17 | html = await get_html(url, params) 18 | if html: 19 | is_blank = await has_trending(html) 20 | if not is_blank: 21 | return await parse_repo(html) 22 | else: 23 | return NO_RESULT 24 | else: 25 | return NO_RESULT 26 | 27 | 28 | async def parse_repo(html) -> dict: 29 | items = [] 30 | articles = html.xpath('//article') 31 | for article in articles: 32 | item = {'repo': article.xpath('./h1/a/@href')[0][1:]} 33 | item['repo_link'] = GITHUB_URL + item['repo'] 34 | tmp = article.xpath('./p/text()') 35 | item['desc'] = tmp[0].replace('\n', '').strip() if len(tmp) > 0 else '' 36 | tmp = article.xpath('./div[last()]/span[1]/span[2]/text()') 37 | item['lang'] = tmp[0].replace('\n', '').strip() if len(tmp) > 0 else '' 38 | tmp = article.xpath('./div[last()]/a[1]/text()') 39 | item['stars'] = "".join(tmp).replace(' ', '').replace('\n', '') 40 | tmp = article.xpath('./div[last()]/a[2]/text()') 41 | item['forks'] = "".join(tmp).replace(' ', '').replace('\n', '') 42 | tmp = article.xpath('./div[last()]/span[3]/text()') 43 | item['added_stars'] = "".join(tmp).replace('\n', '').strip() 44 | item['avatars'] = article.xpath('./div[last()]/span[2]/a/img/@src') 45 | items.append(item) 46 | return { 47 | 'count': len(items), 48 | 'msg': 'suc', 49 | 'items': items 50 | } 51 | 52 | 53 | async def has_trending(html): 54 | blank = html.xpath('//div[contains(@class,"blankslate")]') 55 | if blank or len(blank) > 0: 56 | return html.xpath('string(//div[contains(@class,"blankslate")]/h3)') \ 57 | .replace('\n', '').strip() 58 | else: 59 | return None 60 | 61 | 62 | async def get_html(url: str, params: dict = None): 63 | try: 64 | if params is not None: 65 | url = "{0}?since={1}".format(url, params.get('since')) 66 | response = requests.get(url=url, headers=HEADER, timeout=TIMEOUT) 67 | except Exception: 68 | return None 69 | else: 70 | return etree.HTML(response.text) 71 | 72 | 73 | async def get_all_language() -> list: 74 | html = await get_html(url=REPOSITORY) 75 | return html.xpath('//div[@class="select-menu-list"]/div/a/span/text()') 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |
4 | A REST API to fetch a list of trending GitHub repositories
5 | A fork of xdgongs/github-trending, but with CORS enabled
6 |
8 |
9 |