cdhigh / KindleEar

Aggregates RSS and web content(Calibre recipe), sends to Kindle, and includes an e-ink optimized online reader.
http://cdhigh.github.io/KindleEar/
MIT License
2.71k stars 631 forks source link

get_or_none() got an unexpected keyword argument 'user' #674

Closed Steven630 closed 3 months ago

Steven630 commented 4 months ago

用chrome插件做了个recipe,上传,高级设置手动立即推送。提示:Failed to execute recipe "주요뉴스 연합뉴스": get_or_none() got an unexpected keyword argument 'user'

另外这个扩展真的很好用,有三个小建议供大佬参考:

  1. 目录页面可选择获取不同section
  2. 目录页可获取文章发布时间,再设定时区/时间格式,方便筛选oldest articles
  3. 正文部分除了获取需要内容的规则,能否考虑增加remove rules。这样可以更快去除广告等内容。
cdhigh commented 4 months ago

是因为我在写扩展的过程中发现在扩展界面显示爬取规则比较麻烦,太复杂,小小的对话框显得特别乱,后来就翻BeautifulSoup的文档,发现使用CSS选择器比直接使用其字典参数格式简洁多了。 现在同时支持这两种格式,你如果更熟悉以前的方式,也可以使用。 我在代码注释中已经写明,注释里面的两行代码是等效的,可见CSS选择器的简洁:

#为一个二维列表,可以保存多个标签规则,每个规则都很灵活,只要是BeautifulSoup的合法规则即可(字典或CSS选择器字符串)
#每个顶层元素为一个html标签的查找规则列表,从父节点到子节点,依次往下一直到最后一个元素为止
#最后一个元素必须为链接,或其子节点有链接,则此链接为文章最终链接,链接的文本为文章标题
#比如:url_extract_rules = [[{'name': 'div', 'attrs': {'class': 'art', 'data': True}}, {'name': 'a'}],]
#或:url_extract_rules = [['div.art[data]', 'a'],]
url_extract_rules = []

python2的recipe一般需要稍微改动,如果很简单的话,可能不需要改动,根据你脚本的复杂程度决定。

“自动排除重复文章” 是支持的。

Steven630 commented 4 months ago

只有一个网页的recipe怎么写。现在的recipe几乎都是默认每个recipe都有好多篇文章的。如果知道只有一篇文章,不需要再做目录,应该怎么修改。内置Espresso的recipe就是只有一篇文章,base url直接就是内容本身,但还是多做了个The world in brief的目录。请教该怎么修改?

Steven630 commented 4 months ago

觉得Chrome扩展很好用,所以明明是RSS源,也用了扩展去生成规则。最后把import和Class后面括号的内容都改成BasicNewsRecipe,url extract rules删除,最后的feeds改成RSS链接。但是推送的结果是没有采用那些规则,整个页面都抓取了。是有什么地方没改对吗?是否可以加上为RSS源的文章生成规则的选项(直接跳过第一步目录抓取),这样以后也不用再改。

Steven630 commented 4 months ago

是我没表达清楚,现在RSS大部分都不是全文的,所以最后也是要打开正文再解析正文的结构。我写recipe的时候偷了个懒,想用扩展提取出正文的元素。目录页是随便找了个其他的(原来是BBC China的RSS,就去找了对应的网页China版块,最近文章的列表都是一样的,我只是想要去第二第三步生成正文提取规则罢了)。最后生成了py文件,所以才去把url提取规则删除什么的。我以为extract rules和remove rules是通用的。能不能考虑为Basic Recipe也增加这两个功能呢?毕竟正文都是网页。扩展的话可以增加一个RSS的选项,由用户自己粘贴一篇文章的链接,直接进入第二步正文提取。最后生成的Recipe选择对应的类别,如果KE更新的时候能让基本recipe也有那两个功能,那这个扩展就能为RSS解析文章正文也发挥作用了。

cdhigh commented 4 months ago

如果是WebPageUrlNewsRecipe派生的子类,是的,但是如果还是传统的RSS,则受限与最旧文章时间。

