// Gotowe workflow

Monitoring trendów GitHub

▸ // rozdziały lekcji

Monitoring trendów GitHub - Wolniś: „Te repo rosną w tym tygodniu”

Cały workflow na kanwie po uruchomieniu, z liczbami items na połączeniach (1, 20, 20...)

Codzienne przeglądanie trendujących repozytoriów zjada 15-20 minut i kończy się tak samo: otwierasz te same projekty co wczoraj, bo nie pamiętasz, które już widziałeś.

Ten workflow robi to o 7:45, zanim usiądziesz do kawy. Czyta stronę github.com/trending, wyciąga nazwę, opis, język i gwiazdki każdego repozytorium i dopisuje do tabeli tylko te, których jeszcze w niej nie ma. W teście: 20 repozytoriów z trendu tygodniowego.

Poprzednia wersja tej lekcji pobierała stronę przez Jinę, a dane wyciągał agent AI z modelem Gemini. Wyrzuciłem to wszystko. Strona trendów ma stały układ HTML i te same dane da się wyciąć selektorami. Model dokładał tylko koszt, dwa klucze do pilnowania i ryzyko, że coś sobie dopowie.

Co robi ten workflow

  1. Codziennie 7:45 - harmonogram.
  2. Przygotuj tabele - Data Table z operacją Create zakłada tabelę github_trendy z kolumnami repo, link, opis, jezyk, gwiazdki, nowe_gwiazdki, zakres i znaleziono. Jeśli tabela już jest, używa istniejącej.
  3. Ustawienia - Edit Fields z dwoma polami: zakres (weekly) i jezyk (pusty).
  4. Pobierz trending - HTTP Request na stronę trendów, odpowiedź jako tekst.
  5. Wytnij repozytoria - węzeł HTML wycina każdy blok article.Box-row jako osobny kawałek HTML i zwraca je jako listę.
  6. Rozbij repozytoria - Split Out, jeden item na repozytorium.
  7. Dane repozytorium - drugi węzeł HTML wyciąga z każdego bloku ścieżkę, opis, język, gwiazdki i nowe gwiazdki.
  8. Uporzadkuj - Edit Fields składa nazwę i link, czyści opis, zamienia liczby z tekstu na liczby.
  9. Czy nowe repo? - Row Not Exists po kolumnie repo.
  10. Zapisz repo - Insert do tabeli.

Czego potrzebujesz

Niczego poza n8n. Strona trendów jest publiczna, tabela zakłada się sama. Bez konta GitHub, bez tokenu, bez modelu AI.

Jak uruchomić

  1. Zaimportuj plik z dołu lekcji (jak importować workflow).
  2. W węźle Ustawienia wybierz zakres: daily, weekly albo monthly. To te same zakresy co na stronie GitHuba.
  3. W polu jezyk wpisz język, na przykład python. Puste pole znaczy wszystkie języki.
  4. Kliknij Execute workflow i zajrzyj do tabeli: Overview > zakładka Data tables > github_trendy.
  5. Kliknij Publish, żeby harmonogram ruszył (interfejs i publikacja).

Adres z ustawień

Pobierz trending składa adres z dwóch pól. Język idzie do ścieżki, zakres do parametru since:

https://github.com/trending/{{ $json.jezyk }}?since={{ $json.zakres }}

Przy pustym języku wychodzi github.com/trending/?since=weekly, przy python github.com/trending/python?since=weekly. Odpowiedź jest ustawiona na format tekstowy, więc cały HTML strony trafia do pola data.

Selektory CSS w dwóch krokach

Pierwszy węzeł HTML ma jeden selektor: article.Box-row. Na stronie trendów każde repozytorium to osobny blok article z klasą Box-row. Opcja zwracania tablicy jest włączona, więc węzeł oddaje jedną listę kawałków HTML, po jednym na repozytorium. Split Out robi z niej osobne items.

Drugi węzeł HTML pracuje już na jednym bloku i wyciąga pięć pól:

