diff --git a/searx/data/__init__.py b/searx/data/__init__.py
index 1116e5d47..55a254b13 100644
--- a/searx/data/__init__.py
+++ b/searx/data/__init__.py
@@ -2,7 +2,8 @@ import json
from pathlib import Path
-__init__ = ['ENGINES_LANGUGAGES', 'CURRENCIES', 'USER_AGENTS', 'bangs_loader', 'ahmia_blacklist_loader']
+__init__ = ['ENGINES_LANGUGAGES', 'CURRENCIES', 'USER_AGENTS', 'EXTERNAL_URLS', 'WIKIDATA_UNITS',
+ 'bangs_loader', 'ahmia_blacklist_loader']
data_dir = Path(__file__).parent
@@ -24,3 +25,5 @@ def ahmia_blacklist_loader():
ENGINES_LANGUAGES = load('engines_languages.json')
CURRENCIES = load('currencies.json')
USER_AGENTS = load('useragents.json')
+EXTERNAL_URLS = load('external_urls.json')
+WIKIDATA_UNITS = load('wikidata_units.json')
diff --git a/searx/data/external_urls.json b/searx/data/external_urls.json
new file mode 100644
index 000000000..75b153aba
--- /dev/null
+++ b/searx/data/external_urls.json
@@ -0,0 +1,156 @@
+{
+ "facebook_profile": {
+ "category_name": "Facebook",
+ "url_name": "Facebook profile",
+ "urls": {
+ "default": "https://facebook.com/$1"
+ }
+ },
+ "youtube_channel": {
+ "category_name": "YouTube",
+ "url_name": "YouTube channel",
+ "urls": {
+ "default": "https://www.youtube.com/channel/$1"
+ }
+ },
+ "youtube_video": {
+ "category_name": "YouTube",
+ "url_name": "YouTube video",
+ "urls": {
+ "default": "https://www.youtube.com/watch?v=$1"
+ }
+ },
+ "twitter_profile": {
+ "category_name": "Twitter",
+ "url_name": "Twitter profile",
+ "urls": {
+ "default": "https://twitter.com/$1"
+ }
+ },
+ "instagram_profile": {
+ "category_name": "Instagram",
+ "url_name": "Instagram profile",
+ "urls": {
+ "default": "https://www.instagram.com/$1"
+ }
+ },
+ "imdb_title": {
+ "category_name": "IMDB",
+ "url_name": "IMDB title",
+ "urls": {
+ "default": "https://www.imdb.com/title/$1"
+ }
+ },
+ "imdb_name": {
+ "category_name": "IMDB",
+ "url_name": "IMDB name",
+ "urls": {
+ "default": "https://www.imdb.com/name/$1"
+ }
+ },
+ "imdb_character": {
+ "category_name": "IMDB",
+ "url_name": "IMDB character",
+ "urls": {
+ "default": "https://www.imdb.com/character/$1"
+ }
+ },
+ "imdb_company": {
+ "category_name": "IMDB",
+ "url_name": "IMDB company",
+ "urls": {
+ "default": "https://www.imdb.com/company/$1"
+ }
+ },
+ "imdb_event": {
+ "category_name": "IMDB",
+ "url_name": "IMDB event",
+ "urls": {
+ "default": "https://www.imdb.com/event/$1"
+ }
+ },
+ "rotten_tomatoes": {
+ "category_name": "Rotten tomatoes",
+ "url_name": "Rotten tomatoes title",
+ "urls": {
+ "default": "https://www.rottentomatoes.com/$1"
+ }
+ },
+ "spotify_artist_id": {
+ "category_name": "Spotify",
+ "url_name": "Spotify artist",
+ "urls": {
+ "default": "https://open.spotify.com/artist/$1"
+ }
+ },
+ "itunes_artist_id": {
+ "category_name": "iTunes",
+ "url_name": "iTunes artist",
+ "urls": {
+ "default": "https://music.apple.com/us/artist/$1"
+ }
+ },
+ "soundcloud_id": {
+ "category_name": "Soundcloud",
+ "url_name": "Soundcloud artist",
+ "urls": {
+ "default": "https://soundcloud.com/$1"
+ }
+ },
+ "netflix_id": {
+ "category_name": "Netflix",
+ "url_name": "Netflix movie",
+ "urls": {
+ "default": "https://www.netflix.com/watch/$1"
+ }
+ },
+ "github_profile": {
+ "category_name": "Github",
+ "url_name": "Github profile",
+ "urls": {
+ "default": "https://wwww.github.com/$1"
+ }
+ },
+ "musicbrainz_artist": {
+ "category_name": "Musicbrainz",
+ "url_name": "Musicbrainz artist",
+ "urls": {
+ "default": "http://musicbrainz.org/artist/$1"
+ }
+ },
+ "musicbrainz_work": {
+ "category_name": "Musicbrainz",
+ "url_name": "Musicbrainz work",
+ "urls": {
+ "default": "http://musicbrainz.org/work/$1"
+ }
+ },
+ "musicbrainz_release_group": {
+ "category_name": "Musicbrainz",
+ "url_name": "Musicbrainz release group",
+ "urls": {
+ "default": "http://musicbrainz.org/release-group/$1"
+ }
+ },
+ "musicbrainz_label": {
+ "category_name": "Musicbrainz",
+ "url_name": "Musicbrainz label",
+ "urls": {
+ "default": "http://musicbrainz.org/label/$1"
+ }
+ },
+ "wikimedia_image": {
+ "category_name": "Wikipedia",
+ "url_name": "Wikipedia image",
+ "urls": {
+ "default": "https://commons.wikimedia.org/wiki/Special:FilePath/$1?width=500&height=400"
+ }
+ },
+ "map": {
+ "category_name": "Map",
+ "url_name": "geo map",
+ "urls": {
+ "default": "https://www.openstreetmap.org/?lat=${latitude}&lon=${longitude}&zoom=${zoom}&layers=M"
+ }
+ }
+}
\ No newline at end of file
diff --git a/searx/data/wikidata_units.json b/searx/data/wikidata_units.json
new file mode 100644
index 000000000..966e5e812
--- /dev/null
+++ b/searx/data/wikidata_units.json
@@ -0,0 +1,1006 @@
+{
+ "Q199": "1",
+ "Q100149279": "°We",
+ "Q100995": "lb",
+ "Q1022113": "cm³",
+ "Q102573": "Bq",
+ "Q103246": "Sv",
+ "Q103510": "bar",
+ "Q10380431": "TJ",
+ "Q1040401": "das",
+ "Q1040427": "hs",
+ "Q1042866": "Zibit",
+ "Q1050958": "inHg",
+ "Q1051665": "m/s²",
+ "Q1052397": "rad",
+ "Q1054140": "Mm",
+ "Q10543042": "Ym",
+ "Q1057069": "hg",
+ "Q1063756": "rad/s",
+ "Q1063786": "in²",
+ "Q1065153": "mrad",
+ "Q1066138": "Ps",
+ "Q1067722": "Fg",
+ "Q1069725": "p.",
+ "Q1084321": "Tb/s",
+ "Q1086691": "fg",
+ "Q1091257": "tex",
+ "Q1092296": "a",
+ "Q1104069": "CAD$",
+ "Q11061003": "μm²",
+ "Q11061005": "nm²",
+ "Q1131660": "st",
+ "Q1137675": "cr",
+ "Q1140444": "Zbit",
+ "Q1140577": "Ybit",
+ "Q1152074": "Pbit",
+ "Q1152323": "Tbit",
+ "Q1165799": "mil",
+ "Q11776930": "Mg",
+ "Q11830636": "psf",
+ "Q11929860": "kpc",
+ "Q1194225": "lbf",
+ "Q1194580": "Mibit",
+ "Q1195111": "Ebit",
+ "Q1196837": "ω_P",
+ "Q1197459": "Ms",
+ "Q11982285": "Em³",
+ "Q11982288": "Zm³",
+ "Q11982289": "Tm³",
+ "Q12011178": "Zs",
+ "Q1204894": "Gibit",
+ "Q12257695": "Eb/s",
+ "Q12257696": "EB/s",
+ "Q12261466": "kB/s",
+ "Q12265780": "Pb/s",
+ "Q12265783": "PB/s",
+ "Q12269121": "Yb/s",
+ "Q12269122": "YB/s",
+ "Q12269308": "Zb/s",
+ "Q12269309": "ZB/s",
+ "Q1247300": "cm H₂O",
+ "Q12714022": "sh cwt",
+ "Q12789864": "GeV",
+ "Q12874593": "W h",
+ "Q128822": "kn",
+ "Q13035094": "J/mol",
+ "Q130964": "cal",
+ "Q131255": "F",
+ "Q13147228": "g/cm³",
+ "Q1322380": "Ts",
+ "Q1323615": "oz t",
+ "Q132643": "kr",
+ "Q13400897": "g",
+ "Q13479685": "mm wg",
+ "Q1351253": "Eibit",
+ "Q1351334": "Pibit",
+ "Q13542672": "Ry",
+ "Q13548586": "THz",
+ "Q13582667": "kgf/cm²",
+ "Q1361854": "dwt",
+ "Q1363007": "slug",
+ "Q1374438": "ks",
+ "Q13753469": "MB/s",
+ "Q1377051": "Gs",
+ "Q1394540": "bm",
+ "Q1396128": "F",
+ "Q1413142": "Gb",
+ "Q14158377": "A_P",
+ "Q14623803": "MDa",
+ "Q14623804": "kDa",
+ "Q1472674": "Sv",
+ "Q14754979": "Zg",
+ "Q14786969": "MJ",
+ "Q14913554": "Ys",
+ "Q14914907": "th",
+ "Q14916719": "Gpc",
+ "Q14923662": "Pm³",
+ "Q1511773": "LSd",
+ "Q15120301": "l atm",
+ "Q1542309": "xu",
+ "Q1545979": "ft³",
+ "Q1550511": "yd²",
+ "Q15551713": "Sh",
+ "Q1569733": "St",
+ "Q15784325": "apc",
+ "Q160680": "Br",
+ "Q160857": "hp",
+ "Q1628990": "hph",
+ "Q163343": "T",
+ "Q163354": "H",
+ "Q1640501": "hyl",
+ "Q1645498": "μg",
+ "Q16859309": "lb·ft",
+ "Q169893": "S",
+ "Q170804": "Wb",
+ "Q17093295": "m/h",
+ "Q17255465": "v_P",
+ "Q173117": "R$",
+ "Q1741429": "kpm",
+ "Q174467": "Lm",
+ "Q174728": "cm",
+ "Q174789": "mm",
+ "Q175821": "μm",
+ "Q1768929": "p",
+ "Q1770733": "Tg",
+ "Q1772386": "dg",
+ "Q177493": "Gs",
+ "Q177612": "sr",
+ "Q1777507": "fs",
+ "Q177974": "atm",
+ "Q178506": "bbl",
+ "Q178674": "nm",
+ "Q1793863": "sn",
+ "Q179836": "lx",
+ "Q180154": "km/h",
+ "Q180892": "M☉",
+ "Q1815100": "cl",
+ "Q182098": "kWh",
+ "Q1823150": "μW",
+ "Q182429": "m/s",
+ "Q1826195": "dl",
+ "Q18413919": "cm/s",
+ "Q184172": "FF",
+ "Q185078": "a",
+ "Q185153": "erg",
+ "Q185648": "Torr",
+ "Q185759": "span",
+ "Q1872619": "zs",
+ "Q189097": "₧",
+ "Q190095": "Gy",
+ "Q190951": "S$",
+ "Q191118": "t",
+ "Q1913097": "fg",
+ "Q1916026": "μV",
+ "Q192027": "Bd",
+ "Q192274": "pm",
+ "Q193098": "KD",
+ "Q1935515": "mA s",
+ "Q19392152": "TL",
+ "Q193933": "dpt",
+ "Q194339": "B$",
+ "Q1970718": "mam",
+ "Q1972579": "pdl",
+ "Q199462": "LE",
+ "Q199471": "Afs",
+ "Q200323": "dm",
+ "Q200337": "Kz",
+ "Q201880": "LL",
+ "Q201933": "dyn",
+ "Q2029156": "quad",
+ "Q2029519": "hl",
+ "Q203567": "₦",
+ "Q2042279": "m H₂O",
+ "Q204737": "៛",
+ "Q2051195": "GWh",
+ "Q2055118": "ppb",
+ "Q2064166": "fc",
+ "Q206600": "ރ",
+ "Q20706220": "cmm",
+ "Q20706221": "dmm",
+ "Q2080811": "vol%",
+ "Q208526": "NT$",
+ "Q208528": "gon",
+ "Q208634": "kat",
+ "Q208788": "fm",
+ "Q209351": "b",
+ "Q209426": "′",
+ "Q21006887": "ppm",
+ "Q2100949": "P",
+ "Q21014455": "m/min",
+ "Q210472": "B/.",
+ "Q21061369": "g/kg",
+ "Q21062777": "MPa",
+ "Q21064807": "kPa",
+ "Q21064845": "mol/L",
+ "Q21075844": "ml/l",
+ "Q21077820": "mg/m³",
+ "Q21091747": "mg/kg",
+ "Q211256": "mph",
+ "Q211580": "BTU (th)",
+ "Q212120": "A h",
+ "Q2140397": "in³",
+ "Q214377": "ell",
+ "Q2143992": "kHz",
+ "Q21489891": "nm³",
+ "Q21489892": "Gm³",
+ "Q21489893": "Mm³",
+ "Q21489894": "μm³",
+ "Q21500224": "mas",
+ "Q2151240": "mag",
+ "Q215571": "N m",
+ "Q21604951": "g/m³",
+ "Q2165290": "yd³",
+ "Q216880": "kp",
+ "Q217208": "a",
+ "Q2175964": "dm³",
+ "Q218593": "in",
+ "Q2199357": "dec",
+ "Q22137107": "mas/y",
+ "Q2215478": "ppt",
+ "Q2221356": "mW h",
+ "Q22350885": "da",
+ "Q2243141": "Gb/s",
+ "Q2254856": "ca",
+ "Q22673229": "ft/min",
+ "Q2269250": "kb/s",
+ "Q2282891": "μl",
+ "Q2282906": "ng",
+ "Q229354": "Ci",
+ "Q232291": "mi²",
+ "Q2332346": "ml",
+ "Q23823681": "TW",
+ "Q23925410": "gal (UK)",
+ "Q23925413": "gal (US)",
+ "Q23931040": "dam²",
+ "Q23931103": "nmi²",
+ "Q2414435": "$b.",
+ "Q242988": "Lib$",
+ "Q2438073": "ag",
+ "Q2448803": "mV",
+ "Q2451296": "μF",
+ "Q246868": "lot",
+ "Q2474258": "mSv",
+ "Q2483628": "as",
+ "Q2489298": "cm²",
+ "Q249439": "q_P",
+ "Q2518569": "nSv",
+ "Q253276": "mi",
+ "Q25472681": "GB/s",
+ "Q25472693": "TB/s",
+ "Q25499149": "oct",
+ "Q25511288": "mb",
+ "Q2553708": "MV",
+ "Q2554092": "kV",
+ "Q259502": "AU$",
+ "Q260126": "rem",
+ "Q2612219": "Pg",
+ "Q261247": "ct",
+ "Q2619500": "foe",
+ "Q2636421": "nH",
+ "Q2637946": "dal",
+ "Q2642547": "ha",
+ "Q2652700": "Osm",
+ "Q2655272": "Eg",
+ "Q2659078": "TW h",
+ "Q2670039": "₶",
+ "Q26708069": "kcal",
+ "Q267391": "K",
+ "Q2679083": "μH",
+ "Q2682463": "nF",
+ "Q2691798": "cg",
+ "Q271206": "sud£",
+ "Q2737347": "mm²",
+ "Q2739114": "μSv",
+ "Q275112": "Bz$",
+ "Q2756030": "pF",
+ "Q2757753": "PW h",
+ "Q2762458": "ys",
+ "Q27864215": "μW h",
+ "Q2793566": "GV",
+ "Q27949241": "R",
+ "Q2799294": "Gg",
+ "Q281096": "cd/m²",
+ "Q28719934": "keV",
+ "Q28924752": "g/mol",
+ "Q28924753": "kg/mol",
+ "Q2924137": "mH",
+ "Q296936": "toe",
+ "Q29924639": "kVA",
+ "Q30001811": "aBq",
+ "Q30001813": "aC",
+ "Q30001814": "aHz",
+ "Q30001815": "aJ",
+ "Q30001816": "akat",
+ "Q30001818": "aL",
+ "Q30001819": "alm",
+ "Q30001820": "alx",
+ "Q30001822": "aN",
+ "Q30001823": "aΩ",
+ "Q30001825": "aPa",
+ "Q30001826": "arad",
+ "Q30001827": "aS",
+ "Q30001828": "aSv",
+ "Q30001829": "asr",
+ "Q30001830": "aT",
+ "Q30001831": "aV",
+ "Q30001832": "aW",
+ "Q30001833": "aWb",
+ "Q3013059": "kyr",
+ "Q3194304": "kbit",
+ "Q3207456": "mW",
+ "Q321017": "R",
+ "Q3221356": "ym",
+ "Q3239557": "pg",
+ "Q3241121": "mg",
+ "Q324923": "Hart",
+ "Q3249364": "cs",
+ "Q3251645": "ds",
+ "Q3267417": "Tm",
+ "Q3270676": "zm",
+ "Q32750621": "liq pt (US)",
+ "Q32750759": "fl oz (US)",
+ "Q32750816": "bu (US)",
+ "Q32751272": "dry pt (US)",
+ "Q32751296": "bbl (US)",
+ "Q3276763": "GHz",
+ "Q3277907": "Em",
+ "Q3277915": "Zm",
+ "Q3277919": "Pm",
+ "Q3312063": "fL",
+ "Q3320608": "kW",
+ "Q3331719": "dm²",
+ "Q3332689": "ToR",
+ "Q3332814": "Mbit",
+ "Q3396758": "daa",
+ "Q3414243": "rps",
+ "Q3421309": "R_J",
+ "Q3495543": "mbar",
+ "Q355198": "px",
+ "Q3674704": "km/s",
+ "Q3675550": "mm³",
+ "Q3712659": "$",
+ "Q376660": "nat",
+ "Q37732658": "°R",
+ "Q3773454": "Mpc",
+ "Q3815076": "Kibit",
+ "Q3833309": "£",
+ "Q3858002": "mA h",
+ "Q3867152": "ft/s²",
+ "Q389062": "Tibit",
+ "Q3902688": "pl",
+ "Q3902709": "ps",
+ "Q39360235": "US lea",
+ "Q39360471": "nl",
+ "Q39362962": "µin",
+ "Q39363132": "UK lg",
+ "Q39363209": "UK nl",
+ "Q39380159": "US nmi",
+ "Q39462789": "µin²",
+ "Q39467934": "kgf/m²",
+ "Q39469927": "N/m²",
+ "Q39617688": "cwt long",
+ "Q39617818": "t lb",
+ "Q39628023": "y",
+ "Q39699418": "cm/s²",
+ "Q39708248": "S",
+ "Q39709980": "bd",
+ "Q39710113": "bhp EDR",
+ "Q3972226": "kL",
+ "Q4041686": "iwg",
+ "Q4068266": "Ʒ",
+ "Q4176683": "aC",
+ "Q420266": "oz. fl.",
+ "Q42319606": "people/m²",
+ "Q4243638": "km³",
+ "Q4456994": "mF",
+ "Q469356": "tn. sh.",
+ "Q476572": "Ha",
+ "Q482798": "yd",
+ "Q483261": "Da",
+ "Q483725": "A.M.",
+ "Q484092": "lm",
+ "Q4861171": "H",
+ "Q494083": "fur",
+ "Q4989854": "kJ",
+ "Q500515": "Gal",
+ "Q5042194": "£",
+ "Q50808017": "kg m²",
+ "Q5139563": "hPa",
+ "Q514845": "pz",
+ "Q5195628": "hm³",
+ "Q5198770": "dam³",
+ "Q524410": "byr",
+ "Q53393488": "PHz",
+ "Q53393490": "EHz",
+ "Q53393494": "ZHz",
+ "Q53393498": "YHz",
+ "Q53393659": "ML",
+ "Q53393664": "GL",
+ "Q53393674": "ZL",
+ "Q53393678": "YL",
+ "Q53393771": "yL",
+ "Q53393868": "GJ",
+ "Q53393886": "PJ",
+ "Q53393890": "EJ",
+ "Q53448786": "yHz",
+ "Q53448790": "zHz",
+ "Q53448794": "fHz",
+ "Q53448797": "pHz",
+ "Q53448801": "nHz",
+ "Q53448806": "μHz",
+ "Q53448808": "mHz",
+ "Q53448813": "cHz",
+ "Q53448817": "dHz",
+ "Q53448820": "daHz",
+ "Q53448826": "hHz",
+ "Q53448828": "yJ",
+ "Q53448832": "zJ",
+ "Q53448842": "pJ",
+ "Q53448844": "nJ",
+ "Q53448847": "μJ",
+ "Q53448851": "mJ",
+ "Q53448856": "cJ",
+ "Q53448860": "dJ",
+ "Q53448864": "daJ",
+ "Q53448875": "hJ",
+ "Q53448879": "yPa",
+ "Q53448883": "zPa",
+ "Q53448886": "fPa",
+ "Q53448892": "pPa",
+ "Q53448897": "nPa",
+ "Q53448900": "μPa",
+ "Q53448906": "mPa",
+ "Q53448909": "cPa",
+ "Q53448914": "dPa",
+ "Q53448918": "daPa",
+ "Q53448922": "GPa",
+ "Q53448927": "TPa",
+ "Q53448931": "PPa",
+ "Q53448936": "EPa",
+ "Q53448939": "ZPa",
+ "Q53448943": "YPa",
+ "Q53448949": "yV",
+ "Q53448952": "zV",
+ "Q53448957": "fV",
+ "Q53448960": "pV",
+ "Q53448965": "nV",
+ "Q53448969": "cV",
+ "Q53448973": "dV",
+ "Q53448977": "daV",
+ "Q53448981": "hV",
+ "Q53448985": "TV",
+ "Q53448990": "PV",
+ "Q53448994": "EV",
+ "Q53448996": "ZV",
+ "Q53449001": "YV",
+ "Q53449006": "yW",
+ "Q53449008": "zW",
+ "Q53449013": "fW",
+ "Q53449018": "pW",
+ "Q53449021": "nW",
+ "Q53449025": "cW",
+ "Q53449029": "dW",
+ "Q53449033": "daW",
+ "Q53449036": "hW",
+ "Q53449040": "PW",
+ "Q53449045": "EW",
+ "Q53449049": "ZW",
+ "Q53449054": "YW",
+ "Q53561461": "wf",
+ "Q53561822": "wf",
+ "Q53651160": "zm³",
+ "Q53651201": "Ym³",
+ "Q53651356": "ym³",
+ "Q53651512": "pm³",
+ "Q53651713": "fm³",
+ "Q536785": "ρ_P",
+ "Q53951982": "Mt",
+ "Q53952048": "kt",
+ "Q54006645": "ZWb",
+ "Q54081925": "ZSv",
+ "Q54082468": "ZS",
+ "Q54083144": "ZΩ",
+ "Q54083318": "ZN",
+ "Q54083566": "Zlm",
+ "Q54083579": "Zlx",
+ "Q54083712": "ZBq",
+ "Q54083746": "ZC",
+ "Q54083766": "ZF",
+ "Q54083779": "ZGy",
+ "Q54083795": "ZH",
+ "Q54083813": "Zkat",
+ "Q5409016": "MVA",
+ "Q5465723": "ft-pdl",
+ "Q549389": "bit/s",
+ "Q550341": "V A",
+ "Q552299": "ch",
+ "Q55442349": "U/L",
+ "Q55726194": "mg/L",
+ "Q56156859": "mmol",
+ "Q56156949": "μmol",
+ "Q56157046": "nmol",
+ "Q56157048": "pmol",
+ "Q56160603": "fmol",
+ "Q56302633": "UM",
+ "Q56317116": "mgal",
+ "Q56317622": "Q_P",
+ "Q56318907": "kbar",
+ "Q56349362": "Bs.S",
+ "Q56402798": "kN",
+ "Q5711261": "am³",
+ "Q581432": "‴",
+ "Q5879479": "GW",
+ "Q6003257": "am",
+ "Q6009164": "MW h",
+ "Q6014364": "in/s",
+ "Q603071": "E°",
+ "Q605704": "doz",
+ "Q60742631": "AU/yr",
+ "Q608697": "Mx",
+ "Q610135": "G",
+ "Q613726": "Yg",
+ "Q6170164": "yg",
+ "Q6171168": "zg",
+ "Q61756607": "yd",
+ "Q61793198": "rd",
+ "Q61794766": "ch (US survey)",
+ "Q61994988": "Wth",
+ "Q61995006": "KWth",
+ "Q626299": "psi",
+ "Q630369": "var",
+ "Q636200": "U",
+ "Q640907": "sb",
+ "Q6414556": "kip",
+ "Q648908": "bya",
+ "Q64996135": "gal (US)/min",
+ "Q65028392": "mm/yr",
+ "Q651336": "M_J",
+ "Q6517513": "dag",
+ "Q667419": "UK t",
+ "Q681996": "M⊕",
+ "Q685662": "p_P",
+ "Q6859652": "mm Hg",
+ "Q686163": "$",
+ "Q68725821": "°Rø",
+ "Q68726230": "°De",
+ "Q68726625": "°N",
+ "Q69362731": "°C",
+ "Q69363953": "K",
+ "Q693944": "gr",
+ "Q6982035": "MW",
+ "Q69878540": "fl oz (UK)",
+ "Q70378044": "dmol",
+ "Q70378549": "dK",
+ "Q70393458": "kmol",
+ "Q70395375": "Tmol",
+ "Q70395643": "Mmol",
+ "Q70395830": "kK",
+ "Q70396179": "mK",
+ "Q70397275": "μK",
+ "Q70397725": "cmol",
+ "Q70397932": "cK",
+ "Q70398457": "nK",
+ "Q70398619": "MK",
+ "Q70398813": "Gmol",
+ "Q70398991": "GK",
+ "Q70440025": "daK",
+ "Q70440438": "hK",
+ "Q70440620": "damol",
+ "Q70440823": "hmol",
+ "Q70443020": "EK",
+ "Q70443154": "yK",
+ "Q70443282": "zK",
+ "Q70443367": "fK",
+ "Q70443453": "TK",
+ "Q70443757": "pK",
+ "Q70443901": "YK",
+ "Q70444029": "PK",
+ "Q70444141": "Emol",
+ "Q70444284": "ymol",
+ "Q70444386": "zmol",
+ "Q70444514": "Ymol",
+ "Q70444609": "Pmol",
+ "Q712226": "km²",
+ "Q72081071": "MeV",
+ "Q723733": "ms",
+ "Q730251": "ft·lbf",
+ "Q732707": "MHz",
+ "Q73408": "K",
+ "Q7350781": "Mb/s",
+ "Q743895": "bpm",
+ "Q748716": "ft/s",
+ "Q750178": "‱",
+ "Q752197": "kJ/mol",
+ "Q7672057": "TU",
+ "Q777017": "dBm",
+ "Q78754556": "rot",
+ "Q78756901": "rev",
+ "Q78757683": "windings",
+ "Q79726": "kB",
+ "Q79735": "MB",
+ "Q79738": "GB",
+ "Q79741": "TB",
+ "Q79744": "PB",
+ "Q79745": "EB",
+ "Q79747": "ZB",
+ "Q7974920": "W s",
+ "Q79752": "YB",
+ "Q79756": "KiB",
+ "Q79758": "MiB",
+ "Q79765": "GiB",
+ "Q79769": "TiB",
+ "Q79774": "PiB",
+ "Q79777": "EiB",
+ "Q79779": "ZiB",
+ "Q79781": "YiB",
+ "Q80237579": "J/nm",
+ "Q809678": "Ba",
+ "Q81062869": "W/nm",
+ "Q81073100": "W/(sr nm)",
+ "Q81292": "acre",
+ "Q81454": "Å",
+ "Q8229770": "B/s",
+ "Q828224": "km",
+ "Q829073": "\"",
+ "Q83216": "cd",
+ "Q83327": "eV",
+ "Q834105": "g/L",
+ "Q835916": "IU",
+ "Q838801": "ns",
+ "Q842015": "μs",
+ "Q842981": "thm (US)",
+ "Q844211": "kg/m³",
+ "Q844338": "hm",
+ "Q844976": "Oe",
+ "Q845958": "¥",
+ "Q848856": "dam",
+ "Q851872": "o",
+ "Q854546": "Gm",
+ "Q855161": "Yibit",
+ "Q856240": "ft³/min",
+ "Q857027": "ft²",
+ "Q85854198": "MN",
+ "Q864818": "abA",
+ "Q87262709": "kΩ",
+ "Q87416053": "MΩ",
+ "Q88296091": "tsp",
+ "Q9026416": "MWth",
+ "Q9048643": "nl",
+ "Q905912": "L",
+ "Q906223": "Es",
+ "Q909066": "at",
+ "Q911730": "nx",
+ "Q914151": "P_P",
+ "Q915169": "F_P",
+ "Q93318": "nmi",
+ "Q940052": "q",
+ "Q94076025": "dalm",
+ "Q94076717": "dakat",
+ "Q942092": "BWI$",
+ "Q94414053": "Prad",
+ "Q94414499": "PC",
+ "Q94415026": "Grad",
+ "Q94415255": "GC",
+ "Q94415438": "Yrad",
+ "Q94415526": "YC",
+ "Q94415782": "Mrad",
+ "Q94416260": "GN",
+ "Q94416535": "cN",
+ "Q94416879": "YN",
+ "Q94417138": "PN",
+ "Q94417481": "μGy",
+ "Q94417583": "μS",
+ "Q94417598": "μT",
+ "Q94417933": "μlm",
+ "Q94418102": "μN",
+ "Q94418220": "μsr",
+ "Q94418481": "μBq",
+ "Q94479580": "GΩ",
+ "Q94480021": "PΩ",
+ "Q94480081": "YΩ",
+ "Q94480128": "cΩ",
+ "Q94480131": "TΩ",
+ "Q94480136": "pΩ",
+ "Q94480254": "nΩ",
+ "Q94480476": "dΩ",
+ "Q94480633": "EΩ",
+ "Q94480967": "daΩ",
+ "Q94481176": "hΩ",
+ "Q94481339": "fΩ",
+ "Q94481646": "yΩ",
+ "Q94487174": "zΩ",
+ "Q94487366": "mΩ",
+ "Q94487561": "μΩ",
+ "Q94487750": "kGy",
+ "Q94488007": "klx",
+ "Q94488361": "MF",
+ "Q94488759": "GBq",
+ "Q94489041": "PBq",
+ "Q94489223": "YBq",
+ "Q94489429": "MBq",
+ "Q94489465": "kBq",
+ "Q94489476": "TBq",
+ "Q94489494": "kWb",
+ "Q94489520": "kS",
+ "Q94490951": "klm",
+ "Q94491129": "kkat",
+ "Q94634634": "cC",
+ "Q94634655": "MC",
+ "Q94634666": "kC",
+ "Q94634677": "TC",
+ "Q94634684": "μC",
+ "Q94634699": "mC",
+ "Q94693759": "csr",
+ "Q94693773": "msr",
+ "Q94693786": "mWb",
+ "Q94693805": "μWb",
+ "Q94693819": "GS",
+ "Q94693849": "cS",
+ "Q94693918": "MS",
+ "Q94694019": "TS",
+ "Q94694096": "pS",
+ "Q94694154": "nS",
+ "Q94694206": "mS",
+ "Q94731530": "mlm",
+ "Q94731808": "mkat",
+ "Q94731887": "μkat",
+ "Q94732218": "nkat",
+ "Q94732627": "pkat",
+ "Q94733432": "fkat",
+ "Q94733760": "cGy",
+ "Q94734107": "dGy",
+ "Q94734232": "mGy",
+ "Q94734359": "daGy",
+ "Q94734468": "aGy",
+ "Q94734527": "pGy",
+ "Q94734593": "nGy",
+ "Q94734689": "kT",
+ "Q94734788": "mT",
+ "Q94939947": "Gkat",
+ "Q94940018": "Pkat",
+ "Q94940081": "ykat",
+ "Q94940160": "dkat",
+ "Q94940232": "Ekat",
+ "Q94940295": "Ykat",
+ "Q94940582": "Tkat",
+ "Q94940892": "hkat",
+ "Q94941461": "zkat",
+ "Q94942602": "MGy",
+ "Q94942863": "GGy",
+ "Q94986863": "YWb",
+ "Q94986889": "PWb",
+ "Q94986906": "cWb",
+ "Q94986920": "GWb",
+ "Q94986942": "MWb",
+ "Q94986962": "TWb",
+ "Q95178536": "Mlm",
+ "Q95178777": "Tlm",
+ "Q95178881": "clm",
+ "Q95179024": "plm",
+ "Q95179137": "nlm",
+ "Q95179382": "hlm",
+ "Q95179467": "flm",
+ "Q95179608": "zlm",
+ "Q95179695": "Mkat",
+ "Q95179788": "ckat",
+ "Q95179882": "PGy",
+ "Q95377836": "PF",
+ "Q95377853": "YF",
+ "Q95378017": "kF",
+ "Q95378296": "TF",
+ "Q95379145": "cF",
+ "Q95379382": "GF",
+ "Q95379491": "daC",
+ "Q95379580": "hC",
+ "Q95379588": "dC",
+ "Q95379596": "EC",
+ "Q95445986": "nC",
+ "Q95446327": "pC",
+ "Q95446670": "fC",
+ "Q95447079": "zC",
+ "Q95447237": "yC",
+ "Q95447253": "fF",
+ "Q95447263": "zF",
+ "Q95447276": "aF",
+ "Q95447555": "dF",
+ "Q95447863": "EF",
+ "Q95448262": "yF",
+ "Q95448479": "hF",
+ "Q95448689": "daF",
+ "Q95448950": "kSv",
+ "Q95559229": "GSv",
+ "Q95559368": "YSv",
+ "Q95559441": "MSv",
+ "Q95559576": "TSv",
+ "Q95559603": "PSv",
+ "Q95609154": "nWb",
+ "Q95609210": "fWb",
+ "Q95609261": "zWb",
+ "Q95609291": "dWb",
+ "Q95609317": "EWb",
+ "Q95676212": "pWb",
+ "Q95676232": "yWb",
+ "Q95676243": "hWb",
+ "Q95676250": "daWb",
+ "Q95676257": "PS",
+ "Q95676260": "YS",
+ "Q95676273": "zS",
+ "Q95676275": "fS",
+ "Q95676279": "yS",
+ "Q95676287": "hS",
+ "Q95676291": "daS",
+ "Q95676297": "dS",
+ "Q95676298": "ES",
+ "Q95720731": "YGy",
+ "Q95720734": "TGy",
+ "Q95720736": "fGy",
+ "Q95720739": "yGy",
+ "Q95720741": "zGy",
+ "Q95720742": "EGy",
+ "Q95720746": "hGy",
+ "Q95720749": "mlx",
+ "Q95720758": "μlx",
+ "Q95720773": "dalx",
+ "Q95720777": "hlx",
+ "Q95720781": "dlx",
+ "Q95720786": "clx",
+ "Q95857671": "zSv",
+ "Q95859071": "fSv",
+ "Q95860960": "daSv",
+ "Q95861107": "hSv",
+ "Q95861296": "dSv",
+ "Q95862182": "ESv",
+ "Q95863358": "cSv",
+ "Q95863591": "ySv",
+ "Q95863894": "pSv",
+ "Q95864194": "zBq",
+ "Q95864378": "fBq",
+ "Q95864695": "daBq",
+ "Q95864940": "hBq",
+ "Q95865286": "dBq",
+ "Q95865530": "EBq",
+ "Q95865716": "cBq",
+ "Q95865877": "yBq",
+ "Q95866173": "pBq",
+ "Q95866344": "nBq",
+ "Q95866767": "mBq",
+ "Q95867993": "mN",
+ "Q95948345": "crad",
+ "Q95948364": "drad",
+ "Q95948734": "daN",
+ "Q95948739": "hN",
+ "Q95948747": "dN",
+ "Q95976839": "Plm",
+ "Q95976853": "Glm",
+ "Q95976869": "Ylm",
+ "Q95976889": "ylm",
+ "Q95976917": "dlm",
+ "Q95976919": "Elm",
+ "Q95976921": "nT",
+ "Q95993516": "TN",
+ "Q95993522": "nN",
+ "Q95993524": "fN",
+ "Q95993526": "yN",
+ "Q95993528": "zN",
+ "Q95993530": "EN",
+ "Q95993532": "pN",
+ "Q95993537": "μrad",
+ "Q95993542": "nrad",
+ "Q95993547": "frad",
+ "Q95993553": "prad",
+ "Q95993554": "darad",
+ "Q95993557": "hrad",
+ "Q95993619": "pT",
+ "Q96025401": "daT",
+ "Q96025405": "Trad",
+ "Q96025407": "Zrad",
+ "Q96025409": "zrad",
+ "Q96025413": "yrad",
+ "Q96025414": "Erad",
+ "Q96025419": "Ylx",
+ "Q96025422": "Glx",
+ "Q96025427": "Plx",
+ "Q96025431": "Mlx",
+ "Q96025433": "Tlx",
+ "Q96025435": "nlx",
+ "Q96025441": "flx",
+ "Q96050953": "GH",
+ "Q96051010": "PH",
+ "Q96051029": "YH",
+ "Q96051052": "cH",
+ "Q96051074": "TH",
+ "Q96051106": "MH",
+ "Q96051123": "kH",
+ "Q96051126": "fH",
+ "Q96051133": "yH",
+ "Q96051139": "hH",
+ "Q96051142": "dH",
+ "Q96051144": "EH",
+ "Q96051150": "pH",
+ "Q96051160": "daH",
+ "Q96051186": "zH",
+ "Q96051199": "aH",
+ "Q96051245": "ylx",
+ "Q96051267": "Elx",
+ "Q96051282": "plx",
+ "Q96051312": "zlx",
+ "Q96070067": "PT",
+ "Q96070074": "YT",
+ "Q96070076": "GT",
+ "Q96070087": "cT",
+ "Q96070103": "MT",
+ "Q96070125": "hT",
+ "Q96070145": "fT",
+ "Q96070174": "TT",
+ "Q96070195": "zT",
+ "Q96070247": "yT",
+ "Q96070254": "dT",
+ "Q96070264": "ET",
+ "Q96070276": "m°C",
+ "Q96070318": "dsr",
+ "Q96070329": "nsr",
+ "Q96070341": "psr",
+ "Q96095866": "fsr",
+ "Q96095897": "zsr",
+ "Q96095917": "ysr",
+ "Q96095927": "dasr",
+ "Q96095928": "hsr",
+ "Q96095931": "ksr",
+ "Q96095933": "Msr",
+ "Q96095939": "Gsr",
+ "Q96095941": "μ°C",
+ "Q96095955": "n°C",
+ "Q96095960": "k°C",
+ "Q96106290": "Tsr",
+ "Q96106298": "Psr",
+ "Q96106311": "Esr",
+ "Q96106319": "Zsr",
+ "Q96106332": "Ysr",
+ "Q96106346": "c°C",
+ "Q96106360": "d°C",
+ "Q96106368": "da°C",
+ "Q96106385": "h°C",
+ "Q96106393": "M°C",
+ "Q96236286": "G°C",
+ "Q97059641": "p°C",
+ "Q97059652": "T°C",
+ "Q97143826": "P°C",
+ "Q97143831": "y°C",
+ "Q97143835": "f°C",
+ "Q97143838": "Z°C",
+ "Q97143842": "E°C",
+ "Q97143843": "z°C",
+ "Q97143849": "Y°C",
+ "Q97143851": "a°C",
+ "Q98538634": "eV/m²",
+ "Q98635536": "eV/m",
+ "Q98642859": "eV m²/kg",
+ "Q11229": "%",
+ "Q11570": "kg",
+ "Q11573": "m",
+ "Q11574": "s",
+ "Q11579": "K",
+ "Q11582": "L",
+ "Q12129": "pc",
+ "Q12438": "N",
+ "Q16068": "DM",
+ "Q1811": "ua",
+ "Q20764": "Myr",
+ "Q2101": "e",
+ "Q25235": "h",
+ "Q25236": "W",
+ "Q25250": "V",
+ "Q25267": "°C",
+ "Q25269": "J",
+ "Q25272": "A",
+ "Q25343": "m²",
+ "Q25406": "C",
+ "Q25517": "m³",
+ "Q33680": "rad",
+ "Q35852": "ha",
+ "Q36384": "equiv",
+ "Q3710": "ft",
+ "Q39274": "Sv",
+ "Q39369": "Hz",
+ "Q41509": "mol",
+ "Q41803": "g",
+ "Q42289": "°F",
+ "Q4406": "TV$",
+ "Q44395": "Pa",
+ "Q4587": "Le",
+ "Q4588": "WS$",
+ "Q4592": "F$",
+ "Q4596": "Rs",
+ "Q4597": "$",
+ "Q47083": "Ω",
+ "Q48013": "oz",
+ "Q50094": "Np",
+ "Q50098": "B",
+ "Q531": "ly",
+ "Q5329": "dB",
+ "Q573": "d",
+ "Q577": "a",
+ "Q7727": "min",
+ "Q8799": "B"
+}
\ No newline at end of file
diff --git a/searx/engines/duckduckgo_definitions.py b/searx/engines/duckduckgo_definitions.py
index 7ce54f056..f8bc44e46 100644
--- a/searx/engines/duckduckgo_definitions.py
+++ b/searx/engines/duckduckgo_definitions.py
@@ -12,28 +12,53 @@ DuckDuckGo (definitions)
import json
from urllib.parse import urlencode
from lxml import html
-from re import compile
-from searx.engines.duckduckgo import _fetch_supported_languages, supported_languages_url, language_aliases
-from searx.utils import extract_text, html_to_text, match_language
-url = 'https://api.duckduckgo.com/'\
+from searx import logger
+from searx.data import WIKIDATA_UNITS
+from searx.engines.duckduckgo import _fetch_supported_languages, supported_languages_url, language_aliases
+from searx.utils import extract_text, html_to_text, match_language, get_string_replaces_function
+from searx.external_urls import get_external_url, get_earth_coordinates_url, area_to_osm_zoom
+
+
+logger = logger.getChild('duckduckgo_definitions')
+
+URL = 'https://api.duckduckgo.com/'\
+ '?{query}&format=json&pretty=0&no_redirect=1&d=1'
-http_regex = compile(r'^http:')
+WIKIDATA_PREFIX = [
+ 'http://www.wikidata.org/entity/',
+ 'https://www.wikidata.org/entity/'
+]
+
+replace_http_by_https = get_string_replaces_function({'http:': 'https:'})
-def result_to_text(url, text, htmlResult):
+def is_broken_text(text):
+ """ duckduckgo may return something like "http://somewhere Related website"
+
+ The href URL is broken, the "Related website" may contains some HTML.
+
+ The best solution seems to ignore these results.
+ """
+ return text.startswith('http') and ' ' in text
+
+
+def result_to_text(text, htmlResult):
# TODO : remove result ending with "Meaning" or "Category"
+ result = None
dom = html.fromstring(htmlResult)
a = dom.xpath('//a')
if len(a) >= 1:
- return extract_text(a[0])
+ result = extract_text(a[0])
else:
- return text
+ result = text
+ if not is_broken_text(result):
+ return result
+ return None
def request(query, params):
- params['url'] = url.format(query=urlencode({'q': query}))
+ params['url'] = URL.format(query=urlencode({'q': query}))
language = match_language(params['language'], supported_languages, language_aliases)
language = language.split('-')[0]
params['headers']['Accept-Language'] = language
@@ -45,6 +70,14 @@ def response(resp):
search_res = json.loads(resp.text)
+ # search_res.get('Entity') possible values (not exhaustive) :
+ # * continent / country / department / location / waterfall
+ # * actor / musician / artist
+ # * book / performing art / film / television / media franchise / concert tour / playwright
+ # * prepared food
+ # * website / software / os / programming language / file format / software engineer
+ # * compagny
+
content = ''
heading = search_res.get('Heading', '')
attributes = []
@@ -55,7 +88,8 @@ def response(resp):
# add answer if there is one
answer = search_res.get('Answer', '')
if answer:
- if search_res.get('AnswerType', '') not in ['calc']:
+ logger.debug('AnswerType="%s" Answer="%s"', search_res.get('AnswerType'), answer)
+ if search_res.get('AnswerType') not in ['calc', 'ip']:
results.append({'answer': html_to_text(answer)})
# add infobox
@@ -66,42 +100,36 @@ def response(resp):
content = content + search_res.get('Abstract', '')
# image
- image = search_res.get('Image', '')
+ image = search_res.get('Image')
image = None if image == '' else image
- # attributes
- if 'Infobox' in search_res:
- infobox = search_res.get('Infobox', None)
- if 'content' in infobox:
- for info in infobox.get('content'):
- attributes.append({'label': info.get('label'),
- 'value': info.get('value')})
-
# urls
+ # Official website, Wikipedia page
for ddg_result in search_res.get('Results', []):
- if 'FirstURL' in ddg_result:
- firstURL = ddg_result.get('FirstURL', '')
- text = ddg_result.get('Text', '')
+ firstURL = ddg_result.get('FirstURL')
+ text = ddg_result.get('Text')
+ if firstURL is not None and text is not None:
urls.append({'title': text, 'url': firstURL})
results.append({'title': heading, 'url': firstURL})
# related topics
for ddg_result in search_res.get('RelatedTopics', []):
if 'FirstURL' in ddg_result:
- suggestion = result_to_text(ddg_result.get('FirstURL', None),
- ddg_result.get('Text', None),
- ddg_result.get('Result', None))
- if suggestion != heading:
- results.append({'suggestion': suggestion})
+ firstURL = ddg_result.get('FirstURL')
+ text = ddg_result.get('Text')
+ if not is_broken_text(text):
+ suggestion = result_to_text(text,
+ ddg_result.get('Result'))
+ if suggestion != heading and suggestion is not None:
+ results.append({'suggestion': suggestion})
elif 'Topics' in ddg_result:
suggestions = []
relatedTopics.append({'name': ddg_result.get('Name', ''),
- 'suggestions': suggestions})
+ 'suggestions': suggestions})
for topic_result in ddg_result.get('Topics', []):
- suggestion = result_to_text(topic_result.get('FirstURL', None),
- topic_result.get('Text', None),
- topic_result.get('Result', None))
- if suggestion != heading:
+ suggestion = result_to_text(topic_result.get('Text'),
+ topic_result.get('Result'))
+ if suggestion != heading and suggestion is not None:
suggestions.append(suggestion)
# abstract
@@ -110,7 +138,10 @@ def response(resp):
# add as result ? problem always in english
infobox_id = abstractURL
urls.append({'title': search_res.get('AbstractSource'),
- 'url': abstractURL})
+ 'url': abstractURL,
+ 'official': True})
+ results.append({'url': abstractURL,
+ 'title': heading})
# definition
definitionURL = search_res.get('DefinitionURL', '')
@@ -118,53 +149,107 @@ def response(resp):
# add as result ? as answer ? problem always in english
infobox_id = definitionURL
urls.append({'title': search_res.get('DefinitionSource'),
- 'url': definitionURL})
+ 'url': definitionURL})
# to merge with wikidata's infobox
if infobox_id:
- infobox_id = http_regex.sub('https:', infobox_id)
+ infobox_id = replace_http_by_https(infobox_id)
- # entity
- entity = search_res.get('Entity', None)
- # TODO continent / country / department / location / waterfall /
- # mountain range :
- # link to map search, get weather, near by locations
- # TODO musician : link to music search
- # TODO concert tour : ??
- # TODO film / actor / television / media franchise :
- # links to IMDB / rottentomatoes (or scrap result)
- # TODO music : link tu musicbrainz / last.fm
- # TODO book : ??
- # TODO artist / playwright : ??
- # TODO compagny : ??
- # TODO software / os : ??
- # TODO software engineer : ??
- # TODO prepared food : ??
- # TODO website : ??
- # TODO performing art : ??
- # TODO prepared food : ??
- # TODO programming language : ??
- # TODO file format : ??
+ # attributes
+ # some will be converted to urls
+ if 'Infobox' in search_res:
+ infobox = search_res.get('Infobox')
+ if 'content' in infobox:
+ osm_zoom = 17
+ coordinates = None
+ for info in infobox.get('content'):
+ data_type = info.get('data_type')
+ data_label = info.get('label')
+ data_value = info.get('value')
+
+ # Workaround: ddg may return a double quote
+ if data_value == '""':
+ continue
+
+ # Is it an external URL ?
+ # * imdb_id / facebook_profile / youtube_channel / youtube_video / twitter_profile
+ # * instagram_profile / rotten_tomatoes / spotify_artist_id / itunes_artist_id / soundcloud_id
+ # * netflix_id
+ external_url = get_external_url(data_type, data_value)
+ if external_url is not None:
+ urls.append({'title': data_label,
+ 'url': external_url})
+ elif data_type in ['instance', 'wiki_maps_trigger', 'google_play_artist_id']:
+ # ignore instance: Wikidata value from "Instance Of" (Qxxxx)
+ # ignore wiki_maps_trigger: reference to a javascript
+ # ignore google_play_artist_id: service shutdown
+ pass
+ elif data_type == 'string' and data_label == 'Website':
+ # There is already an URL for the website
+ pass
+ elif data_type == 'area':
+ attributes.append({'label': data_label,
+ 'value': area_to_str(data_value),
+ 'entity': 'P2046'})
+ osm_zoom = area_to_osm_zoom(data_value.get('amount'))
+ elif data_type == 'coordinates':
+ if data_value.get('globe') == 'http://www.wikidata.org/entity/Q2':
+ # coordinate on Earth
+ # get the zoom information from the area
+ coordinates = info
+ else:
+ # coordinate NOT on Earth
+ attributes.append({'label': data_label,
+ 'value': data_value,
+ 'entity': 'P625'})
+ elif data_type == 'string':
+ attributes.append({'label': data_label,
+ 'value': data_value})
+
+ if coordinates:
+ data_label = coordinates.get('label')
+ data_value = coordinates.get('value')
+ latitude = data_value.get('latitude')
+ longitude = data_value.get('longitude')
+ url = get_earth_coordinates_url(latitude, longitude, osm_zoom)
+ urls.append({'title': 'OpenStreetMap',
+ 'url': url,
+ 'entity': 'P625'})
if len(heading) > 0:
# TODO get infobox.meta.value where .label='article_title'
if image is None and len(attributes) == 0 and len(urls) == 1 and\
len(relatedTopics) == 0 and len(content) == 0:
- results.append({
- 'url': urls[0]['url'],
- 'title': heading,
- 'content': content
- })
+ results.append({'url': urls[0]['url'],
+ 'title': heading,
+ 'content': content})
else:
- results.append({
- 'infobox': heading,
- 'id': infobox_id,
- 'entity': entity,
- 'content': content,
- 'img_src': image,
- 'attributes': attributes,
- 'urls': urls,
- 'relatedTopics': relatedTopics
- })
+ results.append({'infobox': heading,
+ 'id': infobox_id,
+ 'content': content,
+ 'img_src': image,
+ 'attributes': attributes,
+ 'urls': urls,
+ 'relatedTopics': relatedTopics})
return results
+
+
+def unit_to_str(unit):
+ for prefix in WIKIDATA_PREFIX:
+ if unit.startswith(prefix):
+ wikidata_entity = unit[len(prefix):]
+ return WIKIDATA_UNITS.get(wikidata_entity, unit)
+ return unit
+
+
+def area_to_str(area):
+ """parse {'unit': 'http://www.wikidata.org/entity/Q712226', 'amount': '+20.99'}"""
+ unit = unit_to_str(area.get('unit'))
+ if unit is not None:
+ try:
+ amount = float(area.get('amount'))
+ return '{} {}'.format(amount, unit)
+ except ValueError:
+ pass
+ return '{} {}'.format(area.get('amount', ''), area.get('unit', ''))
diff --git a/searx/engines/wikidata.py b/searx/engines/wikidata.py
index c557f4e59..01e873de9 100644
--- a/searx/engines/wikidata.py
+++ b/searx/engines/wikidata.py
@@ -3,501 +3,686 @@
Wikidata
@website https://wikidata.org
- @provide-api yes (https://wikidata.org/w/api.php)
+ @provide-api yes (https://query.wikidata.org/)
- @using-api partially (most things require scraping)
- @results JSON, HTML
- @stable no (html can change)
+ @using-api yes
+ @results JSON
+ @stable yes
@parse url, infobox
"""
-from searx import logger
-from searx.poolrequests import get
-from searx.engines.wikipedia import _fetch_supported_languages, supported_languages_url
-from searx.utils import extract_text, match_language, eval_xpath
from urllib.parse import urlencode
from json import loads
-from lxml.html import fromstring
-from lxml import etree
+
+from dateutil.parser import isoparse
+from babel.dates import format_datetime, format_date, format_time, get_datetime_format
+
+from searx import logger
+from searx.data import WIKIDATA_UNITS
+from searx.poolrequests import post, get
+from searx.engines.wikipedia import _fetch_supported_languages, supported_languages_url
+from searx.utils import match_language, searx_useragent, get_string_replaces_function
+from searx.external_urls import get_external_url, get_earth_coordinates_url, area_to_osm_zoom
logger = logger.getChild('wikidata')
-result_count = 1
-# urls
-wikidata_host = 'https://www.wikidata.org'
-url_search = wikidata_host \
- + '/w/index.php?{query}&ns0=1'
+# SPARQL
+SPARQL_ENDPOINT_URL = 'https://query.wikidata.org/sparql'
+SPARQL_EXPLAIN_URL = 'https://query.wikidata.org/bigdata/namespace/wdq/sparql?explain'
+WIKIDATA_PROPERTIES = {
+ 'P434': 'MusicBrainz',
+ 'P435': 'MusicBrainz',
+ 'P436': 'MusicBrainz',
+ 'P966': 'MusicBrainz',
+ 'P345': 'IMDb',
+ 'P2397': 'YouTube',
+ 'P1651': 'YouTube',
+ 'P2002': 'Twitter',
+ 'P2013': 'Facebook',
+ 'P2003': 'Instagram',
+}
-wikidata_api = wikidata_host + '/w/api.php'
-url_detail = wikidata_api\
- + '?action=parse&format=json&{query}'\
- + '&redirects=1&prop=text%7Cdisplaytitle%7Cparsewarnings'\
- + '&disableeditsection=1&preview=1§ionpreview=1&disabletoc=1&utf8=1&formatversion=2'
+# SERVICE wikibase:mwapi : https://www.mediawiki.org/wiki/Wikidata_Query_Service/User_Manual/MWAPI
+# SERVICE wikibase:label: https://en.wikibooks.org/wiki/SPARQL/SERVICE_-_Label#Manual_Label_SERVICE
+# https://en.wikibooks.org/wiki/SPARQL/WIKIDATA_Precision,_Units_and_Coordinates
+# https://www.mediawiki.org/wiki/Wikibase/Indexing/RDF_Dump_Format#Data_model
+# optmization:
+# * https://www.wikidata.org/wiki/Wikidata:SPARQL_query_service/query_optimization
+# * https://github.com/blazegraph/database/wiki/QueryHints
+QUERY_TEMPLATE = """
+SELECT ?item ?itemLabel ?itemDescription ?lat ?long %SELECT%
+WHERE
+{
+ SERVICE wikibase:mwapi {
+ bd:serviceParam wikibase:endpoint "www.wikidata.org";
+ wikibase:api "EntitySearch";
+ wikibase:limit 1;
+ mwapi:search "%QUERY%";
+ mwapi:language "%LANGUAGE%".
+ ?item wikibase:apiOutputItem mwapi:item.
+ }
-url_map = 'https://www.openstreetmap.org/'\
- + '?lat={latitude}&lon={longitude}&zoom={zoom}&layers=M'
-url_image = 'https://commons.wikimedia.org/wiki/Special:FilePath/{filename}?width=500&height=400'
+ %WHERE%
-# xpaths
-div_ids_xpath = '//div[@id]'
-wikidata_ids_xpath = '//ul[@class="mw-search-results"]/li//a/@href'
-title_xpath = '//*[contains(@class,"wikibase-title-label")]'
-description_xpath = '//div[contains(@class,"wikibase-entitytermsview-heading-description")]'
-label_xpath = './/div[contains(@class,"wikibase-statementgroupview-property-label")]/a'
-url_xpath = './/a[contains(@class,"external free") or contains(@class, "wb-external-id")]'
-wikilink_xpath = './/ul[contains(@class,"wikibase-sitelinklistview-listview")]'\
- + '/li[contains(@data-wb-siteid,"{wikiid}")]//a/@href'
-property_row_xpath = './/div[contains(@class,"wikibase-statementview")]'
-preferred_rank_xpath = './/span[contains(@class,"wikibase-rankselector-preferred")]'
-value_xpath = './/div[contains(@class,"wikibase-statementview-mainsnak")]'\
- + '/*/div[contains(@class,"wikibase-snakview-value")]'
-language_fallback_xpath = '//sup[contains(@class,"wb-language-fallback-indicator")]'
-calendar_name_xpath = './/sup[contains(@class,"wb-calendar-name")]'
-media_xpath = value_xpath + '//div[contains(@class,"commons-media-caption")]//a'
+ SERVICE wikibase:label {
+ bd:serviceParam wikibase:language "%LANGUAGE%,en".
+ ?item rdfs:label ?itemLabel .
+ ?item schema:description ?itemDescription .
+ %WIKIBASE_LABELS%
+ }
+
+}
+GROUP BY ?item ?itemLabel ?itemDescription ?lat ?long %GROUP_BY%
+"""
+
+# Get the calendar names and the property names
+QUERY_PROPERTY_NAMES = """
+SELECT ?item ?name
+WHERE {
+ {
+ SELECT ?item
+ WHERE { ?item wdt:P279* wd:Q12132 }
+ } UNION {
+ VALUES ?item { %ATTRIBUTES% }
+ }
+ OPTIONAL { ?item rdfs:label ?name. }
+}
+"""
-def get_id_cache(result):
- id_cache = {}
- for e in eval_xpath(result, div_ids_xpath):
- id = e.get('id')
- if id.startswith('P'):
- id_cache[id] = e
- return id_cache
+# https://www.w3.org/TR/sparql11-query/#rSTRING_LITERAL1
+# https://lists.w3.org/Archives/Public/public-rdf-dawg/2011OctDec/0175.html
+sparql_string_escape = get_string_replaces_function({'\t': '\\\t',
+ '\n': '\\\n',
+ '\r': '\\\r',
+ '\b': '\\\b',
+ '\f': '\\\f',
+ '\"': '\\\"',
+ '\'': '\\\'',
+ '\\': '\\\\'})
+
+replace_http_by_https = get_string_replaces_function({'http:': 'https:'})
+
+
+def get_headers():
+ # user agent: https://www.mediawiki.org/wiki/Wikidata_Query_Service/User_Manual#Query_limits
+ return {
+ 'Accept': 'application/sparql-results+json',
+ 'User-Agent': searx_useragent()
+ }
+
+
+def get_label_for_entity(entity_id, language):
+ name = WIKIDATA_PROPERTIES.get(entity_id)
+ if name is None:
+ name = WIKIDATA_PROPERTIES.get((entity_id, language))
+ if name is None:
+ name = WIKIDATA_PROPERTIES.get((entity_id, language.split('-')[0]))
+ if name is None:
+ name = WIKIDATA_PROPERTIES.get((entity_id, 'en'))
+ if name is None:
+ name = entity_id
+ return name
+
+
+def send_wikidata_query(query, method='GET'):
+ if method == 'GET':
+ # query will be cached by wikidata
+ http_response = get(SPARQL_ENDPOINT_URL + '?' + urlencode({'query': query}), headers=get_headers())
+ else:
+ # query won't be cached by wikidata
+ http_response = post(SPARQL_ENDPOINT_URL, data={'query': query}, headers=get_headers())
+ if http_response.status_code != 200:
+ logger.debug('SPARQL endpoint error %s', http_response.content.decode())
+ logger.debug('request time %s', str(http_response.elapsed))
+ http_response.raise_for_status()
+ return loads(http_response.content.decode())
def request(query, params):
- params['url'] = url_search.format(
- query=urlencode({'search': query}))
+ language = params['language'].split('-')[0]
+ if language == 'all':
+ language = 'en'
+ else:
+ language = match_language(params['language'], supported_languages, language_aliases).split('-')[0]
+
+ query, attributes = get_query(query, language)
+
+ params['method'] = 'POST'
+ params['url'] = SPARQL_ENDPOINT_URL
+ params['data'] = {'query': query}
+ params['headers'] = get_headers()
+
+ params['language'] = language
+ params['attributes'] = attributes
return params
def response(resp):
results = []
- htmlparser = etree.HTMLParser()
- html = fromstring(resp.content.decode(), parser=htmlparser)
- search_results = eval_xpath(html, wikidata_ids_xpath)
+ if resp.status_code != 200:
+ logger.debug('SPARQL endpoint error %s', resp.content.decode())
+ resp.raise_for_status()
+ jsonresponse = loads(resp.content.decode())
- if resp.search_params['language'].split('-')[0] == 'all':
- language = 'en'
- else:
- language = match_language(resp.search_params['language'], supported_languages, language_aliases).split('-')[0]
+ language = resp.search_params['language'].lower()
+ attributes = resp.search_params['attributes']
- # TODO: make requests asynchronous to avoid timeout when result_count > 1
- for search_result in search_results[:result_count]:
- wikidata_id = search_result.split('/')[-1]
- url = url_detail.format(query=urlencode({'page': wikidata_id, 'uselang': language}))
- htmlresponse = get(url)
- jsonresponse = loads(htmlresponse.content.decode())
- results += getDetail(jsonresponse, wikidata_id, language, resp.search_params['language'], htmlparser)
+ seen_entities = set()
+
+ for result in jsonresponse.get('results', {}).get('bindings', []):
+ attribute_result = {key: value['value'] for key, value in result.items()}
+ entity_url = attribute_result['item']
+ if entity_url not in seen_entities:
+ seen_entities.add(entity_url)
+ results += get_results(attribute_result, attributes, language)
+ else:
+ logger.debug('The SPARQL request returns duplicate entities: %s', str(attribute_result))
return results
-def getDetail(jsonresponse, wikidata_id, language, locale, htmlparser):
+def get_results(attribute_result, attributes, language):
results = []
- urls = []
+ infobox_title = attribute_result.get('itemLabel')
+ infobox_id = attribute_result['item']
+ infobox_id_lang = None
+ infobox_urls = []
+ infobox_attributes = []
+ infobox_content = attribute_result.get('itemDescription')
+ img_src = None
+ img_src_priority = 100
+
+ for attribute in attributes:
+ value = attribute.get_str(attribute_result, language)
+ if value is not None and value != '':
+ attribute_type = type(attribute)
+
+ if attribute_type in (WDURLAttribute, WDArticle):
+ # get_select() method : there is group_concat(distinct ...;separator=", ")
+ # split the value here
+ for url in value.split(', '):
+ infobox_urls.append({'title': attribute.get_label(language), 'url': url, **attribute.kwargs})
+ # "normal" results (not infobox) include official website and Wikipedia links.
+ if attribute.kwargs.get('official') or attribute_type == WDArticle:
+ results.append({'title': infobox_title, 'url': url})
+ # update the infobox_id with the wikipedia URL
+ # first the local wikipedia URL, and as fallback the english wikipedia URL
+ if attribute_type == WDArticle\
+ and ((attribute.language == 'en' and infobox_id_lang is None)
+ or attribute.language != 'en'):
+ infobox_id_lang = attribute.language
+ infobox_id = url
+ elif attribute_type == WDImageAttribute:
+ # this attribute is an image.
+ # replace the current image only the priority is lower
+ # (the infobox contain only one image).
+ if attribute.priority < img_src_priority:
+ img_src = value
+ img_src_priority = attribute.priority
+ elif attribute_type == WDGeoAttribute:
+ # geocoordinate link
+ # use the area to get the OSM zoom
+ # Note: ignre the unit (must be km² otherwise the calculation is wrong)
+ # Should use normalized value p:P2046/psn:P2046/wikibase:quantityAmount
+ area = attribute_result.get('P2046')
+ osm_zoom = area_to_osm_zoom(area) if area else 19
+ url = attribute.get_str(attribute_result, language, osm_zoom=osm_zoom)
+ if url:
+ infobox_urls.append({'title': attribute.get_label(language),
+ 'url': url,
+ 'entity': attribute.name})
+ else:
+ infobox_attributes.append({'label': attribute.get_label(language),
+ 'value': value,
+ 'entity': attribute.name})
+
+ if infobox_id:
+ infobox_id = replace_http_by_https(infobox_id)
+
+ # add the wikidata URL at the end
+ infobox_urls.append({'title': 'Wikidata', 'url': attribute_result['item']})
+
+ if img_src is None and len(infobox_attributes) == 0 and len(infobox_urls) == 1 and\
+ len(infobox_content) == 0:
+ results.append({
+ 'url': infobox_urls[0]['url'],
+ 'title': infobox_title,
+ 'content': infobox_content
+ })
+ else:
+ results.append({
+ 'infobox': infobox_title,
+ 'id': infobox_id,
+ 'content': infobox_content,
+ 'img_src': img_src,
+ 'urls': infobox_urls,
+ 'attributes': infobox_attributes
+ })
+ return results
+
+
+def get_query(query, language):
+ attributes = get_attributes(language)
+ select = [a.get_select() for a in attributes]
+ where = list(filter(lambda s: len(s) > 0, [a.get_where() for a in attributes]))
+ wikibase_label = list(filter(lambda s: len(s) > 0, [a.get_wikibase_label() for a in attributes]))
+ group_by = list(filter(lambda s: len(s) > 0, [a.get_group_by() for a in attributes]))
+ query = QUERY_TEMPLATE\
+ .replace('%QUERY%', sparql_string_escape(query))\
+ .replace('%SELECT%', ' '.join(select))\
+ .replace('%WHERE%', '\n '.join(where))\
+ .replace('%WIKIBASE_LABELS%', '\n '.join(wikibase_label))\
+ .replace('%GROUP_BY%', ' '.join(group_by))\
+ .replace('%LANGUAGE%', language)
+ return query, attributes
+
+
+def get_attributes(language):
attributes = []
- title = jsonresponse.get('parse', {}).get('displaytitle', {})
- result = jsonresponse.get('parse', {}).get('text', {})
+ def add_value(name):
+ attributes.append(WDAttribute(name))
- if not title or not result:
- return results
+ def add_amount(name):
+ attributes.append(WDAmountAttribute(name))
- title = fromstring(title, parser=htmlparser)
- for elem in eval_xpath(title, language_fallback_xpath):
- elem.getparent().remove(elem)
- title = extract_text(eval_xpath(title, title_xpath))
+ def add_label(name):
+ attributes.append(WDLabelAttribute(name))
- result = fromstring(result, parser=htmlparser)
- for elem in eval_xpath(result, language_fallback_xpath):
- elem.getparent().remove(elem)
+ def add_url(name, url_id=None, **kwargs):
+ attributes.append(WDURLAttribute(name, url_id, kwargs))
- description = extract_text(eval_xpath(result, description_xpath))
+ def add_image(name, url_id=None, priority=1):
+ attributes.append(WDImageAttribute(name, url_id, priority))
- id_cache = get_id_cache(result)
+ def add_date(name):
+ attributes.append(WDDateAttribute(name))
- # URLS
+ # Dates
+ for p in ['P571', # inception date
+ 'P576', # dissolution date
+ 'P580', # start date
+ 'P582', # end date
+ 'P569', # date of birth
+ 'P570', # date of death
+ 'P619', # date of spacecraft launch
+ 'P620']: # date of spacecraft landing
+ add_date(p)
- # official website
- add_url(urls, result, id_cache, 'P856', results=results)
+ for p in ['P27', # country of citizenship
+ 'P495', # country of origin
+ 'P17', # country
+ 'P159']: # headquarters location
+ add_label(p)
- # wikipedia
- wikipedia_link_count = 0
- wikipedia_link = get_wikilink(result, language + 'wiki')
- if wikipedia_link:
- wikipedia_link_count += 1
- urls.append({'title': 'Wikipedia (' + language + ')',
- 'url': wikipedia_link})
+ # Places
+ for p in ['P36', # capital
+ 'P35', # head of state
+ 'P6', # head of government
+ 'P122', # basic form of government
+ 'P37']: # official language
+ add_label(p)
- if language != 'en':
- wikipedia_en_link = get_wikilink(result, 'enwiki')
- if wikipedia_en_link:
- wikipedia_link_count += 1
- urls.append({'title': 'Wikipedia (en)',
- 'url': wikipedia_en_link})
+ add_value('P1082') # population
+ add_amount('P2046') # area
+ add_amount('P281') # postal code
+ add_label('P38') # currency
+ add_amount('P2048') # heigth (building)
- # TODO: get_wiki_firstlanguage
- # if wikipedia_link_count == 0:
+ # Media
+ for p in ['P400', # platform (videogames, computing)
+ 'P50', # author
+ 'P170', # creator
+ 'P57', # director
+ 'P175', # performer
+ 'P178', # developer
+ 'P162', # producer
+ 'P176', # manufacturer
+ 'P58', # screenwriter
+ 'P272', # production company
+ 'P264', # record label
+ 'P123', # publisher
+ 'P449', # original network
+ 'P750', # distributed by
+ 'P86']: # composer
+ add_label(p)
- # more wikis
- add_url(urls, result, id_cache, default_label='Wikivoyage (' + language + ')', link_type=language + 'wikivoyage')
- add_url(urls, result, id_cache, default_label='Wikiquote (' + language + ')', link_type=language + 'wikiquote')
- add_url(urls, result, id_cache, default_label='Wikimedia Commons', link_type='commonswiki')
+ add_date('P577') # publication date
+ add_label('P136') # genre (music, film, artistic...)
+ add_label('P364') # original language
+ add_value('P212') # ISBN-13
+ add_value('P957') # ISBN-10
+ add_label('P275') # copyright license
+ add_label('P277') # programming language
+ add_value('P348') # version
+ add_label('P840') # narrative location
- add_url(urls, result, id_cache, 'P625', 'OpenStreetMap', link_type='geo')
+ # Languages
+ add_value('P1098') # number of speakers
+ add_label('P282') # writing system
+ add_label('P1018') # language regulatory body
+ add_value('P218') # language code (ISO 639-1)
- # musicbrainz
- add_url(urls, result, id_cache, 'P434', 'MusicBrainz', 'http://musicbrainz.org/artist/')
- add_url(urls, result, id_cache, 'P435', 'MusicBrainz', 'http://musicbrainz.org/work/')
- add_url(urls, result, id_cache, 'P436', 'MusicBrainz', 'http://musicbrainz.org/release-group/')
- add_url(urls, result, id_cache, 'P966', 'MusicBrainz', 'http://musicbrainz.org/label/')
+ # Other
+ add_label('P169') # ceo
+ add_label('P112') # founded by
+ add_label('P1454') # legal form (company, organization)
+ add_label('P137') # operator (service, facility, ...)
+ add_label('P1029') # crew members (tripulation)
+ add_label('P225') # taxon name
+ add_value('P274') # chemical formula
+ add_label('P1346') # winner (sports, contests, ...)
+ add_value('P1120') # number of deaths
+ add_value('P498') # currency code (ISO 4217)
- # IMDb
- add_url(urls, result, id_cache, 'P345', 'IMDb', 'https://www.imdb.com/', link_type='imdb')
- # source code repository
- add_url(urls, result, id_cache, 'P1324')
- # blog
- add_url(urls, result, id_cache, 'P1581')
- # social media links
- add_url(urls, result, id_cache, 'P2397', 'YouTube', 'https://www.youtube.com/channel/')
- add_url(urls, result, id_cache, 'P1651', 'YouTube', 'https://www.youtube.com/watch?v=')
- add_url(urls, result, id_cache, 'P2002', 'Twitter', 'https://twitter.com/')
- add_url(urls, result, id_cache, 'P2013', 'Facebook', 'https://facebook.com/')
- add_url(urls, result, id_cache, 'P2003', 'Instagram', 'https://instagram.com/')
+ # URL
+ add_url('P856', official=True) # official website
+ attributes.append(WDArticle(language)) # wikipedia (user language)
+ if not language.startswith('en'):
+ attributes.append(WDArticle('en')) # wikipedia (english)
- urls.append({'title': 'Wikidata',
- 'url': 'https://www.wikidata.org/wiki/'
- + wikidata_id + '?uselang=' + language})
+ add_url('P1324') # source code repository
+ add_url('P1581') # blog
+ add_url('P434', url_id='musicbrainz_artist')
+ add_url('P435', url_id='musicbrainz_work')
+ add_url('P436', url_id='musicbrainz_release_group')
+ add_url('P966', url_id='musicbrainz_label')
+ add_url('P345', url_id='imdb_id')
+ add_url('P2397', url_id='youtube_channel')
+ add_url('P1651', url_id='youtube_video')
+ add_url('P2002', url_id='twitter_profile')
+ add_url('P2013', url_id='facebook_profile')
+ add_url('P2003', url_id='instagram_profile')
- # INFOBOX ATTRIBUTES (ROWS)
+ # Map
+ attributes.append(WDGeoAttribute('P625'))
- # DATES
- # inception date
- add_attribute(attributes, id_cache, 'P571', date=True)
- # dissolution date
- add_attribute(attributes, id_cache, 'P576', date=True)
- # start date
- add_attribute(attributes, id_cache, 'P580', date=True)
- # end date
- add_attribute(attributes, id_cache, 'P582', date=True)
- # date of birth
- add_attribute(attributes, id_cache, 'P569', date=True)
- # date of death
- add_attribute(attributes, id_cache, 'P570', date=True)
- # date of spacecraft launch
- add_attribute(attributes, id_cache, 'P619', date=True)
- # date of spacecraft landing
- add_attribute(attributes, id_cache, 'P620', date=True)
+ # Image
+ add_image('P15', priority=1, url_id='wikimedia_image') # route map
+ add_image('P242', priority=2, url_id='wikimedia_image') # locator map
+ add_image('P154', priority=3, url_id='wikimedia_image') # logo
+ add_image('P18', priority=4, url_id='wikimedia_image') # image
+ add_image('P41', priority=5, url_id='wikimedia_image') # flag
+ add_image('P2716', priority=6, url_id='wikimedia_image') # collage
+ add_image('P2910', priority=7, url_id='wikimedia_image') # icon
- # nationality
- add_attribute(attributes, id_cache, 'P27')
- # country of origin
- add_attribute(attributes, id_cache, 'P495')
- # country
- add_attribute(attributes, id_cache, 'P17')
- # headquarters
- add_attribute(attributes, id_cache, 'Q180')
-
- # PLACES
- # capital
- add_attribute(attributes, id_cache, 'P36', trim=True)
- # head of state
- add_attribute(attributes, id_cache, 'P35', trim=True)
- # head of government
- add_attribute(attributes, id_cache, 'P6', trim=True)
- # type of government
- add_attribute(attributes, id_cache, 'P122')
- # official language
- add_attribute(attributes, id_cache, 'P37')
- # population
- add_attribute(attributes, id_cache, 'P1082', trim=True)
- # area
- add_attribute(attributes, id_cache, 'P2046')
- # currency
- add_attribute(attributes, id_cache, 'P38', trim=True)
- # heigth (building)
- add_attribute(attributes, id_cache, 'P2048')
-
- # MEDIA
- # platform (videogames)
- add_attribute(attributes, id_cache, 'P400')
- # author
- add_attribute(attributes, id_cache, 'P50')
- # creator
- add_attribute(attributes, id_cache, 'P170')
- # director
- add_attribute(attributes, id_cache, 'P57')
- # performer
- add_attribute(attributes, id_cache, 'P175')
- # developer
- add_attribute(attributes, id_cache, 'P178')
- # producer
- add_attribute(attributes, id_cache, 'P162')
- # manufacturer
- add_attribute(attributes, id_cache, 'P176')
- # screenwriter
- add_attribute(attributes, id_cache, 'P58')
- # production company
- add_attribute(attributes, id_cache, 'P272')
- # record label
- add_attribute(attributes, id_cache, 'P264')
- # publisher
- add_attribute(attributes, id_cache, 'P123')
- # original network
- add_attribute(attributes, id_cache, 'P449')
- # distributor
- add_attribute(attributes, id_cache, 'P750')
- # composer
- add_attribute(attributes, id_cache, 'P86')
- # publication date
- add_attribute(attributes, id_cache, 'P577', date=True)
- # genre
- add_attribute(attributes, id_cache, 'P136')
- # original language
- add_attribute(attributes, id_cache, 'P364')
- # isbn
- add_attribute(attributes, id_cache, 'Q33057')
- # software license
- add_attribute(attributes, id_cache, 'P275')
- # programming language
- add_attribute(attributes, id_cache, 'P277')
- # version
- add_attribute(attributes, id_cache, 'P348', trim=True)
- # narrative location
- add_attribute(attributes, id_cache, 'P840')
-
- # LANGUAGES
- # number of speakers
- add_attribute(attributes, id_cache, 'P1098')
- # writing system
- add_attribute(attributes, id_cache, 'P282')
- # regulatory body
- add_attribute(attributes, id_cache, 'P1018')
- # language code
- add_attribute(attributes, id_cache, 'P218')
-
- # OTHER
- # ceo
- add_attribute(attributes, id_cache, 'P169', trim=True)
- # founder
- add_attribute(attributes, id_cache, 'P112')
- # legal form (company/organization)
- add_attribute(attributes, id_cache, 'P1454')
- # operator
- add_attribute(attributes, id_cache, 'P137')
- # crew members (tripulation)
- add_attribute(attributes, id_cache, 'P1029')
- # taxon
- add_attribute(attributes, id_cache, 'P225')
- # chemical formula
- add_attribute(attributes, id_cache, 'P274')
- # winner (sports/contests)
- add_attribute(attributes, id_cache, 'P1346')
- # number of deaths
- add_attribute(attributes, id_cache, 'P1120')
- # currency code
- add_attribute(attributes, id_cache, 'P498')
-
- image = add_image(id_cache)
-
- if len(attributes) == 0 and len(urls) == 2 and len(description) == 0:
- results.append({
- 'url': urls[0]['url'],
- 'title': title,
- 'content': description
- })
- else:
- results.append({
- 'infobox': title,
- 'id': wikipedia_link,
- 'content': description,
- 'img_src': image,
- 'attributes': attributes,
- 'urls': urls
- })
-
- return results
+ return attributes
-# only returns first match
-def add_image(id_cache):
- # P15: route map, P242: locator map, P154: logo, P18: image, P242: map, P41: flag, P2716: collage, P2910: icon
- property_ids = ['P15', 'P242', 'P154', 'P18', 'P242', 'P41', 'P2716', 'P2910']
+class WDAttribute:
- for property_id in property_ids:
- image = id_cache.get(property_id, None)
- if image is not None:
- image_name = eval_xpath(image, media_xpath)
- image_src = url_image.replace('{filename}', extract_text(image_name[0]))
- return image_src
+ __slots__ = 'name',
+
+ def __init__(self, name):
+ self.name = name
+
+ def get_select(self):
+ return '(group_concat(distinct ?{name};separator=", ") as ?{name}s)'.replace('{name}', self.name)
+
+ def get_label(self, language):
+ return get_label_for_entity(self.name, language)
+
+ def get_where(self):
+ return "OPTIONAL { ?item wdt:{name} ?{name} . }".replace('{name}', self.name)
+
+ def get_wikibase_label(self):
+ return ""
+
+ def get_group_by(self):
+ return ""
+
+ def get_str(self, result, language):
+ return result.get(self.name + 's')
+
+ def __repr__(self):
+ return '<' + str(type(self).__name__) + ':' + self.name + '>'
-# setting trim will only returned high ranked rows OR the first row
-def add_attribute(attributes, id_cache, property_id, default_label=None, date=False, trim=False):
- attribute = id_cache.get(property_id, None)
- if attribute is not None:
+class WDAmountAttribute(WDAttribute):
- if default_label:
- label = default_label
- else:
- label = extract_text(eval_xpath(attribute, label_xpath))
- label = label[0].upper() + label[1:]
+ def get_select(self):
+ return '?{name} ?{name}Unit'.replace('{name}', self.name)
- if date:
- trim = True
- # remove calendar name
- calendar_name = eval_xpath(attribute, calendar_name_xpath)
- for calendar in calendar_name:
- calendar.getparent().remove(calendar)
+ def get_where(self):
+ return """ OPTIONAL { ?item p:{name} ?{name}Node .
+ ?{name}Node rdf:type wikibase:BestRank ; ps:{name} ?{name} .
+ OPTIONAL { ?{name}Node psv:{name}/wikibase:quantityUnit ?{name}Unit. } }""".replace('{name}', self.name)
- concat_values = ""
- values = []
- first_value = None
- for row in eval_xpath(attribute, property_row_xpath):
- if not first_value or not trim or eval_xpath(row, preferred_rank_xpath):
- value = eval_xpath(row, value_xpath)
- if not value:
- continue
- value = extract_text(value)
+ def get_group_by(self):
+ return self.get_select()
- # save first value in case no ranked row is found
- if trim and not first_value:
- first_value = value
- else:
- # to avoid duplicate values
- if value not in values:
- concat_values += value + ", "
- values.append(value)
-
- if trim and not values:
- attributes.append({'label': label,
- 'value': first_value})
- else:
- attributes.append({'label': label,
- 'value': concat_values[:-2]})
+ def get_str(self, result, language):
+ value = result.get(self.name)
+ unit = result.get(self.name + "Unit")
+ if unit is not None:
+ unit = unit.replace('http://www.wikidata.org/entity/', '')
+ return value + " " + get_label_for_entity(unit, language)
+ return value
-# requires property_id unless it's a wiki link (defined in link_type)
-def add_url(urls, result, id_cache, property_id=None, default_label=None, url_prefix=None, results=None,
- link_type=None, only_first=True):
- links = []
+class WDArticle(WDAttribute):
- # wiki links don't have property in wikidata page
- if link_type and 'wiki' in link_type:
- links.append(get_wikilink(result, link_type))
- else:
- dom_element = id_cache.get(property_id, None)
- if dom_element is not None:
- if not default_label:
- label = extract_text(eval_xpath(dom_element, label_xpath))
- label = label[0].upper() + label[1:]
+ __slots__ = 'language', 'kwargs'
- if link_type == 'geo':
- links.append(get_geolink(dom_element))
+ def __init__(self, language, kwargs=None):
+ super().__init__('wikipedia')
+ self.language = language
+ self.kwargs = kwargs or {}
- elif link_type == 'imdb':
- links.append(get_imdblink(dom_element, url_prefix))
+ def get_label(self, language):
+ # language parameter is ignored
+ return "Wikipedia ({language})".replace('{language}', self.language)
- else:
- url_results = eval_xpath(dom_element, url_xpath)
- for link in url_results:
- if link is not None:
- if url_prefix:
- link = url_prefix + extract_text(link)
- else:
- link = extract_text(link)
- links.append(link)
+ def get_select(self):
+ return "?article{language} ?articleName{language}".replace('{language}', self.language)
- # append urls
- for url in links:
- if url is not None:
- u = {'title': default_label or label, 'url': url}
- if property_id == 'P856':
- u['official'] = True
- u['domain'] = url.split('/')[2]
- urls.append(u)
- if results is not None:
- results.append(u)
- if only_first:
- break
+ def get_where(self):
+ return """OPTIONAL { ?article{language} schema:about ?item ;
+ schema:inLanguage "{language}" ;
+ schema:isPartOf