Jak analyzovat soubory PDF v měřítku v NodeJS: co dělat a co dělat

Udělejte krok do architektury programu a naučte se, jak pomocí tohoto článku vytvořit praktické řešení skutečného obchodního problému s NodeJS Streams.

Váš partner, poté, co je uložíte bezpočet hodin, procházejte soubory PDF a získejte svá data. (Zdroj: GIPHY)

A Detour: Mechanika tekutin

Jednou z největších silných stránek softwaru je to, že můžeme vyvinout abstrakce, které nám umožní uvažovat o kódu a manipulovat s daty způsobem, kterému rozumíme. Proudy jsou jednou z takových tříd abstrakce.

V jednoduché mechanice tekutin je koncepce proudnice užitečná pro uvažování o tom, jak se částice tekutiny budou pohybovat, a omezení na ně aplikovaná v různých bodech systému.

Řekněme například, že trubkou rovnoměrně protéká voda. V polovině potrubí se větví. Obecně se proud vody rozdělí rovnoměrně do každé větve. Inženýři používají abstraktní pojetí proudnice k uvažování o vlastnostech vody, jako je její průtok, pro libovolný počet větví nebo složitých konfigurací potrubí. Pokud byste se zeptali inženýra, co předpokládal, že bude průtok přes každou větev, správně odpověděl „polovinou“ intuitivně. Tím se matematicky rozšíří na libovolný počet zefektivnění.

Proudy mají koncepčně kódovat, jaké zefektivnění jsou příliš tekuté mechaniky. Můžeme uvažovat o datech v kterémkoli daném bodě tím, že je považujeme za součást toku. Spíše než se starat o podrobnosti implementace mezi tím, jak je uloženo. Pravděpodobně byste to mohli zobecnit na nějaký univerzální koncept potrubí, které můžeme použít mezi disciplínami. Prodává se nám prodejní cesta, ale je to tangenciální a my ji zakryjeme později. Nejlepší příklad toků a ten, se kterým se bezpodmínečně musíte seznámit, pokud ještě nejste potrubí UNIX:

cat server.log | grep 400 | méně

Láskavě nazýváme | charakter potrubí. Na základě jeho funkce připravujeme výstup jednoho programu jako vstup jiného programu. Efektivní nastavení potrubí.

(Také to vypadá jako dýmka.)

Pokud se vám líbí a v této chvíli se ptáte, proč je to nutné, zeptejte se sami sebe, proč používáme potrubí v reálném životě. V zásadě jde o strukturu, která eliminuje ukládání mezi body zpracování. Nemusíme si dělat starosti s uskladněním barelů ropy, pokud je přečerpaná.

Jděte na to, že v softwaru. Chytrí vývojáři a inženýři, kteří psali kód pro data potrubí, jej nastavili tak, aby nikdy na počítači nezabírali příliš mnoho paměti. Bez ohledu na to, jak velký je logfile výše, nezastaví terminál. Celý program je proces zpracovávající infinitesimální datové body v proudu, spíše než kontejnery těchto bodů. Logfile se nikdy nenačte do paměti najednou, ale spíše do spravovatelných částí.

Nechci zde znovu objevovat kolo. Teď, když jsem se zabýval metaforou pro streamování a důvodem pro jejich použití, Flavio Copes má skvělý blogový příspěvek, který popisuje, jak jsou implementovány v Node. Vezměte si tak dlouho, jak budete potřebovat, abyste tam pokryli základy, a až budete připraveni, vraťte se a projdeme případ použití.

Situace

Teď, když máte tento nástroj v pásu nástrojů, představte si toto:

Jste v práci a váš manažer / právní / HR / váš klient / (vložte zde zúčastněné strany) se k vám přiblížil s problémem. Stráví příliš dlouho portováním přes strukturované PDF. Normálně vám lidé něco takového neřeknou. Uslyšíte: „Strávím 4 hodiny zadáváním údajů.“ Nebo „Prohlédněte si cenové tabulky.“ Nebo „Vyplňuji správné formuláře, abychom každé čtvrtletí dostávali tužky se značkou naší společnosti“.

Ať už je to cokoli, pokud jejich práce zahrnuje jak (a) čtení strukturovaných dokumentů PDF, tak i (b) hromadné využití těchto strukturovaných informací. Pak můžete vstoupit a říct: „Hej, mohli bychom to automatizovat a uvolnit váš čas na práci na jiných věcech“.

Rychlost bez námahy. Váš kód je vůně, nyní pozdravte vaši televizní reklamu. (Zdroj: Chris Peeters)

