on master help sign in
Merge pull request #2224 from dalf/update-infobox-engines

[enh] update infobox engines

commit10ddd421f22c993a8cd3f4a02798dc3335c59709 authorNoémi Ványi <kvch@users.noreply.github.com> dateWed 28 Oct 2020, 22:36 (6 years ago) parentd3d50ef95bd603
11 files changed+2120−538
searx/data/__init__.py+4−1 searx/data/external_urls.json+156 searx/data/wikidata_units.json+1006 searx/engines/duckduckgo_definitions.py+156−71 searx/engines/wikidata.py+637−452 searx/external_urls.py+77 searx/results.py+26−8 searx/templates/oscar/infobox.html+1−5 searx/templates/simple/infobox.html−1 searx/utils.py+10 utils/fetch_wikidata_units.py+47
searx/data/__init__.py+4−1
@@ -2,7 +2,8 @@ import json
22from pathlib import Path
33
44
5−__init__ = ['ENGINES_LANGUGAGES', 'CURRENCIES', 'USER_AGENTS', 'bangs_loader', 'ahmia_blacklist_loader']
5+__init__ = ['ENGINES_LANGUGAGES', 'CURRENCIES', 'USER_AGENTS', 'EXTERNAL_URLS', 'WIKIDATA_UNITS',
6+ 'bangs_loader', 'ahmia_blacklist_loader']
67data_dir = Path(__file__).parent
78
89
@@ -24,3 +25,5 @@ def ahmia_blacklist_loader():
2425ENGINES_LANGUAGES = load('engines_languages.json')
2526CURRENCIES = load('currencies.json')
2627USER_AGENTS = load('useragents.json')
28+EXTERNAL_URLS = load('external_urls.json')
29+WIKIDATA_UNITS = load('wikidata_units.json')
searx/data/external_urls.jsonnew file+156
@@ -0,0 +1,156 @@
1+{
2+ "facebook_profile": {
3+ "category_name": "Facebook",
4+ "url_name": "Facebook profile",
5+ "urls": {
6+ "default": "https://facebook.com/$1"
7+ }
8+ },
9+ "youtube_channel": {
10+ "category_name": "YouTube",
11+ "url_name": "YouTube channel",
12+ "urls": {
13+ "default": "https://www.youtube.com/channel/$1"
14+ }
15+ },
16+ "youtube_video": {
17+ "category_name": "YouTube",
18+ "url_name": "YouTube video",
19+ "urls": {
20+ "default": "https://www.youtube.com/watch?v=$1"
21+ }
22+ },
23+ "twitter_profile": {
24+ "category_name": "Twitter",
25+ "url_name": "Twitter profile",
26+ "urls": {
27+ "default": "https://twitter.com/$1"
28+ }
29+ },
30+ "instagram_profile": {
31+ "category_name": "Instagram",
32+ "url_name": "Instagram profile",
33+ "urls": {
34+ "default": "https://www.instagram.com/$1"
35+ }
36+ },
37+ "imdb_title": {
38+ "category_name": "IMDB",
39+ "url_name": "IMDB title",
40+ "urls": {
41+ "default": "https://www.imdb.com/title/$1"
42+ }
43+ },
44+ "imdb_name": {
45+ "category_name": "IMDB",
46+ "url_name": "IMDB name",
47+ "urls": {
48+ "default": "https://www.imdb.com/name/$1"
49+ }
50+ },
51+ "imdb_character": {
52+ "category_name": "IMDB",
53+ "url_name": "IMDB character",
54+ "urls": {
55+ "default": "https://www.imdb.com/character/$1"
56+ }
57+ },
58+ "imdb_company": {
59+ "category_name": "IMDB",
60+ "url_name": "IMDB company",
61+ "urls": {
62+ "default": "https://www.imdb.com/company/$1"
63+ }
64+ },
65+ "imdb_event": {
66+ "category_name": "IMDB",
67+ "url_name": "IMDB event",
68+ "urls": {
69+ "default": "https://www.imdb.com/event/$1"
70+ }
71+ },
72+ "rotten_tomatoes": {
73+ "category_name": "Rotten tomatoes",
74+ "url_name": "Rotten tomatoes title",
75+ "urls": {
76+ "default": "https://www.rottentomatoes.com/$1"
77+ }
78+ },
79+ "spotify_artist_id": {
80+ "category_name": "Spotify",
81+ "url_name": "Spotify artist",
82+ "urls": {
83+ "default": "https://open.spotify.com/artist/$1"
84+ }
85+ },
86+ "itunes_artist_id": {
87+ "category_name": "iTunes",
88+ "url_name": "iTunes artist",
89+ "urls": {
90+ "default": "https://music.apple.com/us/artist/$1"
91+ }
92+ },
93+ "soundcloud_id": {
94+ "category_name": "Soundcloud",
95+ "url_name": "Soundcloud artist",
96+ "urls": {
97+ "default": "https://soundcloud.com/$1"
98+ }
99+ },
100+ "netflix_id": {
101+ "category_name": "Netflix",
102+ "url_name": "Netflix movie",
103+ "urls": {
104+ "default": "https://www.netflix.com/watch/$1"
105+ }
106+ },
107+ "github_profile": {
108+ "category_name": "Github",
109+ "url_name": "Github profile",
110+ "urls": {
111+ "default": "https://wwww.github.com/$1"
112+ }
113+ },
114+ "musicbrainz_artist": {
115+ "category_name": "Musicbrainz",
116+ "url_name": "Musicbrainz artist",
117+ "urls": {
118+ "default": "http://musicbrainz.org/artist/$1"
119+ }
120+ },
121+ "musicbrainz_work": {
122+ "category_name": "Musicbrainz",
123+ "url_name": "Musicbrainz work",
124+ "urls": {
125+ "default": "http://musicbrainz.org/work/$1"
126+ }
127+ },
128+ "musicbrainz_release_group": {
129+ "category_name": "Musicbrainz",
130+ "url_name": "Musicbrainz release group",
131+ "urls": {
132+ "default": "http://musicbrainz.org/release-group/$1"
133+ }
134+ },
135+ "musicbrainz_label": {
136+ "category_name": "Musicbrainz",
137+ "url_name": "Musicbrainz label",
138+ "urls": {
139+ "default": "http://musicbrainz.org/label/$1"
140+ }
141+ },
142+ "wikimedia_image": {
143+ "category_name": "Wikipedia",
144+ "url_name": "Wikipedia image",
145+ "urls": {
146+ "default": "https://commons.wikimedia.org/wiki/Special:FilePath/$1?width=500&height=400"
147+ }
148+ },
149+ "map": {
150+ "category_name": "Map",
151+ "url_name": "geo map",
152+ "urls": {
153+ "default": "https://www.openstreetmap.org/?lat=${latitude}&lon=${longitude}&zoom=${zoom}&layers=M"
154+ }
155+ }
156+}
searx/data/wikidata_units.jsonnew file+1006
@@ -0,0 +1,1006 @@
1+{
2+ "Q199": "1",
3+ "Q100149279": "°We",
4+ "Q100995": "lb",
5+ "Q1022113": "cm³",
6+ "Q102573": "Bq",
7+ "Q103246": "Sv",
8+ "Q103510": "bar",
9+ "Q10380431": "TJ",
10+ "Q1040401": "das",
11+ "Q1040427": "hs",
12+ "Q1042866": "Zibit",
13+ "Q1050958": "inHg",
14+ "Q1051665": "m/s²",
15+ "Q1052397": "rad",
16+ "Q1054140": "Mm",
17+ "Q10543042": "Ym",
18+ "Q1057069": "hg",
19+ "Q1063756": "rad/s",
20+ "Q1063786": "in²",
21+ "Q1065153": "mrad",
22+ "Q1066138": "Ps",
23+ "Q1067722": "Fg",
24+ "Q1069725": "p.",
25+ "Q1084321": "Tb/s",
26+ "Q1086691": "fg",
27+ "Q1091257": "tex",
28+ "Q1092296": "a",
29+ "Q1104069": "CAD$",
30+ "Q11061003": "μm²",
31+ "Q11061005": "nm²",
32+ "Q1131660": "st",
33+ "Q1137675": "cr",
34+ "Q1140444": "Zbit",
35+ "Q1140577": "Ybit",
36+ "Q1152074": "Pbit",
37+ "Q1152323": "Tbit",
38+ "Q1165799": "mil",
39+ "Q11776930": "Mg",
40+ "Q11830636": "psf",
41+ "Q11929860": "kpc",
42+ "Q1194225": "lbf",
43+ "Q1194580": "Mibit",
44+ "Q1195111": "Ebit",
45+ "Q1196837": "ω_P",
46+ "Q1197459": "Ms",
47+ "Q11982285": "Em³",
48+ "Q11982288": "Zm³",
49+ "Q11982289": "Tm³",
50+ "Q12011178": "Zs",
51+ "Q1204894": "Gibit",
52+ "Q12257695": "Eb/s",
53+ "Q12257696": "EB/s",
54+ "Q12261466": "kB/s",
55+ "Q12265780": "Pb/s",
56+ "Q12265783": "PB/s",
57+ "Q12269121": "Yb/s",
58+ "Q12269122": "YB/s",
59+ "Q12269308": "Zb/s",
60+ "Q12269309": "ZB/s",
61+ "Q1247300": "cm H₂O",
62+ "Q12714022": "sh cwt",
63+ "Q12789864": "GeV",
64+ "Q12874593": "W h",
65+ "Q128822": "kn",
66+ "Q13035094": "J/mol",
67+ "Q130964": "cal",
68+ "Q131255": "F",
69+ "Q13147228": "g/cm³",
70+ "Q1322380": "Ts",
71+ "Q1323615": "oz t",
72+ "Q132643": "kr",
73+ "Q13400897": "g",
74+ "Q13479685": "mm wg",
75+ "Q1351253": "Eibit",
76+ "Q1351334": "Pibit",
77+ "Q13542672": "Ry",
78+ "Q13548586": "THz",
79+ "Q13582667": "kgf/cm²",
80+ "Q1361854": "dwt",
81+ "Q1363007": "slug",
82+ "Q1374438": "ks",
83+ "Q13753469": "MB/s",
84+ "Q1377051": "Gs",
85+ "Q1394540": "bm",
86+ "Q1396128": "F",
87+ "Q1413142": "Gb",
88+ "Q14158377": "A_P",
89+ "Q14623803": "MDa",
90+ "Q14623804": "kDa",
91+ "Q1472674": "Sv",
92+ "Q14754979": "Zg",
93+ "Q14786969": "MJ",
94+ "Q14913554": "Ys",
95+ "Q14914907": "th",
96+ "Q14916719": "Gpc",
97+ "Q14923662": "Pm³",
98+ "Q1511773": "LSd",
99+ "Q15120301": "l atm",
100+ "Q1542309": "xu",
101+ "Q1545979": "ft³",
102+ "Q1550511": "yd²",
103+ "Q15551713": "Sh",
104+ "Q1569733": "St",
105+ "Q15784325": "apc",
106+ "Q160680": "Br",
107+ "Q160857": "hp",
108+ "Q1628990": "hph",
109+ "Q163343": "T",
110+ "Q163354": "H",
111+ "Q1640501": "hyl",
112+ "Q1645498": "μg",
113+ "Q16859309": "lb·ft",
114+ "Q169893": "S",
115+ "Q170804": "Wb",
116+ "Q17093295": "m/h",
117+ "Q17255465": "v_P",
118+ "Q173117": "R$",
119+ "Q1741429": "kpm",
120+ "Q174467": "Lm",
121+ "Q174728": "cm",
122+ "Q174789": "mm",
123+ "Q175821": "μm",
124+ "Q1768929": "p",
125+ "Q1770733": "Tg",
126+ "Q1772386": "dg",
127+ "Q177493": "Gs",
128+ "Q177612": "sr",
129+ "Q1777507": "fs",
130+ "Q177974": "atm",
131+ "Q178506": "bbl",
132+ "Q178674": "nm",
133+ "Q1793863": "sn",
134+ "Q179836": "lx",
135+ "Q180154": "km/h",
136+ "Q180892": "M☉",
137+ "Q1815100": "cl",
138+ "Q182098": "kWh",
139+ "Q1823150": "μW",
140+ "Q182429": "m/s",
141+ "Q1826195": "dl",
142+ "Q18413919": "cm/s",
143+ "Q184172": "FF",
144+ "Q185078": "a",
145+ "Q185153": "erg",
146+ "Q185648": "Torr",
147+ "Q185759": "span",
148+ "Q1872619": "zs",
149+ "Q189097": "₧",
150+ "Q190095": "Gy",
151+ "Q190951": "S$",
152+ "Q191118": "t",
153+ "Q1913097": "fg",
154+ "Q1916026": "μV",
155+ "Q192027": "Bd",
156+ "Q192274": "pm",
157+ "Q193098": "KD",
158+ "Q1935515": "mA s",
159+ "Q19392152": "TL",
160+ "Q193933": "dpt",
161+ "Q194339": "B$",
162+ "Q1970718": "mam",
163+ "Q1972579": "pdl",
164+ "Q199462": "LE",
165+ "Q199471": "Afs",
166+ "Q200323": "dm",
167+ "Q200337": "Kz",
168+ "Q201880": "LL",
169+ "Q201933": "dyn",
170+ "Q2029156": "quad",
171+ "Q2029519": "hl",
172+ "Q203567": "₦",
173+ "Q2042279": "m H₂O",
174+ "Q204737": "៛",
175+ "Q2051195": "GWh",
176+ "Q2055118": "ppb",
177+ "Q2064166": "fc",
178+ "Q206600": "ރ",
179+ "Q20706220": "cmm",
180+ "Q20706221": "dmm",
181+ "Q2080811": "vol%",
182+ "Q208526": "NT$",
183+ "Q208528": "gon",
184+ "Q208634": "kat",
185+ "Q208788": "fm",
186+ "Q209351": "b",
187+ "Q209426": "′",
188+ "Q21006887": "ppm",
189+ "Q2100949": "P",
190+ "Q21014455": "m/min",
191+ "Q210472": "B/.",
192+ "Q21061369": "g/kg",
193+ "Q21062777": "MPa",
194+ "Q21064807": "kPa",
195+ "Q21064845": "mol/L",
196+ "Q21075844": "ml/l",
197+ "Q21077820": "mg/m³",
198+ "Q21091747": "mg/kg",
199+ "Q211256": "mph",
200+ "Q211580": "BTU (th)",
201+ "Q212120": "A h",
202+ "Q2140397": "in³",
203+ "Q214377": "ell",
204+ "Q2143992": "kHz",
205+ "Q21489891": "nm³",
206+ "Q21489892": "Gm³",
207+ "Q21489893": "Mm³",
208+ "Q21489894": "μm³",
209+ "Q21500224": "mas",
210+ "Q2151240": "mag",
211+ "Q215571": "N m",
212+ "Q21604951": "g/m³",
213+ "Q2165290": "yd³",
214+ "Q216880": "kp",
215+ "Q217208": "a",
216+ "Q2175964": "dm³",
217+ "Q218593": "in",
218+ "Q2199357": "dec",
219+ "Q22137107": "mas/y",
220+ "Q2215478": "ppt",
221+ "Q2221356": "mW h",
222+ "Q22350885": "da",
223+ "Q2243141": "Gb/s",
224+ "Q2254856": "ca",
225+ "Q22673229": "ft/min",
226+ "Q2269250": "kb/s",
227+ "Q2282891": "μl",
228+ "Q2282906": "ng",
229+ "Q229354": "Ci",
230+ "Q232291": "mi²",
231+ "Q2332346": "ml",
232+ "Q23823681": "TW",
233+ "Q23925410": "gal (UK)",
234+ "Q23925413": "gal (US)",
235+ "Q23931040": "dam²",
236+ "Q23931103": "nmi²",
237+ "Q2414435": "$b.",
238+ "Q242988": "Lib$",
239+ "Q2438073": "ag",
240+ "Q2448803": "mV",
241+ "Q2451296": "μF",
242+ "Q246868": "lot",
243+ "Q2474258": "mSv",
244+ "Q2483628": "as",
245+ "Q2489298": "cm²",
246+ "Q249439": "q_P",
247+ "Q2518569": "nSv",
248+ "Q253276": "mi",
249+ "Q25472681": "GB/s",
250+ "Q25472693": "TB/s",
251+ "Q25499149": "oct",
252+ "Q25511288": "mb",
253+ "Q2553708": "MV",
254+ "Q2554092": "kV",
255+ "Q259502": "AU$",
256+ "Q260126": "rem",
257+ "Q2612219": "Pg",
258+ "Q261247": "ct",
259+ "Q2619500": "foe",
260+ "Q2636421": "nH",
261+ "Q2637946": "dal",
262+ "Q2642547": "ha",
263+ "Q2652700": "Osm",
264+ "Q2655272": "Eg",
265+ "Q2659078": "TW h",
266+ "Q2670039": "₶",
267+ "Q26708069": "kcal",
268+ "Q267391": "K",
269+ "Q2679083": "μH",
270+ "Q2682463": "nF",
271+ "Q2691798": "cg",
272+ "Q271206": "sud£",
273+ "Q2737347": "mm²",
274+ "Q2739114": "μSv",
275+ "Q275112": "Bz$",
276+ "Q2756030": "pF",
277+ "Q2757753": "PW h",
278+ "Q2762458": "ys",
279+ "Q27864215": "μW h",
280+ "Q2793566": "GV",
281+ "Q27949241": "R",
282+ "Q2799294": "Gg",
283+ "Q281096": "cd/m²",
284+ "Q28719934": "keV",
285+ "Q28924752": "g/mol",
286+ "Q28924753": "kg/mol",
287+ "Q2924137": "mH",
288+ "Q296936": "toe",
289+ "Q29924639": "kVA",
290+ "Q30001811": "aBq",
291+ "Q30001813": "aC",
292+ "Q30001814": "aHz",
293+ "Q30001815": "aJ",
294+ "Q30001816": "akat",
295+ "Q30001818": "aL",
296+ "Q30001819": "alm",
297+ "Q30001820": "alx",
298+ "Q30001822": "aN",
299+ "Q30001823": "aΩ",
300+ "Q30001825": "aPa",
301+ "Q30001826": "arad",
302+ "Q30001827": "aS",
303+ "Q30001828": "aSv",
304+ "Q30001829": "asr",
305+ "Q30001830": "aT",
306+ "Q30001831": "aV",
307+ "Q30001832": "aW",
308+ "Q30001833": "aWb",
309+ "Q3013059": "kyr",
310+ "Q3194304": "kbit",
311+ "Q3207456": "mW",
312+ "Q321017": "R",
313+ "Q3221356": "ym",
314+ "Q3239557": "pg",
315+ "Q3241121": "mg",
316+ "Q324923": "Hart",
317+ "Q3249364": "cs",
318+ "Q3251645": "ds",
319+ "Q3267417": "Tm",
320+ "Q3270676": "zm",
321+ "Q32750621": "liq pt (US)",
322+ "Q32750759": "fl oz (US)",
323+ "Q32750816": "bu (US)",
324+ "Q32751272": "dry pt (US)",
325+ "Q32751296": "bbl (US)",
326+ "Q3276763": "GHz",
327+ "Q3277907": "Em",
328+ "Q3277915": "Zm",
329+ "Q3277919": "Pm",
330+ "Q3312063": "fL",
331+ "Q3320608": "kW",
332+ "Q3331719": "dm²",
333+ "Q3332689": "ToR",
334+ "Q3332814": "Mbit",
335+ "Q3396758": "daa",
336+ "Q3414243": "rps",
337+ "Q3421309": "R_J",
338+ "Q3495543": "mbar",
339+ "Q355198": "px",
340+ "Q3674704": "km/s",
341+ "Q3675550": "mm³",
342+ "Q3712659": "$",
343+ "Q376660": "nat",
344+ "Q37732658": "°R",
345+ "Q3773454": "Mpc",
346+ "Q3815076": "Kibit",
347+ "Q3833309": "£",
348+ "Q3858002": "mA h",
349+ "Q3867152": "ft/s²",
350+ "Q389062": "Tibit",
351+ "Q3902688": "pl",
352+ "Q3902709": "ps",
353+ "Q39360235": "US lea",
354+ "Q39360471": "nl",
355+ "Q39362962": "µin",
356+ "Q39363132": "UK lg",
357+ "Q39363209": "UK nl",
358+ "Q39380159": "US nmi",
359+ "Q39462789": "µin²",
360+ "Q39467934": "kgf/m²",
361+ "Q39469927": "N/m²",
362+ "Q39617688": "cwt long",
363+ "Q39617818": "t lb",
364+ "Q39628023": "y",
365+ "Q39699418": "cm/s²",
366+ "Q39708248": "S",
367+ "Q39709980": "bd",
368+ "Q39710113": "bhp EDR",
369+ "Q3972226": "kL",
370+ "Q4041686": "iwg",
371+ "Q4068266": "Ʒ",
372+ "Q4176683": "aC",
373+ "Q420266": "oz. fl.",
374+ "Q42319606": "people/m²",
375+ "Q4243638": "km³",
376+ "Q4456994": "mF",
377+ "Q469356": "tn. sh.",
378+ "Q476572": "Ha",
379+ "Q482798": "yd",
380+ "Q483261": "Da",
381+ "Q483725": "A.M.",
382+ "Q484092": "lm",
383+ "Q4861171": "H",
384+ "Q494083": "fur",
385+ "Q4989854": "kJ",
386+ "Q500515": "Gal",
387+ "Q5042194": "£",
388+ "Q50808017": "kg m²",
389+ "Q5139563": "hPa",
390+ "Q514845": "pz",
391+ "Q5195628": "hm³",
392+ "Q5198770": "dam³",
393+ "Q524410": "byr",
394+ "Q53393488": "PHz",
395+ "Q53393490": "EHz",
396+ "Q53393494": "ZHz",
397+ "Q53393498": "YHz",
398+ "Q53393659": "ML",
399+ "Q53393664": "GL",
400+ "Q53393674": "ZL",
401+ "Q53393678": "YL",
402+ "Q53393771": "yL",
403+ "Q53393868": "GJ",
404+ "Q53393886": "PJ",
405+ "Q53393890": "EJ",
406+ "Q53448786": "yHz",
407+ "Q53448790": "zHz",
408+ "Q53448794": "fHz",
409+ "Q53448797": "pHz",
410+ "Q53448801": "nHz",
411+ "Q53448806": "μHz",
412+ "Q53448808": "mHz",
413+ "Q53448813": "cHz",
414+ "Q53448817": "dHz",
415+ "Q53448820": "daHz",
416+ "Q53448826": "hHz",
417+ "Q53448828": "yJ",
418+ "Q53448832": "zJ",
419+ "Q53448842": "pJ",
420+ "Q53448844": "nJ",
421+ "Q53448847": "μJ",
422+ "Q53448851": "mJ",
423+ "Q53448856": "cJ",
424+ "Q53448860": "dJ",
425+ "Q53448864": "daJ",
426+ "Q53448875": "hJ",
427+ "Q53448879": "yPa",
428+ "Q53448883": "zPa",
429+ "Q53448886": "fPa",
430+ "Q53448892": "pPa",
431+ "Q53448897": "nPa",
432+ "Q53448900": "μPa",
433+ "Q53448906": "mPa",
434+ "Q53448909": "cPa",
435+ "Q53448914": "dPa",
436+ "Q53448918": "daPa",
437+ "Q53448922": "GPa",
438+ "Q53448927": "TPa",
439+ "Q53448931": "PPa",
440+ "Q53448936": "EPa",
441+ "Q53448939": "ZPa",
442+ "Q53448943": "YPa",
443+ "Q53448949": "yV",
444+ "Q53448952": "zV",
445+ "Q53448957": "fV",
446+ "Q53448960": "pV",
447+ "Q53448965": "nV",
448+ "Q53448969": "cV",
449+ "Q53448973": "dV",
450+ "Q53448977": "daV",
451+ "Q53448981": "hV",
452+ "Q53448985": "TV",
453+ "Q53448990": "PV",
454+ "Q53448994": "EV",
455+ "Q53448996": "ZV",
456+ "Q53449001": "YV",
457+ "Q53449006": "yW",
458+ "Q53449008": "zW",
459+ "Q53449013": "fW",
460+ "Q53449018": "pW",
461+ "Q53449021": "nW",
462+ "Q53449025": "cW",
463+ "Q53449029": "dW",
464+ "Q53449033": "daW",
465+ "Q53449036": "hW",
466+ "Q53449040": "PW",
467+ "Q53449045": "EW",
468+ "Q53449049": "ZW",
469+ "Q53449054": "YW",
470+ "Q53561461": "wf",
471+ "Q53561822": "wf",
472+ "Q53651160": "zm³",
473+ "Q53651201": "Ym³",
474+ "Q53651356": "ym³",
475+ "Q53651512": "pm³",
476+ "Q53651713": "fm³",
477+ "Q536785": "ρ_P",
478+ "Q53951982": "Mt",
479+ "Q53952048": "kt",
480+ "Q54006645": "ZWb",
481+ "Q54081925": "ZSv",
482+ "Q54082468": "ZS",
483+ "Q54083144": "ZΩ",
484+ "Q54083318": "ZN",
485+ "Q54083566": "Zlm",
486+ "Q54083579": "Zlx",
487+ "Q54083712": "ZBq",
488+ "Q54083746": "ZC",
489+ "Q54083766": "ZF",
490+ "Q54083779": "ZGy",
491+ "Q54083795": "ZH",
492+ "Q54083813": "Zkat",
493+ "Q5409016": "MVA",
494+ "Q5465723": "ft-pdl",
495+ "Q549389": "bit/s",
496+ "Q550341": "V A",
497+ "Q552299": "ch",
498+ "Q55442349": "U/L",
499+ "Q55726194": "mg/L",
500+ "Q56156859": "mmol",
501+ "Q56156949": "μmol",
502+ "Q56157046": "nmol",
503+ "Q56157048": "pmol",
504+ "Q56160603": "fmol",
505+ "Q56302633": "UM",
506+ "Q56317116": "mgal",
507+ "Q56317622": "Q_P",
508+ "Q56318907": "kbar",
509+ "Q56349362": "Bs.S",
510+ "Q56402798": "kN",
511+ "Q5711261": "am³",
512+ "Q581432": "‴",
513+ "Q5879479": "GW",
514+ "Q6003257": "am",
515+ "Q6009164": "MW h",
516+ "Q6014364": "in/s",
517+ "Q603071": "E°",
518+ "Q605704": "doz",
519+ "Q60742631": "AU/yr",
520+ "Q608697": "Mx",
521+ "Q610135": "G",
522+ "Q613726": "Yg",
523+ "Q6170164": "yg",
524+ "Q6171168": "zg",
525+ "Q61756607": "yd",
526+ "Q61793198": "rd",
527+ "Q61794766": "ch (US survey)",
528+ "Q61994988": "Wth",
529+ "Q61995006": "KWth",
530+ "Q626299": "psi",
531+ "Q630369": "var",
532+ "Q636200": "U",
533+ "Q640907": "sb",
534+ "Q6414556": "kip",
535+ "Q648908": "bya",
536+ "Q64996135": "gal (US)/min",
537+ "Q65028392": "mm/yr",
538+ "Q651336": "M_J",
539+ "Q6517513": "dag",
540+ "Q667419": "UK t",
541+ "Q681996": "M⊕",
542+ "Q685662": "p_P",
543+ "Q6859652": "mm Hg",
544+ "Q686163": "$",
545+ "Q68725821": "°Rø",
546+ "Q68726230": "°De",
547+ "Q68726625": "°N",
548+ "Q69362731": "°C",
549+ "Q69363953": "K",
550+ "Q693944": "gr",
551+ "Q6982035": "MW",
552+ "Q69878540": "fl oz (UK)",
553+ "Q70378044": "dmol",
554+ "Q70378549": "dK",
555+ "Q70393458": "kmol",
556+ "Q70395375": "Tmol",
557+ "Q70395643": "Mmol",
558+ "Q70395830": "kK",
559+ "Q70396179": "mK",
560+ "Q70397275": "μK",
561+ "Q70397725": "cmol",
562+ "Q70397932": "cK",
563+ "Q70398457": "nK",
564+ "Q70398619": "MK",
565+ "Q70398813": "Gmol",
566+ "Q70398991": "GK",
567+ "Q70440025": "daK",
568+ "Q70440438": "hK",
569+ "Q70440620": "damol",
570+ "Q70440823": "hmol",
571+ "Q70443020": "EK",
572+ "Q70443154": "yK",
573+ "Q70443282": "zK",
574+ "Q70443367": "fK",
575+ "Q70443453": "TK",
576+ "Q70443757": "pK",
577+ "Q70443901": "YK",
578+ "Q70444029": "PK",
579+ "Q70444141": "Emol",
580+ "Q70444284": "ymol",
581+ "Q70444386": "zmol",
582+ "Q70444514": "Ymol",
583+ "Q70444609": "Pmol",
584+ "Q712226": "km²",
585+ "Q72081071": "MeV",
586+ "Q723733": "ms",
587+ "Q730251": "ft·lbf",
588+ "Q732707": "MHz",
589+ "Q73408": "K",
590+ "Q7350781": "Mb/s",
591+ "Q743895": "bpm",
592+ "Q748716": "ft/s",
593+ "Q750178": "‱",
594+ "Q752197": "kJ/mol",
595+ "Q7672057": "TU",
596+ "Q777017": "dBm",
597+ "Q78754556": "rot",
598+ "Q78756901": "rev",
599+ "Q78757683": "windings",
600+ "Q79726": "kB",
601+ "Q79735": "MB",
602+ "Q79738": "GB",
603+ "Q79741": "TB",
604+ "Q79744": "PB",
605+ "Q79745": "EB",
606+ "Q79747": "ZB",
607+ "Q7974920": "W s",
608+ "Q79752": "YB",
609+ "Q79756": "KiB",
610+ "Q79758": "MiB",
611+ "Q79765": "GiB",
612+ "Q79769": "TiB",
613+ "Q79774": "PiB",
614+ "Q79777": "EiB",
615+ "Q79779": "ZiB",
616+ "Q79781": "YiB",
617+ "Q80237579": "J/nm",
618+ "Q809678": "Ba",
619+ "Q81062869": "W/nm",
620+ "Q81073100": "W/(sr nm)",
621+ "Q81292": "acre",
622+ "Q81454": "Å",
623+ "Q8229770": "B/s",
624+ "Q828224": "km",
625+ "Q829073": "\"",
626+ "Q83216": "cd",
627+ "Q83327": "eV",
628+ "Q834105": "g/L",
629+ "Q835916": "IU",
630+ "Q838801": "ns",
631+ "Q842015": "μs",
632+ "Q842981": "thm (US)",
633+ "Q844211": "kg/m³",
634+ "Q844338": "hm",
635+ "Q844976": "Oe",
636+ "Q845958": "¥",
637+ "Q848856": "dam",
638+ "Q851872": "o",
639+ "Q854546": "Gm",
640+ "Q855161": "Yibit",
641+ "Q856240": "ft³/min",
642+ "Q857027": "ft²",
643+ "Q85854198": "MN",
644+ "Q864818": "abA",
645+ "Q87262709": "kΩ",
646+ "Q87416053": "MΩ",
647+ "Q88296091": "tsp",
648+ "Q9026416": "MWth",
649+ "Q9048643": "nl",
650+ "Q905912": "L",
651+ "Q906223": "Es",
652+ "Q909066": "at",
653+ "Q911730": "nx",
654+ "Q914151": "P_P",
655+ "Q915169": "F_P",
656+ "Q93318": "nmi",
657+ "Q940052": "q",
658+ "Q94076025": "dalm",
659+ "Q94076717": "dakat",
660+ "Q942092": "BWI$",
661+ "Q94414053": "Prad",
662+ "Q94414499": "PC",
663+ "Q94415026": "Grad",
664+ "Q94415255": "GC",
665+ "Q94415438": "Yrad",
666+ "Q94415526": "YC",
667+ "Q94415782": "Mrad",
668+ "Q94416260": "GN",
669+ "Q94416535": "cN",
670+ "Q94416879": "YN",
671+ "Q94417138": "PN",
672+ "Q94417481": "μGy",
673+ "Q94417583": "μS",
674+ "Q94417598": "μT",
675+ "Q94417933": "μlm",
676+ "Q94418102": "μN",
677+ "Q94418220": "μsr",
678+ "Q94418481": "μBq",
679+ "Q94479580": "GΩ",
680+ "Q94480021": "PΩ",
681+ "Q94480081": "YΩ",
682+ "Q94480128": "cΩ",
683+ "Q94480131": "TΩ",
684+ "Q94480136": "pΩ",
685+ "Q94480254": "nΩ",
686+ "Q94480476": "dΩ",
687+ "Q94480633": "EΩ",
688+ "Q94480967": "daΩ",
689+ "Q94481176": "hΩ",
690+ "Q94481339": "fΩ",
691+ "Q94481646": "yΩ",
692+ "Q94487174": "zΩ",
693+ "Q94487366": "mΩ",
694+ "Q94487561": "μΩ",
695+ "Q94487750": "kGy",
696+ "Q94488007": "klx",
697+ "Q94488361": "MF",
698+ "Q94488759": "GBq",
699+ "Q94489041": "PBq",
700+ "Q94489223": "YBq",
701+ "Q94489429": "MBq",
702+ "Q94489465": "kBq",
703+ "Q94489476": "TBq",
704+ "Q94489494": "kWb",
705+ "Q94489520": "kS",
706+ "Q94490951": "klm",
707+ "Q94491129": "kkat",
708+ "Q94634634": "cC",
709+ "Q94634655": "MC",
710+ "Q94634666": "kC",
711+ "Q94634677": "TC",
712+ "Q94634684": "μC",
713+ "Q94634699": "mC",
714+ "Q94693759": "csr",
715+ "Q94693773": "msr",
716+ "Q94693786": "mWb",
717+ "Q94693805": "μWb",
718+ "Q94693819": "GS",
719+ "Q94693849": "cS",
720+ "Q94693918": "MS",
721+ "Q94694019": "TS",
722+ "Q94694096": "pS",
723+ "Q94694154": "nS",
724+ "Q94694206": "mS",
725+ "Q94731530": "mlm",
726+ "Q94731808": "mkat",
727+ "Q94731887": "μkat",
728+ "Q94732218": "nkat",
729+ "Q94732627": "pkat",
730+ "Q94733432": "fkat",
731+ "Q94733760": "cGy",
732+ "Q94734107": "dGy",
733+ "Q94734232": "mGy",
734+ "Q94734359": "daGy",
735+ "Q94734468": "aGy",
736+ "Q94734527": "pGy",
737+ "Q94734593": "nGy",
738+ "Q94734689": "kT",
739+ "Q94734788": "mT",
740+ "Q94939947": "Gkat",
741+ "Q94940018": "Pkat",
742+ "Q94940081": "ykat",
743+ "Q94940160": "dkat",
744+ "Q94940232": "Ekat",
745+ "Q94940295": "Ykat",
746+ "Q94940582": "Tkat",
747+ "Q94940892": "hkat",
748+ "Q94941461": "zkat",
749+ "Q94942602": "MGy",
750+ "Q94942863": "GGy",
751+ "Q94986863": "YWb",
752+ "Q94986889": "PWb",
753+ "Q94986906": "cWb",
754+ "Q94986920": "GWb",
755+ "Q94986942": "MWb",
756+ "Q94986962": "TWb",
757+ "Q95178536": "Mlm",
758+ "Q95178777": "Tlm",
759+ "Q95178881": "clm",
760+ "Q95179024": "plm",
761+ "Q95179137": "nlm",
762+ "Q95179382": "hlm",
763+ "Q95179467": "flm",
764+ "Q95179608": "zlm",
765+ "Q95179695": "Mkat",
766+ "Q95179788": "ckat",
767+ "Q95179882": "PGy",
768+ "Q95377836": "PF",
769+ "Q95377853": "YF",
770+ "Q95378017": "kF",
771+ "Q95378296": "TF",
772+ "Q95379145": "cF",
773+ "Q95379382": "GF",
774+ "Q95379491": "daC",
775+ "Q95379580": "hC",
776+ "Q95379588": "dC",
777+ "Q95379596": "EC",
778+ "Q95445986": "nC",
779+ "Q95446327": "pC",
780+ "Q95446670": "fC",
781+ "Q95447079": "zC",
782+ "Q95447237": "yC",
783+ "Q95447253": "fF",
784+ "Q95447263": "zF",
785+ "Q95447276": "aF",
786+ "Q95447555": "dF",
787+ "Q95447863": "EF",
788+ "Q95448262": "yF",
789+ "Q95448479": "hF",
790+ "Q95448689": "daF",
791+ "Q95448950": "kSv",
792+ "Q95559229": "GSv",
793+ "Q95559368": "YSv",
794+ "Q95559441": "MSv",
795+ "Q95559576": "TSv",
796+ "Q95559603": "PSv",
797+ "Q95609154": "nWb",
798+ "Q95609210": "fWb",
799+ "Q95609261": "zWb",
800+ "Q95609291": "dWb",
801+ "Q95609317": "EWb",
802+ "Q95676212": "pWb",
803+ "Q95676232": "yWb",
804+ "Q95676243": "hWb",
805+ "Q95676250": "daWb",
806+ "Q95676257": "PS",
807+ "Q95676260": "YS",
808+ "Q95676273": "zS",
809+ "Q95676275": "fS",
810+ "Q95676279": "yS",
811+ "Q95676287": "hS",
812+ "Q95676291": "daS",
813+ "Q95676297": "dS",
814+ "Q95676298": "ES",
815+ "Q95720731": "YGy",
816+ "Q95720734": "TGy",
817+ "Q95720736": "fGy",
818+ "Q95720739": "yGy",
819+ "Q95720741": "zGy",
820+ "Q95720742": "EGy",
821+ "Q95720746": "hGy",
822+ "Q95720749": "mlx",
823+ "Q95720758": "μlx",
824+ "Q95720773": "dalx",
825+ "Q95720777": "hlx",
826+ "Q95720781": "dlx",
827+ "Q95720786": "clx",
828+ "Q95857671": "zSv",
829+ "Q95859071": "fSv",
830+ "Q95860960": "daSv",
831+ "Q95861107": "hSv",
832+ "Q95861296": "dSv",
833+ "Q95862182": "ESv",
834+ "Q95863358": "cSv",
835+ "Q95863591": "ySv",
836+ "Q95863894": "pSv",
837+ "Q95864194": "zBq",
838+ "Q95864378": "fBq",
839+ "Q95864695": "daBq",
840+ "Q95864940": "hBq",
841+ "Q95865286": "dBq",
842+ "Q95865530": "EBq",
843+ "Q95865716": "cBq",
844+ "Q95865877": "yBq",
845+ "Q95866173": "pBq",
846+ "Q95866344": "nBq",
847+ "Q95866767": "mBq",
848+ "Q95867993": "mN",
849+ "Q95948345": "crad",
850+ "Q95948364": "drad",
851+ "Q95948734": "daN",
852+ "Q95948739": "hN",
853+ "Q95948747": "dN",
854+ "Q95976839": "Plm",
855+ "Q95976853": "Glm",
856+ "Q95976869": "Ylm",
857+ "Q95976889": "ylm",
858+ "Q95976917": "dlm",
859+ "Q95976919": "Elm",
860+ "Q95976921": "nT",
861+ "Q95993516": "TN",
862+ "Q95993522": "nN",
863+ "Q95993524": "fN",
864+ "Q95993526": "yN",
865+ "Q95993528": "zN",
866+ "Q95993530": "EN",
867+ "Q95993532": "pN",
868+ "Q95993537": "μrad",
869+ "Q95993542": "nrad",
870+ "Q95993547": "frad",
871+ "Q95993553": "prad",
872+ "Q95993554": "darad",
873+ "Q95993557": "hrad",
874+ "Q95993619": "pT",
875+ "Q96025401": "daT",
876+ "Q96025405": "Trad",
877+ "Q96025407": "Zrad",
878+ "Q96025409": "zrad",
879+ "Q96025413": "yrad",
880+ "Q96025414": "Erad",
881+ "Q96025419": "Ylx",
882+ "Q96025422": "Glx",
883+ "Q96025427": "Plx",
884+ "Q96025431": "Mlx",
885+ "Q96025433": "Tlx",
886+ "Q96025435": "nlx",
887+ "Q96025441": "flx",
888+ "Q96050953": "GH",
889+ "Q96051010": "PH",
890+ "Q96051029": "YH",
891+ "Q96051052": "cH",
892+ "Q96051074": "TH",
893+ "Q96051106": "MH",
894+ "Q96051123": "kH",
895+ "Q96051126": "fH",
896+ "Q96051133": "yH",
897+ "Q96051139": "hH",
898+ "Q96051142": "dH",
899+ "Q96051144": "EH",
900+ "Q96051150": "pH",
901+ "Q96051160": "daH",
902+ "Q96051186": "zH",
903+ "Q96051199": "aH",
904+ "Q96051245": "ylx",
905+ "Q96051267": "Elx",
906+ "Q96051282": "plx",
907+ "Q96051312": "zlx",
908+ "Q96070067": "PT",
909+ "Q96070074": "YT",
910+ "Q96070076": "GT",
911+ "Q96070087": "cT",
912+ "Q96070103": "MT",
913+ "Q96070125": "hT",
914+ "Q96070145": "fT",
915+ "Q96070174": "TT",
916+ "Q96070195": "zT",
917+ "Q96070247": "yT",
918+ "Q96070254": "dT",
919+ "Q96070264": "ET",
920+ "Q96070276": "m°C",
921+ "Q96070318": "dsr",
922+ "Q96070329": "nsr",
923+ "Q96070341": "psr",
924+ "Q96095866": "fsr",
925+ "Q96095897": "zsr",
926+ "Q96095917": "ysr",
927+ "Q96095927": "dasr",
928+ "Q96095928": "hsr",
929+ "Q96095931": "ksr",
930+ "Q96095933": "Msr",
931+ "Q96095939": "Gsr",
932+ "Q96095941": "μ°C",
933+ "Q96095955": "n°C",
934+ "Q96095960": "k°C",
935+ "Q96106290": "Tsr",
936+ "Q96106298": "Psr",
937+ "Q96106311": "Esr",
938+ "Q96106319": "Zsr",
939+ "Q96106332": "Ysr",
940+ "Q96106346": "c°C",
941+ "Q96106360": "d°C",
942+ "Q96106368": "da°C",
943+ "Q96106385": "h°C",
944+ "Q96106393": "M°C",
945+ "Q96236286": "G°C",
946+ "Q97059641": "p°C",
947+ "Q97059652": "T°C",
948+ "Q97143826": "P°C",
949+ "Q97143831": "y°C",
950+ "Q97143835": "f°C",
951+ "Q97143838": "Z°C",
952+ "Q97143842": "E°C",
953+ "Q97143843": "z°C",
954+ "Q97143849": "Y°C",
955+ "Q97143851": "a°C",
956+ "Q98538634": "eV/m²",
957+ "Q98635536": "eV/m",
958+ "Q98642859": "eV m²/kg",
959+ "Q11229": "%",
960+ "Q11570": "kg",
961+ "Q11573": "m",
962+ "Q11574": "s",
963+ "Q11579": "K",
964+ "Q11582": "L",
965+ "Q12129": "pc",
966+ "Q12438": "N",
967+ "Q16068": "DM",
968+ "Q1811": "ua",
969+ "Q20764": "Myr",
970+ "Q2101": "e",
971+ "Q25235": "h",
972+ "Q25236": "W",
973+ "Q25250": "V",
974+ "Q25267": "°C",
975+ "Q25269": "J",
976+ "Q25272": "A",
977+ "Q25343": "m²",
978+ "Q25406": "C",
979+ "Q25517": "m³",
980+ "Q33680": "rad",
981+ "Q35852": "ha",
982+ "Q36384": "equiv",
983+ "Q3710": "ft",
984+ "Q39274": "Sv",
985+ "Q39369": "Hz",
986+ "Q41509": "mol",
987+ "Q41803": "g",
988+ "Q42289": "°F",
989+ "Q4406": "TV$",
990+ "Q44395": "Pa",
991+ "Q4587": "Le",
992+ "Q4588": "WS$",
993+ "Q4592": "F$",
994+ "Q4596": "Rs",
995+ "Q4597": "$",
996+ "Q47083": "Ω",
997+ "Q48013": "oz",
998+ "Q50094": "Np",
999+ "Q50098": "B",
1000+ "Q531": "ly",
1001+ "Q5329": "dB",
1002+ "Q573": "d",
1003+ "Q577": "a",
1004+ "Q7727": "min",
1005+ "Q8799": "B"
1006+}
searx/engines/duckduckgo_definitions.py+156−71
@@ -12,28 +12,53 @@ DuckDuckGo (definitions)
1212import json
1313from urllib.parse import urlencode
1414from lxml import html
15−from re import compile
15+
16+from searx import logger
17+from searx.data import WIKIDATA_UNITS
1618from searx.engines.duckduckgo import _fetch_supported_languages, supported_languages_url, language_aliases
17−from searx.utils import extract_text, html_to_text, match_language
19+from searx.utils import extract_text, html_to_text, match_language, get_string_replaces_function
20+from searx.external_urls import get_external_url, get_earth_coordinates_url, area_to_osm_zoom
21+
22+
23+logger = logger.getChild('duckduckgo_definitions')
1824
19−url = 'https://api.duckduckgo.com/'\
25+URL = 'https://api.duckduckgo.com/'\
2026 + '?{query}&format=json&pretty=0&no_redirect=1&d=1'
2127
22−http_regex = compile(r'^http:')
28+WIKIDATA_PREFIX = [
29+ 'http://www.wikidata.org/entity/',
30+ 'https://www.wikidata.org/entity/'
31+]
32+
33+replace_http_by_https = get_string_replaces_function({'http:': 'https:'})
34+
35+
36+def is_broken_text(text):
37+ """ duckduckgo may return something like "<a href="xxxx">http://somewhere Related website<a/>"
2338
39+ The href URL is broken, the "Related website" may contains some HTML.
2440
25−def result_to_text(url, text, htmlResult):
41+ The best solution seems to ignore these results.
42+ """
43+ return text.startswith('http') and ' ' in text
44+
45+
46+def result_to_text(text, htmlResult):
2647 # TODO : remove result ending with "Meaning" or "Category"
48+ result = None
2749 dom = html.fromstring(htmlResult)
2850 a = dom.xpath('//a')
2951 if len(a) >= 1:
30− return extract_text(a[0])
52+ result = extract_text(a[0])
3153 else:
32− return text
54+ result = text
55+ if not is_broken_text(result):
56+ return result
57+ return None
3358
3459
3560def request(query, params):
36− params['url'] = url.format(query=urlencode({'q': query}))
61+ params['url'] = URL.format(query=urlencode({'q': query}))
3762 language = match_language(params['language'], supported_languages, language_aliases)
3863 language = language.split('-')[0]
3964 params['headers']['Accept-Language'] = language
@@ -45,6 +70,14 @@ def response(resp):
4570
4671 search_res = json.loads(resp.text)
4772
73+ # search_res.get('Entity') possible values (not exhaustive) :
74+ # * continent / country / department / location / waterfall
75+ # * actor / musician / artist
76+ # * book / performing art / film / television / media franchise / concert tour / playwright
77+ # * prepared food
78+ # * website / software / os / programming language / file format / software engineer
79+ # * compagny
80+
4881 content = ''
4982 heading = search_res.get('Heading', '')
5083 attributes = []
@@ -55,7 +88,8 @@ def response(resp):
5588 # add answer if there is one
5689 answer = search_res.get('Answer', '')
5790 if answer:
58− if search_res.get('AnswerType', '') not in ['calc']:
91+ logger.debug('AnswerType="%s" Answer="%s"', search_res.get('AnswerType'), answer)
92+ if search_res.get('AnswerType') not in ['calc', 'ip']:
5993 results.append({'answer': html_to_text(answer)})
6094
6195 # add infobox
@@ -66,42 +100,36 @@ def response(resp):
66100 content = content + search_res.get('Abstract', '')
67101
68102 # image
69− image = search_res.get('Image', '')
103+ image = search_res.get('Image')
70104 image = None if image == '' else image
71105
72− # attributes
73− if 'Infobox' in search_res:
74− infobox = search_res.get('Infobox', None)
75− if 'content' in infobox:
76− for info in infobox.get('content'):
77− attributes.append({'label': info.get('label'),
78− 'value': info.get('value')})
79−
80106 # urls
107+ # Official website, Wikipedia page
81108 for ddg_result in search_res.get('Results', []):
82− if 'FirstURL' in ddg_result:
83− firstURL = ddg_result.get('FirstURL', '')
84− text = ddg_result.get('Text', '')
109+ firstURL = ddg_result.get('FirstURL')
110+ text = ddg_result.get('Text')
111+ if firstURL is not None and text is not None:
85112 urls.append({'title': text, 'url': firstURL})
86113 results.append({'title': heading, 'url': firstURL})
87114
88115 # related topics
89116 for ddg_result in search_res.get('RelatedTopics', []):
90117 if 'FirstURL' in ddg_result:
91− suggestion = result_to_text(ddg_result.get('FirstURL', None),
92− ddg_result.get('Text', None),
93− ddg_result.get('Result', None))
94− if suggestion != heading:
95− results.append({'suggestion': suggestion})
118+ firstURL = ddg_result.get('FirstURL')
119+ text = ddg_result.get('Text')
120+ if not is_broken_text(text):
121+ suggestion = result_to_text(text,
122+ ddg_result.get('Result'))
123+ if suggestion != heading and suggestion is not None:
124+ results.append({'suggestion': suggestion})
96125 elif 'Topics' in ddg_result:
97126 suggestions = []
98127 relatedTopics.append({'name': ddg_result.get('Name', ''),
99− 'suggestions': suggestions})
128+ 'suggestions': suggestions})
100129 for topic_result in ddg_result.get('Topics', []):
101− suggestion = result_to_text(topic_result.get('FirstURL', None),
102− topic_result.get('Text', None),
103− topic_result.get('Result', None))
104− if suggestion != heading:
130+ suggestion = result_to_text(topic_result.get('Text'),
131+ topic_result.get('Result'))
132+ if suggestion != heading and suggestion is not None:
105133 suggestions.append(suggestion)
106134
107135 # abstract
@@ -110,7 +138,10 @@ def response(resp):
110138 # add as result ? problem always in english
111139 infobox_id = abstractURL
112140 urls.append({'title': search_res.get('AbstractSource'),
113− 'url': abstractURL})
141+ 'url': abstractURL,
142+ 'official': True})
143+ results.append({'url': abstractURL,
144+ 'title': heading})
114145
115146 # definition
116147 definitionURL = search_res.get('DefinitionURL', '')
@@ -118,53 +149,107 @@ def response(resp):
118149 # add as result ? as answer ? problem always in english
119150 infobox_id = definitionURL
120151 urls.append({'title': search_res.get('DefinitionSource'),
121− 'url': definitionURL})
152+ 'url': definitionURL})
122153
123154 # to merge with wikidata's infobox
124155 if infobox_id:
125− infobox_id = http_regex.sub('https:', infobox_id)
126−
127− # entity
128− entity = search_res.get('Entity', None)
129− # TODO continent / country / department / location / waterfall /
130− # mountain range :
131− # link to map search, get weather, near by locations
132− # TODO musician : link to music search
133− # TODO concert tour : ??
134− # TODO film / actor / television / media franchise :
135− # links to IMDB / rottentomatoes (or scrap result)
136− # TODO music : link tu musicbrainz / last.fm
137− # TODO book : ??
138− # TODO artist / playwright : ??
139− # TODO compagny : ??
140− # TODO software / os : ??
141− # TODO software engineer : ??
142− # TODO prepared food : ??
143− # TODO website : ??
144− # TODO performing art : ??
145− # TODO prepared food : ??
146− # TODO programming language : ??
147− # TODO file format : ??
156+ infobox_id = replace_http_by_https(infobox_id)
157+
158+ # attributes
159+ # some will be converted to urls
160+ if 'Infobox' in search_res:
161+ infobox = search_res.get('Infobox')
162+ if 'content' in infobox:
163+ osm_zoom = 17
164+ coordinates = None
165+ for info in infobox.get('content'):
166+ data_type = info.get('data_type')
167+ data_label = info.get('label')
168+ data_value = info.get('value')
169+
170+ # Workaround: ddg may return a double quote
171+ if data_value == '""':
172+ continue
173+
174+ # Is it an external URL ?
175+ # * imdb_id / facebook_profile / youtube_channel / youtube_video / twitter_profile
176+ # * instagram_profile / rotten_tomatoes / spotify_artist_id / itunes_artist_id / soundcloud_id
177+ # * netflix_id
178+ external_url = get_external_url(data_type, data_value)
179+ if external_url is not None:
180+ urls.append({'title': data_label,
181+ 'url': external_url})
182+ elif data_type in ['instance', 'wiki_maps_trigger', 'google_play_artist_id']:
183+ # ignore instance: Wikidata value from "Instance Of" (Qxxxx)
184+ # ignore wiki_maps_trigger: reference to a javascript
185+ # ignore google_play_artist_id: service shutdown
186+ pass
187+ elif data_type == 'string' and data_label == 'Website':
188+ # There is already an URL for the website
189+ pass
190+ elif data_type == 'area':
191+ attributes.append({'label': data_label,
192+ 'value': area_to_str(data_value),
193+ 'entity': 'P2046'})
194+ osm_zoom = area_to_osm_zoom(data_value.get('amount'))
195+ elif data_type == 'coordinates':
196+ if data_value.get('globe') == 'http://www.wikidata.org/entity/Q2':
197+ # coordinate on Earth
198+ # get the zoom information from the area
199+ coordinates = info
200+ else:
201+ # coordinate NOT on Earth
202+ attributes.append({'label': data_label,
203+ 'value': data_value,
204+ 'entity': 'P625'})
205+ elif data_type == 'string':
206+ attributes.append({'label': data_label,
207+ 'value': data_value})
208+
209+ if coordinates:
210+ data_label = coordinates.get('label')
211+ data_value = coordinates.get('value')
212+ latitude = data_value.get('latitude')
213+ longitude = data_value.get('longitude')
214+ url = get_earth_coordinates_url(latitude, longitude, osm_zoom)
215+ urls.append({'title': 'OpenStreetMap',
216+ 'url': url,
217+ 'entity': 'P625'})
148218
149219 if len(heading) > 0:
150220 # TODO get infobox.meta.value where .label='article_title'
151221 if image is None and len(attributes) == 0 and len(urls) == 1 and\
152222 len(relatedTopics) == 0 and len(content) == 0:
153− results.append({
154− 'url': urls[0]['url'],
155− 'title': heading,
156− 'content': content
157− })
223+ results.append({'url': urls[0]['url'],
224+ 'title': heading,
225+ 'content': content})
158226 else:
159− results.append({
160− 'infobox': heading,
161− 'id': infobox_id,
162− 'entity': entity,
163− 'content': content,
164− 'img_src': image,
165− 'attributes': attributes,
166− 'urls': urls,
167− 'relatedTopics': relatedTopics
168− })
227+ results.append({'infobox': heading,
228+ 'id': infobox_id,
229+ 'content': content,
230+ 'img_src': image,
231+ 'attributes': attributes,
232+ 'urls': urls,
233+ 'relatedTopics': relatedTopics})
169234
170235 return results
236+
237+
238+def unit_to_str(unit):
239+ for prefix in WIKIDATA_PREFIX:
240+ if unit.startswith(prefix):
241+ wikidata_entity = unit[len(prefix):]
242+ return WIKIDATA_UNITS.get(wikidata_entity, unit)
243+ return unit
244+
245+
246+def area_to_str(area):
247+ """parse {'unit': 'http://www.wikidata.org/entity/Q712226', 'amount': '+20.99'}"""
248+ unit = unit_to_str(area.get('unit'))
249+ if unit is not None:
250+ try:
251+ amount = float(area.get('amount'))
252+ return '{} {}'.format(amount, unit)
253+ except ValueError:
254+ pass
255+ return '{} {}'.format(area.get('amount', ''), area.get('unit', ''))
searx/engines/wikidata.py+637−452
@@ -3,501 +3,686 @@
33 Wikidata
44
55 @website https://wikidata.org
6− @provide-api yes (https://wikidata.org/w/api.php)
6+ @provide-api yes (https://query.wikidata.org/)
77
8− @using-api partially (most things require scraping)
9− @results JSON, HTML
10− @stable no (html can change)
8+ @using-api yes
9+ @results JSON
10+ @stable yes
1111 @parse url, infobox
1212"""
1313
14−from searx import logger
15−from searx.poolrequests import get
16−from searx.engines.wikipedia import _fetch_supported_languages, supported_languages_url
17−from searx.utils import extract_text, match_language, eval_xpath
1814
1915from urllib.parse import urlencode
2016from json import loads
21−from lxml.html import fromstring
22−from lxml import etree
17+
18+from dateutil.parser import isoparse
19+from babel.dates import format_datetime, format_date, format_time, get_datetime_format
20+
21+from searx import logger
22+from searx.data import WIKIDATA_UNITS
23+from searx.poolrequests import post, get
24+from searx.engines.wikipedia import _fetch_supported_languages, supported_languages_url
25+from searx.utils import match_language, searx_useragent, get_string_replaces_function
26+from searx.external_urls import get_external_url, get_earth_coordinates_url, area_to_osm_zoom
2327
2428logger = logger.getChild('wikidata')
25−result_count = 1
26−
27−# urls
28−wikidata_host = 'https://www.wikidata.org'
29−url_search = wikidata_host \
30− + '/w/index.php?{query}&ns0=1'
31−
32−wikidata_api = wikidata_host + '/w/api.php'
33−url_detail = wikidata_api\
34− + '?action=parse&format=json&{query}'\
35− + '&redirects=1&prop=text%7Cdisplaytitle%7Cparsewarnings'\
36− + '&disableeditsection=1&preview=1&sectionpreview=1&disabletoc=1&utf8=1&formatversion=2'
37−
38−url_map = 'https://www.openstreetmap.org/'\
39− + '?lat={latitude}&lon={longitude}&zoom={zoom}&layers=M'
40−url_image = 'https://commons.wikimedia.org/wiki/Special:FilePath/{filename}?width=500&height=400'
41−
42−# xpaths
43−div_ids_xpath = '//div[@id]'
44−wikidata_ids_xpath = '//ul[@class="mw-search-results"]/li//a/@href'
45−title_xpath = '//*[contains(@class,"wikibase-title-label")]'
46−description_xpath = '//div[contains(@class,"wikibase-entitytermsview-heading-description")]'
47−label_xpath = './/div[contains(@class,"wikibase-statementgroupview-property-label")]/a'
48−url_xpath = './/a[contains(@class,"external free") or contains(@class, "wb-external-id")]'
49−wikilink_xpath = './/ul[contains(@class,"wikibase-sitelinklistview-listview")]'\
50− + '/li[contains(@data-wb-siteid,"{wikiid}")]//a/@href'
51−property_row_xpath = './/div[contains(@class,"wikibase-statementview")]'
52−preferred_rank_xpath = './/span[contains(@class,"wikibase-rankselector-preferred")]'
53−value_xpath = './/div[contains(@class,"wikibase-statementview-mainsnak")]'\
54− + '/*/div[contains(@class,"wikibase-snakview-value")]'
55−language_fallback_xpath = '//sup[contains(@class,"wb-language-fallback-indicator")]'
56−calendar_name_xpath = './/sup[contains(@class,"wb-calendar-name")]'
57−media_xpath = value_xpath + '//div[contains(@class,"commons-media-caption")]//a'
58−
59−
60−def get_id_cache(result):
61− id_cache = {}
62− for e in eval_xpath(result, div_ids_xpath):
63− id = e.get('id')
64− if id.startswith('P'):
65− id_cache[id] = e
66− return id_cache
6729
30+# SPARQL
31+SPARQL_ENDPOINT_URL = 'https://query.wikidata.org/sparql'
32+SPARQL_EXPLAIN_URL = 'https://query.wikidata.org/bigdata/namespace/wdq/sparql?explain'
33+WIKIDATA_PROPERTIES = {
34+ 'P434': 'MusicBrainz',
35+ 'P435': 'MusicBrainz',
36+ 'P436': 'MusicBrainz',
37+ 'P966': 'MusicBrainz',
38+ 'P345': 'IMDb',
39+ 'P2397': 'YouTube',
40+ 'P1651': 'YouTube',
41+ 'P2002': 'Twitter',
42+ 'P2013': 'Facebook',
43+ 'P2003': 'Instagram',
44+}
45+
46+# SERVICE wikibase:mwapi : https://www.mediawiki.org/wiki/Wikidata_Query_Service/User_Manual/MWAPI
47+# SERVICE wikibase:label: https://en.wikibooks.org/wiki/SPARQL/SERVICE_-_Label#Manual_Label_SERVICE
48+# https://en.wikibooks.org/wiki/SPARQL/WIKIDATA_Precision,_Units_and_Coordinates
49+# https://www.mediawiki.org/wiki/Wikibase/Indexing/RDF_Dump_Format#Data_model
50+# optmization:
51+# * https://www.wikidata.org/wiki/Wikidata:SPARQL_query_service/query_optimization
52+# * https://github.com/blazegraph/database/wiki/QueryHints
53+QUERY_TEMPLATE = """
54+SELECT ?item ?itemLabel ?itemDescription ?lat ?long %SELECT%
55+WHERE
56+{
57+ SERVICE wikibase:mwapi {
58+ bd:serviceParam wikibase:endpoint "www.wikidata.org";
59+ wikibase:api "EntitySearch";
60+ wikibase:limit 1;
61+ mwapi:search "%QUERY%";
62+ mwapi:language "%LANGUAGE%".
63+ ?item wikibase:apiOutputItem mwapi:item.
64+ }
65+
66+ %WHERE%
67+
68+ SERVICE wikibase:label {
69+ bd:serviceParam wikibase:language "%LANGUAGE%,en".
70+ ?item rdfs:label ?itemLabel .
71+ ?item schema:description ?itemDescription .
72+ %WIKIBASE_LABELS%
73+ }
74+
75+}
76+GROUP BY ?item ?itemLabel ?itemDescription ?lat ?long %GROUP_BY%
77+"""
6878
69−def request(query, params):
70− params['url'] = url_search.format(
71− query=urlencode({'search': query}))
72− return params
79+# Get the calendar names and the property names
80+QUERY_PROPERTY_NAMES = """
81+SELECT ?item ?name
82+WHERE {
83+ {
84+ SELECT ?item
85+ WHERE { ?item wdt:P279* wd:Q12132 }
86+ } UNION {
87+ VALUES ?item { %ATTRIBUTES% }
88+ }
89+ OPTIONAL { ?item rdfs:label ?name. }
90+}
91+"""
7392
7493
75−def response(resp):
76− results = []
77− htmlparser = etree.HTMLParser()
78− html = fromstring(resp.content.decode(), parser=htmlparser)
79− search_results = eval_xpath(html, wikidata_ids_xpath)
94+# https://www.w3.org/TR/sparql11-query/#rSTRING_LITERAL1
95+# https://lists.w3.org/Archives/Public/public-rdf-dawg/2011OctDec/0175.html
96+sparql_string_escape = get_string_replaces_function({'\t': '\\\t',
97+ '\n': '\\\n',
98+ '\r': '\\\r',
99+ '\b': '\\\b',
100+ '\f': '\\\f',
101+ '\"': '\\\"',
102+ '\'': '\\\'',
103+ '\\': '\\\\'})
104+
105+replace_http_by_https = get_string_replaces_function({'http:': 'https:'})
106+
107+
108+def get_headers():
109+ # user agent: https://www.mediawiki.org/wiki/Wikidata_Query_Service/User_Manual#Query_limits
110+ return {
111+ 'Accept': 'application/sparql-results+json',
112+ 'User-Agent': searx_useragent()
113+ }
114+
115+
116+def get_label_for_entity(entity_id, language):
117+ name = WIKIDATA_PROPERTIES.get(entity_id)
118+ if name is None:
119+ name = WIKIDATA_PROPERTIES.get((entity_id, language))
120+ if name is None:
121+ name = WIKIDATA_PROPERTIES.get((entity_id, language.split('-')[0]))
122+ if name is None:
123+ name = WIKIDATA_PROPERTIES.get((entity_id, 'en'))
124+ if name is None:
125+ name = entity_id
126+ return name
127+
128+
129+def send_wikidata_query(query, method='GET'):
130+ if method == 'GET':
131+ # query will be cached by wikidata
132+ http_response = get(SPARQL_ENDPOINT_URL + '?' + urlencode({'query': query}), headers=get_headers())
133+ else:
134+ # query won't be cached by wikidata
135+ http_response = post(SPARQL_ENDPOINT_URL, data={'query': query}, headers=get_headers())
136+ if http_response.status_code != 200:
137+ logger.debug('SPARQL endpoint error %s', http_response.content.decode())
138+ logger.debug('request time %s', str(http_response.elapsed))
139+ http_response.raise_for_status()
140+ return loads(http_response.content.decode())
141+
80142
81− if resp.search_params['language'].split('-')[0] == 'all':
143+def request(query, params):
144+ language = params['language'].split('-')[0]
145+ if language == 'all':
82146 language = 'en'
83147 else:
84− language = match_language(resp.search_params['language'], supported_languages, language_aliases).split('-')[0]
148+ language = match_language(params['language'], supported_languages, language_aliases).split('-')[0]
149+
150+ query, attributes = get_query(query, language)
85151
86− # TODO: make requests asynchronous to avoid timeout when result_count > 1
87− for search_result in search_results[:result_count]:
88− wikidata_id = search_result.split('/')[-1]
89− url = url_detail.format(query=urlencode({'page': wikidata_id, 'uselang': language}))
90− htmlresponse = get(url)
91− jsonresponse = loads(htmlresponse.content.decode())
92− results += getDetail(jsonresponse, wikidata_id, language, resp.search_params['language'], htmlparser)
152+ params['method'] = 'POST'
153+ params['url'] = SPARQL_ENDPOINT_URL
154+ params['data'] = {'query': query}
155+ params['headers'] = get_headers()
156+
157+ params['language'] = language
158+ params['attributes'] = attributes
159+ return params
160+
161+
162+def response(resp):
163+ results = []
164+ if resp.status_code != 200:
165+ logger.debug('SPARQL endpoint error %s', resp.content.decode())
166+ resp.raise_for_status()
167+ jsonresponse = loads(resp.content.decode())
168+
169+ language = resp.search_params['language'].lower()
170+ attributes = resp.search_params['attributes']
171+
172+ seen_entities = set()
173+
174+ for result in jsonresponse.get('results', {}).get('bindings', []):
175+ attribute_result = {key: value['value'] for key, value in result.items()}
176+ entity_url = attribute_result['item']
177+ if entity_url not in seen_entities:
178+ seen_entities.add(entity_url)
179+ results += get_results(attribute_result, attributes, language)
180+ else:
181+ logger.debug('The SPARQL request returns duplicate entities: %s', str(attribute_result))
93182
94183 return results
95184
96185
97−def getDetail(jsonresponse, wikidata_id, language, locale, htmlparser):
186+def get_results(attribute_result, attributes, language):
98187 results = []
99− urls = []
100− attributes = []
188+ infobox_title = attribute_result.get('itemLabel')
189+ infobox_id = attribute_result['item']
190+ infobox_id_lang = None
191+ infobox_urls = []
192+ infobox_attributes = []
193+ infobox_content = attribute_result.get('itemDescription')
194+ img_src = None
195+ img_src_priority = 100
196+
197+ for attribute in attributes:
198+ value = attribute.get_str(attribute_result, language)
199+ if value is not None and value != '':
200+ attribute_type = type(attribute)
201+
202+ if attribute_type in (WDURLAttribute, WDArticle):
203+ # get_select() method : there is group_concat(distinct ...;separator=", ")
204+ # split the value here
205+ for url in value.split(', '):
206+ infobox_urls.append({'title': attribute.get_label(language), 'url': url, **attribute.kwargs})
207+ # "normal" results (not infobox) include official website and Wikipedia links.
208+ if attribute.kwargs.get('official') or attribute_type == WDArticle:
209+ results.append({'title': infobox_title, 'url': url})
210+ # update the infobox_id with the wikipedia URL
211+ # first the local wikipedia URL, and as fallback the english wikipedia URL
212+ if attribute_type == WDArticle\
213+ and ((attribute.language == 'en' and infobox_id_lang is None)
214+ or attribute.language != 'en'):
215+ infobox_id_lang = attribute.language
216+ infobox_id = url
217+ elif attribute_type == WDImageAttribute:
218+ # this attribute is an image.
219+ # replace the current image only the priority is lower
220+ # (the infobox contain only one image).
221+ if attribute.priority < img_src_priority:
222+ img_src = value
223+ img_src_priority = attribute.priority
224+ elif attribute_type == WDGeoAttribute:
225+ # geocoordinate link
226+ # use the area to get the OSM zoom
227+ # Note: ignre the unit (must be km² otherwise the calculation is wrong)
228+ # Should use normalized value p:P2046/psn:P2046/wikibase:quantityAmount
229+ area = attribute_result.get('P2046')
230+ osm_zoom = area_to_osm_zoom(area) if area else 19
231+ url = attribute.get_str(attribute_result, language, osm_zoom=osm_zoom)
232+ if url:
233+ infobox_urls.append({'title': attribute.get_label(language),
234+ 'url': url,
235+ 'entity': attribute.name})
236+ else:
237+ infobox_attributes.append({'label': attribute.get_label(language),
238+ 'value': value,
239+ 'entity': attribute.name})
240+
241+ if infobox_id:
242+ infobox_id = replace_http_by_https(infobox_id)
101243
102− title = jsonresponse.get('parse', {}).get('displaytitle', {})
103− result = jsonresponse.get('parse', {}).get('text', {})
104−
105− if not title or not result:
106− return results
107−
108− title = fromstring(title, parser=htmlparser)
109− for elem in eval_xpath(title, language_fallback_xpath):
110− elem.getparent().remove(elem)
111− title = extract_text(eval_xpath(title, title_xpath))
112−
113− result = fromstring(result, parser=htmlparser)
114− for elem in eval_xpath(result, language_fallback_xpath):
115− elem.getparent().remove(elem)
116−
117− description = extract_text(eval_xpath(result, description_xpath))
118−
119− id_cache = get_id_cache(result)
120−
121− # URLS
122−
123− # official website
124− add_url(urls, result, id_cache, 'P856', results=results)
125−
126− # wikipedia
127− wikipedia_link_count = 0
128− wikipedia_link = get_wikilink(result, language + 'wiki')
129− if wikipedia_link:
130− wikipedia_link_count += 1
131− urls.append({'title': 'Wikipedia (' + language + ')',
132− 'url': wikipedia_link})
133−
134− if language != 'en':
135− wikipedia_en_link = get_wikilink(result, 'enwiki')
136− if wikipedia_en_link:
137− wikipedia_link_count += 1
138− urls.append({'title': 'Wikipedia (en)',
139− 'url': wikipedia_en_link})
140−
141− # TODO: get_wiki_firstlanguage
142− # if wikipedia_link_count == 0:
143−
144− # more wikis
145− add_url(urls, result, id_cache, default_label='Wikivoyage (' + language + ')', link_type=language + 'wikivoyage')
146− add_url(urls, result, id_cache, default_label='Wikiquote (' + language + ')', link_type=language + 'wikiquote')
147− add_url(urls, result, id_cache, default_label='Wikimedia Commons', link_type='commonswiki')
148−
149− add_url(urls, result, id_cache, 'P625', 'OpenStreetMap', link_type='geo')
150−
151− # musicbrainz
152− add_url(urls, result, id_cache, 'P434', 'MusicBrainz', 'http://musicbrainz.org/artist/')
153− add_url(urls, result, id_cache, 'P435', 'MusicBrainz', 'http://musicbrainz.org/work/')
154− add_url(urls, result, id_cache, 'P436', 'MusicBrainz', 'http://musicbrainz.org/release-group/')
155− add_url(urls, result, id_cache, 'P966', 'MusicBrainz', 'http://musicbrainz.org/label/')
156−
157− # IMDb
158− add_url(urls, result, id_cache, 'P345', 'IMDb', 'https://www.imdb.com/', link_type='imdb')
159− # source code repository
160− add_url(urls, result, id_cache, 'P1324')
161− # blog
162− add_url(urls, result, id_cache, 'P1581')
163− # social media links
164− add_url(urls, result, id_cache, 'P2397', 'YouTube', 'https://www.youtube.com/channel/')
165− add_url(urls, result, id_cache, 'P1651', 'YouTube', 'https://www.youtube.com/watch?v=')
166− add_url(urls, result, id_cache, 'P2002', 'Twitter', 'https://twitter.com/')
167− add_url(urls, result, id_cache, 'P2013', 'Facebook', 'https://facebook.com/')
168− add_url(urls, result, id_cache, 'P2003', 'Instagram', 'https://instagram.com/')
169−
170− urls.append({'title': 'Wikidata',
171− 'url': 'https://www.wikidata.org/wiki/'
172− + wikidata_id + '?uselang=' + language})
173−
174− # INFOBOX ATTRIBUTES (ROWS)
175−
176− # DATES
177− # inception date
178− add_attribute(attributes, id_cache, 'P571', date=True)
179− # dissolution date
180− add_attribute(attributes, id_cache, 'P576', date=True)
181− # start date
182− add_attribute(attributes, id_cache, 'P580', date=True)
183− # end date
184− add_attribute(attributes, id_cache, 'P582', date=True)
185− # date of birth
186− add_attribute(attributes, id_cache, 'P569', date=True)
187− # date of death
188− add_attribute(attributes, id_cache, 'P570', date=True)
189− # date of spacecraft launch
190− add_attribute(attributes, id_cache, 'P619', date=True)
191− # date of spacecraft landing
192− add_attribute(attributes, id_cache, 'P620', date=True)
193−
194− # nationality
195− add_attribute(attributes, id_cache, 'P27')
196− # country of origin
197− add_attribute(attributes, id_cache, 'P495')
198− # country
199− add_attribute(attributes, id_cache, 'P17')
200− # headquarters
201− add_attribute(attributes, id_cache, 'Q180')
202−
203− # PLACES
204− # capital
205− add_attribute(attributes, id_cache, 'P36', trim=True)
206− # head of state
207− add_attribute(attributes, id_cache, 'P35', trim=True)
208− # head of government
209− add_attribute(attributes, id_cache, 'P6', trim=True)
210− # type of government
211− add_attribute(attributes, id_cache, 'P122')
212− # official language
213− add_attribute(attributes, id_cache, 'P37')
214− # population
215− add_attribute(attributes, id_cache, 'P1082', trim=True)
216− # area
217− add_attribute(attributes, id_cache, 'P2046')
218− # currency
219− add_attribute(attributes, id_cache, 'P38', trim=True)
220− # heigth (building)
221− add_attribute(attributes, id_cache, 'P2048')
222−
223− # MEDIA
224− # platform (videogames)
225− add_attribute(attributes, id_cache, 'P400')
226− # author
227− add_attribute(attributes, id_cache, 'P50')
228− # creator
229− add_attribute(attributes, id_cache, 'P170')
230− # director
231− add_attribute(attributes, id_cache, 'P57')
232− # performer
233− add_attribute(attributes, id_cache, 'P175')
234− # developer
235− add_attribute(attributes, id_cache, 'P178')
236− # producer
237− add_attribute(attributes, id_cache, 'P162')
238− # manufacturer
239− add_attribute(attributes, id_cache, 'P176')
240− # screenwriter
241− add_attribute(attributes, id_cache, 'P58')
242− # production company
243− add_attribute(attributes, id_cache, 'P272')
244− # record label
245− add_attribute(attributes, id_cache, 'P264')
246− # publisher
247− add_attribute(attributes, id_cache, 'P123')
248− # original network
249− add_attribute(attributes, id_cache, 'P449')
250− # distributor
251− add_attribute(attributes, id_cache, 'P750')
252− # composer
253− add_attribute(attributes, id_cache, 'P86')
254− # publication date
255− add_attribute(attributes, id_cache, 'P577', date=True)
256− # genre
257− add_attribute(attributes, id_cache, 'P136')
258− # original language
259− add_attribute(attributes, id_cache, 'P364')
260− # isbn
261− add_attribute(attributes, id_cache, 'Q33057')
262− # software license
263− add_attribute(attributes, id_cache, 'P275')
264− # programming language
265− add_attribute(attributes, id_cache, 'P277')
266− # version
267− add_attribute(attributes, id_cache, 'P348', trim=True)
268− # narrative location
269− add_attribute(attributes, id_cache, 'P840')
270−
271− # LANGUAGES
272− # number of speakers
273− add_attribute(attributes, id_cache, 'P1098')
274− # writing system
275− add_attribute(attributes, id_cache, 'P282')
276− # regulatory body
277− add_attribute(attributes, id_cache, 'P1018')
278− # language code
279− add_attribute(attributes, id_cache, 'P218')
280−
281− # OTHER
282− # ceo
283− add_attribute(attributes, id_cache, 'P169', trim=True)
284− # founder
285− add_attribute(attributes, id_cache, 'P112')
286− # legal form (company/organization)
287− add_attribute(attributes, id_cache, 'P1454')
288− # operator
289− add_attribute(attributes, id_cache, 'P137')
290− # crew members (tripulation)
291− add_attribute(attributes, id_cache, 'P1029')
292− # taxon
293− add_attribute(attributes, id_cache, 'P225')
294− # chemical formula
295− add_attribute(attributes, id_cache, 'P274')
296− # winner (sports/contests)
297− add_attribute(attributes, id_cache, 'P1346')
298− # number of deaths
299− add_attribute(attributes, id_cache, 'P1120')
300− # currency code
301− add_attribute(attributes, id_cache, 'P498')
302−
303− image = add_image(id_cache)
304−
305− if len(attributes) == 0 and len(urls) == 2 and len(description) == 0:
244+ # add the wikidata URL at the end
245+ infobox_urls.append({'title': 'Wikidata', 'url': attribute_result['item']})
246+
247+ if img_src is None and len(infobox_attributes) == 0 and len(infobox_urls) == 1 and\
248+ len(infobox_content) == 0:
306249 results.append({
307− 'url': urls[0]['url'],
308− 'title': title,
309− 'content': description
310− })
250+ 'url': infobox_urls[0]['url'],
251+ 'title': infobox_title,
252+ 'content': infobox_content
253+ })
311254 else:
312255 results.append({
313− 'infobox': title,
314− 'id': wikipedia_link,
315− 'content': description,
316− 'img_src': image,
317− 'attributes': attributes,
318− 'urls': urls
319− })
320−
256+ 'infobox': infobox_title,
257+ 'id': infobox_id,
258+ 'content': infobox_content,
259+ 'img_src': img_src,
260+ 'urls': infobox_urls,
261+ 'attributes': infobox_attributes
262+ })
321263 return results
322264
323265
324−# only returns first match
325−def add_image(id_cache):
326− # P15: route map, P242: locator map, P154: logo, P18: image, P242: map, P41: flag, P2716: collage, P2910: icon
327− property_ids = ['P15', 'P242', 'P154', 'P18', 'P242', 'P41', 'P2716', 'P2910']
266+def get_query(query, language):
267+ attributes = get_attributes(language)
268+ select = [a.get_select() for a in attributes]
269+ where = list(filter(lambda s: len(s) > 0, [a.get_where() for a in attributes]))
270+ wikibase_label = list(filter(lambda s: len(s) > 0, [a.get_wikibase_label() for a in attributes]))
271+ group_by = list(filter(lambda s: len(s) > 0, [a.get_group_by() for a in attributes]))
272+ query = QUERY_TEMPLATE\
273+ .replace('%QUERY%', sparql_string_escape(query))\
274+ .replace('%SELECT%', ' '.join(select))\
275+ .replace('%WHERE%', '\n '.join(where))\
276+ .replace('%WIKIBASE_LABELS%', '\n '.join(wikibase_label))\
277+ .replace('%GROUP_BY%', ' '.join(group_by))\
278+ .replace('%LANGUAGE%', language)
279+ return query, attributes
328280
329− for property_id in property_ids:
330− image = id_cache.get(property_id, None)
331− if image is not None:
332− image_name = eval_xpath(image, media_xpath)
333− image_src = url_image.replace('{filename}', extract_text(image_name[0]))
334− return image_src
335281
282+def get_attributes(language):
283+ attributes = []
336284
337−# setting trim will only returned high ranked rows OR the first row
338−def add_attribute(attributes, id_cache, property_id, default_label=None, date=False, trim=False):
339− attribute = id_cache.get(property_id, None)
340− if attribute is not None:
285+ def add_value(name):
286+ attributes.append(WDAttribute(name))
287+
288+ def add_amount(name):
289+ attributes.append(WDAmountAttribute(name))
290+
291+ def add_label(name):
292+ attributes.append(WDLabelAttribute(name))
293+
294+ def add_url(name, url_id=None, **kwargs):
295+ attributes.append(WDURLAttribute(name, url_id, kwargs))
296+
297+ def add_image(name, url_id=None, priority=1):
298+ attributes.append(WDImageAttribute(name, url_id, priority))
299+
300+ def add_date(name):
301+ attributes.append(WDDateAttribute(name))
302+
303+ # Dates
304+ for p in ['P571', # inception date
305+ 'P576', # dissolution date
306+ 'P580', # start date
307+ 'P582', # end date
308+ 'P569', # date of birth
309+ 'P570', # date of death
310+ 'P619', # date of spacecraft launch
311+ 'P620']: # date of spacecraft landing
312+ add_date(p)
313+
314+ for p in ['P27', # country of citizenship
315+ 'P495', # country of origin
316+ 'P17', # country
317+ 'P159']: # headquarters location
318+ add_label(p)
319+
320+ # Places
321+ for p in ['P36', # capital
322+ 'P35', # head of state
323+ 'P6', # head of government
324+ 'P122', # basic form of government
325+ 'P37']: # official language
326+ add_label(p)
327+
328+ add_value('P1082') # population
329+ add_amount('P2046') # area
330+ add_amount('P281') # postal code
331+ add_label('P38') # currency
332+ add_amount('P2048') # heigth (building)
333+
334+ # Media
335+ for p in ['P400', # platform (videogames, computing)
336+ 'P50', # author
337+ 'P170', # creator
338+ 'P57', # director
339+ 'P175', # performer
340+ 'P178', # developer
341+ 'P162', # producer
342+ 'P176', # manufacturer
343+ 'P58', # screenwriter
344+ 'P272', # production company
345+ 'P264', # record label
346+ 'P123', # publisher
347+ 'P449', # original network
348+ 'P750', # distributed by
349+ 'P86']: # composer
350+ add_label(p)
351+
352+ add_date('P577') # publication date
353+ add_label('P136') # genre (music, film, artistic...)
354+ add_label('P364') # original language
355+ add_value('P212') # ISBN-13
356+ add_value('P957') # ISBN-10
357+ add_label('P275') # copyright license
358+ add_label('P277') # programming language
359+ add_value('P348') # version
360+ add_label('P840') # narrative location
361+
362+ # Languages
363+ add_value('P1098') # number of speakers
364+ add_label('P282') # writing system
365+ add_label('P1018') # language regulatory body
366+ add_value('P218') # language code (ISO 639-1)
367+
368+ # Other
369+ add_label('P169') # ceo
370+ add_label('P112') # founded by
371+ add_label('P1454') # legal form (company, organization)
372+ add_label('P137') # operator (service, facility, ...)
373+ add_label('P1029') # crew members (tripulation)
374+ add_label('P225') # taxon name
375+ add_value('P274') # chemical formula
376+ add_label('P1346') # winner (sports, contests, ...)
377+ add_value('P1120') # number of deaths
378+ add_value('P498') # currency code (ISO 4217)
379+
380+ # URL
381+ add_url('P856', official=True) # official website
382+ attributes.append(WDArticle(language)) # wikipedia (user language)
383+ if not language.startswith('en'):
384+ attributes.append(WDArticle('en')) # wikipedia (english)
385+
386+ add_url('P1324') # source code repository
387+ add_url('P1581') # blog
388+ add_url('P434', url_id='musicbrainz_artist')
389+ add_url('P435', url_id='musicbrainz_work')
390+ add_url('P436', url_id='musicbrainz_release_group')
391+ add_url('P966', url_id='musicbrainz_label')
392+ add_url('P345', url_id='imdb_id')
393+ add_url('P2397', url_id='youtube_channel')
394+ add_url('P1651', url_id='youtube_video')
395+ add_url('P2002', url_id='twitter_profile')
396+ add_url('P2013', url_id='facebook_profile')
397+ add_url('P2003', url_id='instagram_profile')
398+
399+ # Map
400+ attributes.append(WDGeoAttribute('P625'))
401+
402+ # Image
403+ add_image('P15', priority=1, url_id='wikimedia_image') # route map
404+ add_image('P242', priority=2, url_id='wikimedia_image') # locator map
405+ add_image('P154', priority=3, url_id='wikimedia_image') # logo
406+ add_image('P18', priority=4, url_id='wikimedia_image') # image
407+ add_image('P41', priority=5, url_id='wikimedia_image') # flag
408+ add_image('P2716', priority=6, url_id='wikimedia_image') # collage
409+ add_image('P2910', priority=7, url_id='wikimedia_image') # icon
410+
411+ return attributes
412+
413+
414+class WDAttribute:
415+
416+ __slots__ = 'name',
417+
418+ def __init__(self, name):
419+ self.name = name
420+
421+ def get_select(self):
422+ return '(group_concat(distinct ?{name};separator=", ") as ?{name}s)'.replace('{name}', self.name)
423+
424+ def get_label(self, language):
425+ return get_label_for_entity(self.name, language)
426+
427+ def get_where(self):
428+ return "OPTIONAL { ?item wdt:{name} ?{name} . }".replace('{name}', self.name)
429+
430+ def get_wikibase_label(self):
431+ return ""
432+
433+ def get_group_by(self):
434+ return ""
435+
436+ def get_str(self, result, language):
437+ return result.get(self.name + 's')
341438
342− if default_label:
343− label = default_label
344− else:
345− label = extract_text(eval_xpath(attribute, label_xpath))
346− label = label[0].upper() + label[1:]
347−
348− if date:
349− trim = True
350− # remove calendar name
351− calendar_name = eval_xpath(attribute, calendar_name_xpath)
352− for calendar in calendar_name:
353− calendar.getparent().remove(calendar)
354−
355− concat_values = ""
356− values = []
357− first_value = None
358− for row in eval_xpath(attribute, property_row_xpath):
359− if not first_value or not trim or eval_xpath(row, preferred_rank_xpath):
360− value = eval_xpath(row, value_xpath)
361− if not value:
362− continue
363− value = extract_text(value)
364−
365− # save first value in case no ranked row is found
366− if trim and not first_value:
367− first_value = value
368− else:
369− # to avoid duplicate values
370− if value not in values:
371− concat_values += value + ", "
372− values.append(value)
373−
374− if trim and not values:
375− attributes.append({'label': label,
376− 'value': first_value})
377− else:
378− attributes.append({'label': label,
379− 'value': concat_values[:-2]})
439+ def __repr__(self):
440+ return '<' + str(type(self).__name__) + ':' + self.name + '>'
380441
381442
382−# requires property_id unless it's a wiki link (defined in link_type)
383−def add_url(urls, result, id_cache, property_id=None, default_label=None, url_prefix=None, results=None,
384− link_type=None, only_first=True):
385− links = []
443+class WDAmountAttribute(WDAttribute):
386444
387− # wiki links don't have property in wikidata page
388− if link_type and 'wiki' in link_type:
389− links.append(get_wikilink(result, link_type))
390− else:
391− dom_element = id_cache.get(property_id, None)
392− if dom_element is not None:
393− if not default_label:
394− label = extract_text(eval_xpath(dom_element, label_xpath))
395− label = label[0].upper() + label[1:]
445+ def get_select(self):
446+ return '?{name} ?{name}Unit'.replace('{name}', self.name)
396447
397− if link_type == 'geo':
398− links.append(get_geolink(dom_element))
448+ def get_where(self):
449+ return """ OPTIONAL { ?item p:{name} ?{name}Node .
450+ ?{name}Node rdf:type wikibase:BestRank ; ps:{name} ?{name} .
451+ OPTIONAL { ?{name}Node psv:{name}/wikibase:quantityUnit ?{name}Unit. } }""".replace('{name}', self.name)
399452
400− elif link_type == 'imdb':
401− links.append(get_imdblink(dom_element, url_prefix))
453+ def get_group_by(self):
454+ return self.get_select()
402455
403− else:
404− url_results = eval_xpath(dom_element, url_xpath)
405− for link in url_results:
406− if link is not None:
407− if url_prefix:
408− link = url_prefix + extract_text(link)
409− else:
410− link = extract_text(link)
411− links.append(link)
412−
413− # append urls
414− for url in links:
415− if url is not None:
416− u = {'title': default_label or label, 'url': url}
417− if property_id == 'P856':
418− u['official'] = True
419− u['domain'] = url.split('/')[2]
420− urls.append(u)
421− if results is not None:
422− results.append(u)
423− if only_first:
424− break
425−
426−
427−def get_imdblink(result, url_prefix):
428− imdb_id = eval_xpath(result, value_xpath)
429− if imdb_id:
430− imdb_id = extract_text(imdb_id)
431− id_prefix = imdb_id[:2]
432− if id_prefix == 'tt':
433− url = url_prefix + 'title/' + imdb_id
434− elif id_prefix == 'nm':
435− url = url_prefix + 'name/' + imdb_id
436− elif id_prefix == 'ch':
437− url = url_prefix + 'character/' + imdb_id
438− elif id_prefix == 'co':
439− url = url_prefix + 'company/' + imdb_id
440− elif id_prefix == 'ev':
441− url = url_prefix + 'event/' + imdb_id
442− else:
443− url = None
444− return url
456+ def get_str(self, result, language):
457+ value = result.get(self.name)
458+ unit = result.get(self.name + "Unit")
459+ if unit is not None:
460+ unit = unit.replace('http://www.wikidata.org/entity/', '')
461+ return value + " " + get_label_for_entity(unit, language)
462+ return value
445463
446464
447−def get_geolink(result):
448− coordinates = eval_xpath(result, value_xpath)
449− if not coordinates:
450− return None
451− coordinates = extract_text(coordinates[0])
452− latitude, longitude = coordinates.split(',')
453−
454− # convert to decimal
455− lat = int(latitude[:latitude.find('°')])
456− if latitude.find('\'') >= 0:
457− lat += int(latitude[latitude.find('°') + 1:latitude.find('\'')] or 0) / 60.0
458− if latitude.find('"') >= 0:
459− lat += float(latitude[latitude.find('\'') + 1:latitude.find('"')] or 0) / 3600.0
460− if latitude.find('S') >= 0:
461− lat *= -1
462− lon = int(longitude[:longitude.find('°')])
463− if longitude.find('\'') >= 0:
464− lon += int(longitude[longitude.find('°') + 1:longitude.find('\'')] or 0) / 60.0
465− if longitude.find('"') >= 0:
466− lon += float(longitude[longitude.find('\'') + 1:longitude.find('"')] or 0) / 3600.0
467− if longitude.find('W') >= 0:
468− lon *= -1
469−
470− # TODO: get precision
471− precision = 0.0002
472− # there is no zoom information, deduce from precision (error prone)
473− # samples :
474− # 13 --> 5
475− # 1 --> 6
476− # 0.016666666666667 --> 9
477− # 0.00027777777777778 --> 19
478− # wolframalpha :
479− # quadratic fit { {13, 5}, {1, 6}, {0.0166666, 9}, {0.0002777777,19}}
480− # 14.1186-8.8322 x+0.625447 x^2
481− if precision < 0.0003:
482− zoom = 19
483− else:
484− zoom = int(15 - precision * 8.8322 + precision * precision * 0.625447)
465+class WDArticle(WDAttribute):
466+
467+ __slots__ = 'language', 'kwargs'
468+
469+ def __init__(self, language, kwargs=None):
470+ super().__init__('wikipedia')
471+ self.language = language
472+ self.kwargs = kwargs or {}
473+
474+ def get_label(self, language):
475+ # language parameter is ignored
476+ return "Wikipedia ({language})".replace('{language}', self.language)
477+
478+ def get_select(self):
479+ return "?article{language} ?articleName{language}".replace('{language}', self.language)
480+
481+ def get_where(self):
482+ return """OPTIONAL { ?article{language} schema:about ?item ;
483+ schema:inLanguage "{language}" ;
484+ schema:isPartOf <https://{language}.wikipedia.org/> ;
485+ schema:name ?articleName{language} . }""".replace('{language}', self.language)
486+
487+ def get_group_by(self):
488+ return self.get_select()
489+
490+ def get_str(self, result, language):
491+ key = 'article{language}'.replace('{language}', self.language)
492+ return result.get(key)
493+
494+
495+class WDLabelAttribute(WDAttribute):
496+
497+ def get_select(self):
498+ return '(group_concat(distinct ?{name}Label;separator=", ") as ?{name}Labels)'.replace('{name}', self.name)
485499
486− url = url_map\
487− .replace('{latitude}', str(lat))\
488− .replace('{longitude}', str(lon))\
489− .replace('{zoom}', str(zoom))
500+ def get_where(self):
501+ return "OPTIONAL { ?item wdt:{name} ?{name} . }".replace('{name}', self.name)
490502
491− return url
503+ def get_wikibase_label(self):
504+ return "?{name} rdfs:label ?{name}Label .".replace('{name}', self.name)
492505
506+ def get_str(self, result, language):
507+ return result.get(self.name + 'Labels')
493508
494−def get_wikilink(result, wikiid):
495− url = eval_xpath(result, wikilink_xpath.replace('{wikiid}', wikiid))
496− if not url:
509+
510+class WDURLAttribute(WDAttribute):
511+
512+ HTTP_WIKIMEDIA_IMAGE = 'http://commons.wikimedia.org/wiki/Special:FilePath/'
513+
514+ __slots__ = 'url_id', 'kwargs'
515+
516+ def __init__(self, name, url_id=None, kwargs=None):
517+ super().__init__(name)
518+ self.url_id = url_id
519+ self.kwargs = kwargs
520+
521+ def get_str(self, result, language):
522+ value = result.get(self.name + 's')
523+ if self.url_id and value is not None and value != '':
524+ value = value.split(',')[0]
525+ url_id = self.url_id
526+ if value.startswith(WDURLAttribute.HTTP_WIKIMEDIA_IMAGE):
527+ value = value[len(WDURLAttribute.HTTP_WIKIMEDIA_IMAGE):]
528+ url_id = 'wikimedia_image'
529+ return get_external_url(url_id, value)
530+ return value
531+
532+
533+class WDGeoAttribute(WDAttribute):
534+
535+ def get_label(self, language):
536+ return "OpenStreetMap"
537+
538+ def get_select(self):
539+ return "?{name}Lat ?{name}Long".replace('{name}', self.name)
540+
541+ def get_where(self):
542+ return """OPTIONAL { ?item p:{name}/psv:{name} [
543+ wikibase:geoLatitude ?{name}Lat ;
544+ wikibase:geoLongitude ?{name}Long ] }""".replace('{name}', self.name)
545+
546+ def get_group_by(self):
547+ return self.get_select()
548+
549+ def get_str(self, result, language, osm_zoom=19):
550+ latitude = result.get(self.name + 'Lat')
551+ longitude = result.get(self.name + 'Long')
552+ if latitude and longitude:
553+ return get_earth_coordinates_url(latitude, longitude, osm_zoom)
497554 return None
498− url = url[0]
499− if url.startswith('http://'):
500− url = url.replace('http://', 'https://')
501− elif url.startswith('//'):
502− url = 'https:' + url
503− return url
555+
556+
557+class WDImageAttribute(WDURLAttribute):
558+
559+ __slots__ = 'priority',
560+
561+ def __init__(self, name, url_id=None, priority=100):
562+ super().__init__(name, url_id)
563+ self.priority = priority
564+
565+
566+class WDDateAttribute(WDAttribute):
567+
568+ def get_select(self):
569+ return '?{name} ?{name}timePrecision ?{name}timeZone ?{name}timeCalendar'.replace('{name}', self.name)
570+
571+ def get_where(self):
572+ # To remove duplicate, add
573+ # FILTER NOT EXISTS { ?item p:{name}/psv:{name}/wikibase:timeValue ?{name}bis FILTER (?{name}bis < ?{name}) }
574+ # this filter is too slow, so the response function ignore duplicate results
575+ # (see the seen_entities variable)
576+ return """OPTIONAL { ?item p:{name}/psv:{name} [
577+ wikibase:timeValue ?{name} ;
578+ wikibase:timePrecision ?{name}timePrecision ;
579+ wikibase:timeTimezone ?{name}timeZone ;
580+ wikibase:timeCalendarModel ?{name}timeCalendar ] . }
581+ hint:Prior hint:rangeSafe true;""".replace('{name}', self.name)
582+
583+ def get_group_by(self):
584+ return self.get_select()
585+
586+ def format_8(self, value, locale):
587+ # precision: less than a year
588+ return value
589+
590+ def format_9(self, value, locale):
591+ year = int(value)
592+ # precision: year
593+ if year < 1584:
594+ if year < 0:
595+ return str(year - 1)
596+ return str(year)
597+ timestamp = isoparse(value)
598+ return format_date(timestamp, format='yyyy', locale=locale)
599+
600+ def format_10(self, value, locale):
601+ # precision: month
602+ timestamp = isoparse(value)
603+ return format_date(timestamp, format='MMMM y', locale=locale)
604+
605+ def format_11(self, value, locale):
606+ # precision: day
607+ timestamp = isoparse(value)
608+ return format_date(timestamp, format='full', locale=locale)
609+
610+ def format_13(self, value, locale):
611+ timestamp = isoparse(value)
612+ # precision: minute
613+ return get_datetime_format(format, locale=locale) \
614+ .replace("'", "") \
615+ .replace('{0}', format_time(timestamp, 'full', tzinfo=None,
616+ locale=locale)) \
617+ .replace('{1}', format_date(timestamp, 'short', locale=locale))
618+
619+ def format_14(self, value, locale):
620+ # precision: second.
621+ return format_datetime(isoparse(value), format='full', locale=locale)
622+
623+ DATE_FORMAT = {
624+ '0': ('format_8', 1000000000),
625+ '1': ('format_8', 100000000),
626+ '2': ('format_8', 10000000),
627+ '3': ('format_8', 1000000),
628+ '4': ('format_8', 100000),
629+ '5': ('format_8', 10000),
630+ '6': ('format_8', 1000),
631+ '7': ('format_8', 100),
632+ '8': ('format_8', 10),
633+ '9': ('format_9', 1), # year
634+ '10': ('format_10', 1), # month
635+ '11': ('format_11', 0), # day
636+ '12': ('format_13', 0), # hour (not supported by babel, display minute)
637+ '13': ('format_13', 0), # minute
638+ '14': ('format_14', 0) # second
639+ }
640+
641+ def get_str(self, result, language):
642+ value = result.get(self.name)
643+ if value == '' or value is None:
644+ return None
645+ precision = result.get(self.name + 'timePrecision')
646+ date_format = WDDateAttribute.DATE_FORMAT.get(precision)
647+ if date_format is not None:
648+ format_method = getattr(self, date_format[0])
649+ precision = date_format[1]
650+ try:
651+ if precision >= 1:
652+ t = value.split('-')
653+ if value.startswith('-'):
654+ value = '-' + t[1]
655+ else:
656+ value = t[0]
657+ return format_method(value, language)
658+ except Exception:
659+ return value
660+ return value
661+
662+
663+def debug_explain_wikidata_query(query, method='GET'):
664+ if method == 'GET':
665+ http_response = get(SPARQL_EXPLAIN_URL + '&' + urlencode({'query': query}), headers=get_headers())
666+ else:
667+ http_response = post(SPARQL_EXPLAIN_URL, data={'query': query}, headers=get_headers())
668+ http_response.raise_for_status()
669+ return http_response.content
670+
671+
672+def init(engine_settings=None):
673+ # WIKIDATA_PROPERTIES : add unit symbols
674+ WIKIDATA_PROPERTIES.update(WIKIDATA_UNITS)
675+
676+ # WIKIDATA_PROPERTIES : add property labels
677+ wikidata_property_names = []
678+ for attribute in get_attributes('en'):
679+ if type(attribute) in (WDAttribute, WDAmountAttribute, WDURLAttribute, WDDateAttribute, WDLabelAttribute):
680+ if attribute.name not in WIKIDATA_PROPERTIES:
681+ wikidata_property_names.append("wd:" + attribute.name)
682+ query = QUERY_PROPERTY_NAMES.replace('%ATTRIBUTES%', " ".join(wikidata_property_names))
683+ jsonresponse = send_wikidata_query(query)
684+ for result in jsonresponse.get('results', {}).get('bindings', {}):
685+ name = result['name']['value']
686+ lang = result['name']['xml:lang']
687+ entity_id = result['item']['value'].replace('http://www.wikidata.org/entity/', '')
688+ WIKIDATA_PROPERTIES[(entity_id, lang)] = name.capitalize()
searx/external_urls.pynew file+77
@@ -0,0 +1,77 @@
1+import math
2+
3+from searx.data import EXTERNAL_URLS
4+
5+
6+IMDB_PREFIX_TO_URL_ID = {
7+ 'tt': 'imdb_title',
8+ 'mn': 'imdb_name',
9+ 'ch': 'imdb_character',
10+ 'co': 'imdb_company',
11+ 'ev': 'imdb_event'
12+}
13+
14+
15+def get_imdb_url_id(imdb_item_id):
16+ id_prefix = imdb_item_id[:2]
17+ return IMDB_PREFIX_TO_URL_ID.get(id_prefix)
18+
19+
20+def get_external_url(url_id, item_id, alternative="default"):
21+ """Return an external URL or None if url_id is not found.
22+
23+ url_id can take value from data/external_urls.json
24+ The "imdb_id" value is automaticaly converted according to the item_id value.
25+
26+ If item_id is None, the raw URL with the $1 is returned.
27+ """
28+ if url_id == 'imdb_id' and item_id is not None:
29+ url_id = get_imdb_url_id(item_id)
30+
31+ url_description = EXTERNAL_URLS.get(url_id)
32+ if url_description:
33+ url_template = url_description["urls"].get(alternative)
34+ if url_template is not None:
35+ if item_id is not None:
36+ return url_template.replace('$1', item_id)
37+ else:
38+ return url_template
39+ return None
40+
41+
42+def get_earth_coordinates_url(latitude, longitude, osm_zoom, alternative='default'):
43+ url = get_external_url('map', None, alternative)\
44+ .replace('${latitude}', str(latitude))\
45+ .replace('${longitude}', str(longitude))\
46+ .replace('${zoom}', str(osm_zoom))
47+ return url
48+
49+
50+def area_to_osm_zoom(area):
51+ """Convert an area in km² into an OSM zoom. Less reliable if the shape is not round.
52+
53+ logarithm regression using these data:
54+ * 9596961 -> 4 (China)
55+ * 3287263 -> 5 (India)
56+ * 643801 -> 6 (France)
57+ * 6028 -> 9
58+ * 1214 -> 10
59+ * 891 -> 12
60+ * 12 -> 13
61+
62+ In WolframAlpha:
63+ >>> log fit {9596961,15},{3287263, 14},{643801,13},{6028,10},{1214,9},{891,7},{12,6}
64+
65+ with 15 = 19-4 (China); 14 = 19-5 (India) and so on
66+
67+ Args:
68+ area (int,float,str): area in km²
69+
70+ Returns:
71+ int: OSM zoom or 19 in area is not a number
72+ """
73+ try:
74+ amount = float(area)
75+ return max(0, min(19, round(19 - 0.688297 * math.log(226.878 * amount))))
76+ except ValueError:
77+ return 19
searx/results.py+26−8
@@ -20,6 +20,18 @@ def result_content_len(content):
2020
2121
2222def compare_urls(url_a, url_b):
23+ """Lazy compare between two URL.
24+ "www.example.com" and "example.com" are equals.
25+ "www.example.com/path/" and "www.example.com/path" are equals.
26+ "https://www.example.com/" and "http://www.example.com/" are equals.
27+
28+ Args:
29+ url_a (ParseResult): first URL
30+ url_b (ParseResult): second URL
31+
32+ Returns:
33+ bool: True if url_a and url_b are equals
34+ """
2335 # ignore www. in comparison
2436 if url_a.netloc.startswith('www.'):
2537 host_a = url_a.netloc.replace('www.', '', 1)
@@ -68,8 +80,10 @@ def merge_two_infoboxes(infobox1, infobox2):
6880 for url2 in infobox2.get('urls', []):
6981 unique_url = True
7082 parsed_url2 = urlparse(url2.get('url', ''))
83+ entity_url2 = url2.get('entity')
7184 for url1 in urls1:
72− if compare_urls(urlparse(url1.get('url', '')), parsed_url2):
85+ if (entity_url2 is not None and url1.get('entity') == entity_url2)\
86+ or compare_urls(urlparse(url1.get('url', '')), parsed_url2):
7387 unique_url = False
7488 break
7589 if unique_url:
@@ -86,18 +100,22 @@ def merge_two_infoboxes(infobox1, infobox2):
86100 infobox1['img_src'] = img2
87101
88102 if 'attributes' in infobox2:
89− attributes1 = infobox1.get('attributes', None)
103+ attributes1 = infobox1.get('attributes')
90104 if attributes1 is None:
91− attributes1 = []
92− infobox1['attributes'] = attributes1
105+ infobox1['attributes'] = attributes1 = []
93106
94107 attributeSet = set()
95− for attribute in infobox1.get('attributes', []):
96− if attribute.get('label', None) not in attributeSet:
97− attributeSet.add(attribute.get('label', None))
108+ for attribute in attributes1:
109+ label = attribute.get('label')
110+ if label not in attributeSet:
111+ attributeSet.add(label)
112+ entity = attribute.get('entity')
113+ if entity not in attributeSet:
114+ attributeSet.add(entity)
98115
99116 for attribute in infobox2.get('attributes', []):
100− if attribute.get('label', None) not in attributeSet:
117+ if attribute.get('label') not in attributeSet\
118+ and attribute.get('entity') not in attributeSet:
101119 attributes1.append(attribute)
102120
103121 if 'content' in infobox2:
searx/templates/oscar/infobox.html+1−5
@@ -25,11 +25,7 @@
2525 {%- if attribute.image -%}
2626 <td><img class="img-responsive" src="{{ image_proxify(attribute.image.src) }}" alt="{{ attribute.image.alt }}" /></td>
2727 {%- else -%}
28− {% if attribute.label == 'Instance of' %}
29− <td><bdi><a href="https://wikidata.org/wiki/{{ attribute.value.id }}">{{ attribute.value.id }}</a></bdi></td>
30− {% else %}
31− <td><bdi>{{ attribute.value }}</bdi></td>
32− {%- endif -%}
28+ <td><bdi>{{ attribute.value }}</bdi></td>
3329 {%- endif -%}
3430 </tr>
3531 {% endfor -%}
searx/templates/simple/infobox.html−1
@@ -1,7 +1,6 @@
11<aside class="infobox">
22 <h2><bdi>{{ infobox.infobox }}</bdi></h2>
33 {% if infobox.img_src %}<img src="{{ image_proxify(infobox.img_src) }}" title="{{ infobox.infobox|striptags }}" alt="{{ infobox.infobox|striptags }}" />{% endif %}
4− <p><bdi>{{ infobox.entity }}</bdi></p>
54 <p><bdi>{{ infobox.content | safe }}</bdi></p>
65 {% if infobox.attributes %}
76 <div class="attributes">
searx/utils.py+10
@@ -481,6 +481,16 @@ def ecma_unescape(s):
481481 return s
482482
483483
484+def get_string_replaces_function(replaces):
485+ rep = {re.escape(k): v for k, v in replaces.items()}
486+ pattern = re.compile("|".join(rep.keys()))
487+
488+ def f(text):
489+ return pattern.sub(lambda m: rep[re.escape(m.group(0))], text)
490+
491+ return f
492+
493+
484494def get_engine_from_settings(name):
485495 """Return engine configuration from settings.yml of a given engine name"""
486496
utils/fetch_wikidata_units.pynew file+47
@@ -0,0 +1,47 @@
1+#!/usr/bin/env python
2+
3+import json
4+import collections
5+
6+# set path
7+from sys import path
8+from os.path import realpath, dirname, join
9+path.append(realpath(dirname(realpath(__file__)) + '/../'))
10+
11+from searx import searx_dir
12+from searx.engines.wikidata import send_wikidata_query
13+
14+
15+SARQL_REQUEST = """
16+SELECT DISTINCT ?item ?symbol ?P2370 ?P2370Unit ?P2442 ?P2442Unit
17+WHERE
18+{
19+?item wdt:P31/wdt:P279 wd:Q47574.
20+?item wdt:P5061 ?symbol.
21+FILTER(LANG(?symbol) = "en").
22+}
23+ORDER BY ?item
24+"""
25+
26+
27+def get_data():
28+ def get_key(unit):
29+ return unit['item']['value'].replace('http://www.wikidata.org/entity/', '')
30+
31+ def get_value(unit):
32+ return unit['symbol']['value']
33+
34+ result = send_wikidata_query(SARQL_REQUEST)
35+ if result is not None:
36+ # sort the unit by entity name
37+ # so different fetchs keep the file unchanged.
38+ list(result['results']['bindings']).sort(key=get_key)
39+ return collections.OrderedDict([(get_key(unit), get_value(unit)) for unit in result['results']['bindings']])
40+
41+
42+def get_wikidata_units_filename():
43+ return join(join(searx_dir, "data"), "wikidata_units.json")
44+
45+
46+with open(get_wikidata_units_filename(), 'w') as f:
47+ json.dump(get_data(), f, indent=4, ensure_ascii=False)