提示no news feeds available不一定是时间的问题,可能是解释文章列表过程中的异常,建议在后台看看logs

Steven630 commented 4 months ago

是按照昨天你教的,把import和class改回了WebPageUrlNewsRecipe,在class内增加了函数,其他没有更改。

查看了日志,确实是no article found

Steven630 commented 4 months ago

还有个Economist recipe的问题要请教。内置的recipe文章不全,今天Calibre更新了recipe。看到作者在论坛上写的是use the GraphQL query to load the content。

用GAE试了新recipe,后台logging报错 "textPayload": "parse_index() failed: [Errno 30] Read-only file system: 'u7_iuj2x.html'"

不知道是不是新的recipe用了KE不支持的手段。

[https://github.com/kovidgoyal/calibre/blob/refs/heads/master/recipes/economist.recipe]()

cdhigh commented 4 months ago

你可以给它提一个pull request,它的做法有些不适当,随便在当前目录建立临时文件 如果你要修改可以修改里面的PersistentTemporaryFile(),修改为 pt = PersistentTemporaryFile('.html', dir=os.getenv('KE_TEMP_DIR')) 或简单点 pt = PersistentTemporaryFile('.html', dir='/tmp')

我看了一下,我会升级代码兼容它的recipe修改

cdhigh commented 4 months ago

关于nonews 的问题,如果你愿意,可以上传你的recipe,我来看看

Steven630 commented 4 months ago

你可以给它提一个pull request,它的做法有些不适当,随便在当前目录建立临时文件 如果你要修改可以修改里面的PersistentTemporaryFile(),修改为 pt = PersistentTemporaryFile('.html', dir=os.getenv('KE_TEMP_DIR')) 或简单点 pt = PersistentTemporaryFile('.html', dir='/tmp')

我看了一下,我会升级代码兼容它的recipe修改

好的,我改后试试,并期待下次升级。

Steven630 commented 4 months ago

关于nonews 的问题,如果你愿意,可以上传你的recipe,我来看看

这是recipe BBC China.txt

后缀改成txt了

cdhigh commented 4 months ago

修改函数:

def parse_feeds(self):
        BasicNewsRecipe.parse_feeds(self)

增加return

def parse_feeds(self):
        return BasicNewsRecipe.parse_feeds(self)
Steven630 commented 4 months ago

或简单点 pt = PersistentTemporaryFile('.html', dir='/tmp')

我看了一下,我会升级代码兼容它的recipe修改

改成了这个简单的版本,日志报错很多临时文件找不到。最开始还有个封面下载的错误,是permission denied,这个错误是旧版本recipe就有的,文件确实也没有封面

cdhigh commented 4 months ago

感觉是我的代码问题,好像对file:////形式的url分析成//了

cdhigh commented 4 months ago

代码已经升级

Steven630 commented 4 months ago

修改函数:

def parse_feeds(self):
      BasicNewsRecipe.parse_feeds(self)

增加return

def parse_feeds(self):
      return BasicNewsRecipe.parse_feeds(self)

谢谢!现在推送有文章了,不过正文还是没有处理的样子

日志是说extract rules失败,改用readability

Steven630 commented 4 months ago

又来请教了

Failed to execute recipe "컨텍스트 레터": can't subtract offset-naive and offset-aware datetimes"
"Failed to execute input plugin: All feeds are empty, aborting."
"There are no new feeds available."

这又是什么问题呢

cdhigh commented 4 months ago

这个是因为google数据库的处理和sql不一致, 将保存的没有时区信息的时间自动转换为包含时区0的时间。 既然这样,干脆内部就全部使用时区0的时间,代码已经更新。

Steven630 commented 4 months ago

重新部署后Economist还是不行,有很多这样的提示:

"The file '//tmp/gmr6w_ze.html' does not exist"
"Could not fetch link file:////tmp/ih_vfvs2.html : No content at URL 'file:////tmp/ih_vfvs2.html'"
"Failed to download article:How strong is India’s economy? from file:////tmp/7q1kgbbv.html"

封面还是失败 "Failed to download supplied masthead_url: [Errno 13] Permission denied: '/mastheadImage.gif'"

Steven630 commented 4 months ago

另外这个recipe有这样的错误提示: GET https://slownews.kr/wp-content/uploads/2023/11/230324_삼성전기_중국_텐진공장_점검_3-963x800.jpg failed: 'latin-1' codec can't encode characters in position 54-57: ordinal not in range(256)

cdhigh commented 4 months ago

一天只有9小时免费,如果你的rss不是几十个,建议你将worker 的机器调小。 为了避免有人内存不够,我默认调到B4,大部分人B2就够用了,如果只有5个RSS以内,B1就够用了。 B2价格是B1的两倍,B4价格是B2的两倍。 在shell里面打开worker.yaml,修改后只执行gae_deploy.sh即可。

这只是测试时消耗大,因为要经常唤醒,有时候甚至运行两个进程,费用再加倍,每次唤醒都至少运行15分钟,等正常使用后,每天只唤醒一两次,就不会超了。

前台有28小时,一般不会超。

cdhigh commented 4 months ago

可以在线编辑,在shell的上沿偏右有一个按钮“打开编辑器”,然后在左边打开菜单,就可以选择worker.yaml, 修改行:instance_class: B1,保存后再点击“打开终端”,再输入命令:kindleear/tools/gae_deploy.sh

你的情况使用B1足以,资源随便玩,订阅不多,还可以将idle_timeout改小,改成15分钟,甚至10分钟,这个时间决定进程一次运行多长时间,最大进程如果再改成1,就更省了。

instance_class: B1
basic_scaling:
  max_instances: 1
  idle_timeout: 15m

app_engine_apis: true
entrypoint: gunicorn -b :$PORT -w 1 --timeout 900 main:app
cdhigh commented 4 months ago

更好的方法是fork github仓库,然后在你的仓库修改,之后我的仓库有更新,你先同步到你的仓库,然后部署时将github仓库链接修改为你的路径即可。

rm -rf kindleear && \
git clone --depth 1 https://github.com/cdhigh/kindleear.git && \
chmod +x kindleear/tools/gae_deploy.sh && \
kindleear/tools/gae_deploy.sh
cdhigh commented 4 months ago

真不错。

其实KindleEar3还没有完成,所以我就一直没有发布正式版,你现在使用的是测试版,所以会有很多问题。 比如就还没有将抓取网页失败重试的功能移植过来(老版本的KindleEar是有的)。 代码已经更新,增加失败重试功能,现在抓取Economist应该可以了。

额外的,部署脚本也增加了几个参数,方便根据自己情况修改申请的资源类型,比如,下面的配置就很适合你:

kindleear/tools/gae_deploy.sh B1,1,t2,15m

详细可以参考 文档

cdhigh commented 4 months ago

worker.yaml里面有两个timeout,现在我都设置为一样了。 我对gunicorn的timeout作用是确定的,是从路由函数开始执行到返回数据包之间的最长时间,比如如果设置为15m,则15分钟之内必须处理完,否则gunicorn会直接将对应线程终止。 GAE的idle_timeout,是经过timeout时间GAE自动将实例关停,但是从哪里开始计算时间,我没有确切答案,我找了很多资料,官方的和网络上的,都一直没有得到确切答案,究竟是从处理函数开始执行就计算,还是从处理函数返回之后计算。

我本来想做一个实验,但是我基本已经离开GAE而转战VPS了,所以也一直没有去实验。

同样执行时间,使用B2会比B1多消耗一倍的实例小时数。

B1/B2尽管处理器运算能力上有差异,生成电子书会有时间差别,但是在我们的应用中,耗费最大的是网络IO等待时间,所以时间消耗应该差异不大。 对我们影响更大的是内存容量,如果不超,B1应该比B2要更合适,但是B1的内存实在太少了,所以应该支持不了多少RSS,具体我没有去测算。

Steven630 commented 4 months ago

B1的内存小,那是不是把不同RSS推送时间错开就能避免内存不够?

韩语recipe推送有个小问题,所有图片都是空白(至少在Kindle上是空白)。一开始以为是图片没有抓取成功,后来发现是有图片的,只不过全是空白。

cdhigh commented 4 months ago

扩展程序已经升级1.4.0,增加一个KindleEar网页预处理内容,使用你部署的KindleEar网站预先将javascript去掉,然后再使用扩展程序生成爬虫脚本,这样成功率会更高一些。

需要部署新版本的KindleEar配合使用,并且在扩展程序选项里面打开此功能。

使用此功能,之前你希望的 每一步都可以回头 的功能也可以实现,因为每一步都打开一个新的网页。

不过因为网页的复杂性,有一些网页去掉javascript就会出现显示异常的情况,所以不适合所有网页,需要的时候切换会“没有预处理”选项即可。

Steven630 commented 4 months ago

ERROR in app: Exception on /ext/removejs [GET]

Traceback (most recent call last): File "/layers/google.python.pip/pip/lib/python3.8/site-packages/flask/app.py", line 1473, in wsgi_app response = self.full_dispatch_request() File "/layers/google.python.pip/pip/lib/python3.8/site-packages/flask/app.py", line 882, in full_dispatch_request rv = self.handle_user_exception(e) File "/layers/google.python.pip/pip/lib/python3.8/site-packages/flask/app.py", line 880, in full_dispatch_request rv = self.dispatch_request() File "/layers/google.python.pip/pip/lib/python3.8/site-packages/flask/app.py", line 865, in dispatch_request return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args) # type: ignore[no-any-return] File "/workspace/application/view/extension.py", line 40, in ExtRemoveJsRoute return SHARE_INFO_TPL.format(title='Get url failed', info=f'Get url failed: {url}') NameError: name 'SHARE_INFO_TPL' is not defined