Pro účely tohoto článku pojďme tedy na fiktivní společnost. Odkud pocházím, pojem „figurína“ označuje idiota nebo dudlík dítěte. Představme si tedy tuto falešnou společnost, která vyrábí dudlíky. Zatímco jsme u toho, pojďme žraloka skočit a říci, že jsou vytištěni ve 3D. Společnost funguje jako etický dodavatel dudlíků potřebným, kteří si nemohou dovolit prémiové věci sami.

(Vím, jak to zní hloupě, pozastavte prosím vaši nedůvěru.)

Todd dodává tiskové materiály, které jdou do produktů společnosti DummEth, a musí zajistit, aby splňovaly tři klíčová kritéria:

  • jsou to plasty pro potravinářské účely, které chrání zdraví dětí,
  • jsou levné, pro ekonomickou výrobu a
  • jsou získávány co nejblíže, aby podporovaly marketingovou kopii společnosti s tím, že jejich dodavatelský řetězec je také etický a co nejméně znečišťuje.

Projekt

Takže je snazší je sledovat, nastavil jsem repozitář GitLab, který můžete klonovat a používat. Ujistěte se, že vaše instalace Node a NPM jsou také aktuální.

Základní architektura: Omezení

Co se teď snažíme udělat? Předpokládejme, že Todd dobře pracuje v tabulkách, jako mnoho administrativních pracovníků. Aby mohl Todd třídit příslovečnou 3D tiskovou pšenici od plev, je pro něj snazší měřit materiály podle kvality potravin, ceny za kilogram a umístění. Je čas stanovit některá omezení projektu.

Předpokládejme, že kvalita potravin v materiálu je hodnocena na stupnici od nuly do tří. S nulovým významem zakázané plasty bohaté na BPA v Kalifornii. Tři významy běžně používaných nekontaminujících materiálů, jako je polyethylen s nízkou hustotou. To je čistě pro zjednodušení našeho kódu. Ve skutečnosti bychom museli nějak mapovat textové popisy těchto materiálů (např .: „LDPE“) do potravinářské třídy.

Cena za kilogram můžeme považovat za vlastnost materiálu danou jeho výrobcem.

Poloha, budeme se zjednodušovat a budeme předpokládat, že to bude jednoduchá relativní vzdálenost, jak vrána letí. Na opačném konci spektra je nadřazené řešení: pomocí některých API (např .: Google Maps) k rozeznání hrubé vzdálenosti, kterou by daný materiál urazil, aby dosáhl distribučních center Todda. Ať tak či onak, řekněme, že jsme to dali jako hodnotu (kilometry až Todd) v PDF Toddu.

Podívejme se také na kontext, ve kterém pracujeme. Todd účinně funguje jako sběrač informací na dynamickém trhu. Výrobky přicházejí a odcházejí a jejich detaily se mohou měnit. To znamená, že máme libovolný počet souborů PDF, které se mohou kdykoli změnit nebo lépe aktualizovat.

Na základě těchto omezení můžeme konečně zjistit, čeho chceme, aby náš kód splnil. Pokud chcete vyzkoušet své možnosti designu, pozastavte se zde a zvažte, jak strukturujete své řešení. Nemusí to vypadat stejně jako to, co se chystám popsat. To je v pořádku, pokud poskytujete rozumné proveditelné řešení pro Todda a něco, co byste si později neodtrhali, když se snažíte udržet.

Základní architektura: Řešení

Máme tedy libovolný počet souborů PDF a některá pravidla, jak je analyzovat. Zde je návod, jak to udělat:

  1. Nastavte objekt Stream, který dokáže číst z nějakého vstupu. Jako klient HTTP vyžadující stažení PDF. Nebo modul, který jsme napsali, který čte soubory PDF z adresáře v systému souborů.
  2. Nastavte mezipaměť zprostředkovatele. Je to jako číšník v restauraci, který dodává hotové jídlo zamýšlenému zákazníkovi. Pokaždé, když se do datového proudu dostane celý soubor PDF, vyprázdníme tyto bloky do vyrovnávací paměti, aby bylo možné je transportovat.
  3. Číšník (vyrovnávací paměť) doručí jídlo (data PDF) zákazníkovi (naše funkce analýzy). Zákazník s tím udělá, co chce (převede ho do nějakého tabulkového formátu).
  4. Až bude hotový zákazník (Parser), dejte číšníkovi (Buffer) vědět, že je zdarma a může pracovat na nových objednávkách (PDF).

Všimnete si, že tento proces nemá jasný konec. Jako restaurace se naše kombo Stream-Buffer-Parser nikdy nedokončí, dokud samozřejmě nepřijdou žádná další data - žádné další objednávky.

Teď vím, že ještě není lízat kód. To je zásadní. Je důležité mít možnost napsat si o našich systémech dříve, než je zapíšete. Teď nebudeme mít všechno v pořádku, a to ani s apriorním zdůvodněním. Věci se vždy rozbijí ve volné přírodě. Chyby je třeba opravit.