PoleSelektorCo bierze
sciezkah2 aatrybut href, np. /wlasciciel/nazwa
opisptekst opisu
jezykspan[itemprop="programmingLanguage"]tekst
gwiazdkia[href$="/stargazers"]link, którego adres kończy się na /stargazers
nowespan.float-sm-righttekst z liczbą gwiazdek zdobytych w wybranym zakresie

Opcja Clean Up Text jest w tym węźle wyłączona. W moich testach sklejała słowa na łamaniach linii, z „architecture" i „code" wyszło „architecturecode". Spacje w opisie porządkuje wyrażenie w następnym węźle:

{{ ($json.opis || '').replace(/\s+/g, ' ').trim() }}

Liczby przychodzą jako tekst z przecinkami. parseInt(($json.gwiazdki || '0').replace(/[^0-9]/g, '')) wyrzuca wszystko, co nie jest cyfrą, i zamienia wynik na liczbę.

Węzeł Dane repozytorium - pięć selektorów i wynik dla jednego repozytorium

Haczyk. To czytanie HTML strony, nie oficjalne API. Gdy GitHub zmieni wygląd strony trendów, selektory przestaną trafiać. Jeśli nie trafi article.Box-row, workflow po cichu nic nie zapisze. Jeśli nie trafi h2 a, wywali się na węźle Uporzadkuj, bo zabraknie ścieżki. Tabela przestała rosnąć albo przyszedł alert o błędzie? Zacznij od selektorów.

Jak znaleźć selektor, gdy przestanie działać

  1. Otwórz github.com/trending w Chrome.
  2. Kliknij prawym przyciskiem nazwę repozytorium i wybierz Zbadaj. Otworzą się narzędzia deweloperskie z podświetlonym elementem.
  3. Popatrz na znacznik i jego klasy. Idź w górę drzewa, aż trafisz na blok, który obejmuje całe repozytorium.
  4. W panelu Elements naciśnij Cmd+F i wpisz selektor, na przykład article.Box-row. Chrome pokaże, ile elementów pasuje. Dla trendów powinno wyjść tyle, ile repozytoriów widzisz na stronie.
  5. Poprawiony selektor wklej do węzła HTML i kliknij Execute step.

Pro-tip. Row Not Exists po repo zapisuje repozytorium tylko przy pierwszym pojawieniu się w trendach. Liczby gwiazdek w tabeli są więc z dnia, w którym repo trafiło do tabeli. Chcesz śledzić, jak rosną? Zamień Row Not Exists i Insert na jeden węzeł Data Table z operacją Upsert po kolumnie repo.

Tabela zakłada się sama, bo Przygotuj tabele ma włączoną opcję Reuse Existing Tables. Dlaczego to ważne, pokazuję w lekcji o czytniku RSS.

Podsumowanie

Dziesięć węzłów, zero kluczy, zero AI. Dwa węzły HTML z selektorami CSS robią to, co wcześniej robił model: wycinają bloki, a z nich pola. Row Not Exists pilnuje, żeby w tabeli były tylko repozytoria, których jeszcze nie widziałeś. Gdy GitHub zmieni stronę, poprawiasz selektor w pięć minut w narzędziach przeglądarki.

W następnej lekcji, Scraper ogłoszeń z powiadomieniem, ten sam pomysł idzie dalej: n8n co godzinę sprawdza wyszukiwanie mieszkań na Otodom i pisze na Telegram o nowych ogłoszeniach.