cdhigh commented 4 months ago

我尝试了BBC china和reuters,这两个网站还是使用专业的RSS生成工具再订阅好了。 BBC 高度依赖javascript生成网页,其网页源代码内的可视网页内容都在json数据里面,使用javascript渲染到DOM树,简单的爬取是不行的,除非分析其javascript然后手动在json数据中提取。

reuters更狠,使用datadome反爬技术,这个是一个很强的技术组合,浏览器正常访问没问题,但是要使用程序自动爬取网页就不行,即使使用无头浏览器都不行。 要绕过它需要不小的技术实力。

Steven630 commented 4 months ago

BBC的如果直接把RSS链接放到自定义订阅里,是可以自动抓取到内容的,但是文章前面有网站板块等特别多不需要的内容,所以一直在尝试自己写抓取规则,不过还没有成功过。路透社的还没有试过。要大佬都觉得不太可能实现,我干脆放弃吧。这两个前两年在KE旧版本都是可以抓取的。这两年网站是大改版了,没想到这么麻烦。

cdhigh commented 4 months ago

是的,我看了BBC中文网,其返回的页面内容是正常的,我之前看的是BBC的China板块,网站不一样。 所以BBC中文网是可以使用扩展程序生成规则的,我刚测试了,没有使用其RSS链接,直接在其首页上执行脚本,生成的脚本能正常提取文章列表和正文。