To znamená, že je to mocné cvičení omezující a předvídavé naplánovat si kód před jeho napsáním. Pokud dokážete zjednodušit systémy zvyšující se složitosti na zvládnutelné části a analogie, budete moci exponenciálně zvýšit vaši produktivitu, protože kognitivní stres z těchto komplexů zmizí do dobře navržených abstrakcí.

Takže ve velkém schématu věcí to vypadá takto:

Počáteční koncepce našeho programu. Až to bude vypadat, nebude to vypadat, ale proces řešení problému je stejně důležitý jako výsledek. V kroužku zeleně: to, co budeme dělat dál.

Představujeme závislosti

Nyní, jako prohlášení o vyloučení odpovědnosti, bych měl dodat, že existuje celý svět myšlenek ohledně zavedení závislostí do vašeho kódu. Rád bych tento koncept zahrnul do jiného příspěvku. Mezitím mi dovolte říci, že jedním ze základních konfliktů ve hře je konflikt mezi naší touhou, aby se naše práce rychle provedla (tj .: vyhnout se syndromu NIH), a naší touhou vyhnout se riziku třetích stran.

Při použití tohoto projektu jsme se rozhodli odložit většinu našeho zpracování PDF do modulu pdfreader. Zde je několik důvodů, proč:

  • To bylo nedávno zveřejněno, což je dobré znamení, že repo je aktuální.
  • Má jednu závislost - to je pouze abstrakce nad jiným modulem - která je na GitHubu pravidelně udržována. To samo o sobě je skvělé znamení. Navíc, závislost, modul s názvem pdf2json, má stovky hvězd, 22 přispěvatelů a spousty očních bulvů, které na něj pečlivě sledují.
  • Správce, Adrian Joly, provádí dobré účetnictví v nástroji pro sledování problémů GitHub a aktivně se stará o otázky uživatelů a vývojářů.
  • Při auditu prostřednictvím NPM (6.4.1) nejsou nalezeny žádné chyby zabezpečení.

Celkově se tedy jeví jako bezpečná závislost.

Nyní modul funguje docela jednoduše, i když jeho README explicitně nepopisuje strukturu svého výstupu. Útes poznámky:

  1. Vystavuje instanci třídy PdfReader
  2. Tato instance má dvě metody pro analýzu PDF. Vrací stejný výstup a liší se pouze vstupem: PdfReader.parseFileItems pro název souboru a PdfReader.parseBuffer z dat, která nechceme odkazovat ze souborového systému.
  3. Metody vyžadují zpětné volání, které se nazývá pokaždé, když PdfReader najde to, co označuje jako položku PDF. Existují tři druhy. Nejprve je to soubor metadat, který je vždy první položkou. Druhou jsou metadata stránky. Funguje jako návrat vozíku pro souřadnice textových položek, které mají být zpracovány. Poslední jsou textové položky, které můžeme považovat za jednoduché objekty / struktury s textovou vlastností a 2D-AABB s pohyblivou řádovou čárkou na stránce.
  4. Je na našem zpětném volání, abychom zpracovali tyto položky do datové struktury podle našeho výběru a také vyřídili případné chyby, které s tím souvisí.

Zde je příklad kódu:

const {PdfReader} = vyžadovat ('pdfreader');
// Inicializace čtečky
const reader = new PdfReader ();
// Přečtěte si libovolně definovaný buffer
reader.parseBuffer (buffer, (err, item) => {
  if (err)
    console.error (err);
  jinak pokud (! položka)
    / * pdfreader frontuje položky v PDF a předává je
     * zpětné volání. Pokud není předána žádná položka, naznačuje to
     * dokončili jsme čtení PDF. * /
    console.log („Hotovo.“);
  jinak pokud (item.file)
    // Položky souboru odkazují pouze na cestu k souboru PDF.
    console.log (`Analýza $ {item.file && item.file.path || 'a buffer'}`)
  jinak pokud (item.page)
    // Položky stránky jednoduše obsahují číslo stránky.
    console.log (`Dosažená stránka $ {item.page}`);
  jinak if (item.text) {
    // Textové položky mají několik dalších vlastností:
    const itemAsString = [
      item.text,
      'x:' + item.x,
      'y:' + item.y,
      'w:' + item.width,
      'h:' + item.height,
    ] .join ('\ n \ t');
    console.log ('Text Item:', itemAsString);
  }
});

Toddovy soubory PDF

Vraťme se k situaci Todda, abychom poskytli nějaký kontext. Chceme ukládat dudlíky dat na základě tří klíčových kritérií:

  • jejich potravinářské třídy, aby se zachovalo zdraví dětí,
  • - jejich náklady na ekonomickou výrobu a -
  • jejich vzdálenost od Toddu, podpora marketingové kopie společnosti, která uvádí, že jejich dodavatelský řetězec je také etický a znečišťuje co nejméně.

