Một ứng dụng đầy đủ giúp thu thập, xử lý và trình bày nội dung từ web...trên web.
quá tải thông tin ? Ngày nay thật tệ, rất nhiều nguồn thông tin có tín hiệu thấp dẫn đến nhiễu, việc thu hẹp "nguồn cấp dữ liệu" của bạn để bạn không bị choáng ngợp là điều hơi khó. Một công cụ lọc thông tin và trình bày nó ở định dạng vừa dễ hiểu vừa nhanh chóng sẽ rất hữu ích. Đây là lý do tại sao tôi xem xét tổng hợp nội dung một lĩnh vực thường xanh cho sự gián đoạn. Nó đang và sẽ luôn là (miễn là internet miễn phí và tự do ngôn luận) là một cơ hội kinh doanh tốt. Đây là một trong những trường hợp mà tất cả là về việc thực thi (và không có gì về ý tưởng).
Phải nói rằng, ứng dụng của tôi cuối cùng không thực hiện bất kỳ bộ lọc thực tế nào. Trong thực tế nó chỉ đơn giản là uẩn nội dung từ web. Điều này là do tôi đã không xây dựng người dùng trong đó và có rất ít động lực để thực hiện lọc nếu nó không thể được điều chỉnh cho mỗi người dùng.
Sơ đồ kiến trúc:Thực sự có rất nhiều vòng kết nối trong đó!...bạn biết...vi mô...dịch vụ không? Các ứng dụng mà tôi đã tạo là "trình quét" và "máy chủ", trong khi "nhà xuất bản" chỉ là một thói quen được nhúng trong máy chủ. "Tìm kiếm" và "proxy" là các công cụ bên ngoài thực hiện công việc của chúng. "Frontend" không có gì đặc biệt, là sự kết hợp giữa js và css đi kèm với webpack.
Vì có nhiều phần chuyển động khác nhau nên tôi sẽ đi theo dòng chảy của nội dung, bắt đầu từ khi nội dung được xem lần đầu tiên.
Quá trình cạo được thực hiện trong...bạn đoán xem, con trăn. Tuy nhiên, không có mô-đun "cào" đặc biệt nào được sử dụng.
Việc quyết định loại bỏ nội dung nào phụ thuộc vào danh mục nội dung. Chúng tôi gọi các danh mục là "chủ đề".
Mỗi chủ đề có một danh sách các từ khóa.
Danh sách các từ khóa nếu được lấy từ google adwords bằng cách sử dụng chúng api trăn
Các từ khóa được truy vấn trên nhiều công cụ tìm kiếm, theo thứ tự vòng tròn định kỳ. Nếu phiên bản có nhiều chủ đề, thì các chủ đề có ít nội dung khả dụng hơn sẽ được tìm kiếm trước tiên. Để thực hiện các tìm kiếm mà chúng tôi dựa vào tìm kiếm với proxy. Searx không thực sự thân thiện với thư viện, vì mục đích sử dụng chính của nó là dành cho giao diện người dùng, vì vậy nó yêu cầu tìm hiểu kỹ quy trình chính xác để khởi tạo mô-đun nhằm thực hiện các truy vấn. Để tăng tốc mọi thứ bằng cách sử dụng nhóm luồng để thực hiện đồng thời nhiều truy vấn, chúng tôi sẽ sử dụng nhóm luồng thường xuyên trong suốt dự án.
Mọi tìm kiếm từ khóa đều tạo ra một danh sách các nguồn nội dung tiềm năng (Kết quả của công cụ tìm kiếm) được lưu vào bộ nhớ để xử lý sau.
Khi chúng tôi muốn tìm nội dung mới cho một chủ đề cụ thể, trước tiên chúng tôi kiểm tra xem có nguồn sẵn có hay không, nếu không, chúng tôi sẽ tạo nguồn mới từ danh sách từ khóa.
Nguồn được xử lý thông qua hai thư viện trafilatura là cái chính, nếu nó bị lỗi, chúng tôi sao lưu vào con ngỗng . Chúng tôi cũng cố gắng tìm nguồn cấp dữ liệu cho các liên kết bổ sung (được coi là nguồn mới). Đối với nguồn cấp dữ liệu chúng tôi sử dụng công cụ tìm nguồn cấp dữ liệu nhưng một phân tích cú pháp đơn giản của html cho rsslink
thẻ cũng sẽ đủ.
Loại nội dung chính của chúng tôi là mộtArticle
, mà từ phía python, nó chỉ là một lệnh, từ phía nim với phân tích nó như một đối tượng. Phím:
title
: tiêu đề của bài viết
content
: chính bài viết. Để xác định đâu là một bài viết hay, chúng tôi thực hiện các bước lọc khác nhau:
Trước tiên, chúng tôi kiểm tra xem trafilatura hoặc ngỗng có văn bản hay không và liệu nó có đủ dài không. Kích thước tối thiểu của chúng tôi là 300 từ. Nếu kích thước không khớp, chúng tôi sẽ loại bỏ nguồn (không trả lại gì).
Sau đó, chúng tôi tìm nạp tiêu đề và làm sạch tiêu đề đó bằng cách xóa các url và khoảng trắng
Nếu ngôn ngữ nước ngoài, chúng tôi dịch lại sang tiếng Anh (chúng tôi chuẩn hóa qua tiếng Anh) cả nội dung và tiêu đề.
Tại thời điểm này, chúng tôi kiểm tra ngôn từ tục tĩu bằng cách sử dụng thô tục_check . Không phải việc kiểm tra từ tục tĩu là tiếng Anh dựa trên bản dịch trước đó là cần thiết. Nếu không, chúng tôi sẽ cần một mô hình thô tục cho tất cả các ngôn ngữ.
Sau khi chúng tôi đã thay thế các từ xấu bằng bộ lọc thô tục, chúng tôi tiếp tục bằng cách làm sạch nội dung. Chúng tôi kiểm tra xem bài viết có liên quan hay không. Có những quy tắc mà chúng tôi sử dụng là:
Nội dung phải bắt đầu bằng các ký tự chữ và số, nếu không sẽ có khả năng cao là nó là rác.
Cả tiêu đề và nội dung đều không được "nhiễu". Tiếng ồn được xác định bởi biểu thức chính quy nắm bắt các từ khóa như "đăng nhập", "đăng ký", "truy cập bị từ chối"...v.v.
Ít nhất một từ trong tiêu đề phải có mặt trong phần thân. Mặt khác, có thể việc phân tích cú pháp đã chọn sai phần nội dung của trang nguồn.
Nếu các bài kiểm tra mức độ liên quan đã vượt qua, ở bước cuối cùng, chúng tôi sẽ làm sạch nội dung để tránh xuất hiện quá nhiều dấu ngoặc, khoảng trắng, ký tự lặp lại và ký tự đặc biệt.
Nếu việc dọn dẹp vẫn chưa xóa mọi thứ, chúng tôi tiếp tục xử lý bài viết.
source
: liên kết trỏ đến nguồn ban đầu mà chúng tôi đã phân tích cú pháp
lang
: ngôn ngữ của bài viết, chúng tôi sử dụng ngôn ngữ để phát hiện ngôn ngữ
desc
: phần tóm tắt, nếu không thì một đoạn trích từ nội dung
author
: tác giả, nếu không thì tiêu đề của trang chủ của liên kết nguồn
pubDate
: ngày xuất bản của bài báo, hoặc bây giờ
topic
: chủ đề mà bài viết này thuộc về
tags
: các từ khóa có liên quan cho một bài báo, chúng tôi sử dụng lib trích xuất kw nhanh nhất, đó là cái cào , các lựa chọn thay thế được xem xét là pyate (kết hợp), xếp hạng văn bản và cụm từ máy
imageTitle
: văn bản thay thế cho hình ảnh
imageOrigin
: nếu phân tích cú pháp nguồn (đối với hình ảnh chúng tôi sử dụng cô gái ) không tìm thấy hình ảnh, chúng tôi truy vấn các công cụ tìm kiếm để tìm hình ảnh có liên quan, vì vậy imageOrigin trỏ đến trang gốc đã lưu trữ hình ảnh, nếu không thì nó bằng với url nguồn.
imageUrl
: liên kết thực tế đến hình ảnh. Chúng tôi sử dụng kiểm tra bộ lọc nở cho các hình ảnh trùng lặp vì chúng tôi không thích các hình ảnh trùng lặp.
icon
: favicon của liên kết nguồn
Sau khi chúng tôi đã xử lý một từ khóa, chúng tôi lưu các bài viết và nguồn cấp dữ liệu đã tìm thấy của nó vào bộ nhớ. Chúng sẽ được sử dụng bởi publsher.
Cạo xảy ra liên tục, đó là một con quỷ. Mã giả của vòng lặp chính trông như thế này và nó được cấu hình mỗi trang web:
đồng bộ hóa proxy mãi mãi
đối với mỗi chủ đề được sắp xếp theo số lượng (bài báo) chưa được xuất bản (thấp đến cao), hãy làm như sau
nếu khoảng thời gian tối thiểu từ công việc cuối cùng trôi qua, hãy chạy một công việc phân tích cú pháp cho chủ đề. Khoảng thời gian này làm tăng số lượng bài viết chưa xuất bản mà chúng tôi có cho một chủ đề và luôn bằng 0 nếu chúng tôi không có bất kỳ bài viết chưa xuất bản nào.
Làm tương tự nhưng đối với nguồn cấp dữ liệu (mà chúng tôi đã thu thập từ các nguồn, nếu chúng tôi có)
Nếu trang web nhằm mục đích tạo chủ đề mới, hãy tạo một chủ đề. (Điều này chỉ có ý nghĩa nếu bạn không quyết định danh sách chủ đề khi tạo trang web.)
Chọn một bài báo từ những bài đã xuất bản và gửi một tweet (chúng tôi gửi 3 tweet mỗi ngày, sử dụng python-twitter)
Chọn một bài báo từ những bài đã xuất bản và cập nhật trang facebook (chúng tôi cập nhật 1 lần mỗi ngày, sử dụng mặt nhăn nhó)
(Chúng tôi cũng đã kết nối với reddit, nhưng reddit không cho phép đăng chéo, vì vậy đó là một nỗ lực lãng phí.)
Khi chúng tôi có nội dung để xuất bản, chúng tôi phải quyết định xuất bản nội dung gì và tần suất xuất bản. Tôi không nghĩ ra bất kỳ mánh khóe nào ở đây, bởi vì như tôi đã đề cập trước đó, việc chọn hiển thị cái gì là phụ thuộc vào người dùng. Vì vậy, chúng tôi chỉ xuất bản từ mới nhất đến cũ nhất , với lý do rằng thứ gì đó mà chúng tôi thu thập gần đây có liên quan hơn, đó là LIFO xếp hàng. Xuất bản mặc dù khá phụ thuộc vào các công cụ python, nhưng lại xảy ra trong nim, bởi vì nó chạy liền kề với máy chủ, cũng nằm trong nim.
Việc xuất bản diễn ra liên tục và giống như việc cạo có những khoảng thời gian nhàn rỗi, việc xuất bản cũng vậy, ngược lại với việc cạo. Khi chúng tôi cạo, chúng tôi chậm lại khi chúng tôi có một đủ dài bộ đệm của các bài báo chưa xuất bản, với việc xuất bản, chúng tôi làm chậm lại khi bộ đệm của chúng tôi bắt đầu thu hẹp quá nhiều. Bằng cách này, cùng với việc cạo, phải luôn có một số nội dung được công bố thỉnh thoảng trong tương lai. Logic xuất bản thực tế:
Tìm nạp một loạt bài báo chưa xuất bản từ bộ đệm. (Chúng tôi chọn xuất bản 3 bài báo mới mỗi lần chạy.)
Kiểm tra xem chúng có trùng lặp không. Kiểm tra trùng lặp được thực hiện thông qua băm nhạy cảm cục bộ, tận dụng nim lib, minhash . LSH sử dụng khá nhiều CPU (bạn biết đấy...băm) và yêu cầu luồng riêng của nó (có một số tác vụ khác mà chúng tôi xử lý yêu cầu luồng riêng của chúng).
Kết xuất trang: điều này là không bắt buộc, vì máy chủ xử lý các truy vấn nhanh chóng, nhưng kết xuất ở đây là một hình thức lưu trước vào bộ nhớ đệm.
xử lý trang. Vì chúng tôi đang làm việc với một trang web nên chúng tôi phải chọn số lượng bài viết sẽ hiển thị trên mỗi trang và tăng số trang khi chúng tôi xuất bản nhiều bài viết hơn. Chúng tôi chọn nhóm các bài viết trong khoảng 10 trang. Trang mới nhất luôn có ít hơn 10 bài viết.
Lưu trạng thái của các bài báo đã xuất bản, điều này có nghĩa là chuyển các bài báo từ trạng thái "chưa xuất bản" sang "đã xuất bản" và cơ sở dữ liệu của LSH.
Sau khi xuất bản các bài báo mới, chúng tôi phải xóa bộ đệm cũ. Chúng tôi phải xóa trang chủ, trang chủ đề, sơ đồ trang web và nguồn cấp dữ liệu rss.
Chúng tôi thiết lập các công việc để vừa cạo nội dung xuất bản, tất cả những gì còn lại là phục vụ nội dung đó.
Sau khi có đã thử khác nhau trang web may chủ, vì các lỗi khác nhau mà tôi đã giải quyết bọ cạp.
Chúng tôi sử dụng nim triển khai khớp mẫu để khớp với một bộ ảnh chụp regex. Đây là biểu thức chính quy hoàn toàn không phải là RESTful:
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
hiển thị tất cả các nút có thể có mà một đường dẫn có thể có. Sau đó, định tuyến của chúng tôi trông giống như:
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...
Nó không đẹp, nhưng việc không phụ thuộc vào bất kỳ bộ định tuyến cụ thể nào cho phép tôi trao đổi máy chủ web cấp dưới mà không gặp nhiều phiền phức khi thử nghiệm. Là nó hiệu suất? Không rõ! Chưa thực hiện bất kỳ điểm chuẩn nào so sánh nó với thứ gì khác. Tuy nhiên, mùi của regex có thể có lỗi và thực tế là thứ tự của các trường hợp có vấn đề.
Đợi tí...
Có rất nhiều thứ mà chúng tôi thực hiện trên mỗi yêu cầu trước khi định tuyến trang thực tế:
ban đầu chúng tôi thiết lập mã dọn dẹp (vớidefer:
) cái mà Nên đảm bảo không có rò rỉ xảy ra.
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)
Chúng tôi kiểm tra xem chuỗi có được khởi tạo hay không:
initThread()
Điều này thực sự chỉ nên chạy một lần (nó đặt một bool toàn cầu sau khi khởi tạo để kiểm tra) và có thể được thực hiện bên ngoài trình xử lý yêu cầu. Nhưng những gì thực sự được khởi tạo? Vâng ... khá nhiều thứ! Về cơ bản, chúng tôi (ab) sử dụng các hằng số toàn cầu yêu cầu khởi tạo, ngoài ra một số trong số này không thực sự liên quan đến luồng, vì chúng khởi tạo bộ nhớ trên heap và được chia sẻ trên luồng
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
Sau đó, chúng tôi phân tích các tham số
var
relpath = ctx.rawPath
page: string
rqlocked: bool
relpath.removeSuffix('/')
debug "handling: {relpath:.120}"
handleParams()
Chúng ta sử dụng tham số để làm gì? CácParamKey
loại enum mô tả nó:
type
ParamKey = enum
none,
q, p, # sonic
c, # cache
d, # delete
t, # translations
u # imgUrls
chúng tôi làm bộ nhớ đệm siêu nhỏ cho các yêu cầu để mọi yêu cầu được lưu vào bộ đệm theo bộ(path, query, accetEncoding)
, mã hóa là bắt buộc vì chúng tôi có thể phục vụ cả hai nội dung (không) nén. Một bối cảnh yêu cầu trông như thế này:
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
Cáckey
trường được sử dụng để tìm nạp trang (nội dung) được lưu trong bộ nhớ cache chính xác từpageCache
. Khóa là cần thiết để đảm bảo rằng nhiều yêu cầu xảy ra cùng lúc không trùng lặp các công việc hiển thị (nếu một yêu cầu khác đang tạo trang, hãy đợi cho đến khi yêu cầu đó kết thúc). mọi cơ sởHttpRequestRef
từ chronos httpserver được lưu trữ trongrq
đồng ruộng. Cácparams
đã được phân tích cú pháp từ trước đóhandleParams
.
Chúng tôi hỗ trợ xóa nội dung thông quad
param, cho phép chúng tôi nuke các bài báo (trong trường hợp quá trình lọc không thành công, nhưng nó không bao giờ được sử dụng trong thực tế, chỉ để gỡ lỗi) với một yêu cầu nhận http đơn giản. Ai cần các phương thức http khác? Không phải tôi.
Chúng tôi cũng hỗ trợ xóa bộ nhớ cache. Chúng ta có thể xóa trang,c=0
hoặc tất cả các trangc=1
. Điều khó chịu là chúng tôi phải kiểm tra xem đường dẫn có phải là một bài viết, một trang, một hình ảnh hay một nội dung hay không và xóa cấu trúc bộ đệm thích hợp. Có một số trùng lặp logic rõ ràng ở đây với bộ định tuyến, nhưng vì điều này được thực hiện trước khi định tuyến nên nó phải là đặc biệt, chỉ xử lý các trường hợp liên quan đến xóa bộ đệm. Nó được thực hiện trước khi định tuyến vì bộ đệm cũng được phục vụ mà không cần định tuyến, vì nếu yêu cầu đã được tạo, chúng tôi chỉ có thể trả lời với nội dung được lưu trữ trongrespBody
trường (vàrespHeaders
, respCode
).
Sau khi xử lý các hoạt động của bộ đệm, chúng tôi phân tích cú pháp đường dẫn.
Sau đó, có một vụ cướp khác đang diễn ra:
if handleTranslation():
return
Tại sao điều này cũng được thực hiện trước khi định tuyến? Theo mặc định, chúng tôi phục vụ các trang được dịch một phần. Chúng tôi kém :( và các bản dịch dựa trên các dịch vụ miễn phí, nhưng chúng tôi không thể chịu được thời gian tải quá lâu, vì vậy chúng tôi chạy bản dịch bị trì hoãn trong khi chúng tôi phục vụ trang được dịch chỉ với các đoạn trích đã được lưu trong bộ nhớ cache trong cơ sở dữ liệu bản dịch của chúng tôi.
Tại thời điểm này, có định tuyến, được bao bọc trong một ngoại lệ sao cho nếu phục vụ đúng trang không thành công, chúng tôi sẽ đưa ra một503
. phát hành một503
ngụ ý rằng chúng tôi đã cố định tuyến một url hợp lệ nhưng chúng tôi không thể tạo trang. Đối với các url không hợp lệ, chúng tôi đưa ra một301
chuyển hướng ngụ ý rằng url không hợp lệ. Chúng tôi phục vụ 11 loại url khác nhau:
trang chủ: lấy các bài viết từ các chủ đề gần đây nhất, giả ngẫu nhiên, chúng tôi không thực hiện bất kỳ phân loại nào dựa trên mức độ phổ biến.
tài sản chung (theo/assets/
đường dẫn): được ánh xạ trực tiếp tới một thư mục chuyên dụng
hình ảnh chung (dưới/i/
pah): chúng tôi ủy quyền các hình ảnh bên ngoài để tạo ra các kích thước phù hợp với trang web phản hồi của chúng tôi, khi không có hình ảnh thì pixel trong suốt hoặc biểu tượng hình ảnh sẽ được cung cấp làm mặc định.
tệp robot.txt
sơ đồ trang web (đối với trang chủ và chủ đề và trang): Trang chủ lưu trữ sitemapindex trỏ đến tất cả các chủ đề sơ đồ trang web, sơ đồ trang web chủ đề trỏ đến tất cả các trang của chủ đề, các trang sơ đồ trang web trỏ đến tất cả các bài viết của trang.
bảng kê khai pwa: bảng kê khai pwa sẽ cho phép trang web được cài đặt dưới dạng pwa (nhưng thẳng thắn mà nói thì chưa kiểm tra điều này)
tìm kiếm: Các đòn bẩy tìm kiếm âm thanh với pysonic ràng buộc.
đề xuất: Các đề xuất cũng được xử lý thông qua các thư viện âm thanh. Nhưng họ yêu cầu
nguồn cấp dữ liệu: Giống như sơ đồ trang web, chúng tôi có các nguồn cấp dữ liệu khác nhau cho trang chủ và cho các chủ đề khác nhau, mặc dù không có nguồn cấp dữ liệu nào cho các trang đơn lẻ vì những lý do rõ ràng.
trang chủ đề: Trang dành riêng cho từng chủ đề (ví dụ: có đường dẫndomain.com/my-topic/
) Kéo các bài báo mới nhất được xuất bản cho chủ đề, thuộc về một chưa xong trang.
trang bài viết: Trang bài viết hiển thị tiêu đề bài viết, mô tả, liên kết nguồn, thẻ, thời gian xuất bản (ở chân trang) và ở dưới cùng, chúng tôi kéo 3 bài viết liên quan. Các bài viết liên quan được tìm nạp bằng truy vấn tìm kiếm trên tiêu đề hoặc thẻ bài viết.
Kết xuất trang được xử lý từ phía nim bằng cách sử dụng karax
Trang web bao gồm một thanh cố định trên cùng hiển thị:
url trang chủ, thông qua hình ảnh logo svg.
nút chủ đề sáng/tối
url hiện tại sử dụng đường dẫn hiện tại mẩu vụn dưới dạng văn bản liên kết
~10 url chủ đề gần đây nhất
Thanh tìm kiếm (có nút tìm kiếm), nơi các đề xuất bật lên khi nhập
nút ngôn ngữ, khi được nhấp vào, danh sách ngôn ngữ sẽ nổi lên
Vì đây là thiết kế đáp ứng nên khi chế độ xem nhỏ hơn, thanh trên cùng chỉ giữ hộp tìm kiếm trong khi phần còn lại xuất hiện trong thanh bên có thể chuyển đổi.
Phần chân trang, giữ liên kết cho sơ đồ trang web, rss, xã hội, pháp lý
Quảng cáo tại các vị trí khác nhau được hỗ trợ
Đây là một chức năng ví dụ, hiển thị những gì chúng tôi làm khi một bài đăng mới được xuất bản để cập nhật nguồn cấp dữ liệu:
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
Đây là cốt lõi của việc thêm url vào sơ đồ trang web:
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()
Chúng tôi không sử dụng công cụ tạo mẫu vì hầu hết quá trình kết xuất được thực hiện bằng karax, nhưng đối với các trang như ToS, chúng tôi sử dụng các mẫu tệp, trong đó chúng tôi chỉ cần thay thế một loạt các biến, chẳng hạn như mộtenvsubst
yêu cầu.
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 == ""))
Khi chúng tôi hiển thị các trang như trang chủ/chủ đề và các trang được đánh số, chúng tôi phải hiển thị danh sách các bài viết, chức năng này được gọi trong một vòng lặp cho số lượng bài viết chúng tôi muốn hiển thị:
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))
Lưu ý làm thế nào trong một số dòng "quảng cáo" leo trong X)
Ở thanh trên cùng, chúng tôi hiển thị danh sách chủ đề, đây là nội dung in ra:
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
Có một số lớp thiết kế vật liệu được mã hóa cứng có mùi ở đây. Thành thật mà nói, các thành phần thiết kế material design của google rất tệ.
Chân trang bài đăng xuất hiện ở dưới cùng bên phải của trang bài viết (tính bằng ltr) và nó thực sự chỉ in ngày xuất bản.
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")
Khi xây dựng các mục bài viết, chúng tôi có thể cần các đoạn trích nếu không có bản tóm tắt.
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 làparseHtml
làm gì ở đây? Đó là trường hợp chúng tôi cho phép html bên trong nội dung bài viết (nhưng chỉ một số thẻ), đây là một tùy chọn từ trafilatura mô-đun python, chúng tôi tiếp tục bật vì nó có thể ảnh hưởng đến định dạng bài viết. Chúng ta cũng phải cẩn thận về việc chunking các chuỗi utf-8...
Nhiệm vụ cuối cùng sau khi chế tạo karaxVNode
tree là để kết xuất các byte. Cây nếu có tiền tố là tiêu đề html và được thu nhỏ tùy ý.
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)}"
Việc thu nhỏ được xử lý bởi rút gọn-html mà chúng tôi đã ràng buộc bằng cách sử dụng c2nim , tệp ràng buộc chứa:
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)
Nhưng để xây dựng, chúng tôi phải cung cấp các thư viện tĩnh, thêm dòng này vàonim.cfg
--passL:"$PROJECT_DIR/src/rust/target/release/libminify_html_c.a"
Ý tôi là... đó là con đường của tôi khi tôi xây dựng thư viện rút gọn btw không thực sự có chức năng c bên ngoài mà nim có thể sử dụng, vì vậy chúng tôi phải tự viết nó.
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;
}
Các ràng buộc Python cho nim phải tự do loại bỏ các đối tượng python. Vấn đề là chúng ta phải kiểm soát khi nim thực hiện GC. Thư viện nhanh nhẹn giả định rằng GIL luôn bị khóa (nó khóa nó ngay từ đầu), để có thể tự do gọi tới python bất cứ khi nào. Nhưng chúng tôi mở khóa gil để cho phép một threadpool python chạy mã trong khi nim chạy các nội dung khác. Nếu python GIL luôn bị khóa bởi nim thì threadpool sẽ không hoạt động trong hầu hết thời gian.
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
Điều này cho phép cách thực thi mã python giữ GIL, nhưng chỉ trên luồng hiện tại. Việc triển khai để lấy/giải phóng GIL trên các luồng nim khác nhau yêu cầu gọi các hàm abi python C khác nhau, vì GIL là một mutex. Sau đó, chúng tôi gọi python bằng mẫu này:
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()
Chúng tôi sử dụng tính năng bảo vệ và khóa nim để đảm bảo các loại python chỉ được truy cập khi GIL được giữ. Tuy nhiên, điều này yêu cầu xác định pyobjects với bảo vệ:
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`
Vì vậy, tôi có thể làm:
pyObjPtr(myVar, pyimport("datetime").datetime))
Và bất cứ khi nào tôi gọimyVar
chứa đối tượng datetime, tôi phải bọc nó như thế này:
withPyLock():
myVar.fromunixtimestamp(1)
Bây giờ chúng ta có thể khóa gil khi chúng ta phải chạy GC, ghi đè nhanh lênPyObject
hàm hủy với cái này:
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
Khóa chúng tôi sử dụng bên trong hàm hủy không phải là khóaAsyncLock
vì điều đó sẽ quá đắt, và chúng tôi đừng luôn khóa, vì điều đó sẽ gây ra tình trạng chết hàng! Nếu chúng tôi không thể khóa gil, chúng tôi sẽ trì hoãn việc thu thập và giữ con trỏ python thô cho đến khi chúng tôi có thể xóa nó. Thành thật mà nói, tôi không biết liệu điều này có gây ra các dạng vấn đề khác hay không, nhưng có vẻ như nó hoạt động đủ tốt.
Chúng tôi có một mô-đun nim được gọi làpyutils.nim
ví dụ như một loạt các công cụ nim<>python:
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)
Cái này được sử dụng khá nhiều:
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)
Cái này được sử dụng khi chúng tôi đã lên lịch cho một công việc python và chúng tôi muốn đợi nó hoàn thành một cách không đồng bộ:
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.")
Liên kết async python thích hợp sẽ yêu cầu hoàn thành một tương lai không đồng bộ nim từ python khi kết thúc công việc theo lịch trình của python, điều mà chúng tôi không làm vì chúng tôi chưa xem xét đủ sâu để xử lý các đối tượng nim từ python.
Chúng tôi hỗ trợ google amp, vì vậy chúng tôi tạo ra các trang amp tương thích với amp. Chúng tôi không nhắm đến hỗ trợ 1:1. Trên thực tế, chúng tôi sử dụng tất cả javascript mà chúng tôi có và chỉ phục vụ html/css. Ngay cả khi đó, chúng tôi phải cẩn thận để không thêm các thuộc tính tùy chỉnh vào thẻ html hoặc chỉ các thẻ html tùy chỉnh, amp như vậy là không tốt... Đối với chuyển đổi trang amp tự động, chúng tôi xử lýhead
vàbody
gắn thẻ khác nhau.
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
Tất cả các kiểu được hợp nhất thành một tập lệnh nội tuyến duy nhất, những gì được giữ lại làlink
các thẻ không phải là style/jscript, như lang. Thẻ tập lệnh choldljson
, meta
thẻ. Verbatim xử lý các nút được nghĩa đen , chúng ta phải chuyển đổi chúng thànhXmlNode
(có nghĩa là phân tích cú pháp) và xử lý chính xác. Phần thân quy trình cũng tương tự, chúng tôi giữ một số thẻ, xóa các thẻ khác, đổi tên các thẻ khác:
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
Cácform
thẻ được thay thế bằngamp-form
, amp có nhiều thẻ này...
Chúng tôi phải đảm bảo rằng các kiểu nội tuyến nằm trong độ dài chính xác:
styleStr = styleStr
# .join("\n")
# NOTE: the replacement should be ordered from most frequent to rarest
# # remove troublesome animations
.replace(pre"""\s*?@(\-[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}")
Thế hệ bộ khuếch đại của chúng tôi không bao gồm đầy đủ thông số kỹ thuật của bộ khuếch đại, nhưng nó phù hợp với nội dung của chúng tôi (thông qua bản dùng thử và lỗi :S).
Bất cứ khi nào một bài báo được xuất bản, nó sẽ được nhập vào cơ sở dữ liệu âm thanh, cơ sở dữ liệu âm thanh xử lý "bộ sưu tập", "nhóm" và "đối tượng"; Chúng tôi định nghĩa một bộ sưu tập là một trang web, vì vậy mỗi trang web muốn triển khai trình tổng hợp nội dung đều có bộ sưu tập của riêng mình. chúng tôi không sử dụngbuckets
, mặc dù chúng tôi có thể coi mỗi chủ đề là một nhóm, điều này sẽ thu hẹp tìm kiếm quá nhiều, vì vậy mọi trang web chỉ có một nhóm "mặc định" và mỗi đối tượng của nhóm là một bài viết (có thể thuộc các chủ đề khác nhau).
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
Khi đẩy nội dung vào âm thanh, chúng tôi phải chia dữ liệu thành nhiều phần, độ dài tối đa được biết khi kết nối. Đôi khi, việc nhập dữ liệu dường như có lỗi vì dữ liệu dường như không thể xử lý một số ký tự cụ thể. Trong trường hợp máy chủ âm thanh bị hỏng bằng cách nào đó, chúng tôi cũng có chức năng nhập lại tất cả nội dung:
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."
Bản dịch là một câu chuyện khá lộn xộn. Tôi đang triển khai trình bao bọc dịch lần thứ 4 (!), sau khi đã viết bằng php, hãy đi và Julia , điều này cũng được viết bằng nim. Các biến thể php/go ngày nay hơi bị hỏng, trong khi biến thể julia được sử dụng tích cực cho blog này. Tuy nhiên, để đạt được độ trễ thấp cho máy chủ web, cách triển khai dịch trong julia không phù hợp lắm với dịch vụ thời gian thực (nó dịch các tệp tĩnh) và dù sao thì việc thêm julia làm phụ thuộc khi chúng ta đã có python sẽ là một vấn đề lớn. yêu cầu.
Vì vậy, tôi đã phải triển khai một mô-đun dịch thuật mới trong nim. Trên thực tế, mô-đun dịch nim ban đầu trông rất giống với triển khai julia, nơi chúng tôi đang dịch các tệp tĩnh[1] . Sau đó, khi máy chủ web bắt đầu hình thành, tôi chuyển nó để dịch các nút karax theo yêu cầu. Điều này cho phép dịch từng trang web đúng lúc theo yêu cầu.
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)
Trên đây là vòng lặp chínhtranslateIter
:
getTforms
ánh xạ các chức năng tới các thẻ html, cho phép thực hiện các thay đổi theo từng trường hợp.
rewriteUrl
chèn đường dẫn lang (.e.g/en/
) trong đường dẫn url.
translateVbtm
xử lý các nút nguyên văn yêu cầu phân tích cú pháp.
Bản dịch được áp dụng cho tất cả các nút văn bản và choalt
vàtitle
thuộc tính.
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
Bởi vì chúng tôi phải dịch riêng từng nút văn bản (nếu không, chúng tôi không thể kết xuất lại html) nên mỗi bản dịch nút là một công việc riêng biệt. Vì các công việc có thể truy vấn các dịch vụ dịch thuật của mạng nên chúng phải được thực hiện không đồng bộ. Chúng tôi thực hiện tách và hợp nhất các truy vấn dịch thuật để dự phòng các lệnh gọi api, nhưng nội bộ của công cụ dịch thuật không quan trọng để biết. Điều duy nhất cần lưu ý là ban đầu tôi đang sử dụng một trình bao bọc trăn (mà tôi vẫn sử dụng để dịch nội dung cóp nhặt) vì trình bao bọc tự quản lý cho apis bên ngoài là một điều khó khăn, nhưng sau đó chuyển sang dịch vụ dịch google và yandex tự gói trong nim, vì python trở thành một nút cổ chai đáng kể khi xử lý nhiều bản dịch đồng thời.
[1] | Trên thực tế, ban đầu trình tổng hợp nội dung được cho là chỉ tạo các tệp tĩnh chocaddy để phục vụ, nhưng vì số lượng trang cần tạo (là ma trận của n_lang(20) x amp(2) x trang), kết xuất chậm là tùy chọn tốt hơn. |
Các trang chủ đề và bài viết được theo dõi để biết số lần truy cập.
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
Chúng tôi sử dụng số lượt truy cập để dọn dẹp các trang có số lượng thấp theo định kỳ.
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)
Chúng tôi sử dụnglibmdbx
xuyên qua thư viện này . Có lẽ là quá mức cần thiết và việc sử dụng leveldb sẽ là đủ. Chúng tôi có một loạiLRUTrans
trong đó ý tưởng ban đầu là thiết lập cơ sở dữ liệu dưới dạng bộ đệm LRU, nhưng nó chậm hơn đáng kể. Việc thực hiện có thể được tìm thấy ở đây
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
Loại này được sử dụng cho bốn cơ sở dữ liệu riêng biệt:
bản dịch
bộ đệm trang
bộ đệm hình ảnh
số liệu thống kê
Loại cơ sở dữ liệu được triển khai với getters và setters sau đó thực hiện tự động khử/nén khi đọc/ghi. Vì lý do này, nó không nên được sử dụng cho hình ảnh...nhưng than ôi... Ngoài ra còn có một loạt các bộ đệm vi mô nhỏ:
vbtm: dành cho nội dung được phân tích cú pháp (nguyên văn)
tìm kiếm: cho các truy vấn tìm kiếm
nguồn cấp dữ liệu: cho nguồn cấp chủ đề VNodes
rxcache: dành cho regex, vì thời gian biên dịch các regex tĩnh chưa được chuẩn hóa (cũng bởi vì có nhiều thư viện regex trong nim)
Chúng được thực hiện như lru bộ đệm[2] , chính xác hơn là bộ đệm lru "bị khóa", trong đó mọi thao tác lấy và đặt được bao quanh một khóa (luồng). Các khóa này không thể gây ra tình trạng dừng với thời gian chạy không đồng bộ vì khóa được mua và giải phóng mà không có bất kỳ câu lệnh lợi nhuận nào, vì vậy chúng là nguyên tử theo nghĩa đó, tuy nhiên chúng vẫn hữu ích vì chúng tôi sử dụng các luồng cho các tác vụ khác nhau.
[2] | Tuy nhiên nim hầm có cách triển khai đơn giản hơn cho lru cache mà tôi đã sử dụng nếu được tìm thấy sớm hơn. |
Một vài tác vụ mà chúng tôi sử dụng đang ngốn CPU, vì vậy chúng tôi sử dụng một luồng khác cho chúng:
lsh: băm nhạy cảm cục bộ thực hiện rất nhiều tính toán
hình ảnh: Thay đổi kích thước hình ảnh đòi hỏi phải giải mã/mã hóa hình ảnh nên rất tốn kém
Hai luồng nữa được sử dụng để cập nhật danh sách tệp nội dung và quảng cáo, mặc dù không đói cpu nhưng cần có một luồng để tránh tình trạng dừng do trình xem tệp gây ra.
Chúng tôi cũng có các tác vụ chạy dài không đồng bộ cho:
bản dịch
yêu cầu http
Các công việc yêu cầu Lsh, hình ảnh, dịch thuật và http được xử lý bằng cách sử dụng thiết lập của nhà sản xuất/người tiêu dùng. Ngoại trừ việc chúng tôi không sử dụng các kênh, vì các kênh chặn và chúng tôi không có triển khai không đồng bộ của chúng cũng là luồng an toàn. Chúng tôi đã sử dụng triển khai không đồng bộ của cái này[1] . Và một bảng không đồng bộ, giống như một xe buýt sự kiện
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
Máy chủ nim cũng xử lý ba tác vụ không đồng bộ:
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
Nhiệm vụ giám sát việc sử dụng mem là rất tốt, để tránh các sự cố OOM giữa quy trình được chứa trong vùng chứa và docker, bởi vì docker (hoặc kernel) không tắt quy trình ngay lập tức và trong khoảng thời gian này, máy chủ có thể không phản hồi, vì vậy tốt hơn là khởi động lại thủ công ngay lập tức.
[1] | mặc dù gói một kênh đơn giản trong các thói quen không đồng bộ có lẽ tốt hơn ... than ôi |
chúng tôi tận dụng luồng hình ảnh để thay đổi kích thước và lưu trữ hình ảnh cục bộ. Các ràng buộc rất đơn giản, nhưng quá trình này có một chút liên quan. VớigetImg
chúng tôi tìm nạp dữ liệu hình ảnh từ url từ xa:
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:
""
Sau đó, chúng ta phải thêm nó vào bối cảnh luồng hình ảnh:
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
Nếu không thể thêm hình ảnh, điều đó có nghĩa là luồng hình ảnh không nhận dạng được dữ liệu là hình ảnh hợp lệ. Sau khi chúng tôi đã gửi dữ liệu, chúng tôi phải gửi một truy vấn đến ngữ cảnh, sau đó đọc phản hồi và nhận đầu ra:
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)
Chúng tôi nhận được loại mime từ phản hồi, loại này sẽ được chuyển tiếp trong phản hồi của máy chủ web. Từ phía máy chủ, quá trình dịch từ đường dẫn url sang luồng hình ảnh được xử lý như sau:
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 hình ảnh được gửi dưới dạng tham số, ở dạng nén zstd. Việc nén rút ngắn các url (hầu hết thời gian). Đây cũng là cách tôi tìm thấy một lỗi trong google chrome, lỗi không thể xử lý các url trong đó truy vấn có dữ liệu nén được mã hóa url. Thay vào đó, Firefox vẫn ổn.
Chúng tôi thêm vào mỗi trang web tập lệnh 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
Và đối với các trang đã dịch:
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
Tương tự như ldjson, chúng tôi cũng cung cấp các thẻ 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[])
Các mẫu và macro Nim có ích khi xử lý tất cả mã nặng soạn sẵn này.
Có một tác vụ khác, xử lý tất cả các yêu cầu http (để tìm nạp hình ảnh, tập lệnh, v.v.) từ phía máy chủ web. Chúng tôi sử dụng 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)
tôi đã phải thêm hỗ trợ cho proxy https và vớ5 đến httpclient để có thể sử dụng các bản dịch một cách hiệu quả.
Bạn có thể đã nhận thấy các biến được viết hoa trong toàn bộ mã. Tất cả đều là các biến cấu hình, được xác định trong một tệp, có thể được tùy chỉnh cho mỗi trang 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
# ...
Có rất nhiều thứ mà tôi chưa đề cập đến, vì ma quỷ nằm trong chi tiết... tuy nhiên đây là chuyến tham quan sơ bộ toàn bộ cơ sở mã có giá trị:
~12k dòng nim
~400 dòng js
~1000 dòng scss
~3500 dòng trăn
74 lines of rust (for bindings :P)
Tôi sẽ làm gì khác đi?
Có thể viết lại toàn bộ mọi thứ một cách rỉ sét, nim hiện không xử lý an toàn bộ nhớ tốt và lượng thời gian tôi phải dựa vào gdb để khắc phục sự cố là quá nhiều và tôi thậm chí còn chưa khắc phục được tất cả chúng. Đó là một vấn đề lớn khi một nửa hệ sinh thái dựa vào GC và nửa còn lại dựa vào ORC (hoặc thậm chí không phải orc và chỉ ARC). Trộn không đồng bộ và các chuỗi cũng gây khó khăn và các dấu vết ngăn xếp không đồng bộ là một cơn ác mộng, (mặc dù tôi không biết liệu rỉ sét có tốt hơn về mặt này không.)
Nhắm mục tiêu một PWA từ nhận đi. Dự án đã có một khởi đầu khá rắc rối. Ban đầu, nó được coi là các trang tĩnh được phục vụ bởi một máy chủ web, sau đó nó trở thành một máy chủ web. Tính tương tác xuất hiện như một suy nghĩ sau, vì vậy nó chỉ trở thành sự kết hợp giữa html được kết xuất cộng với js/css. Điều này khiến tôi quá lỏng lẻo với API, thứ xuất hiện mà không có bất kỳ cấu trúc nào (hoàn toàn BẤT NGỜ ). Trong phần viết lại, tôi sẽ sử dụng một khung giao diện người dùng chuẩn bị trước có hỗ trợ AMP đầy đủ hoặc solidjs.
Thêm nhiều trình phân tích cú pháp đặc biệt hơn cho các nền tảng phổ biến. Phân tích cú pháp bài viết thuần túy không hoạt động tốt (hoặc hoàn toàn không) khi các nền tảng phổ biến nhất hiện nay cung cấp rất ít nội dung và rất nhiều video và hình ảnh, do đó, việc tìm kiếm phải được nhắm mục tiêu nhiều hơn đến đa phương tiện thay vì chỉ văn bản, nếu đây không phải là được tính đến khi lập kế hoạch kiến trúc cạp, nội dung và thông tin mà APP sẽ cung cấp sẽ không cân bằng.