不过这么来说,这个预处理功能也不是毫无作用,至少知道如果预处理不行,说明KindleEar不支持直接爬取这个网站,需要写自定义脚本(类似内置的The economist哪种复杂操作)

cdhigh commented 4 months ago

刚才你说的那个BBC NEWS CHINA,我简单看了一下其json结构,挺容易解析的,我就简单写了一个脚本,你可以以这个为起点,再修改一下,特别是BBC news网站上有两类文章,一类是js生成的,一类是直接写在html里面的, 你可以在 self.log.warning('Cannot find __NEXT_DATA__, perhaps this is a normal page.') 这个分支里面自己在一个正常的html文件里面提取内容,我就不想写了,你自己写吧。 bbc_china.zip

cdhigh commented 4 months ago

我没有订阅这个源,不过你可以使用calibre推送一次看,是否有同样问题? 不过我想这个问题是可以解决的,如果确实是需要一个h4在h1前面,应该可以使用css调整格式。

还有,我碰到了你之前提到的一个目录错位问题,经过调试发现,应该算是python的一个bug,我采用规避的方法解决了此问题。 具体的来说就是有时候一个类型为列表的成员变量重新赋值为空列表不一定会申请一个新地址,而是重用以前的地址,导致出现我们这个BUG。

如果是局部变量,则每次都会申请一个新地址,这个bug只针对成员变量。