Pevně ​​jsem zakódoval jednoduchý skript, který náhodně vybírá některé figuríny, a najdete jej v adresáři / data doprovodného repo tohoto projektu. Tento skript zapíše tato náhodná data do souborů JSON.

Tam je také dokument šablony. Pokud jste obeznámeni s templingovými motory, jako jsou řídítka, pochopíte to. Existují online služby - nebo pokud se cítíte dobrodružně, můžete si své vlastní - které vezmou data JSON a vyplní šablonu a vrátí vám je jako PDF. Možná z důvodu úplnosti to můžeme vyzkoušet v jiném projektu. Každopádně: Takovou službu jsem použil ke generování falešných PDF, které budeme analyzovat.

Jak vypadá jeden (nadbytečné bílé místo bylo oříznuto):

Rádi bychom z tohoto PDF získali nějaký JSON, který nám poskytne:

  • identifikační číslo a datum rekvizice pro účely účetnictví,
  • - SKU dudlíku pro jedinečnou identifikaci a
  • vlastnosti dudlíka (název, kvalita potravin, jednotková cena a vzdálenost), takže je Todd ve své práci skutečně může použít.

Jak to uděláme?

Čtení dat

Nejprve si vytvoříme funkci pro čtení dat z jednoho z těchto PDF a extrahování položek PDF pdfderu do použitelné datové struktury. Prozatím mějte pole představující dokument. Každá položka v poli je objekt představující kolekci všech textových prvků na stránce v indexu daného objektu. Každá vlastnost v objektu stránky má pro svůj klíč hodnotu y a pole textových položek nalezených na této hodnotě y pro tuto hodnotu. Zde je schéma, takže je jednodušší pochopit:

Funkce readPDFPages v /parser/index.js to zpracovává podobně jako výše uvedený příklad kódu:

/ * Přijímá vyrovnávací paměť (např .: z fs.readFile) a analyzuje
 * jako PDF, což dává použitelnou datovou strukturu pro
 * specifická aplikace, druhá úroveň analýzy.
 * /
funkce readPDFPages (buffer) {
  const reader = new PdfReader ();
  // Vracíme zde Promise jako čtení PDF
  // operace je asynchronní.
  vrátit nový Promise ((vyřešit, odmítnout) => {
    // Každá položka v tomto poli představuje stránku v PDF
    nechť stránky = [];
    reader.parseBuffer (buffer, (err, item) => {
      if (err)
        // Pokud máme problém, vysuňte!
        odmítnout (err)
      jinak pokud (! položka)
        // Pokud jsme mimo položky, vyřešte se strukturou dat
        vyřešit (stránky);
      jinak pokud (item.page)
        // Pokud analyzátor dosáhl nové stránky, je čas
        // pracovat na dalším objektu stránky v našem poli stránek.
        pages.push ({});
      jinak if (item.text) {
        // Pokud NEMÁME novou položku stránky, potřebujeme
        // načíst nebo vytvořit nové pole „řádek“
        // představovat sbírku textových položek u nás
        // aktuální pozice Y, což bude Y této položky
        // pozice.
        // Proto tento řádek zní takto:
        // "Buď načtěte pole řádků pro naši aktuální stránku,
        // na naší současné pozici Y nebo vytvořit novou “
        const row = pages [pages.length-1] [item.y] || [];
        // Přidejte položku do referenčního kontejneru (tj .: řádek)
        row.push (item.text);
        // Zahrňte kontejner na aktuální stránku
        stránky [pages.length-1] [item.y] = řádek;
      }
    });
  });
}

Teď, když do této funkce předáváme vyrovnávací paměť PDF, získáme organizovaná data. Tady je to, co jsem dostal od zkušebního běhu a tisk jsem to na JSON:

[{'3.473': ['PODROBNOSTI O PRODUKTECH'],
    '4.329': ['Datum: 23/05/2019'],
    '5.185': ['ID žádosti: 298831'],
    '6.898': ['Pacifier Tech', 'Todd Lerr'],
    '7.754': ['123 Příklad Blvd', 'DummEth Pty. Ltd.' ],
    '8.61': ['Timbuktu', '1337 Leet St'],
    '12. 235 ': [' SKU ',' 6308005 '],
    '13 .466 ': [' Product Name ',' Square Lemon Qartz Pacifier '],
    '14,698 ': [' Food Grade ',' 3 '],
    '15, 928999999999998 ': [' $ / kg ',' 1,29 '],
    '17 .16 ': [' Umístění ',' 55 ']}]

