Apl penuh yang mengikis, memproses dan membentangkan kandungan daripada web...di web.
Maklumat yang berlebihan ? Ia agak teruk hari ini, banyak isyarat rendah kepada sumber maklumat yang bising, mengecilkan "suapan" anda supaya anda tidak terharu adalah agak sukar. Alat yang menapis maklumat dan membentangkannya dalam format yang mudah dan cepat dihadam akan sangat berguna. Inilah sebabnya saya pertimbangkan pengagregatan kandungan padang malar hijau untuk gangguan. Ia adalah dan akan sentiasa (selagi internet percuma dan ada kebebasan bersuara) peluang perniagaan yang baik. Ia adalah salah satu contoh di mana semuanya mengenai pelaksanaan (dan tidak ada sama sekali mengenai idea itu).
Setelah berkata demikian, aplikasi saya pada akhirnya tidak melakukan apa-apa penapisan sebenar. Sebenarnya ia secara ringkas agregat kandungan daripada web. Ini kerana saya tidak membina pengguna ke dalamnya, dan terdapat sedikit insentif untuk melakukan penapisan jika ia tidak boleh disesuaikan bagi setiap pengguna.
Gambar rajah seni bina:Memang terdapat banyak kalangan di dalamnya!...anda tahu...mikro...perkhidmatan? Aplikasi yang saya bina adalah "pengikis" dan "pelayan", manakala "penerbit" hanyalah rutin yang tertanam dalam pelayan. "Carian" dan "proksi" ialah alat luaran yang menjalankan tugas mereka. "Frontend" bukanlah sesuatu yang istimewa, gabungan js dan css yang digabungkan dengan webpack.
Oleh kerana terdapat bahagian bergerak yang berbeza saya akan mengikut aliran kandungan, bermula dari apabila kandungan dilihat buat kali pertama.
Mengikis sudah selesai...kau rasa, ular sawa. Walau bagaimanapun, tiada modul "mengikis" ad hoc digunakan.
Memutuskan perkara yang hendak dikikis bergantung pada kategori kandungan. Kami memanggil kategori "topik".
Setiap topik mempunyai senarai kata kunci.
Senarai kata kunci jika ditarik dari google adwords menggunakan mereka api ular sawa
Kata kunci disoal pada berbilang enjin carian, dalam susunan round robin secara berkala. Jika contoh mempunyai berbilang topik, topik dengan kandungan yang kurang tersedia akan dicari dahulu. Untuk melaksanakan carian yang kami harapkan searx dengan proksi. Searx tidak begitu mesra perpustakaan, kerana penggunaan utamanya adalah untuk bahagian hadapannya jadi ia memerlukan proses yang betul untuk memulakan modul untuk melaksanakan pertanyaan. Untuk mempercepatkan perkara dengan menggunakan threadpool untuk melaksanakan berbilang pertanyaan serentak, kami akan menggunakan threadpool dengan kerap sepanjang projek.
Setiap carian kata kunci menjana senarai sumber kandungan yang berpotensi (Keputusan enjin carian) yang disimpan pada storan untuk diproses kemudian.
Apabila kami ingin mencari kandungan baharu untuk topik tertentu, kami mula-mula menyemak sama ada terdapat sumber yang tersedia, jika tidak, kami menjana sumber baharu daripada senarai kata kunci.
Sumber diproses melalui dua perpustakaan trafilatura adalah yang utama, jika gagal kita sandarkan angsa . Kami juga cuba mencari suapan untuk pautan tambahan (yang akan dianggap sebagai sumber baharu). Untuk suapan yang kami gunakan pencari suapan tetapi penghuraian ringkas html untuk rsslink
tag juga sudah memadai.
Jenis kandungan utama kami ialah aArticle
, yang dari bahagian python ia hanya dict, dari sisi nim dengan menghuraikannya sebagai objek. kunci:
title
: tajuk artikel
content
: artikel itu sendiri. Untuk menentukan artikel yang bagus, kami melalui langkah penapisan yang berbeza:
Mula-mula kita semak sama ada trafilatura atau angsa mempunyai teks dan jika ia cukup panjang. Saiz minimum kami ialah 300 patah perkataan. Jika saiz tidak sepadan, kami membuang sumbernya (tidak mengembalikan apa-apa).
Kemudian kami mengambil tajuk dan membersihkannya dengan mengalih keluar url dan ruang putih
Jika lang itu asing, kami menterjemahkannya kembali ke bahasa inggeris (kami biasakan daripada bahasa inggeris) kedua-dua kandungan dan tajuk.
Pada ketika ini kami menyemak penggunaan kata-kata sesat semakan_cabul . Bukannya semakan kata-kata kotor adalah bahasa Inggeris berdasarkan terjemahan sebelumnya adalah perlu. Jika tidak, kami memerlukan model kata-kata kotor untuk semua bahasa.
Selepas kami menggantikan perkataan buruk menggunakan penapis kata-kata kotor, kami meneruskan dengan membersihkan kandungan. Kami menyemak sama ada artikel itu berkaitan. Terdapat peraturan yang kami gunakan ialah:
Kandungan mesti bermula dengan aksara alfanumerik, jika tidak terdapat perubahan yang tinggi bahawa ia adalah sampah.
Kedua-dua tajuk dan kandungan tidak boleh "bising". Kebisingan ditakrifkan oleh regex yang menangkap kata kunci seperti "log masuk", "pendaftaran", "akses ditolak"...dsb.
Sekurang-kurangnya satu perkataan dalam tajuk mesti ada dalam badan. Jika tidak, penghuraian mungkin memilih bahagian halaman sumber yang salah untuk kandungan.
Jika ujian perkaitan telah lulus, sebagai langkah terakhir kami membersihkan kandungan daripada kejadian terlalu banyak kurungan, ruang putih, aksara berulang dan aksara khas.
Jika pembersihan belum memadamkan semua, kami terus memproses artikel.
source
: pautan yang menunjuk kepada sumber asal yang kami huraikan
lang
: bahasa artikel, kami gunakan lingua untuk mengesan bahasa
desc
: ringkasan, jika tidak petikan daripada kandungan
author
: pengarang, sebaliknya tajuk halaman utama pautan sumber
pubDate
: tarikh penerbitan artikel, atau sekarang
topic
: topik kepunyaan artikel ini
tags
: kata kunci yang relevan untuk artikel, kami menggunakan lib pengekstrakan kw terpantas, iaitu menyapu , alternatif yang dipertimbangkan ialah pyate (komboasas), pangkat teks dan mesin frasa
imageTitle
: teks ganti untuk imej
imageOrigin
: jika penghuraian sumber (untuk imej yang kami gunakan lassie ) tidak menemui imej, kami menanya enjin carian untuk imej yang berkaitan, jadi imageOrigin menghala ke halaman asal yang menjadi hos imej, jika tidak, ia sama dengan url sumber.
imageUrl
: pautan sebenar kepada imej. Kami menggunakan semakan penapis bloom untuk imej pendua, kerana kami tidak suka pendua.
icon
: favicon pautan sumber
Selepas kami memproses kata kunci, kami menyimpan artikel dan suapan yang ditemui pada storan. Ia akan digunakan oleh penerbit.
Mengikis berlaku secara berterusan, ia adalah syaitan. Kod pseudo gelung utama kelihatan seperti ini, dan ia dikonfigurasikan setiap tapak:
segerakan proksi selama-lamanya
untuk setiap topik yang disusun mengikut kiraan (artikel) yang tidak diterbitkan (rendah ke tinggi) lakukan perkara berikut
jika selang minimum daripada kerja terakhir berlalu, jalankan kerja parse untuk topik tersebut. Selang itu meningkatkan lebih banyak artikel tidak diterbitkan yang kami ada untuk topik, dan sentiasa 0 jika kami tidak mempunyai artikel yang tidak diterbitkan.
Lakukan perkara yang sama tetapi untuk suapan (yang kami kumpulkan daripada sumber, jika kami ada)
Jika tapak itu bertujuan untuk mencipta topik baharu, buat satu. (Ini hanya masuk akal jika anda tidak memutuskan senarai topik semasa penciptaan tapak.)
Pilih artikel daripada yang diterbitkan dan hantar tweet (kami menghantar 3 tweet setiap hari, menggunakan python-twitter)
Pilih artikel daripada yang diterbitkan dan kemas kini halaman facebook (kami melakukan 1 kemas kini setiap hari, menggunakan muka muka)
(Kami juga menyambungkan reddit, tetapi reddit tidak membenarkan siaran silang, jadi ia adalah satu usaha yang sia-sia.)
Sebaik sahaja kami mempunyai kandungan untuk diterbitkan, kami perlu memutuskan perkara yang hendak diterbitkan dan berapa kerap. Saya tidak membuat sebarang helah di sini, kerana seperti yang saya nyatakan sebelum ini memilih apa yang hendak dipaparkan adalah bergantung kepada pengguna. Jadi kami hanya menerbitkan daripada terbaru kepada tertua , dengan alasan bahawa sesuatu yang kami kikis baru-baru ini adalah lebih relevan, ia adalah a LIFO beratur. Penerbitan walaupun agak bergantung pada alat python, berlaku dalam nim, kerana ia berjalan bersebelahan dengan pelayan, yang juga dalam nim.
Penerbitan berlaku secara berterusan, dan seperti mengikis mempunyai selang terbiar, penerbitan berlaku, secara songsang kepada mengikis. Apabila kita mengikis, kita perlahan apabila kita mempunyai a cukup panjang cache artikel yang tidak diterbitkan, dengan penerbitan kami perlahan apabila cache kami mula mengecut terlalu banyak. Dengan cara ini, ditambah dengan mengikis, perlu sentiasa ada beberapa kandungan untuk diterbitkan beberapa waktu pada masa hadapan. Logik penerbitan sebenar:
Ambil sekumpulan artikel yang tidak diterbitkan daripada cache. (Kami memilih untuk menerbitkan 3 artikel baharu setiap larian.)
Semak sama ada ia adalah pendua. Semakan dup dilakukan melalui pencincangan sensitif lokaliti, memanfaatkan nim lib, minhash . LSH agak intensif CPU (anda tahu...pencincangan), dan memerlukan utasnya sendiri, (terdapat beberapa tugas lain yang kami kendalikan yang memerlukan utas mereka sendiri).
Paparan halaman: ini tidak diperlukan, kerana pelayan mengendalikan pertanyaan dengan segera, tetapi pemaparan di sini ialah satu bentuk pra-cache.
Pengendalian halaman. Memandangkan kami berurusan dengan tapak, kami perlu memilih berapa banyak artikel untuk dipaparkan setiap halaman, dan menambah halaman semasa kami menerbitkan lebih banyak artikel. Kami memilih untuk mengumpulkan artikel dalam halaman ~10. Halaman terkini sentiasa kurang daripada 10 artikel.
Simpan keadaan artikel yang diterbitkan, ini bermakna mengalihkan artikel daripada status "tidak diterbitkan" kepada status "terbit" dan pangkalan data LSH.
Selepas menerbitkan artikel baharu, kami perlu mengosongkan cache yang lapuk. Kami perlu mengosongkan halaman utama, halaman topik dan peta laman serta suapan rss.
Kami menyediakan kerja untuk mengikis kandungan penerbitan, yang tinggal hanyalah menyajikannya.
Selepas mempunyai mencuba berbeza web pelayan, kerana pepijat yang berbeza yang saya selesaikan pengukir.
Kami menggunakan nim pelaksanaan padanan corak untuk memadankan sekumpulan tangkapan regex. Ini adalah regex yang tidak RESTful sama sekali:
const
rxend = "(?=/+|(?=[?].*)|$)"
rxAmp = fmt"(/+amp{rxend})"
rxLang = "(/[a-z]{2}(?:-[A-Z]{2})?" & fmt"{rxend})" # split to avoid formatting regex `{}` usage
rxTopic = fmt"(/+.*?{rxend})"
rxPage = fmt"(/+(?:[0-9]+|s|g|feed\.xml|sitemap\.xml){rxend})"
rxArt = fmt"(/+.*?{rxend})"
rxPath = fmt"{rxAmp}?{rxLang}?{rxTopic}?{rxPage}?{rxArt}?"
rxPath
menunjukkan semua kemungkinan nod yang boleh dimiliki oleh laluan. Kemudian penghalaan kami kelihatan seperti:
let capts = uriTuple(reqCtx.url.path)
case capts:
of (topic: ""): # homepage...
of (topic: "assets"): # assets
of (topic: "i"): # images
of (topic: "robots.txt"): # robots.txt
of (page: "sitemap.xml"): # sitemap for topics
of (art: "index.xml"): # sitemap index for topic pages
etc...
Ia tidak cantik, tetapi tidak bergantung pada mana-mana penghala tertentu membenarkan saya menukar pelayan web bawah tanpa banyak kekecohan semasa menguji. Adakah ia berprestasi? Tidak jelas! Tidak melakukan sebarang penanda aras membandingkannya dengan sesuatu yang lain. Walau bagaimanapun, yang berbau adalah regex yang boleh mempunyai pepijat, dan hakikat bahawa pesanan kes itu penting.
Tunggu sekejap...
Terdapat banyak perkara yang kami lakukan pada setiap permintaan sebelum penghalaan halaman sebenar:
pada mulanya kami menyediakan kod pembersihan (dengandefer:
) yang semestinya pastikan tiada kebocoran berlaku.
defer:
# FIXME: is this cleanup required?
var futs: seq[Future[void]]
let resp =
if ctx.response.issome: ctx.response.get
else: nil
if not resp.isnil and not resp.connection.isnil:
futs.add resp.connection.closeWait()
if not ctx.isnil:
if not ctx.connection.isnil:
futs.add ctx.connection.closeWait()
futs.add ctx.closeWait()
await allFutures(futs)
Kami menyemak sama ada benang dimulakan:
initThread()
Ini sepatutnya hanya dijalankan sekali sahaja (ia menetapkan bool global selepas pemulaan untuk menyemak), dan boleh dilakukan di luar pengendali permintaan. Tetapi apa yang sebenarnya dimulakan? Nah...barang yang agak banyak! Pada asasnya kami (ab) menggunakan pemalar global yang memerlukan permulaan, juga sebahagian daripada ini tidak benar-benar berkaitan dengan benang, kerana ia memulakan ingatan pada timbunan dan dikongsi merentas utas
if threadInitialized:
debug "thread: already initialized"
return
debug "thread: base"
initThreadBase()
debug "thread: sonic"
initSonic() # Must be on top
debug "thread: http"
initHttp()
debug "thread: html"
initHtml()
debug "thread: ldj"
initLDJ()
debug "thread: feed"
initFeed()
debug "thread: img"
startImgFlow()
debug "thread: lsh"
startLsh()
debug "thread: mimes"
initMimes()
# ... and other stuff
Kemudian kami menghuraikan parameter
var
relpath = ctx.rawPath
page: string
rqlocked: bool
relpath.removeSuffix('/')
debug "handling: {relpath:.120}"
handleParams()
Untuk apa kita menggunakan parameter? TheParamKey
jenis enum menerangkannya:
type
ParamKey = enum
none,
q, p, # sonic
c, # cache
d, # delete
t, # translations
u # imgUrls
Kami buat microcaching untuk permintaan supaya setiap permintaan dicache mengikut tupel(path, query, accetEncoding)
, pengekodan diperlukan kerana kami boleh melayani kedua-dua badan (bukan)mampat. Konteks permintaan kelihatan seperti ini:
let reqCtx = reqCtxCache.lcheckOrPut(reqCacheKey):
let reqCtx {.gensym.} = new(ReqContext)
block:
let l = newAsyncLock()
checkNil(l):
reqCtx.lock = l
reqCtx.url = move url
reqCtx.params = params
reqCtx.file = reqCtx.url.path.fp
reqCtx.key = hash(reqCtx.file)
reqCtx.rq = initTable[ReqId, HttpRequestRef]()
new(reqCtx.respBody)
reqCtx
Thekey
medan digunakan untuk mengambil halaman cache yang betul (badan) daripadapageCache
. Kunci diperlukan untuk memastikan berbilang permintaan yang berlaku pada masa yang sama tidak menduplikasi kerja pemaparan (jika permintaan lain sudah menjana halaman, tunggu sehingga ia selesai). Setiap pangkalanHttpRequestRef
daripada chronos httpserver disimpan dalamrq
padang. Theparams
sudah dihuraikan daripada sebelumnyahandleParams
.
Kami menyokong pemadaman kandungan melaluid
param, yang membolehkan kami untuk nuke artikel (sekiranya penapisan gagal, tetapi ia tidak boleh digunakan dalam amalan, hanya untuk nyahpepijat) dengan permintaan http get yang mudah. Siapa yang memerlukan kaedah http lain? Bukan saya.
Kami menyokong pembersihan cache juga. Kita boleh sama ada memadam halaman itu,c=0
atau semua halamanc=1
. Perkara yang menjengkelkan ialah kita perlu menyemak sama ada laluan itu sama ada artikel, halaman, imej atau aset, dan bersihkan struktur cache yang sesuai. Terdapat beberapa pertindihan logik yang jelas di sini dengan penghala, tetapi memandangkan ini dilakukan sebelum penghalaan, ia mestilah ad-hoc, hanya mengendalikan kes yang berkaitan untuk pembersihan cache. Ia dilakukan sebelum penghalaan kerana cache juga dihidangkan tanpa penghalaan, kerana jika permintaan telah dijana, kita hanya boleh membalas dengan badan yang disimpan dalamrespBody
padang (danrespHeaders
, respCode
).
Selepas mengendalikan operasi cache, kami menghuraikan laluan.
Selepas ini terdapat satu lagi rampasan yang berlaku:
if handleTranslation():
return
Mengapa ini juga dilakukan sebelum penghalaan? Secara lalai, kami menyediakan halaman yang diterjemahkan separa. Kami miskin :( dan terjemahan adalah berdasarkan perkhidmatan percuma, tetapi kami tidak mampu membayar masa pemuatan yang teruk, jadi kami menjalankan terjemahan tertunda semasa kami menyediakan halaman yang diterjemahkan dengan hanya coretan yang telah dicache dalam pangkalan data terjemahan kami.
Pada ketika ini terdapat penghalaan, dibalut dengan pengecualian supaya jika penyajian halaman yang betul gagal, kami mengeluarkan503
. Mengeluarkan a503
membayangkan bahawa kami cuba menghalakan url yang sah tetapi kami tidak dapat menjana halaman. Untuk url yang tidak sah kami mengeluarkan a301
ubah hala yang menunjukkan bahawa url itu tidak sah. Kami menyediakan 11 jenis url yang berbeza:
laman utama: menarik artikel daripada topik terkini, pseudo secara rawak, kami tidak melakukan sebarang pengisihan berdasarkan populariti.
aset generik (di bawah/assets/
laluan): dipetakan terus ke direktori khusus
imej generik (di bawah/i/
pah): kami memproksi imej luaran untuk menjana saiz yang sesuai dengan tapak web responsif kami, apabila imej tidak tersedia sama ada piksel lutsinar atau ikon imej disajikan sebagai lalai.
fail robots.txt
peta laman (untuk halaman utama dan topik dan halaman): Halaman utama mengehoskan indeks peta laman yang menunjuk ke semua topik peta laman, topik peta laman menghala ke semua halaman topik, halaman peta laman menghala ke semua artikel halaman tersebut.
manifes pwa: manifes pwa sepatutnya membenarkan tapak web dipasang sebagai pwa (tetapi terus terang belum mengujinya)
carian: Carian memanfaatkan sonik dengan pysonic pengikatan.
cadangan: Cadangan juga dikendalikan melalui perpustakaan sonik. Tetapi mereka memerlukan
suapan: Seperti peta laman, kami mempunyai suapan yang berbeza untuk halaman utama, dan untuk topik yang berbeza, walaupun tiada suapan untuk halaman tunggal atas sebab yang jelas.
halaman topik: Halaman khusus untuk setiap topik (mis. dengan laluandomain.com/my-topic/
) Menarik artikel terkini yang diterbitkan untuk topik, yang tergolong dalam belum selesai muka surat.
halaman artikel: Halaman artikel menunjukkan tajuk artikel, penerangan, pautan sumber, tag, masa diterbitkan (dalam pengaki) dan di bahagian bawah kami menarik 3 artikel berkaitan. Artikel berkaitan diambil menggunakan pertanyaan carian pada tajuk atau teg artikel.
Paparan halaman diproses dari sisi nim menggunakan karax
Laman web ini terdiri daripada bar tetap teratas yang menunjukkan:
url halaman utama, melalui imej svg logo.
butang tema terang/gelap
url semasa menggunakan laluan semasa serpihan sebagai teks pautan
~10 topik url terbaharu
Bar carian (dengan butang carian), di mana apabila ditaip cadangan muncul
butang bahasa, di mana apabila diklik, senarai bahasa terapung
Memandangkan ia adalah reka bentuk responsif, apabila port pandang lebih kecil, bar atas hanya memegang kotak carian manakala selebihnya muncul dalam bar sisi boleh togol.
Pengaki, memegang pautan untuk peta laman, rss, sosial, undang-undang
Iklan di lokasi berbeza disokong
Ini adalah contoh fungsi, menunjukkan perkara yang kami lakukan apabila siaran baharu diterbitkan untuk mengemas kini suapan:
proc update*(tfeed: Feed, topic: string, newArts: seq[Article], dowrite = false) =
## Load existing feed for given topic and update the feed (in-memory)
## with the new articles provided, it does not write to storage.
checkNil tfeed
let
chann = tfeed.findel("channel")
itms = chann.drainChannel
arl = itms.len
narl = newArts.len
debug "rss: newArts: {narl}, previous: {arl}"
let
fill = RSS_N_ITEMS - arl
rem = max(0, narl - fill)
shrinked = if (rem > 0 and arl > 0):
itms[0..<(max(0, arl-rem))]
else: itms
debug "rss: articles tail len {len(shrinked)}, newarts: {len(newArts)}"
assert shrinked.len + narl <= RSS_N_ITEMS, fmt"shrinked: {shrinked.len}, newarticles: {narl}"
for a in newArts:
chann.add articleItem(a)
for itm in shrinked:
chann.add itm
if dowrite:
pageCache[][topic.feedKey] = tfeed.toXmlString
Ini adalah teras menambah url pada peta laman:
template addUrlToFeed(getLoc, getLocLang) =
if unlikely(nEntries > maxEntries):
warn "Number of URLs for sitemap of topic: {topic} exceeds limit! {nEntries}/{maxEntries}"
break
let
url = newElement("url")
loc = newElement("loc")
loc.add getLoc().escape.newText
url.add loc
addLangs(url, getLocLang)
result.add url
proc buildTopicPagesSitemap*(topic: string): Future[XmlNode] {.async.} =
initSitemapIndex()
await syncTopics()
var nEntries = 0
let done = await topicDonePages(topic)
template langUrl(lang): untyped {.dirty.} = $(WEBSITE_URL / lang / topic / pages[n])
withPyLock:
# add the most recent articles first (pages with higher idx)
let pages = pybi[].list(done.keys()).to(seq[string])
for n in countDown(pages.len - 1, 0):
if not (await isEmptyPage(topic, pages[n].parseInt, false)):
discard sitemapUrl(topic, pages[n]).sitemapEl
template addArticleToFeed() =
template baseUrl(): untyped =
getArticleUrl(a, topic)
template langUrl(lang): untyped =
getArticleUrl(a, topic, lang)
if not a.isValidArticlePy:
continue
addUrlToFeed(baseUrl, langUrl)
proc buildTopicSitemap(topic: string): Future[XmlNode] {.async.} =
initUrlSet()
await syncTopics()
let done = await topicDonePages(topic)
var nEntries = 0
withPyLock:
# add the most recent articles first (pages with higher idx)
for pagenum in countDown(len(done) - 1, 0):
if unlikely(nEntries > maxEntries):
warn "Number of URLs for sitemap of topic: {topic} exceeds limit! {nEntries}/{maxEntries}"
break
checkTrue pagenum in done, "Mismatching number of pages"
for a in done[pagenum]:
addArticleToFeed()
Kami tidak menggunakan enjin templat kerana kebanyakan pemaparan dilakukan dengan karax, tetapi untuk halaman seperti ToS kami menggunakan templat fail, di mana kami hanya menggantikan sekumpulan pembolehubah, sepertienvsubst
perintah.
proc pageFromTemplate*(tpl, lang, amp: string): Future[string] {.async.} =
var txt = await readfileAsync(ASSETS_PATH / "templates" / tpl & ".html")
let (vars, title, desc) =
case tpl:
of "dmca": (tplRep, "DMCA", fmt"dmca compliance for {WEBSITE_DOMAIN}")
of "tos": (ppRep, "Terms of Service",
fmt"Terms of Service for {WEBSITE_DOMAIN}")
of "privacy-policy": (ppRep, "Privacy Policy",
fmt"Privacy Policy for {WEBSITE_DOMAIN}")
else: (tplRep, tpl, "")
txt = multiReplace(txt, vars)
let
slug = slugify(title)
page = await buildPage(title = title, content = txt, wrap = true)
checkNil(page):
let processed = await processPage(lang, amp, page, relpath = tpl)
checkNil(processed, fmt"failed to process template {tpl}, {lang}, {amp}"):
return processed.asHtml(minify_css = (amp == ""))
Apabila kami memaparkan halaman seperti rumah/topik dan halaman bernombor, kami perlu menunjukkan senarai artikel, fungsi ini dipanggil dalam gelung untuk jumlah artikel yang ingin kami paparkan:
import htmlparser
proc articleEntry(ar: Article, topic = ""): Future[VNode] {.async.} =
if ar.topic == "" and topic != "":
ar.topic = topic
let relpath = getArticlePath(ar)
try:
return buildHtml(article(class = "entry")):
h2(class = "entry-title", id = ar.slug):
a(href = relpath):
text ar.title
tdiv(class = "entry-info"):
span(class = "entry-author"):
text ar.getAuthor & ", "
time(class = "entry-date", datetime = ($ar.pubDate)):
italic:
text format(ar.pubDate, "dd/MMM")
tdiv(class = "entry-tags"):
if ar.tags.len == 0:
span(class = "entry-tag-name"):
a(href = (await nextAdsLink()), target = "_blank"):
icon("i-mdi-tag")
text "none"
else:
for t in ar.tags:
if likely(t.isSomething):
span(class = "entry-tag-name"):
a(href = (await nextAdsLink()), target = "_blank"):
icon("i-mdi-tag")
text t
buildImgUrl(ar)
tdiv(class = "entry-content"):
verbatim(articleExcerpt(ar))
a(class = "entry-more", href = relpath):
text "[continue]"
hr()
except Exception as e:
logexc()
warn "articles: entry creation failed."
raise e
proc buildShortPosts*(arts: seq[Article], topic = ""): Future[
string] {.async.} =
for a in arts:
result.add $(await articleEntry(a, topic))
Perhatikan bagaimana dalam beberapa baris "iklan" merayap dalam X)
Di bar atas kami menunjukkan senarai topik, inilah yang mencetaknya:
proc topicsList*(ucls: string; icls: string; small: static[
bool] = true): Future[VNode] {.async.} =
result = newVNode(VNodeKind.ul)
result.setAttr("class", ucls)
let topics = await loadTopics(-MENU_TOPICS) # NOTE: the sign is negative, we load the most recent N topics
result.add buildHtml(tdiv(class = "topics-shadow"))
var topic_slug, topic_name: string
var isEmpty: bool
for i in 0..<topics.len:
withPyLock:
(topic_slug, topic_name) = ($topics[i][0], $topics[i][1])
isEmpty = isEmptyTopic(topic_slug)
if isEmpty:
continue
let liNode = buildHtml(li(class = fmt"{icls}")):
# tdiv(class = "mdc-icon-button__ripple") # not used without material icons
a(href = ($(WEBSITE_URL / topic_slug)), title = topic_name,
class = "mdc-ripple-button"):
tdiv(class = "mdc-ripple-surface mdc-ripple-upgraded")
when small:
# only use the first letter
text $topic_name.runeAt(0).toUpper # loadTopics iterator returns pyobjects
else:
text topic_name
when small:
br()
else:
span(class = "separator")
result.add liNode
Terdapat beberapa kelas reka bentuk bahan berkod keras berbau di sini. Terus terang komponen reka bentuk bahan google menyedut.
Pengaki catatan muncul di bahagian bawah sebelah kanan halaman artikel (dalam ltr) dan ia benar-benar hanya mencetak tarikh diterbitkan.
proc postFooter(pubdate: Time): VNode =
let dt = inZone(pubdate, utc())
buildHtml(tdiv(class = "post-footer")):
time(datetime = ($dt)):
text "Published date: "
italic:
text format(dt, "dd MMM yyyy")
Semasa membina entri artikel, kami mungkin memerlukan petikan jika tiada ringkasan tersedia.
proc articleExcerpt(a: Article): string =
let alen = len(a.content) - 1
let maxlen = min(alen, ARTICLE_EXCERPT_SIZE)
if maxlen == alen:
return a.content
else:
let runesize = runeLenAt(a.content, maxlen)
# If article contains html tags, the excerpt might have broken html
return parseHtml(a.content[0..maxlen+runesize]).innerText & "..."
Wtf adalahparseHtml
buat di sini? Dalam kes kami membenarkan html dalam kandungan artikel (tetapi hanya beberapa teg), ini adalah pilihan daripada trafilatura modul python, yang kami kekalkan didayakan kerana ia boleh menjejaskan format artikel. Kita juga perlu berhati-hati tentang memotong rentetan utf-8...
Tugas terakhir selepas membina karaxVNode
pokok adalah untuk membuang bait. Pokok jika diawali dengan pengepala html, dan secara pilihan dikecilkan.
proc asHtml*(data: string ; minify: static[bool] = true; minify_css: bool = true): string =
let html = "<!DOCTYPE html>" & "\n" & data
sdebug "html: raw size {len(html)}"
result = when minify:
html.minifyHtml(minify_css = false,
minify_js = false,
keep_closing_tags = true,
do_not_minify_doctype = true,
keep_spaces_between_attributes = true,
ensure_spec_compliant_unquoted_attribute_values = true)
else:
html
sdebug "html: minified size {len(result)}"
Minifikasi dikendalikan oleh minify-html yang kami telah terikat menggunakan c2nim , fail yang mengikat mengandungi:
proc minify*(code: cstring,
do_not_minify_doctype = false,
ensure_spec_compliant_unquoted_attribute_values = false,
keep_closing_tags = true,
keep_comments = false,
keep_html_and_head_opening_tags = true,
keep_spaces_between_attributes = false,
minify_css = true,
minify_js = true,
remove_bangs = false,
remove_processing_instructions = true): cstring {.importc: "minify".}
proc minifyHtml*(tree: VNode): string = $minify(($tree).cstring)
proc minifyHtml*(data: string): string = $minify(data.cstring)
template minifyHtml*(data: string, args: varargs[untyped]): string =
$minify(data.cstring, args)
Tetapi untuk membina kita perlu menyediakan perpustakaan statik, menambah baris ini dalam kitanim.cfg
--passL:"$PROJECT_DIR/src/rust/target/release/libminify_html_c.a"
Maksud saya...itulah laluan saya apabila saya membina perpustakaan minify yang btw sebenarnya tidak mempunyai fungsi extern c yang boleh digunakan oleh nim, jadi kami terpaksa menulisnya sendiri.
use minify_html::{Cfg, minify as minify_html_native};
use std::ffi::CStr;
use std::ffi::CString;
use std::os::raw::c_char;
#[no_mangle]
pub extern "C" fn minify(
code: *const c_char,
do_not_minify_doctype: bool,
ensure_spec_compliant_unquoted_attribute_values: bool,
keep_closing_tags: bool,
keep_comments: bool,
keep_html_and_head_opening_tags: bool,
keep_spaces_between_attributes: bool,
minify_css: bool,
minify_js: bool,
remove_bangs: bool,
remove_processing_instructions: bool,
) -> *const c_char {
let code = unsafe { CStr::from_ptr(code) };
let code_vec = code.to_bytes();
let cfg = Cfg {
do_not_minify_doctype,
ensure_spec_compliant_unquoted_attribute_values,
keep_closing_tags,
keep_comments,
keep_html_and_head_opening_tags,
keep_spaces_between_attributes,
minify_css,
minify_js,
remove_bangs,
remove_processing_instructions,
};
let minified = minify_html_native(code_vec, &cfg);
let s = unsafe { CString::from_vec_unchecked(minified).into_raw() };
return s;
}
Pengikatan Python untuk nim perlu ofc percuma membuang objek python. Masalahnya kita kena kawal bila nim buat GC. Perpustakaan nimpy menganggap GIL sentiasa dikunci (ia menguncinya pada mulanya), dan ia adalah percuma untuk memanggil ular sawa bila-bila masa. Tetapi kami membuka kunci gil untuk membolehkan kumpulan benang ular sawa menjalankan kod semasa nim menjalankan perkara lain. Jika GIL ular sawa sentiasa dikunci oleh nim threadpool akan melahu kebanyakan masa.
when defined(pyAsync):
type
PyGilObj = object
lock: ThreadLock
currentLockHolder: int
state: PyGILState_STATE
PyGil = ptr PyGilObj
var pyGil*: PyGil
var pyGilLock*: ThreadLock
var pyMainThread: PyThreadState
proc initPyGil*() =
assert PyGILState_Check()
pyGil = create(PyGilObj)
pyGil.currentLockHolder = getThreadID()
pyGil.lock = newThreadLock()
pyGilLock = pyGil.lock
pyMainThread = PyEval_SaveThread()
proc acquire*(gil: PyGil): Future[void] {.async.} =
await gil.lock.acquire
let id = getThreadId()
gil.currentLockHolder = id
gil.state = Py_GILState_Ensure()
proc tryAcquire*(gil: PyGil): bool =
if gil.lock.tryAcquire():
let id = getThreadId()
gil.currentLockHolder = id
gil.state = Py_GILState_Ensure()
return true
proc release*(gil: PyGil) {.inline.} =
doassert gil.currentLockHolder == getThreadId(), "Can't release gil lock from a different thread."
doassert gilLocked()
Py_GILState_Release(gil.state)
gil.lock.release
Ini membolehkan cara untuk melaksanakan kod python yang memegang GIL, tetapi hanya pada benang semasa. Pelaksanaan untuk memperoleh/melepaskan GIL pada benang nim yang berbeza memerlukan memanggil fungsi python C abi yang berbeza, kerana GIL ialah mutex. Kami kemudian memanggil python menggunakan templat ini:
template withPyLock*(code): untyped =
{.locks: [pyGil].}:
try:
# echo getThreadId(), " -- ", getCurrentProcessId(), " -- ", procName()
await pygil.acquire()
code
except:
raise getCurrentException()
finally:
# echo getThreadId(), " -- ", getCurrentProcessId(), " -- unlocked"
pygil.release()
Kami menggunakan ciri kunci dan pengawal nim, untuk memastikan jenis ular sawa hanya diakses apabila GIL diadakan. Walau bagaimanapun ini memerlukan penentuan pyobjek dengan pengawal:
macro pyObjPtr*(defs: varargs[untyped]): untyped =
result = newNimNode(nnkStmtList)
for d in defs:
let
name = d[0]
def = d[1]
result.add quote do:
let `name` {.guard: pyGil.} = create(PyObject)
`name`[] = `def`
Jadi saya boleh lakukan:
pyObjPtr(myVar, pyimport("datetime").datetime))
Dan setiap kali saya menelefonmyVar
yang memegang objek datetime, saya perlu membungkusnya seperti ini:
withPyLock():
myVar.fromunixtimestamp(1)
Sekarang kita boleh mengunci gil apabila kita perlu menjalankan GC, mengatasi lincahPyObject
pemusnah dengan ini:
var garbage: seq[PPyObject]
proc `=destroy`*(p: var PyObject) =
if pygil.tryAcquire:
if not p.rawPyObj.isnil:
decRef p.rawPyObj
p.rawPyObj = nil
while garbage.len > 1:
var pp = garbage.pop() # TODO: Does this leak a pointer?
if not pp.isnil:
decRef pp
pp = nil
pygil.release
else:
if not p.rawPyObj.isnil:
garbage.add p.rawPyObj
Kunci yang kami gunakan di dalam pemusnah bukanlahAsyncLock
kerana itu akan menjadi terlalu mahal, dan kami jangan sentiasa kunci, kerana itu akan menyebabkan gerai! Jika kami tidak dapat mengunci gil, kami menangguhkan pengumpulan dan menyimpan penuding ular sawa mentah di sekeliling untuk bila kami dapat mengosongkannya. Sejujurnya saya tidak tahu sama ada ini menyebabkan bentuk isu lain, tetapi nampaknya berkesan cukup baik.
Kami mempunyai modul nim yang dipanggilpyutils.nim
yang melakukan sekumpulan nim<>barangan python, sebagai contoh:
from utils import withLocks
proc pyhasAttr*(o: PyObject; a: string): bool {.withLocks: [pyGil].} = pybi[].hasattr(
o, a).to(bool)
proc pyclass(py: PyObject): PyObject {.inline, withLocks: [pyGil].} =
pybi[].type(py)
proc pytype*(py: PyObject): string =
py.pyclass.getattr("__name__").to(string)
proc pyisbool*(py: PyObject): bool {.withLocks: [pyGil].} =
return pybi[].isinstance(py, PyBoolClass[]).to(bool)
proc pyisnone*(py: PyObject): bool {.gcsafe, withLocks: [pyGil].} =
return py.isnil or pybi[].isinstance(py, PyNoneClass[]).to(bool)
Yang ini digunakan agak banyak:
proc pyget*[T](py: PyObject; k: string; def: T = ""): T =
try:
let v = py.callMethod("get", k)
if pyisnone(v):
return def
else:
return v.to(T)
except:
pyErrClear()
if pyisnone(py):
return def
else:
return py.to(T)
Yang ini digunakan apabila kami telah menjadualkan kerja ular sawa, dan kami mahu menunggu sehingga selesai secara tidak segerak:
proc pywait*(j: PyObject): Future[PyObject] {.async, gcsafe.} =
var rdy: bool
var res: PyObject
while true:
withPyLock:
checkNil(j)
rdy = j.callMethod("ready").to(bool)
if rdy:
withPyLock:
checkNil(j)
res = j.callMethod("get")
break
await sleepAsync(250.milliseconds)
withPyLock:
if (not res.isnil) and (not pyisnone(res)) and (not pyErrOccurred()):
return res
else:
raise newException(ValueError, "Python job failed.")
Pengikatan async python yang betul akan memerlukan melengkapkan masa depan nim async daripada python pada penghujung kerja berjadual ular sawa, yang tidak kami lakukan kerana kami tidak melihat cukup mendalam untuk mengendalikan objek nim daripada python.
Kami menyokong google amp, jadi kami menjana halaman amp yang agak mematuhi amp. Kami tidak menyasarkan sokongan 1:1. Sebenarnya kami nuke semua javascript yang kami ada dan hanya menyajikan html/css. Walaupun begitu kita perlu berhati-hati untuk tidak menambah atribut tersuai pada teg html, atau hanya teg html tersuai, amp adalah buruk seperti itu... Untuk penukaran halaman amp automatik kami mengendalikanhead
dan jugabody
tag secara berbeza.
proc processHead(inHead: VNode, outHead: VNode, level = 0) {.async.} =
var canonicalUnset = level == 0
debug "iterating over {inHead.kind}"
for el in inHead.preorder(withStyles = true):
case el.kind:
of VNodeKind.text, skipNodes:
continue
of VNodeKind.style:
if el.len > 0:
el[0].text.maybeStyle
of VNodeKind.link:
if canonicalUnset and el.isLink(canonical):
outHead.add el
canonicalUnset = false
elif el.isLink(stylesheet) and (not ("flags-sprite" in el.getattr("href"))):
await el.fetchStyle()
elif el.isLink(preload) and el.getattr("as") == "style":
await el.fetchStyle()
else:
outHead.add el
of VNodeKind.script:
if el.getAttr("type") == $ldjson:
outHead.add el
of VNodeKind.meta:
if (el.getAttr("name") == "viewport") or (el.getAttr("charset") != ""):
continue
else:
outHead.add el
of VNodeKind.verbatim:
let data = el.toXmlNode
if data.kind == xnElement:
if data.tag == "noscript":
processNoScript()
elif data.tag == "script":
continue
elif data.tag == "style":
if data.len > 0:
data[0].text.maybeStyle
else:
outHead.add el
of VNodekind.noscript:
processNoScript()
else:
debug "amphead: adding element {el.kind} to outHead."
outHead.add el
Semua gaya digabungkan menjadi skrip sebaris tunggal, apa yang disimpan ialahlink
teg yang bukan gaya/jskrip, seperti lang. Tag skrip untukldljson
, meta
tag. Verbatim mengendalikan nod yang literal , kita perlu menukarkannya kepadaXmlNode
(yang bermaksud menghuraikan) dan mengendalikannya dengan betul. Badan proses adalah serupa, kami menyimpan beberapa teg, mengalih keluar yang lain, menamakan yang lain:
template process(el: VNode, after: untyped): bool =
var isprocessed = true
case el.kind:
of skipNodes: discard
of VNodeKind.link:
if el.isLink(stylesheet):
await el.fetchStyle()
else:
outBody.add el
of VNodeKind.style:
el.text.maybeStyle
el.text = ""
of VNodeKind.script:
if el.getAttr("type") == $ldjson:
outHead.add el
el.text = ""
of VNodeKind.form:
el.setAttr("amp-form", "")
else:
isprocessed = false
if isprocessed:
after
isprocessed
Theform
tag digantikan denganamp-form
, amp mempunyai banyak teg ini...
Kami perlu memastikan bahawa gaya sebaris berada dalam panjang yang betul:
styleStr = styleStr
# .join("\n")
# NOTE: the replacement should be ordered from most frequent to rarest
# # remove troublesome animations
.replace(pre"""\s*[email protected](\-[a-zA-Z]+-)?keyframes\s+?.+?{\s*?.+?({.+?})+?\s*?}""", "")
# # remove !important hints
.replace(pre"""!important""", "")
# remove charset since not allowed
.replace(pre"""@charset\s+\"utf-8\"\s*;?/i""", "")
if unlikely(styleStr.len > CSS_MAX_SIZE):
raise newException(ValueError, fmt"Style size above limit for amp pages. {styleStr.len}")
Penjanaan amp kami tidak meliputi spesifikasi amp penuh, tetapi ia berfungsi untuk kandungan kami (melalui percubaan dan ralat :S).
Setiap kali artikel diterbitkan, ia diserap ke dalam pangkalan data sonik, pangkalan data sonik mengendalikan "koleksi", "baldi" dan "objek"; Kami mentakrifkan koleksi sebagai tapak web, jadi setiap tapak web yang ingin menggunakan pengagregat kandungan mempunyai koleksinya sendiri. Kami tidak menggunakanbuckets
, walaupun kami boleh menganggap setiap topik sebagai baldi yang akan menyempitkan carian terlalu banyak, jadi setiap tapak hanya mempunyai satu baldi "lalai", dan setiap objek baldi ialah artikel (yang boleh terdiri daripada topik yang berbeza).
proc push*(capts: UriCaptures, content: string) {.async.} =
## Push the contents of an article page to the search database
## NOTE: NOT thread safe
var ofs = 0
while ofs <= content.len:
let view = content[ofs..^1]
let key = join([capts.topic, capts.page, capts.art], "/")
let cnt = runeSubStr(view, 0, min(view.len, bufsize - key.len))
ofs += cnt.len
if cnt.len == 0:
break
try:
let lang = await capts.lang.toISO3
var pushed: bool
var j: PyObject
withPyLock:
j = pySched[].apply(
pySonic[].push,
WEBSITE_DOMAIN,
"default", # TODO: Should we restrict search to `capts.topic`?
key,
cnt,
lang = if capts.lang != "en": lang else: ""
)
j = await j.pywait()
withPyLock:
pushed = not pyisnone(j) and j.to(bool)
when not defined(release):
if not pushed:
capts.addToBackLog()
break
except Exception:
logexc()
debug "sonic: couldn't push content, \n {capts} \n {key} \n {cnt}"
when not defined(release):
capts.addToBackLog()
block:
var f: File
try:
await pushLock[].acquire
f = open("/tmp/sonic_debug.log", fmWrite)
write(f, cnt)
finally:
pushLock[].release
if not f.isnil:
f.close()
break
Apabila menolak kandungan ke dalam sonik kita perlu membahagikan data dalam ketulan, yang panjang maksimum diketahui semasa sambungan. Memasukkan data nampaknya kadang-kadang bermasalah, kerana ia nampaknya tidak dapat mengendalikan beberapa aksara tertentu. Sekiranya pelayan sonik rosak entah bagaimana, kami juga mempunyai fungsi untuk menyerap semula semua kandungan:
proc pushAllSonic*() {.async.} =
await syncTopics()
var total, c, pagenum: int
let pushLog = await readPushLog()
if pushLog.len == 0:
withPyLock:
discard pySonic[].flush(WEBSITE_DOMAIN)
defer:
withPyLock:
discard pySonic[].consolidate()
for (topic, state) in topicsCache:
if topic notin pushLog:
pushLog[topic] = %0
await pygil.acquire
defer: pygil.release
let done = state.group[]["done"]
for page in done:
pagenum = ($page).parseint
c = len(done[page])
if pushLog[topic].to(int) >= pagenum:
continue
var futs: seq[Future[void]]
for n in 0..<c:
let ar = done[page][n]
if ar.isValidArticlePy:
var relpath = getArticlePath(ar, topic)
relpath.removeSuffix("/")
let
capts = uriTuple(relpath)
content = ar.pyget("content").sanitize
echo "pushing ", relpath
futs.add push(capts, content)
total.inc
pygil.release
await allFutures(futs)
pushLog[topic] = %pagenum
await writePushLog(pushLog)
await pygil.acquire
info "Indexed search for {WEBSITE_DOMAIN} with {total} objects."
Terjemahan adalah cerita yang agak berantakan. Saya berada di peringkat ke-4 (!) pelaksanaan pembalut terjemahan, selepas menulis dalam php, pergi dan julia , ini juga ditulis dalam nim. Varian php/go agak busuk pada masa kini, manakala varian julia aktif digunakan untuk blog ini. Walau bagaimanapun untuk mencapai kelewatan yang rendah untuk pelayan web, cara penterjemahan dilaksanakan dalam julia tidak sesuai untuk servis masa nyata (ia menterjemah fail statik), dan bagaimanapun, menambah julia sebagai pergantungan apabila kita sudah mempunyai python akan menjadi sangat besar. keperluan.
Jadi saya terpaksa melaksanakan modul terjemahan baharu dalam nim. Sebenarnya, modul terjemahan nim awal kelihatan seperti pelaksanaan julia, di mana kami menterjemah fail statik[1] . Selepas itu, apabila pelayan web mula dibentuk, saya menukarnya untuk menterjemah nod karax atas permintaan. Ini membolehkan untuk menterjemah setiap halaman web tepat pada masanya untuk permintaan.
template translateVbtm(node: VNode, q: QueueDom) =
assert node.kind == VNodeKind.verbatim
let tree = ($node).parseHtml() # FIXME: this should be a conversion, but the conversion doesn't preserve whitespace??
if tree.kind == xnElement and tree.tag == "document":
tree.tag = "div"
takeOverFields(tree.toVNode, node)
translateIter(node, vbtm = false)
template translateIter(otree; vbtm: static[bool] = true) =
for el in otree.preorder():
case el.kind:
of vdom.VNodeKind.text:
if el.text.isEmptyOrWhitespace:
continue
if isTranslatable(el):
translate(q.addr, el, srv)
else:
let t = el.kind
if t in tformsTags:
getTForms(dom)[t](el, file_path, url_path, pair)
if t == VNodeKind.a:
if el.hasAttr("href"):
rewriteUrl(el, rewrite_path, hostname)
if t == VNodeKind.verbatim:
when vbtm:
debug "dom: translating verbatim", false
translateVbtm(el, q)
else:
if(el.hasAttr("alt") and el.isTranslatable("alt")) or
(el.hasAttr("title") and el.isTranslatable("title")):
translate(q.addr, el, srv)
Di atas ialah gelung lelaran utamatranslateIter
:
getTforms
fungsi peta kepada teg html, membolehkan untuk melakukan mutasi mengikut kes demi kes.
rewriteUrl
memasukkan laluan lang (.cth/en/
) dalam laluan url.
translateVbtm
mengendalikan nod verbatim yang memerlukan penghuraian.
Terjemahan digunakan pada semua nod teks dan padaalt
dantitle
sifat-sifat.
proc translate*[T](q: ptr[QueueXml | QueueDom], el: T, srv: service) =
if q.isnil:
warn "translate: queue can't be nil"
return
let (success, length) = setFromDB(q[].pair, el)
if not success:
if length > q[].bufsize:
debug "Translating element singularly since it is big"
elUpdate(q[], el, srv)
else:
if reachedBufSize(length, q[]):
q[].push()
q[].bucket.add(el)
q[].sz += length
proc translate*[T](q: ptr[QueueXml | QueueDom], el: T, srv: service,
finish: bool): Future[bool] {.async.} =
if finish:
if q.isnil:
return true
let (success, _) = setFromDB(q[].pair, el)
if not success:
addJob(@[el], q[], el.getText)
debug "translate: waiting for pair: {q[].pair}"
await doTrans()
return true
proc translate*(q: ptr[QueueXml | QueueDom], srv: service,
finish: bool): Future[bool] {.async.} =
if finish and q[].sz > 0:
q[].push()
await doTrans()
saveToDB(force = true)
return true
Kerana kita perlu menterjemah setiap nod teks secara berasingan (jika tidak, kita tidak boleh mengembalikan html) setiap terjemahan nod adalah tugas yang berasingan. Memandangkan kerja boleh menanyakan perkhidmatan terjemahan jaring, ia perlu dilakukan secara tidak segerak. Kami melakukan pemisahan dan penggabungan pertanyaan terjemahan untuk menggantikan panggilan api, tetapi bahagian dalaman enjin terjemahan tidak penting untuk diketahui. Satu-satunya perkara yang perlu diperhatikan ialah pada mulanya saya menggunakan a pembalut ular sawa (yang masih saya gunakan untuk menterjemah kandungan yang dikikis) kerana mengurus sendiri pembalut untuk apis luaran adalah menyakitkan, tetapi kemudian beralih kepada perkhidmatan terjemahan google dan yandex yang dibalut sendiri dalam nim, kerana python menjadi halangan yang besar apabila mengendalikan banyak terjemahan serentak.
[1] | Sebenarnya pada asalnya agregator kandungan sepatutnya hanya menjana fail statik untukcaddy untuk disiarkan, tetapi kerana jumlah halaman untuk dijana (iaitu matriks n_lang(20) x amp(2) x halaman), pemaparan malas ialah pilihan yang lebih baik. |
Halaman topik dan artikel dijejaki untuk jumlah hit.
proc updateHits*(capts: UriCaptures) =
let ak = join([capts.topic, capts.art])
let tk = capts.topic
var
artCount: int32 = statsDB[ak]
topicCount: int32 = statsDB[tk]
artCount += 1
topicCount += 1
statsDB[ak] = artCount
statsDB[tk] = topicCount
Kami menggunakan kiraan hit untuk membersihkan halaman dengan kiraan rendah secara berkala.
proc deleteLowTrafficArts*(topic: string): Future[void] {.gcsafe, async.} =
let now = getTime()
var
pagenum: int
pagesToReset: seq[int]
pubTime: Time
pubTimeTs: int
var capts = mUriCaptures()
capts.topic = topic
for (art, _) in (await publishedArticles[string](topic, "")):
withPyLock:
if pyisnone(art):
continue
capts.art = pyget[string](art, "slug")
pagenum = pyget(art, "page", 0)
capts.page = pagenum.intToStr
try:
withPyLock:
pubTimeTs = pyget(art, "pubTime", 0)
pubTime = fromUnix(pubTimeTs)
except:
pubTime = default(Time)
if pubTime == default(Time):
if not (pagenum in pagesToReset):
debug "tasks: resetting pubTime for page {pagenum}"
pagesToReset.add pagenum
# article is old enough
elif inSeconds(now - pubTime) > cfg.CLEANUP_AGE:
let hits = topic.getHits(capts.art)
# article has low hit count
if hits < cfg.CLEANUP_HITS:
await deleteArt(capts)
for n in pagesToReset:
withPyLock:
discard site[].update_pubtime(topic, n)
Kami gunalibmdbx
melalui lib ini . Mungkin berlebihan, dan menggunakan leveldb sudah memadai. Kami mempunyai jenisLRUTrans
di mana idea awal adalah untuk menyediakan pangkalan data sebagai cache LRU, tetapi ia jauh lebih perlahan. Pelaksanaannya boleh didapati di sini
type
CollectionNotNil = ptr Collection not nil
LRUTransObj = object
db: nimdbx.Database.Database not nil
coll: CollectionNotNil
zstd_c: ptr ZSTD_CCtx
zstd_d: ptr ZSTD_DCtx
LRUTrans* = ptr LRUTransObj
proc getImpl(t: LRUTrans, k: int64, throw: static bool): string =
withLock(tLock):
var o: seq[byte]
t.coll.inSnapshot do (cs: CollectionSnapshot):
# debug "nimdbx: looking for key {k}, {v}"
o.add cs[k.asData].asByteSeq
if len(o) > 0:
result = cast[string](decompress(t.zstd_d, o))
# debug "nimdbx: got key {k}, with {o.len} bytes"
elif throw:
raise newException(KeyError, "nimdbx: key not found")
proc getImpl[T: not int64](t: LRUTrans, k: T, throw: static bool): string =
getImpl(t, hash(k).int64, throw)
proc `[]`*[T](t: LRUTrans, k: T): auto = t.getImpl(k, false)
proc `get`*[K](t: LRUTrans, k: K): auto = t.getImpl(k, true)
proc `[]=`*(t: LRUTrans, k: int64, v: string) {.gcsafe.} =
var o: seq[byte]
if likely(v.len != 0):
o = compress(t.zstd_c, v, cfg.ZSTD_COMPRESSION_LEVEL)
withLock(tLock):
logall "nimdbx: saving key {k}"
t.coll.inTransaction do (ct: CollectionTransaction):
{.cast(gcsafe).}:
ct[k] = o
ct.commit()
logall "nimdbx: commited key {k}"
proc `[]=`*[K: not int64](t: LRUTrans, k: K, v: string) = t[hash(k).int64] = v
Jenis ini digunakan untuk empat pangkalan data berasingan:
terjemahan
cache halaman
cache imej
statistik
Jenis pangkalan data dilaksanakan dengan getter dan setter kemudian lakukan penyahmampatan automatik pada baca/tulis. Atas sebab ini ia tidak sepatutnya digunakan untuk imej...tetapi malangnya... Terdapat juga sekumpulan cache mikro kecil:
vbtm: untuk kandungan yang dihuraikan (verbatim).
carian: untuk pertanyaan carian
suapan: untuk suapan topik VNodes
rxcache: untuk regex, kerana regex statik masa penyusunan masih belum diseragamkan (juga kerana terdapat berbilang perpustakaan regex dalam nim)
Ini dilaksanakan sebagai lru cache[2] , lebih tepat sebagai cache lru "terkunci", di mana setiap operasi perolehan dan set dililitkan pada kunci (benang). Kunci ini tidak boleh menyebabkan gerai dengan masa jalan tak segerak kerana kunci diperoleh dan dilepaskan tanpa sebarang pernyataan hasil, jadi kunci itu adalah atom dalam erti kata itu, walau bagaimanapun ia masih berguna kerana kami menggunakan benang untuk tugasan yang berbeza.
[2] | Namun begitu nim rebus mempunyai pelaksanaan yang lebih mudah untuk cache lru yang saya akan gunakan jika ditemui lebih awal. |
Beberapa tugasan yang kami gunakan adalah CPU lapar, jadi kami menggunakan urutan yang berbeza untuk mereka:
lsh: pencincangan sensitif lokaliti melakukan banyak pengiraan
imej: Saiz semula imej memerlukan penyahkodan/pengekodan imej supaya ia mahal
Dua lagi urutan digunakan untuk mengemas kini senarai fail aset dan iklan, walaupun tidak cpu lapar, benang diperlukan untuk mengelakkan gerai yang disebabkan oleh pemerhati fail.
Kami juga mempunyai tugasan berjalan lama async untuk:
terjemahan
permintaan http
Lsh, imej, terjemahan dan kerja permintaan http dikendalikan menggunakan persediaan pengeluar/pengguna. Kecuali kami tidak menggunakan saluran, kerana saluran menyekat dan kami tidak mempunyai pelaksanaan async untuknya yang juga selamat untuk benang. Kami menggunakan pelaksanaan async bagi ini[1] . Dan jadual async, yang seperti bas acara
type
AsyncTableObj[K, V] = object
lock: ThreadLock
waiters: Table[K, seq[ptr Future[V]]]
table: Table[K, V]
AsyncTable*[K, V] = ptr AsyncTableObj[K, V]
proc pop*[K, V](t: AsyncTable[K, V], k: K): Future[V] {.async.} =
var popped = false
withLock(t.lock):
if k in t.table:
popped = t.table.pop(k, result)
if not popped:
if k notin t.waiters:
t.waiters[k] = newSeq[ptr Future[V]]()
var fut = newFuture[V]("AsyncTable.pop")
t.waiters[k].add fut.addr
result = await fut
proc put*[K, V](t: AsyncTable[K, V], k: K, v: V) {.async.} =
withLock(t.lock):
if k in t.waiters:
var ws: seq[ptr Future[V]]
doassert t.waiters.pop(k, ws)
while ws.len > 0:
let w = ws.pop()
if not w.isnil and not w[].isnil and not w[].finished:
w[].complete(v)
else:
t.table[k] = v
Pelayan nim juga mengendalikan tiga tugas async:
type
TaskKind = enum pub, cleanup, mem
proc scheduleTasks(): TaskTable =
template addTask(t) =
let fut = (selectTask t)()
result[t] = fut
# Publishes new articles for one topic every x seconds
addTask pub
# cleanup task for deleting low traffic articles
addTask cleanup
# quit when max memory usage reached
addTask mem
Tugas yang memantau penggunaan mem adalah bagus untuk dilakukan, untuk mengelakkan masalah OOM antara proses kontena dan docker, kerana docker (atau kernel) tidak mematikan proses serta-merta, dan dalam tempoh masa ini pelayan boleh menjadi tidak bertindak balas, jadi adalah lebih baik untuk memulakan semula secara manual dengan segera.
[1] | walaupun membungkus saluran biasa dalam rutin async mungkin lebih baik...sayangnya |
Kami memanfaatkan aliran imej untuk mengubah saiz dan cache imej secara setempat. Pengikatannya mudah, tetapi prosesnya sedikit terlibat. DengangetImg
kami mengambil data imej dari url jauh:
proc getImg*(src: string, kind: Source): Future[string] {.async.} =
return case kind:
of urlsrc:
(await get(src.parseUri, decode = false, proxied = false)).body
elif fileExists(src):
await readFileAsync(src)
else:
""
Kemudian kita perlu menambahkannya ke konteks aliran imej:
proc addImg*(img: string): bool =
## a lock should be held here throughout the `processImg` call.
if img == "": return false
reset(ctx)
doassert ctx.check
let a = imageflow_context_add_input_buffer(
ctx.p,
inputIoId,
# NOTE: The image is held in cache, but it might be collected
cast[ptr uint8](img[0].unsafeAddr),
img.len.csize_t,
imageflow_lifetime_lifetime_outlives_context)
if not a:
doassert ctx.check
cmdStr["decode"] = %inputIoId
return true
Jika imej tidak boleh ditambah, ini bermakna aliran imej gagal mengenali data sebagai imej yang sah. Selepas kami menghantar data, kami perlu menghantar pertanyaan kepada konteks, kemudian membaca respons, dan mendapatkan output:
proc doProcessImg(input: string, mtd = execMethod): (string, string) =
setCmd(input)
let c = $cmd
# debug "{hash(c)} - {c}"
let json_res = imageflow_context_send_json(
ctx.p,
mtd,
cast[ptr uint8](c[0].unsafeAddr),
c.len.csize_t
)
discard imageflow_json_response_read(ctx.p, json_res,
status.addr,
resPtr,
resLen)
defer: doassert imageflow_json_response_destroy(ctx.p, json_res)
var mime: string
if status != 200:
let msg = resPtr[].toString(resLen[].int)
debug "imageflow: conversion failed {msg}"
doassert ctx.check
else:
mime = getMime()
discard imageflow_context_get_output_buffer_by_id(
ctx.p,
outputIoId,
outputBuffer,
outputBufferLen)
doassert ctx.check
result = (outputBuffer[].toString(outputBufferLen[].int), mime)
Kami mendapat jenis mime daripada respons, yang akan dimajukan dalam respons pelayan web. Dari sisi pelayan terjemahan dari laluan url ke aliran imej dikendalikan seperti ini:
proc processImgData(q: ptr ImgQuery) {.async.} =
# push img to imageflow context
initImageFlow() # NOTE: this initializes thread vars
var acquired, submitted: bool
let data = (await q.url.rawImg)
defer:
if acquired: imgLock[].release
if not submitted:
imgOut[q] = true
if data.len > 0:
try:
await imgLock[].acquire
acquired = true
if addImg(data):
let query = fmt"width={q.width}&height={q.height}&mode=max&format=webp"
logall "ifl server: serving image hash: {hash(await q.url.rawImg)}, size: {q.width}x{q.height}"
# process and send back
(q.processed.data, q.processed.mime) = processImg(query)
imgOut[q] = true
submitted = true
except Exception:
discard
Url imej dihantar sebagai parameter, dalam bentuk termampat zstd. Pemampatan memendekkan url (kebanyakan masa). Ini juga cara saya menemui pepijat dalam google chrome, di mana ia tidak dapat mengendalikan url yang pertanyaannya mempunyai data termampat yang dikodkan url. Firefox baik-baik saja.
Kami menambah pada setiap halaman web skrip ldjson .
proc jwebpage(id, title, url, mtime, selector, description: auto, keywords: seq[string], name = "", headline = "",
image = "", entity = "Article", status = "Published", lang = "english", mentions: seq[
string] = (@[]), access_mode = (@["textual", "visual"]), access_sufficient: seq[
string] = @[], access_summary = "", created = "", published = "",
props = default(JsonNode)): JsonNode =
let
d_mtime = coerce(mtime, "")
s_created = created.toIsoDate
description = coerce(description, to = title)
prd = (v: seq[string]) => v.len == 0
let data = %*{
"@context": "https://schema.org",
"@type": "https://schema.org/WebPage",
"@id": id,
"url": url,
"lastReviewed": coerce(mtime, ""),
"mainEntityOfPage": {
"@type": entity,
"@id": url
},
"mainContentOfPage":
{
"@type": "WebPageElement", "cssSelector": selector},
"accessMode": access_mode,
"accessModeSufficient": {
"@type": "itemList",
"itemListElement": coercf(access_sufficient, prd, to = access_mode),
},
"creativeWorkStatus": status,
# NOTE: datePublished should always be provided
"datePublished": ensure_time(d_mtime.toIsoDate, s_created),
"dateModified": d_mtime,
"dateCreated": coerce(s_created, to = d_mtime),
"name": coerce(name, to = title),
"description": coerce(description, ""),
"keywords": coerce(keywords, to = (@[]))
}
setArgs data, %*{"inLanguage": lang, "accessibilitySummary": access_summary,
"headline": coerce(headline, to = description), "image": image,
"mentions": mentions}
setProps
data
Dan untuk halaman terjemahan:
proc translation*(src_url, trg_url, lang, title, mtime, selector, description: auto, keywords: seq[string],
image = "", headline = "", props = default(JsonNode),
translator_name = "Google", translator_url = "https://translate.google.com/"): auto =
## file path must be relative to the project directory, assumes the published website is under '__site/'
# id, title, url, mtime, selector, description: auto, keywords: seq[string], name = "", headline = "",
let data = jwebpage(id = trg_url, title, url = trg_url, mtime, selector, description,
keywords = keywords, image = image, headline = headline, lang = lang, props = props)
data["translator"] = %*{"@type": "https://schema.org/Organization",
"name": translator_name,
"url": translator_url}
data["translationOfWork"] = %*{"@id": src_url}
data
Sama seperti ldjson, kami juga menyediakan tag meta opengraph:
proc opgBasic(title, tp, url, image: string, prefix = ""): seq[XmlNode] =
if prefix != "":
result.add metaTag(fmt"{prefix}:title", title)
result.add metaTag(fmt"{prefix}:type", tp)
result.add metaTag(fmt"{prefix}:url", url)
result.add metaTag(fmt"{prefix}:image", image)
else:
result.add metaTag("title", image)
result.add metaTag("type", image)
result.add metaTag("url", image)
result.add metaTag("image", image)
proc opgTags(title, tp, url,
image: string,
description = "",
siteName = "",
locale = "",
audio = "",
video = "",
determiner = "",
prefix = ""): seq[XmlNode] {.gcsafe.} =
## Generates an HTML String containing opengraph meta result for one item.
var result = opgBasic(title, tp, url, image, prefix)
result.add opgOptional(description, siteName, locale, audio, video, determiner)
return result
proc opgPage*(a: Article): seq[XmlNode] =
let locale = static(DEFAULT_LOCALE)
let
tp = static("article")
url = getArticleUrl(a)
siteName = static(WEBSITE_TITLE)
result = opgTags(a.title, tp, url, a.imageUrl, a.desc, siteName, locale, prefix = "article")
for t in a.tags:
result.add metaTag("article:tag", t)
result.add metaTag("article:author", a.author)
result.add metaTag("article:published_time", $a.pubTime)
result.add metaTag("article:section", a.desc)
# result.add metaTag("article:modified_time", a.pubTime)
# result.add metaTag("article:expiration_time", a.pubTime)
result.add twitterMeta("card", "summary")
result.add twitterMeta("creator", twitterUrl[])
Makro dan templat nim berguna apabila berurusan dengan semua kod berat boilerplate ini.
Terdapat satu lagi tugas, yang mengendalikan semua permintaan http (untuk mengambil imej, skrip, dll) dari sisi pelayan web. Kami menggunakan chronos httpclient:
const proxiedFlags = {NoVerifyHost, NoVerifyServerName, NewConnectionAlways}
const sessionFlags = {NoVerifyHost, NoVerifyServerName, NoInet6Resolution}
proc requestTask(q: sink ptr Request) {.async.} =
var trial = 0
var
sess: HttpSessionRef
req: HttpClientRequestRef
resp: HttpClientResponseRef
cleanup: seq[Future[void]]
while trial < q[].retries:
try:
trial.inc
sess = new(HttpSessionRef,
proxyTimeout = 10.seconds.div(3),
headersTimeout = 10.seconds.div(2),
connectTimeout = 10.seconds,
proxy = if q[].proxied: selectProxy(trial) else: "",
flags = if q[].proxied: proxiedFlags else: sessionFlags
)
req = new(HttpClientRequestRef,
sess,
sess.getAddress(q[].url).get,
q[].meth,
headers = q[].headers.toHeaderTuple,
body = q[].body.tobytes,
)
resp = await req.fetch(followRedirects = q[].redir, raw = true)
checkNil(resp):
defer:
cleanup.add resp.closeWait()
resp = nil
q.response.code = httpcore.HttpCode(resp.status)
checkNil(resp.connection):
q.response.body = bytesToString (await resp.getBodyBytes)
q.response.headers = newHttpHeaders(cast[seq[(string, string)]](
resp.headers.toList))
break
except:
cdebug():
logexc()
debug "cronhttp: request failed"
finally:
if not req.isnil:
cleanup.add req.closeWait()
if not resp.isnil:
cleanup.add resp.closeWait()
if not sess.isnil:
cleanup.add sess.closeWait()
httpOut[q] = true
await allFutures(cleanup)
Saya terpaksa menambah sokongan untuk proksi https dan socks5 kepada httpclient untuk dapat menggunakan terjemahan dengan berkesan.
Anda mungkin perasan pembolehubah huruf besar sepanjang kod. Semua ini adalah pembolehubah konfigurasi, yang ditakrifkan dalam fail, yang boleh disesuaikan setiap tapak web.
const
BASE_URL* = Uri()
SITE_PATH* = PROJECT_PATH / "site"
SITE_ASSETS_PATH* = BASE_URL / "assets" / WEBSITE_NAME
SITE_ASSETS_DIR* = SITE_PATH / "assets" / WEBSITE_NAME
DATA_PATH* = PROJECT_PATH / "data"
DATA_ASSETS_PATH* = DATA_PATH / "assets" / WEBSITE_NAME
DATA_ADS_PATH* = DATA_PATH / "ads" / WEBSITE_NAME
ASSETS_PATH* = PROJECT_PATH / "src" / "assets"
DEFAULT_IMAGE* = ASSETS_PATH / "empty.png"
DEFAULT_IMAGE_MIME* = "image/png"
CSS_BUN_URL* = $(SITE_ASSETS_PATH / "bundle.css")
CSS_CRIT_PATH* = SITE_ASSETS_DIR / "bundle-crit.css"
JS_REL_URL* = $(SITE_ASSETS_PATH / "bundle.js")
LOGO_PATH* = BASE_URL / "assets" / "logo" / WEBSITE_NAME
LOGO_URL* = $(LOGO_PATH / "logo.svg")
LOGO_SMALL_URL* = $(LOGO_PATH / "logo-small.svg")
LOGO_ICON_URL* = $(LOGO_PATH / "logo-icon.svg")
LOGO_DARK_URL* = $(LOGO_PATH / "logo-dark.svg")
LOGO_DARK_SMALL_URL* = $(LOGO_PATH / "logo-small-dark.svg")
LOGO_DARK_ICON_URL* = $(LOGO_PATH / "logo-icon-dark.svg")
FAVICON_PNG_URL* = $(LOGO_PATH / "logo-icon.png")
FAVICON_SVG_URL* = $(LOGO_PATH / "logo-icon.svg")
APPLE_PNG180_URL* = $(LOGO_PATH / "apple-touch-icon.png")
MAX_DIR_FILES* = 10
# ...
Terdapat banyak perkara yang saya tidak nyatakan, kerana syaitan ada dalam butirannya...namun ini adalah lawatan kasar ke seluruh pangkalan kod yang berjumlah:
~12k baris nim
~400 baris js
~1000 baris scss
~3500 baris ular sawa
74 lines of rust (for bindings :P)
Apa yang akan saya lakukan secara berbeza?
Mungkin menulis semula semuanya dalam karat, nim pada masa ini tidak mengendalikan keselamatan memori dengan baik, dan jumlah masa yang saya terpaksa bergantung pada gdb untuk membetulkan ranap adalah terlalu banyak, malah saya tidak berjaya membetulkan kesemuanya. Ia adalah masalah besar apabila separuh ekosistem bergantung pada GC dan separuh lagi pada ORC (atau bukan orc dan hanya ARC). Mencampur async dan benang juga menyakitkan dan jejak tindanan async adalah mimpi ngeri, (walaupun saya tidak tahu sama ada karat adalah lebih baik dalam hal ini.)
Sasaran a PWA dari getgo. Projek ini mempunyai permulaan yang agak menyusahkan. Pada mulanya ia sepatutnya menjadi halaman statik yang disampaikan oleh pelayan web, kemudian ia menjadi pelayan web itu sendiri. Interaktiviti datang sebagai renungan, jadi ia hanya menjadi gabungan html yang diberikan ditambah js/css. Ini menjadikan saya terlalu lemah pada API, yang keluar tanpa sebarang struktur (sepenuhnya tidak tenang ). Dalam penulisan semula saya akan menggunakan rangka kerja ui, sama ada berdakwah yang mempunyai sokongan AMP penuh, atau solidjs.
Tambahkan lebih banyak penghurai ad-hoc untuk platform popular. Penghuraian artikel biasa tidak berfungsi dengan baik (atau sama sekali) apabila platform paling popular pada masa kini menawarkan kandungan yang sangat sedikit, dan banyak video serta imej, oleh itu pengikisan mesti lebih disasarkan kepada media kaya dan bukannya teks sahaja, jika ini tidak diambil kira semasa merancang seni bina pengikis, kandungan dan maklumat yang akan disampaikan oleh APP akan menjadi tidak seimbang.