JSのボットパーサーの電報を書き込みます





Telegram用のボットを作成するというテーマはますます人気が高まっており、プログラマーがこの分野で手を試すようになっています。 誰もが定期的にアイデアやタスクを持っていますが、それらはテーマ別のボットを書くことで解決できます。 私にとって、JSプログラマーとして、このような緊急のタスクの例は、関連するトピックに関する雇用市場の監視です。



ただし、ボット作成の分野で最も人気のある言語とテクノロジーの1つはPythonであり、これはプログラマーにテキスト形式のさまざまな情報ソースを処理および解析するための膨大な数の優れたライブラリを提供します。 しかし、私はJavaScript-私の好きな言語の1つで正確にそれをやりたかった。



挑戦する



主なタスク:タグ付けと見栄えのよいマークアップを使用して、詳細なジョブフィードを作成します。 個別のサブタスクに分割できます。





最初に、 @ TheFeedReaderBotなどの汎用の既製ボットを使用することを考えました 。 しかし、詳細な調査の結果、タグ付けは完全になく、コンテンツの表示をカスタマイズする機能は非常に限られていることが判明しました。 幸いなことに、最新のJavascriptはこれらの問題の解決に役立つ多くのライブラリを提供します。 しかし、まず最初に。



ボットフレーム



もちろん、Telegram REST APIと直接やり取りすることは可能ですが、人件費の観点からは、既製のソリューションを採用する方が簡単です。 そこで、ボット作成の公式チュートリアルで参照されているnpm slimbotパッケージを選択しました。 また、メッセージのみを送信しますが、このパッケージは生活を大幅に簡素化し、内部ボットAPIをエンティティとして作成できるようにします。



const Slimbot = require('slimbot'); const config = require('./config.json'); const bot = new Slimbot(config.TELEGRAM_API_KEY); bot.startPolling(); function logMessageToAdmin(message, type='Error') { bot.sendMessage(config.ADMIN_USER, `<b>${type}</b>\n<code>${message}</code>`, { parse_mode: 'HTML' }); } function postVacancy(message) { bot.sendMessage(config.TARGET_CHANNEL, message, { parse_mode: 'HTML', disable_web_page_preview: true, disable_notification: true }); } module.exports = { postVacancy, logMessageToAdmin };
      
      





通常のsetIntervalをスケジューラとして使用し、RSSを解析するためのフィードリードを使用します。空席のソースはMy Circleとhh.ruになります。



 const feed = require("feed-read"); const config = require('./config.json'); const HhAdapter = require('./adapters/hh'); const MoikrugAdapter = require('./adapters/moikrug'); const bot = require('./bot'); const { FeedItemModel } = require('./lib/models'); function processFeed(articles, adapter) { articles.forEach(article => { if (adapter.isValid((article))) { const key = adapter.getKey(article); new FeedItemModel({ key, data: article }).save().then( model => adapter.parseItem(article).then(bot.postVacancy), () => {} ); } }); } setInterval(() => { feed(config.HH_FEED, function (err, articles) { if (err) { bot.logMessageToAdmin(err); return; } processFeed(articles, HhAdapter); }); feed(config.MOIKRUG_FEED, function (err, articles) { if (err) { bot.logMessageToAdmin(err); return; } processFeed(articles, MoikrugAdapter); }); }, config.REQUEST_PERIOD_TIME);
      
      





単一のジョブの解析



ソースサイトごとに空席があるページの構造が異なるため、解析の実装は異なります。 したがって、統一されたインターフェースを提供するアダプターが使用されました。 サーバーでDOMを操作するために、 jsdomライブラリを使用して標準操作を実行できます。CSSセレクターを使用して要素を検索し、アクティブに使用する要素のコンテンツを取得します。



モイクルガダプター
 const request = require('superagent'); const jsdom = require('jsdom'); const { JSDOM } = jsdom; const { getTags } = require('../lib/tagger'); const { getJobType } = require('../lib/jobType'); const { render } = require('../lib/render'); function parseItem(item) { return new Promise((resolve, reject) => { request .get(item.link) .end(function(err, res) { if(err) { console.log(err); reject(err); return; } const dom = new JSDOM(res.text); const element = dom.window.document.querySelector(".vacancy_description"); const salaryElem = dom.window.document.querySelector(".footer_meta .salary"); const salary = salaryElem ? salaryElem.textContent : ' .'; const locationElem = dom.window.document.querySelector(".footer_meta .location"); const location = locationElem && locationElem.textContent; const title = dom.window.document.querySelector(".company_name").textContent; const titleFooter = dom.window.document.querySelector(".footer_meta").textContent; const pureContent = element.textContent; resolve(render({ tags: getTags(pureContent), salary: `: ${salary}`, location, title, link: item.link, description: element.innerHTML, jobType: getJobType(titleFooter), important: Array.from(element.querySelectorAll('strong')).map(e => e.textContent) })) }); }); } function getKey(item) { return item.link; } function isValid() { return true } module.exports = { getKey, isValid, parseItem };
      
      