Pokud se podíváte pozorně, všimnete si, že v původním PDF je pravopisná chyba. „Žádost“ je chybně označena jako „žádost“. Krása našeho analyzátoru spočívá v tom, že se v našich vstupních dokumentech nestaráme o takové chyby. Pokud jsou strukturována správně, můžeme z nich extrahovat data přesně.

Teď to potřebujeme uspořádat do něčeho více použitelného (jako bychom to vystavovali přes API). Struktura, kterou hledáme, je něco podobného:

{
  reqID: '000000',
  datum: 'DD / MM / RRRR', // Nebo něco jiného na základě geografie
  sku: '000000',
  name: 'Some String We Trimmed',
  foodGrade: 'X',
  unitPrice: 'D.CC', // D pro dolary, C pro centy
  umístění: 'XX',
}

Kromě: Integrita dat

Proč přidáváme čísla jako řetězce? Je založeno na riziku analýzy. Řekněme, že jsme všechna naše čísla donutili k řetězcům:

Jednotková cena a umístění by byly v pořádku - měly by to být nakonec všechna čísla.

Potravinová třída je pro tento velmi omezený projekt technicky bezpečná. Když je donutíme, neztratí se žádná data - ale pokud je to skutečně klasifikátor, jako je Enum, je lepší se chovat jako řetězec.

ID rekvizice a SKU však, pokud budou vynuceny na řetězce, by mohly ztratit důležitá data. Pokud ID pro danou rekvizici začíná třemi nulami a vynucujeme to na číslo, dobře jsme tyto nuly ztratili a data jsme zkomolili.

Protože chceme při čtení souborů PDF integritu dat, necháme vše jako řetězec. Pokud kód aplikace chce převést některá pole na čísla, aby byla použitelná pro aritmetické nebo statistické operace, necháme nátlak na této vrstvě. Zde chceme pouze něco, co důsledně a přesně analyzuje soubory PDF.

Restrukturalizace dat

Takže teď máme informace Todda, potřebujeme je pouze uspořádat použitelným způsobem. Můžeme použít celou řadu funkcí pro manipulaci s maticemi a objekty a zde je váš přítel MDN.

Toto je krok, ve kterém má každý své preference. Někteří dávají přednost metodě, která právě dělá práci a minimalizuje dev čas. Jiní dávají přednost hledání nejlepšího algoritmu pro danou úlohu (např .: zkrácení doby iterace). Je to dobré cvičení zjistit, jestli můžete přijít na způsob, jak to udělat, a porovnat to, co jsem dostal. Rád bych viděl lepší, jednodušší, rychlejší nebo dokonce různé způsoby, jak dosáhnout stejného cíle.

Každopádně jsem to udělal takto: funkce parseToddPDF v souboru /parser/index.js.

funkce parseToddPDF (stránky) {
  const page = pages [0]; // Víme, že bude pouze jedna stránka
  // Deklarativní mapa dat PDF, která očekáváme, na základě struktury Todda
  const fields = {
    // "Očekáváme, že pole reqID bude na řádku v 5.185 a
    // první položka v tomto poli “
    reqID: {řádek: '5.185', index: 0},
    datum: {řádek: '4,329', index: 0},
    sku: {řádek: '12. 235', index: 1},
    jméno: {řádek: '13,466', index: 1},
    foodGrade: {řádek: '14,698', index: 1},
    unitPrice: {row: '15 .928999999999998 ', index: 1},
    umístění: {řádek: '17,16', index: 1},
  };
  const data = {};
  // Přiřaďte data stránky objektu, který můžeme vrátit, podle
  // specifikace našich polí
  Object.keys (pole)
    .forEach ((key) => {
      const field = field [key];
      const val = page [field.row] [field.index];
      // Nechceme zde ztratit přední nuly a můžeme věřit
      // jakákoli manipulace s aplikacemi / daty. Tohle je
      // proč nenucujeme na číslo.
      data [klíč] = val;
    });
  // Ruční oprava některých textových polí tak, aby byla použitelná
  data.reqID = data.reqID.slice ('Requsition ID:' .length);
  data.date = data.date.slice ('Datum:' .length);
  návratové údaje;
}

Maso a brambory jsou zde v přední smyčce a jak je používáme. Po načtení pozic Y každé textové položky dříve je snadné určit každé pole, které chceme, jako pozici v našem objektu stránek. Účinně poskytuje mapu, kterou je třeba sledovat.

Vše, co musíme udělat, je deklarovat datový objekt k výstupu, iterovat přes každé pole, které jsme určili, sledovat trasu podle našich specifikací a přiřadit hodnotu, kterou najdeme na konci, našemu datovému objektu.

Po několika liniích, které uklidí některá pole strun, můžeme vrátit datový objekt a jsme mimo závody. Vypadá to takto:

{reqID: '298831',
  datum: '23/05/209',
  sku: '6308005',
  název: 'Square Lemon Qartz Pacifier',
  foodGrade: '3',
  unitPrice: '1,29',
  umístění: '55'}