我看你对python挺感兴趣的,可以围观一下这个bug,查找recipe_input.py,里面的convert()函数, self.recipe_objects = []

你可以使用 print(id(self.recipe_objects)) 来看其内存地址,就会明白这个bug,理论上每次执行这个函数时的地址都不一样,如果一样,至少要保证里面的数据已经被清除,但是如果你打印其内容就会发现,以前的内容还保留着。 尽管python有对小对象的优化,但是明显这个优化是错的。

更新: calibre的代码默认配置里面会在h1/h2前强制分页,我屏蔽了这个默认配置,现在应该排版正常了,不过之后要看看对其他的书籍有多少影响。

Steven630 commented 4 months ago

谢谢大佬解答。问了AI这个问题,它也给了段解释的代码,原来python本身也能有bug。

不强制分页后,不知道会不会影响文章之间的分页,等更新后试试。那个Scientific American文章不显示h1的问题,网友结构是这样的:

<h1 class="article_hed-LDnzF"><p>New Prostate Cancer Treatments Offer Hope for Advanced Cases</p></h1>

recipe的keep only tags保留了以article_hed-开头的标签,可就是不显示。我本来怀疑是不是因为h1里面有p,所以不行。后来用preprocess html把p去除了,还是不行。

下一步试试把h1的class删除。(结果也不行)

因为电脑科学上网不太方便,所以还没用Calibre去抓取了看。估计在Calibre上是正常的。

cdhigh commented 4 months ago

当然是因为这个,之前你不说,除了要改这行以外,还需要修改

def populate_article_metadata(self, article, soup, first):
            article.url = soup.find('h3')['title']
Steven630 commented 4 months ago

推送后文章前面有四部分了 h1 div(flytitle) h3 div(subtitle)

h1和h3的内容相同,都是title,只是字号不同。h3显然就是因为我改了,期待的效果是没有第一行的h1。

是不是因为recipe还有这行

E(article, 'h1', replace_entities(data['headline']))

感觉又不对,这个是在else部分的,use_archive是true,不会用到这部分代码。不过recipe其他地方就没有h1了,除了互动页面的文章,增加提示应该用浏览器打开。

cdhigh commented 4 months ago

我猜是h2,而不是h1,开头的标题是KindleEar自动添加的,如果一篇文章找不到h1/h2,就在前面默认添加一个h2. 你可以看news.py line 1074 - line 1086

解决方法就是将h3改成h2即可。

Steven630 commented 4 months ago

明白了,原来是这样。 前两天还没重新部署的时候,用邮件推送 后台日志报错: (Caused by NewConnectionError('<urllib3.connection.HTTPSConnection object at 0x3e159ffe4a60>: Failed to establish a new connection: [Errno -2] Name or service not known'))

cdhigh commented 4 months ago

这个不是BUG,是有时候网络连接问题,换个时间就可以正常了。

还有,因为你经常在线调试,告诉你一个技巧。 调试时在 "高级设置" 里面的 "Calibre配置" 输入:

{"test": 1}

可以加快调试进展,因为有了这个配置参数,KindleEar不会完整下载所有文章,而只下载每个源的前两篇文章,这样节省时间和资源消耗。

你如果熟悉calibre,这个json字典里面有很多参数都可以设置。

Steven630 commented 4 months ago

学到了,很有用。输入这个参数后,哪怕是定时推送也都只有前两篇了吗

Steven630 commented 4 months ago