ハダプター
 const request = require('superagent'); const jsdom = require('jsdom'); const { JSDOM } = jsdom; const { getTags } = require('../lib/tagger'); const { getJobType } = require('../lib/jobType'); const { render } = require('../lib/render'); function parseItem(item) { const splited = item.content.split(/\n<p>|<\/p><p>|<\/p>\n/).filter(i => i); const [ title, date, region, salary ] = splited; return new Promise((resolve, reject) => { request .get(item.link) .end(function(err, res) { if(err) { console.log(err); reject(err); return; } const dom = new JSDOM(res.text); const element = dom.window.document.querySelector('.b-vacancy-desc-wrapper'); const title = dom.window.document.querySelector('.companyname').textContent; const pureContent = element.textContent; const tags = getTags(pureContent); resolve(render({ title, location: region.split(': ')[1] || region, salary: `: ${salary.split(': ')[1] || salary}`, tags, description: element.innerHTML, link: item.link, jobType: getJobType(pureContent), important: Array.from(element.querySelectorAll('strong')).map(e => e.textContent) })) }); }); } function getKey(item) { return item.link; } function isValid() { return true } module.exports = { getKey, isValid, parseItem };
      
      







書式設定



解析後、便利な形式で情報を提示する必要がありますが、Telegram APIを使用する場合、これを行う機会はあまりありません。ユニコードタグとシンボルのみをメールに入れることができます(顔文字とステッカーはカウントされません)。 入力では、説明のいくつかのセマンティックフィールドと、「生の」HTMLの説明自体を取得します。 短い検索の後、解決策が見つかりました-html-to-textライブラリです。 APIとその実装の詳細な調査の後、なぜ動的な構成からではなく、クロージャーを介してフォーマット関数が呼び出されるのか不思議に思われます。これにより、構成パラメーターによって提供される多くの利点がなくなります。 また、リストにli



ではなく箇条書きを美しく表示するには、少しチートする必要があります。



 const htmlToText = require('html-to-text'); const whiteSpaceRegex = /^\s*$/; function render({ title, location, salary, tags, description, link, important = [], jobType='' }) { let formattedDescription = htmlToText .fromString(description, { wordwrap: null, noLinkBrackets: true, hideLinkHrefIfSameAsText: true, format: { unorderedList: function formatUnorderedList(elem, fn, options) { let result = ''; const nonWhiteSpaceChildren = (elem.children || []).filter( c => c.type !== 'text' || !whiteSpaceRegex.test(c.data) ); nonWhiteSpaceChildren.forEach(function(elem) { result += ' <b>●</b> ' + fn(elem.children, options) + '\n'; }); return '\n' + result + '\n'; } } }) .replace(/\n\s*\n/g, '\n'); important.filter(text => text.includes(':')).forEach(text => { formattedDescription = formattedDescription.replace( new RegExp(text, 'g'), `<b>${text}</b>` ) }); const formattedTags = tags.map(t => '#' + t).join(' '); const locationFormatted = location ? `#${location.replace(/ |-/g, '_')} `: ''; return `<b>${title}</b>\n${locationFormatted}#${jobType}\n<b>${salary}</b>\n${formattedTags}\n${formattedDescription}\n${link}`; } module.exports = { render };
      
      





タグ付け



良いジョブの説明はあるが、タグ付けが十分ではないとしましょう。 この問題を解決するために、 azライブラリを使用して自然ロシア語をトークン化しました。 そのため、トークンストリーム内の単語をフィルター処理し、タグ辞書に対応する単語がある場合はタグに置き換えることができました。



 const Az = require('az'); const namesMap = require('../resources/tagNames.json'); function onlyUnique(value, index, self) { return self.indexOf(value) === index; } function getTags(pureContent) { const tokens = Az.Tokens(pureContent).done(); const tags = tokens.filter(t => t.type.toString() === 'WORD') .map(t => t.toString().toLowerCase().replace('-', '_')) .map(name => namesMap[name]) .filter(t => t) .filter(onlyUnique); return tags; } module.exports = { getTags };
      
      





辞書形式
 { "js": "JS", "javascript": "JS", "sql": "SQL", "": "Angular", "angular": "Angular", "angularjs": "Angular", "react": "React", "reactjs": "React", "": "React", "node": "NodeJS", "nodejs": "NodeJS", "linux": "Linux", "ubuntu": "Ubuntu", "unix": "UNIX", "windows": "Windows" .... }
      
      







デプロイおよびその他すべて



各空席を1回だけ公開するために、MongoDBデータベースを使用して、空席自体のリンクの一意性にすべてを減らしました。 サーバー上のプロセスとそのログを監視するために、通常のbashスクリプトによって展開が実行されるpm2プロセスマネージャーを選択しました。 ところで、Digital Oceanの最も単純なDropletがサーバーとして使用されます。



展開スクリプト
 #!/usr/bin/env bash # rs -       rsync ./ rs:/var/www/js_jobs_bot --delete -r --exclude=node_modules ssh rs " . ~/.nvm/nvm.sh cd /var/www/js_jobs_bot/ mv prod-config.json config.json npm i && pm2 restart processes.json "
      
      







結論



単純なボットを作成するのは難しくないことが判明しました。必要なのは、プログラミング言語(PythonまたはJSが望ましい)の知識と、数日間の空き時間だけです。 ボットの結果(およびテーマ別のジョブフィード)は、対応するチャネル@jsjobsで確認できます。



PS完全なソースコードは私のリポジトリにあります。



All Articles