Dává to všechno dohromady

Nyní přejdeme k tomu, abychom pro tento modul analýzy analyzovali souběžnost, abychom mohli pracovat v měřítku a rozpoznávat některé důležité překážky. Výše uvedený diagram je skvělý pro pochopení kontextu logiky syntaktického analyzování. Nedělá to moc pro to, abychom pochopili, jak to chceme paralelizovat. Můžeme dělat lépe:

Triviální, vím, a pravděpodobně i příliš učebnice - zobecněné, abychom ji mohli prakticky používat, ale hej, je to základní koncept formalizace.

Nyní musíme především přemýšlet o tom, jak zvládneme vstup a výstup našeho programu, což v podstatě zabalí logiku analýzy a poté ji rozdělí mezi pracovní procesy analyzátoru. Zde je mnoho otázek, které můžeme položit, a mnoho řešení:

  • bude to aplikace příkazového řádku?
  • Bude to konzistentní server se sadou koncových bodů API? To má vlastní řadu otázek - například REST nebo GraphQL?
  • Možná je to jen kosterní modul v širší kódové základně - například, co kdybychom zobecnili náš rozbor napříč sadou binárních dokumentů a chtěli jsme oddělit souběžný model od konkrétního typu zdrojového souboru a implementace rozboru?

Pro jednoduchost se chystám zabalit logiku analýzy do obslužného programu příkazového řádku. To znamená, že je čas udělat spoustu předpokladů:

  • Očekává cesty souboru jako vstup a jsou relativní nebo absolutní?
  • Nebo místo toho očekává, že se do systému vloží zřetězená data PDF?
  • Bude to výstup dat do souboru? Protože pokud ano, musíme tuto možnost poskytnout jako argument pro uživatele, který musí určit ...

Zpracování vstupu příkazového řádku

Znovu udržuji co nejjednodušší věci: Rozhodl jsem se, že program bude očekávat seznam cest k souborům, buď jako jednotlivé argumenty příkazového řádku:

uzel index file-1.pdf file-2.pdf… file-n.pdf

Nebo se přepíší na standardní vstup jako na seznam řádků oddělených řádkem:

# čte řádky z textového souboru se všemi našimi cestami
cat files-to-parse.txt | index uzlu
# nebo je možná jen vypíšete z adresáře
najít ./data -name “* .pdf” | index uzlu

To umožňuje procesu uzlu manipulovat pořadí těchto cest jakýmkoli způsobem, který považuje za vhodný, což nám umožňuje upravit měřítko kódu zpracování později. Za tímto účelem si přečteme seznam cest k souborům, ať už byly poskytnuty jakýmkoli způsobem, a rozdělíme je podle libovolného čísla do podadresářů. Zde je kód, metoda getTerminalInput v ./input/index.js:

function getTerminalInput (subArrays) {
  vrátit nový Promise ((vyřešit, odmítnout) => {
    const output = [];
  
    if (process.stdin.isTTY) {
      const input = process.argv.slice (2);
      const len ​​= Math.min (subArrays, Math.ceil (input.length / subArrays));
      while (input.length) {
        output.push (input.splice (0, len));
      }
      vyřešit (výstup);
    } jinde {
    
      let input = '';
      process.stdin.setEncoding ('utf-8');
      process.stdin.on ('readable', () => {
        nechat kus;
        while (chunk = process.stdin.read ())
          vstup + = kus;
      });
      process.stdin.on ('end', () => {
        input = input.trim (). split ('\ n');
        const len ​​= Math.min (input.length, Math.ceil (input.length / subArrays));
        while (input.length) {
          output.push (input.splice (0, len));
        }
        vyřešit (výstup);
      })
    
    }
    
  });
}

Proč rozdávat seznam? Řekněme, že máte 8jádrový procesor na hardwaru pro spotřebitele a 500 souborů PDF k analýze.

Bohužel pro Node, i když s asynchronním kódem pracuje skvěle díky smyčce událostí, běží pouze na jednom vlákně. Chcete-li zpracovat těchto 500 souborů PDF, pokud nepoužíváte vícevláknový (tj. Vícenásobný proces) kód, používáte pouze osminu své kapacity zpracování. Za předpokladu, že efektivita paměti není problém, můžete zpracovat data až osmkrát rychleji využitím vestavěných modulů paralelismu Node.

Rozdělení našeho vstupu na kousky nám to umožňuje.

Kromě toho je to v zásadě primitivní vyrovnávač zátěže a jasně se předpokládá, že pracovní zátěže prezentované analýzou každého PDF jsou zaměnitelné. To znamená, že soubory PDF mají stejnou velikost a mají stejnou strukturu.