Pokaż JSON do skopiowania - monitoring-trendow-github.json
{
  "name": "Monitoring trendujacych repozytoriow GitHub",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "triggerAtHour": 7,
              "triggerAtMinute": 45
            }
          ]
        }
      },
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        0
      ],
      "id": "5700a8fc-ab0a-73a4-f8ff-f52d7031db1d",
      "name": "Codziennie 7:45"
    },
    {
      "parameters": {
        "resource": "table",
        "operation": "create",
        "tableName": "github_trendy",
        "columns": {
          "column": [
            {
              "name": "repo",
              "type": "string"
            },
            {
              "name": "link",
              "type": "string"
            },
            {
              "name": "opis",
              "type": "string"
            },
            {
              "name": "jezyk",
              "type": "string"
            },
            {
              "name": "gwiazdki",
              "type": "number"
            },
            {
              "name": "nowe_gwiazdki",
              "type": "number"
            },
            {
              "name": "zakres",
              "type": "string"
            },
            {
              "name": "znaleziono",
              "type": "date"
            }
          ]
        },
        "options": {
          "createIfNotExists": true
        }
      },
      "type": "n8n-nodes-base.dataTable",
      "typeVersion": 1.1,
      "position": [
        220,
        0
      ],
      "id": "f7888cd5-c2a1-8ffd-92a3-2cf3a731fbeb",
      "name": "Przygotuj tabele"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "2a54f3da-17f1-2743-d43a-a9d5159ff51b",
              "name": "zakres",
              "value": "weekly",
              "type": "string"
            },
            {
              "id": "523616ca-7c19-eba2-3a32-adbc0aa9af55",
              "name": "jezyk",
              "value": "",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        440,
        0
      ],
      "id": "15a13955-753a-dbb2-7c08-39db2fdf3d44",
      "name": "Ustawienia"
    },
    {
      "parameters": {
        "url": "=https://github.com/trending/{{ $json.jezyk }}",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "since",
              "value": "={{ $json.zakres }}"
            }
          ]
        },
        "options": {
          "response": {
            "response": {
              "responseFormat": "text"
            }
          }
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        660,
        0
      ],
      "id": "95b056d6-1283-84fe-6361-5387c7ab8f62",
      "name": "Pobierz trending"
    },
    {
      "parameters": {
        "operation": "extractHtmlContent",
        "dataPropertyName": "data",
        "extractionValues": {
          "values": [
            {
              "key": "repozytoria",
              "cssSelector": "article.Box-row",
              "returnValue": "html",
              "returnArray": true
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.html",
      "typeVersion": 1.2,
      "position": [
        880,
        0
      ],
      "id": "1f75ac3e-957e-a77c-22ba-64f84ad96938",
      "name": "Wytnij repozytoria"
    },
    {
      "parameters": {
        "fieldToSplitOut": "repozytoria",
        "options": {
          "destinationFieldName": "html"
        }
      },
      "type": "n8n-nodes-base.splitOut",
      "typeVersion": 1,
      "position": [
        1100,
        0
      ],
      "id": "05477e69-1fe4-ad34-6561-356702b548a4",
      "name": "Rozbij repozytoria"
    },
    {
      "parameters": {
        "operation": "extractHtmlContent",
        "dataPropertyName": "html",
        "extractionValues": {
          "values": [
            {
              "key": "sciezka",
              "cssSelector": "h2 a",
              "returnValue": "attribute",
              "attribute": "href"
            },
            {
              "key": "opis",
              "cssSelector": "p",
              "returnValue": "text"
            },
            {
              "key": "jezyk",
              "cssSelector": "span[itemprop=\"programmingLanguage\"]",
              "returnValue": "text"
            },
            {
              "key": "gwiazdki",
              "cssSelector": "a[href$=\"/stargazers\"]",
              "returnValue": "text"
            },
            {
              "key": "nowe",
              "cssSelector": "span.float-sm-right",
              "returnValue": "text"
            }
          ]
        },
        "options": {
          "trimValues": true,
          "cleanUpText": false
        }
      },
      "type": "n8n-nodes-base.html",
      "typeVersion": 1.2,
      "position": [
        1320,
        0
      ],
      "id": "8c9e240f-7139-8f72-f109-abe2948983ea",
      "name": "Dane repozytorium"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "5fa576f5-806b-e2dc-246d-c246046c10b5",
              "name": "repo",
              "value": "={{ $json.sciezka.replace(/^\\//, '') }}",
              "type": "string"
            },
            {
              "id": "2f2da79d-4c33-4605-9262-a1f337541c24",
              "name": "link",
              "value": "={{ 'https://github.com' + $json.sciezka }}",
              "type": "string"
            },
            {
              "id": "328494d8-30e7-a5ab-c366-277c0404fab9",
              "name": "opis",
              "value": "={{ ($json.opis || '').replace(/\\s+/g, ' ').trim() }}",
              "type": "string"
            },
            {
              "id": "fe1797e1-4090-0c7f-c6f9-d854bd87c2b7",
              "name": "jezyk",
              "value": "={{ $json.jezyk || '' }}",
              "type": "string"
            },
            {
              "id": "996e3a4f-4c72-3b1b-d1eb-13d5adb6815b",
              "name": "gwiazdki",
              "value": "={{ parseInt(($json.gwiazdki || '0').replace(/[^0-9]/g, '')) }}",
              "type": "number"
            },
            {
              "id": "13b8ccde-2524-0073-6621-abba0374b04b",
              "name": "nowe_gwiazdki",
              "value": "={{ parseInt(($json.nowe || '0').replace(/[^0-9]/g, '')) }}",
              "type": "number"
            },
            {
              "id": "f6b3afa6-6972-6b62-0fcf-45f7039559a7",
              "name": "zakres",
              "value": "={{ $('Ustawienia').first().json.zakres }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        1540,
        0
      ],
      "id": "1a557615-08de-977d-4c3c-759bf61fa01a",
      "name": "Uporzadkuj"
    },
    {
      "parameters": {
        "operation": "rowNotExists",
        "dataTableId": {
          "__rl": true,
          "value": "github_trendy",
          "mode": "name"
        },
        "filters": {
          "conditions": [
            {
              "keyName": "repo",
              "condition": "eq",
              "keyValue": "={{ $json.repo }}"
            }
          ]
        }
      },
      "type": "n8n-nodes-base.dataTable",
      "typeVersion": 1.1,
      "position": [
        1760,
        0
      ],
      "id": "0b103b29-badc-ce04-3cc5-13d109f510e2",
      "name": "Czy nowe repo?"
    },
    {
      "parameters": {
        "operation": "insert",
        "dataTableId": {
          "__rl": true,
          "value": "github_trendy",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "repo": "={{ $json.repo }}",
            "link": "={{ $json.link }}",
            "opis": "={{ $json.opis }}",
            "jezyk": "={{ $json.jezyk }}",
            "gwiazdki": "={{ $json.gwiazdki }}",
            "nowe_gwiazdki": "={{ $json.nowe_gwiazdki }}",
            "zakres": "={{ $json.zakres }}",
            "znaleziono": "={{ $now.toISO() }}"
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "repo",
              "displayName": "repo",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "link",
              "displayName": "link",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "opis",
              "displayName": "opis",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "jezyk",
              "displayName": "jezyk",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "gwiazdki",
              "displayName": "gwiazdki",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "number",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "nowe_gwiazdki",
              "displayName": "nowe_gwiazdki",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "number",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "zakres",
              "displayName": "zakres",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "znaleziono",
              "displayName": "znaleziono",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "date",
              "readOnly": false,
              "removed": false
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.dataTable",
      "typeVersion": 1.1,
      "position": [
        1980,
        0
      ],
      "id": "6d6013fd-70cf-f932-a574-4c47c07d5163",
      "name": "Zapisz repo"
    }
  ],
  "connections": {
    "Codziennie 7:45": {
      "main": [
        [
          {
            "node": "Przygotuj tabele",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Przygotuj tabele": {
      "main": [
        [
          {
            "node": "Ustawienia",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Ustawienia": {
      "main": [
        [
          {
            "node": "Pobierz trending",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Pobierz trending": {
      "main": [
        [
          {
            "node": "Wytnij repozytoria",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wytnij repozytoria": {
      "main": [
        [
          {
            "node": "Rozbij repozytoria",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Rozbij repozytoria": {
      "main": [
        [
          {
            "node": "Dane repozytorium",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Dane repozytorium": {
      "main": [
        [
          {
            "node": "Uporzadkuj",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Uporzadkuj": {
      "main": [
        [
          {
            "node": "Czy nowe repo?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Czy nowe repo?": {
      "main": [
        [
          {
            "node": "Zapisz repo",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "active": false
}