有些recipe,比如TE,设置了resolve_internal_links。但是如果KE设置了移除所有链接,这个选项就没用了。不知可否考虑如果启用了这个参数,就不再移除链接

cdhigh commented 4 months ago

暂时不见得启用resolve_internal_links就不移除链接就是一个好的解决方案,这会带来混淆。 这是一个低优先级的需求,暂没有一个好的解决方案,留待以后解决。

Steven630 commented 4 months ago

好的,或者考虑可以在recipe设置参数remove_links=False这样的呢?确实不那么迫切,就是提一下

cdhigh commented 4 months ago

更简单的方法是将这些recipe放到一个单独的账号里面推送,每个账号的设置是相互独立互不影响的。

Steven630 commented 4 months ago

另外建账号对GAE资源消耗大吗

cdhigh commented 4 months ago

账号数量不关键,关键是总订阅数量。

cdhigh commented 4 months ago

这个问题是某些针对标准的不同处理方式导致,暂时不好评价哪种好,可能calibre使用的BeautifulSoup/lxml版本和KindleEar不一致导致。 具体来说,calibre使用的BeautifulSoup/lxml版本会将下面这一行html维持原状

<h1 class="article_hed-LDnzF"><p>Readers Respond to the January 2024 Issue</p></h1>

但KindleEar使用的BeautifulSoup/lxml版本会将上面的html行变成这样,h1标签里面的p提取到后面了。

<h1 class="article_hed-LDnzF"></h1><p>Readers Respond to the January 2024 Issue</p>

(算法层面不叫提取,而是自动关闭未闭合的标签,代码扫描到h1后面的\<p>开始标签时发现前面的\<h1>尚未关闭,就自动添加了结束标签\</h1>来关闭h1,然后等扫描到后面的原先的\</h1>时发现这个结束标签没有对应的开始标签,就直接丢弃)

chatgpt的回答是h1-h6的语义结构应该是纯文本内容,尽管技术上嵌套其他标签是合法的,但是不建议,特别是p是用来定义段落的。

关于依赖库的一些做法差异,我们能做的不多,如果这个标题对你很重要,可以自己修改recipe文件,把h1的下一个兄弟节点抓过来当h1内容,然后删除这个兄弟节点;或者删除h1,将下一个兄弟节点的节点名修改为h1。

Steven630 commented 4 months ago

明白了。一开始看到h1里面有p也觉得很奇怪,以为是亚马逊转换文件过不去,就用preprocess_html尝试把h1里的p去掉,却不知道原始的soup里,p已经到h1后面去了。现在有个小疑惑,preprocess_html好像是在keep only tags和remove tags后面一步执行的。这个recipe是靠class以什么开头筛选的,h1的class开头是article_hed-,所以才保留下来了。后面的p没有class,按照keep only tags应该是不会保留的。这样preprocess_html更改的时候已经没有这个p了。有什么办法可以在keep only tags生效之前就把p的内容移到h1里吗?

cdhigh commented 4 months ago

你是对的,还是需要处理原始字符串,尽管将原始字符串解释为beautifulsoup对象,处理完再转换会字符串也可以,但是效率低一些。

既然知道了原始文本内容,几行语句就可以了,我没有测试,如果不行再微调一下。

pattern = r'<h1 class="article_hed-(.*?)"><p>(.*?)</p></h1>'
match = re.search(pattern, raw_html)
if match:
    h1 = match.group(0)
    name = match.group(1)
    content = match.group(2)
    return raw_html.replace(h1, f'<h1 class="article_hed-{name}">{content}</h1>', 1)
else:
    return raw_html
cdhigh commented 4 months ago

我刚才试了,没有任何错误的就推送到了Kindle,不管是在网页版上发的邮件,还是gmail app里面发的邮件。

https://manual.calibre-ebook.com/news_recipe.html
https://manual.calibre-ebook.com/news.html

你再检查一下是哪个地方的问题? 看你给出的截图,感觉不像是gmail app,也不是gmail网页版,所以我怀疑邮件app的问题? 可以使用此app发邮件给其他正常的地址看看,接收到的链接地址是怎么样的。