To je zjevně triviální případ, zejména proto, že nezohledňujeme řešení chyb v pracovních procesech a který pracovník je v současné době k dispozici pro zpracování nových zátěží. V případě, že bychom nastavili server API pro zpracování příchozích požadavků na analýzu, museli bychom tyto zvláštní potřeby zvážit.

Shlukování našeho kódu

Nyní, když máme svůj vstup rozdělen do zvládnutelné pracovní zátěže, je to sice vynalézavým způsobem - rád bych to později změnil - pojďme se podívat, jak je můžeme seskupit. Ukázalo se tedy, že Node má dva samostatné moduly pro nastavení paralelního kódu.

Ten, který hodláme použít, klastrový modul, v zásadě umožňuje procesu uzlu vytvářet kopie sebe sama a zpracovávat rovnováhu mezi nimi, jak uzná za vhodné.

Toto je postaveno na modulu child_process, který je méně pevně spojen s paralelizací samotných programů Node a umožňuje vám vytvářet jiné procesy, jako jsou shellové programy nebo jiné spustitelné binární soubory, a rozhraní s nimi pomocí standardních vstupů, výstupů atd.

Vřele doporučuji číst dokumenty API pro každý modul, protože jsou fantasticky napsané, ai když jste jako já a najdete bezúčelné ruční čtení nudné a naprosto zaneprázdněnou práci, alespoň se seznamte s úvodem do každého modulu pomůžeme vám dostat se do tématu a rozšířit své znalosti o ekosystému Node.

Pojďme tedy projít kód. Zde je to hromadně:

const cluster = vyžadovat ('cluster');
const numCPUs = vyžadovat ('os'). cpus (). délka;
const {getTerminalInput} = vyžadovat ('./ input');
(asynchronní funkce main () {
  if (cluster.isMaster) {
    const workerData = await getTerminalInput (numCPUs);
    pro (let i = 0; i 
      const worker = cluster.fork ();
      const params = {filenames: workerData [i]};
      worker.send (params);
    }
  } jinde {
    vyžadovat („./ pracovník“);
  }
}) ();

Naše závislosti jsou tedy celkem jednoduché. Nejprve je zde klastrový modul, jak je popsáno výše. Zadruhé, vyžadujeme modul os pro výslovný účel zjišťování, kolik procesorových jader je na našem počítači - což je základní parametr rozdělení naší pracovní zátěže. Nakonec je zde naše funkce zpracování vstupů, kterou jsem pro úplnost zadal externě jinému souboru.

Nyní je hlavní metoda ve skutečnosti poměrně jednoduchá. Ve skutečnosti bychom to mohli rozdělit do několika kroků:

  1. Pokud jsme hlavní proces, rozdělte nám vstup odeslaný rovnoměrně podle počtu jader CPU tohoto stroje
  2. Pro každý náklad, který má být na pracovišti, vytvořte pracovníka pomocí clusteru. Vytvořte objekt, který mu můžeme poslat kanálem meziprocesových zpráv RPC modulu [cluster], a pošlete mu tu zatracenou věc.
  3. Pokud nejsme ve skutečnosti hlavním modulem, musíme být pracovníkem - stačí spustit kód v našem pracovním souboru a zavolat jej den.

Nic bláznivého se tu neděje a to nám umožňuje zaměřit se na skutečné zvedání, což ukazuje, jak pracovník použije seznam názvů souborů, které mu dáme.

Zprávy, Async a Streams, všechny prvky výživné stravy

Nejprve, jak výše, dovolte mi vyhodit kód, na který se můžete odkazovat. Věřte mi, když si to nejprve prohlédnete, necháte přeskočit jakékoli vysvětlení, které považujete za banální.

const Bufferer = vyžadovat ('../ bufferer');
const Parser = vyžadovat ('../ parser');
const {createReadStream} = vyžadovat ('fs');
process.on ('message', async (options) => {
  const {filenames} = volby;
  const parser = new Parser ();
  const parseAndLog = async (buf) => console.log (čeká parser.parse (buf) + ',');
  const parsingQueue = filenames.reduce (async (výsledek, název souboru) => {
    čekat na výsledek;
    vrátit nový Promise ((vyřešit, odmítnout) => {
      const reader = createReadStream (název souboru);
      const bufferer = new Bufferer ({onEnd: parseAndLog});
      čtenář
        .pipe (bufferer)
        .once ('dokončit', vyřešit)
        .once ('chyba', odmítnout)
    
    });
  
  }, skutečný);
  Snaž se {
    čekat na parsingQueue;
    proces.exit (0);
  } catch (err) {
    console.error (err);
    proces.konec (1);
  }
});

Nyní jsou tu nějaké špinavé hacky, takže buďte opatrní, pokud jste jedním z nezasvěcených (jen žert). Podívejme se, co se stane jako první:

Prvním krokem je vyžadovat všechny potřebné ingredience. Nezapomeňte, že je to založeno na tom, co dělá samotný kód. Dovolte mi tedy jen říci, že použijeme na míru vytvořený zapisovatelný datový proud, který jsem se nazval Bufferer, obálka pro naši logiku syntaktické analýzy od minulého času, také složitě pojmenovaná Parser, a starý starý spolehlivý createReadStream z modulu fs.

Tady je magie. Všimnete si, že ve funkci není nic zabalené. Celý pracovní kód jen čeká na zprávu, která má přijít do procesu - zprávu od svého pána s prací, kterou musí udělat pro daný den. Promiňte středověký jazyk.

Takže v první řadě vidíme, že je asynchronní. Nejprve extrahujeme názvy souborů ze samotné zprávy - pokud by to byl výrobní kód, ověřoval bych je zde. Vlastně, sakra, ověřuji je v našem kódu pro zpracování vstupů dříve. Pak provedeme instanci našeho parsovacího objektu - pouze jeden pro celý proces - tak můžeme analyzovat více vyrovnávacích pamětí jednou sadou metod. Můj problém spočívá v tom, že interně spravuje paměť a po zvážení je dobré si ji později prohlédnout.

Pak je zde jednoduchý obal, parseAndLog kolem analýzy, který zaznamenává vyrovnávací paměť PDF JSON-ified s čárkou připojenou k ní, jen aby se usnadnil život pro zřetězení výsledků analýzy více PDF.

Váš pracovník, připravený a připravený na rande s osudem.

Konečně maso hmoty, asynchronní fronta. Nech mě to vysvětlit:

Tento pracovník obdržel seznam názvů souborů. Pro každý název souboru (nebo cestu, opravdu), musíme otevřít čitelný proud přes souborový systém, abychom mohli získat data PDF. Pak musíme vytvořit náš Bufferer (náš číšník, který bude následovat dříve z analogie restaurace), abychom mohli data přenést do našeho Parseru.

Vyrovnávací paměť je přizpůsobena na míru. Všechno, co opravdu dělá, je přijmout funkci, která zavolá, když obdrží všechna potřebná data - zde ji pouze žádáme o analýzu a protokolování těchto dat.

Takže teď máme všechny kusy, jen je dáme dohromady:

  1. Čitelný proud - soubor PDF, potrubí do vyrovnávací paměti
  2. Bufferer dokončí a volá naši metodu parseAndLog pro celé pracovníky

Celý tento proces je zabalen do příslibu, který je sám vrácen funkci redukce, do které sedí uvnitř. Po vyřešení operace zmenšení pokračuje.

Tato asynchronní fronta je ve skutečnosti opravdu užitečným vzorem, takže ji podrobněji pojednám ve svém příštím příspěvku, který bude pravděpodobně větší velikosti než poslední.

Ostatně zbytek kódu právě ukončí proces založený na zpracování chyb. Opět platí, že pokud se jednalo o výrobní kód, můžete se vsadit, že zde bude robustnější protokolování a zpracování chyb, ale jako důkaz koncepce se to zdá v pořádku.

Funguje to, ale je to užitečné?

Takže to máte. Byla to trochu cesta a určitě to funguje, ale jako každý kód je důležité přezkoumat, jaké jsou její silné a slabé stránky. Z hlavy:

  • Proudy musí být nashromážděny v pufrech. To, bohužel, překonává účel použití toků a efektivita paměti podle toho trpí. Toto je nezbytná duální páska pro práci s modulem pdfreader. Rád bych viděl, jestli existuje způsob, jak streamovat data PDF a analyzovat je na jemnější úrovni. Zejména pokud na ni lze stále použít modulární funkční logiku syntaktického analyzování.
  • V tomto dětském stádiu je logika syntaktického analyzování také nepříjemně křehká. Jen si pomyslete, co když mám dokument, který je delší než stránka? Z okna vylétá spousta předpokladů a ještě silnější je potřeba streamovat data PDF.
  • Nakonec by bylo skvělé vidět, jak bychom mohli tuto funkci vybudovat pomocí protokolování a koncových bodů API, které bychom měli poskytnout veřejnosti - za cenu nebo pro bono, v závislosti na kontextech, ve kterých se používá.

Pokud máte nějaké konkrétní kritiky nebo obavy, rád bych je také slyšel, protože odhalení slabých stránek v kódu je prvním krokem k jejich vyřešení. A pokud víte o nějaké lepší metodě současného streamování a analýzy souborů PDF, dejte mi vědět, abych ji mohl nechat pro kohokoli, kdo tento příspěvek přečte, aby odpověděl. Ať tak či onak, nebo z jakéhokoli jiného důvodu, pošlete mi e-mail nebo se spojte s Reddit.